Slim Framework - Um micro framework para PHP

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++.