Introdução ao Slim Framework
O Slim Framework é um microframework PHP para aplicações web, APIs REST e sites. Ele fornece sistema de rotas, middlewares, requests e responses ideal para quem precisa de algo simples e leve e pode ser utilizado em conjunto com outros frameworks como Twig para ter um sistema de template e o Doctrine para acesso a banco por exemplo.
Instalando o Slim Framework
Realizamos a instalação do Slim Framework através do composer. Para isso devemos adicionar algumas dependências ao projeto, começando pelo próprio framework.
composer require slim/slim:"4.*"
Então devemos escolher uma uma implementação do PSR-7(conjunto de interfaces que representam as mensagens de request e response) para o Slim utilizar. No caso vamos com a própria do Slim.
composer require slim/psr7
Também adicionamos uma implementação do PSR-11(container de injeção de dependência).
composer require php-di/php-di
E finalmente configuramos o autoload das nossas classes no package.json
que deve ficar mais ou menos assim:
{
"require": {
"slim/slim": "4.*",
"slim/psr7": "^1.3",
"php-di/php-di": "^6.3"
},
"autoload": {
"psr-4": {
"App\\": "src"
}
}
}
Hello World
Como primeiro exemplo vamos ao Hello World, onde ao acessar /hello-world
retorna somente o texto Hello World. Então criamos o arquivo /public/index.php
com o seguinte conteúdo:
declare(strict_types=1);
require '../vendor/autoload.php';
use DI\Container;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Factory\AppFactory;
$container = new Container();
AppFactory::setContainer($container);
$app = AppFactory::create();
$app->get('/hello-world' , function(ServerRequestInterface $request, ResponseInterface $response){
$response->getBody()->write('Hello World');
return $response;
});
$app->run();
Na linha 9 instanciamos o container de injeção de dependência, e logo após(linha 10), adicionamos ele ao AppFactory
que será utilizado na linha 11 para criar o objeto da aplicação Slim\App
ao qual será utilizado para adicionar as rotas e rodar a aplicação.
Na linha 13 adicionamos a rota que desejamos através do método get
que como o próprio nome sugere responde a uma requisição HTTP do tipo GET no endereço que corresponde ao padrão(pattern) passado como primeiro parâmetro. O segundo parâmetro recebe uma função de callback que será executada quando uma requisição for feita a esta rota. A função recebe dois argumentos, um objeto de request da interface ServerRequestInterface
contendo os dados da requisição do cliente e um objeto ResponseInterface
que representa a resposta que será retornada ao cliente. Esses dois objetos são criados pelo framework e injetados na função automaticamente.
Uma vez que todas as rotas esteja configuradas chamamos o método run()
na linha 20, que irá iniciar o processo do Slim Framework.
Caso esteja rodando o Slim Framework em um servidor apache, é necessário criar um arquivo .htaccess
dentro do diretório public
com o seguinte conteúdo:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
e outro .htaccess
na raiz com:
RewriteEngine on
RewriteRule ^$ public/ [L]
RewriteRule (.*) public/$1 [L]
E se estiver rodando Slim Framework em um subdiretório deve configurar seu caminho através do método setBasePath
.
$app->setBasePath('/minha-app-dir');
$app->run();
Rotas
Um dos recursos mais importantes fornecidos por um framework web é seu sistema de rotas. O Slim Framework possuí um mecanismo bem robusto e flexível de lidar com rotas.
Como vimos no Hello World a criação de uma rota para um endereço que atenda somente requisições do tipo GET é bastante simples. Da mesma forma podemos criar rotas que respondem a outros mehtods HTTP simplesmente chamando o método do objeto Slim\App
correspondente ao método HTTP. Veja alguns exemplos:
$app->post('/livros', function (ServerRequestInterface $request, ResponseInterface $response) {
$response->getBody()->write('Rota para method POST');
return $response;
});
$app->delete('/livros', function (ServerRequestInterface $request, ResponseInterface $response) {
$response->getBody()->write('rota para method DELETE');
return $response;
});
$app->put('/livros', function (ServerRequestInterface $request, ResponseInterface $response) {
$response->getBody()->write('rota para method PUT');
return $response;
});
Como já visto o primeiro argumento é o pattern da rota, ou seja, o padrão de URL que quando acessada irá corresponder a rota, o segundo argumento é uma função de callback que será chamada quando a rota for acessada. Essa função recebe dois parâmetros, um objeto da interface ServerRequestInterface
contendo as informações da requisição do cliente e o ResponseInterface
um objeto que representa a resposta que será retornada ao cliente. Esses dois objetos são criados pelo framework e injetados na função automaticamente e os veremos melhor mais para frente.
Também podemos criar uma rota que responda a qualquer HTTP method através do método any
:
$app->any('/clientes', function (ServerRequestInterface $request, ResponseInterface $response) {
$response->getBody()->write('rota para /clientes através de qualquer method ');
return $response;
});
Ou ainda definimos quais métodos através do método map
passando um array com os HTTP methods desejados, e depois o pattern da url:
$app->map(['GET', 'POST'], '/pedidos', function (ServerRequestInterface $request, ResponseInterface $response) {
$response->getBody()->write('rota para /pedidos para os methods GET e POST ');
return $response;
});
* Cada um dos métodos de rotas retorna um objeto Slim\Route
, que podem ser utilizados, por exemplo, para adicionar um middleware.
Rotas com Placeholders(Path variable)
O Padrão de url fornecido a cada um dos métodos de rota pode aceitar um placeholder, que são partes da url que podem variar. Esses placeholders devem ser marcados entre chaves( { } ) e podem ser recuperados no método de callback através de um terceiro parâmetro, um array de string $args
, que deve ser adicionado. Esse é um array associativo onde a chave é o valor marcado entre chaves no padrão de url da rota, e o valor é o valor correspondente a ele na uri acessada pelo cliente.
$app->get('/livros/{id}', function (ServerRequestInterface $request, ResponseInterface$response, array $args) {
$response->getBody()->write("Rota com variavel de url {$args['id']} ");
return $response;
});
// acessando http://localhost:3000/livros/42
// a resposta será "Rota com variavel de url 42"
Podemos também tornar um segmento de uma rota opcional, para isso basta marcar esse segmento com colchetes( [ ] ).
$app->get('/clientes[/{id}]', function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
// responde a ambos os endereços '/clientes' e `/clientes/42'
// mas não responde a '/clientes/'
return $response;
});
Os placeholders por padrão podem aceitar qualquer valor, mas podemos limitar isso fazendo que os valores aceitos correspondam a uma expressão regular, para isso basta adicionar dois pontos ” : ” após o placeholder e inserir a expressão regular ao qual ele deve corresponder.
$app->get('/livros/{id:[0-9]+}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
$response->getBody()->write("Rota com variavel de url {$args['id']} ");
return $response;
});
Rotas nomeadas
Podemos atribuir um nome para uma rota para melhor identifica-las e tornar a criação de links mais fácil. Para isso chamamos o métodos setName
do objeto retornado pelo método que utilizamos para definir a rota, passando como argumento o nome da rota.
Para criar um link para um rota nomeada precisamos de um objeto $routeParser
obtido do $app
através do método getRouteCollector
. O RouterParser
possui o método urlFor
que recebe o nome da rota e um array associativo onde a chave é o placeholder da rota e valor é utilizado para substituí-lo. Também podemos passar um terceiro parâmetro, um array associativo, que será utilizado para gerar a query string.
$app->get('/usuarios/{id}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) use ($app) {
$routeParser = $app->getRouteCollector()->getRouteParser();
$response->getBody()->write( $routeParser->urlFor('usuarios', ['id'=>42]), ['foo'=>'bar'] );
return $response;
})->setName('usuarios');
// irá retornar /usuarios/42?foo=bar
Agrupando rotas
Podemos organizar as rotas da nossa aplicação em grupo utilizando o método group
. Ele aceita como primeiro parâmetro um padrão que será adicionado ao inicio de cada uma das rotas do grupo. Como segundo parâmetro recebe uma função onde devemos criar as rotas pertencentes a este grupo. A função recebe um parâmetro RouteCollectorProxy
o qual devemos utilizar para definir as rotas do grupo em ver do objeto Slim\App
.
$app->group('/admin' , function ( RouteCollectorProxy $group){
$group->get('/usuarios', function(ServerRequestInterface $request, ResponseInterface $response){
$response->getBody()->write('Usuário no grupo admin');
return $response;
});
$group->post('/usuarios', function(ServerRequestInterface $request, ResponseInterface $response){
$response->getBody()->write('POST usuário no grupo admin');
return $response;
});
});
// Estarão disponíveis as rotas
// /admin/usuarios <- GET
// /admin/usuarios <- POST
Rotas com Controller
Embora utilizar uma função de callback nas rotas seja simples e fácil, quando o número de rotas ou a complexidade delas aumenta pode tornar o código difícil de manter. Para isso podemos criar uma classe de controller com os métodos a serem chamados pelas rotas, para isso primeiro criamos uma classe com o método que queremos que seja chamado.
declare(strict_types=1);
namespace App;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class HelloWorldController
{
public function hello(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$response->getBody()->write('Rota do controller');
return $response;
}
}
Depois basta criar a rota informando a classe de controller e método desejado como um array ou uma string. Veja os exemplos:
$app->get('/hello-world', [HelloWorldController::class, 'hello']);
//ou
$app->get('/hello-world', 'App\HelloWorldController:hello');
Arquivo routes.php
Uma prática comum é separar as definições rotas do FrontController (arquivo /public/index.php
). Para isso, criamos um arquivo separado /src/routes.php
com uma função que recebe Slim\App
e cria as rotas da aplicação. Incluímos ele no FrontController e chamamos esta função
// /src/routes.php
use App\HelloWorldController;
use Slim\App;
return function(App $app){
$app->get('/hello-world', 'App\HelloWorldController:hello');
};
// public/index.php
//...
$routes = require('../src/routes.php');
$routes($app);
$app->run();
O Response
Como vimos todos os métodos que tratam a requisição retornam um objeto $response
recebido como parâmetro. Este objeto é criado automaticamente pelo Slim Framework e injetado no método, o Slim o utiliza este objeto retornado para criar a resposta que será mandada para o cliente.
Já vimos um dos seus principais métodos getBody
que retorna um StreamInterface
que será o corpo de retorno da requisição, nele que gravamos o conteúdo que desejamos retornar para o cliente seja um documento html para uma página, ou um json.
Através do objeto $response
que definimos o status code de retorno da requisição, para isso chamamos withStatus
passando o status, mas devemos lembrar que o objeto $response
é imutável então seus métodos não alteram seus valores mas retornam um novo objeto com o valor alterado.
Da mesma forma podemos definir um header, para isso utilizamos o método withHeader
, passando como primeiro valor o nome do header e como segundo seu valor.
public function get_cliente(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$cliente = [
"id" => "3fbecec0-11a2-4c97-92ac-9422a625a4dc",
"nome" => 'João da Silva',
"cpf" => '123.456.789.98'
];
$response->getBody()->write( json_encode($cliente));
return $response->withStatus(200)->withHeader('content-type', 'application/json');
}
O Request
O $request
como o response é injetado automaticamente pelo Slim, e possui os dados da requisição como os query params, os dados do formulário, a uri acessada, etc.
Pegando o query params
Pegamos os dados passados via query params através do método getQueryParams
que retorna um array associativo contendo os valores
// requisição: /requisicao-com-params?nome=rodrigo&sobrenome=aramburu
public function requisicao_com_params(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$params = $request->getQueryParams();
$nome = $params['nome'];
$sobrenome = $params['sobrenome'];
$data = [
'nome' => $nome,
'sobrenome' => $sobrenome
];
$response->getBody()->write(json_encode($data));
return $response;
}
Recebendo valores de um formulário
Os dados enviados via formulário são recebidos dentro do body, podemos pegá-los já processados através do método getParsedBody
que retorna um array associativo onde a chave é o name
do campo associado ao valor.
<!-- formulário -->
<form method="POST" action="cliente-store">
<div>
<label for="nome">Nome: </label>
<input type="text" name="nome" id="nome" />
</div>
<div>
<label for="sobrenome">Sobrenome: </label>
<input type="text" name="sobrenome" id="sobrenome" />
</div>
<div>
<label for="cpf">CPF: </label>
<input type="text" name="cpf" id="cpf" />
</div>
<input type="submit" value="Enviar" />
</form>
// método da rota
public function cliente_store(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$params = (array) $request->getParsedBody();
$body = "";
$body .= "Nome: " . $params['nome']. '<br>';
$body .= "Sobreome: " . $params['sobrenome']. '<br>';
$body .= "CPF: " . $params['cpf']. '<br>';
$response->getBody()->write( $body );
return $response;
}
Pegando json enviado através da requisição
Para pegar um json enviado no corpo da requisição, algo comum em requisições em API REST, utilizamos o método getBody
que retorna um Stream
que pode ser convertido em texto através do método __toString
e a partir dai parseado(?) como desejar.
curl --location --request POST 'http://localhost:8080/api/clientes' \
--header 'Content-Type: application/json' \
--data-raw '{
"nome": "Rodrigo",
"sobrenome": "Aramburu",
"cpf": "123.456.789-89"
}'
public function api_cliente(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$jsonData = json_decode( $request->getBody()->__toString() ,true );
$body = "";
$body .= "Nome: " . $jsonData['nome']. '<br>';
$body .= "Sobreome: " . $jsonData['sobrenome']. '<br>';
$body .= "CPF: " . $jsonData['cpf']. '<br>';
$response->getBody()->write( $body );
return $response;
}
Outros dados do request
Em alguns casos necessitamos de outros dados do request, podemos pegar o HTTP method da requisição através do método getMethod
.
Podemos pegar um objeto com os dados do Uri através do método $request->getUri()
que retorna um objeto da interface UriInterface
contendo diversos métodos para pegar partes como path, porta, etc. Se necessário utilize o método __toString()
para pegar a uri completa.
Também podemos obter as informações da super-global $_SERVER
através do método $request->getServerParams()
, ele retorna um array associativo onde podemos obter informações como o ip do usuário($request->getServerParams()['REMOTE_ADDR']
).
Podemos pegar qualquer valor do header da requisição através do método getHeader
.
public function request_data(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$ip = $request->getServerParams()['REMOTE_ADDR'];
$method = $request->getMethod();
$contentType = $request->getHeader('content-type');
$data = [
'uri' => $request->getUri()->__toString(),
'ip' => $ip,
'method' => $method,
'content-type'=> $contentType
];
$response->getBody()->write(json_encode($data));
return $response->withStatus(200)->withHeader('content-type', 'application/json');
}
Upload de arquivos
Para realizar o envio de arquivo podemos utilizar um formulário, não esquecendo do atributo enctype="multipart/form-data"
.
<form method="POST" action="cliente-upload" enctype="multipart/form-data">
<div>
<label for="image">Imagem: </label>
<input type="file" name="image" id="image" />
</div>
<input type="submit" value="Enviar" />
</form>
Na função de tratamento da requisição pegamos os arquivos enviados através do método getUploadedFiles
do objeto $request
que retorna um array associativo onde a chave é o name do campo file do formulário e valor um objeto da interface UploadedFileInterface
para manipularmos. Esse objeto possui diversos métodos como o getError
que retorna o erro associado ao upload do arquivo, podemos comparar com a constante UPLOAD_ERR_OK
verificar se o envio foi bem sucedido.
Outros métodos que podem ser úteis são getSize
, que retorna o tamanho do arquivo; getClientFilename
retorna o nome do arquivo enviado, getClientMediaType
retorna o mime-type do arquivo.
Outro método muito importante é o moveTo
, que move o arquivo do local temporário para um novo local passado como parâmetro.
public function cliente_upload_process(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$uploadDir = __DIR__ . '/../store/uploads';
$uploadedFile = $request->getUploadedFiles()['image'];
if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
$size = $uploadedFile->getSize();
$originalName = $uploadedFile->getClientFilename();
$contentType = $uploadedFile->getClientMediaType();
$extension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION);
$filename = date('Ymdhis') . '.'.$extension;
$uploadedFile->moveTo($uploadDir . DIRECTORY_SEPARATOR . $filename);
// carregando o twig - https://www.botecodigital.dev.br/php/twig-template-engine-para-php/
$loader = new FilesystemLoader('../templates');
$twig = new Environment($loader, [
'cache' => false,
]);
$response->getBody()->write($twig->render('form-upload.html', compact('size','originalName','contentType','extension','filename')));
}
return $response;
}
Middlewares
Middlewares nos permitem executar algum código antes ou depois que o Slim manipule a requisição. Podemos utilizar isso para autenticar os usuário antes da aplicação rodar, ou sanitizar os dados de entrada, ou ainda adicionar algum header na resposta de cada requisição depois de todo o trabalho ser feito entre outras coisas.
Podemos criar um midddleware de duas formas, com uma função closure ou criando uma classe que implementa a interface MiddlewareInterface que possui somente o método process
. Tanto a closure quanto o método process
da MiddlewareInterface
recebem dois parâmetros, um objeto ServerRequestInterface
contendo a requisição do usuário(como já vimos) e um RequestHandlerInterface
que será responsável por processar a requisição através das rotas. O método process
ou a closure devem retorna um objeto da interface ResponseInterface
que será passado para o próximo middleware ou retornado ao cliente.
A RequestHandlerInterface
possui um método handle
que recebe como parâmetro um ServerRequestInterface
(normalmente o próprio recebido por process
ou closure) e os processa através das rotas, controllers, etc e retorna objeto $response
resultante. Então executamos tudo o que queremos executar antes de chegar nos controllers antes de chamar $handler->handle($request)
e após ele tudo que queremos executar depois.
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Response;
class AuthMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// checa a sessão antes de processar o controller, se não autenticado retorna o response com um header de redirecionamento
if( !isset($_SESSION['userAuth']) ){
$response = new Response();
return $response->withHeader('Location', '/login');
}
$response = $handler->handle($request);
return $response;
}
}
Para adicionar um middleware, podemos adicioná-lo globalmente passando como parâmetro para o método add
de $app
ou para alguma rota específica através do método add
do objeto Slim\Route
retornado pelo método da rota. Também podemos adicionar um middleware a um grupo de rotas.
$app->add(new AuthMiddleware());
$app->get('/cliente-form', [ClienteController::class, 'cliente_form'])->add(new AuthMiddleware());
$app->group('/admin', function (RouteCollectorProxy $group){
// rotas
})->add(new AuthMiddleware());
Erros! Ohhh nooo!
Erros ocorrem e lidar com isso é preciso. O Slim já possui um middleware para capturar os erros e exibir alguma mensagem, ele é adicionado através do método addErrorMiddleware
que recebe três parâmetros booleanos $displayErrorDetails
, $logErrors
e $logErrorDetails
. O $displayErrorDetails
determina se os detalhes do erro serão exibidos(configure para false em produção), e os valores de $logErrors
e $logErrorDetails
são passados para o default ErroHandler para informá-lo se deve logar os erros e os detalhes respectivamente.
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
Também podemos criar nosso próprio middleware de erro, basta capturar a exceção lançada e montar a resposta desejada.
class ErrorMiddleware implements MiddlewareInterface
{
private Environment $twig;
public function __construct(Container $container)
{
$this->twig = $container->get('twig');
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
$response = $handler->handle($request);
return $response;
} catch (Exception $e) {
$response = new Response();
$response->getBody()->write($this->twig->render('error.html', ['message' => $e->getMessage(), 'code' => $e->getCode()]));
return $response->withStatus($e->getCode());
}
}
}
Bom, esta é uma introdução sobre o que o Slim Framework pode oferecer, mais detalhes como sempre pode ser visto na página de documentação.
Aqui você pode ver um pequeno exemplo bem básico de uso do Slim com Twig para ter uma noção.
Bom era isso T++.