Construindo API REST para projetos PHP legados
Publicados: 2022-03-11Construir ou arquitetar uma API REST não é uma tarefa fácil, especialmente quando você precisa fazer isso para projetos PHP legados. Existem muitas bibliotecas de terceiros hoje em dia que facilitam a implementação de uma API REST, mas integrá-las em bases de código legadas existentes pode ser bastante assustadora. E nem sempre você tem o luxo de trabalhar com frameworks modernos, como Laravel e Symfony. Com projetos PHP legados, muitas vezes você pode se encontrar em algum lugar no meio de frameworks internos obsoletos, rodando em cima de versões antigas do PHP.
Neste artigo, veremos alguns desafios comuns de tentar implementar APIs REST do zero, algumas maneiras de contornar esses problemas e uma estratégia geral para criar servidores de API personalizados baseados em PHP para projetos legados.
Embora o artigo seja baseado no PHP 5.3 e superior, os conceitos principais são válidos para todas as versões do PHP além da versão 5.0 e podem até ser aplicados a projetos não PHP. Aqui, não abordaremos o que é uma API REST em geral, portanto, se você não estiver familiarizado com ela, leia sobre ela primeiro.
Para facilitar o seu acompanhamento, aqui está uma lista de alguns termos usados ao longo deste artigo e seus significados:
- Servidor API: principal aplicação REST servindo a API, neste caso, escrita em PHP.
- Endpoint da API: um “método” de back-end com o qual o cliente se comunica para realizar uma ação e produzir resultados.
- URL do endpoint da API: URL por meio do qual o sistema de back-end é acessível ao mundo.
- Token de API: um identificador exclusivo passado por meio de cabeçalhos HTTP ou cookies a partir do qual o usuário pode ser identificado.
- App: aplicativo cliente que se comunicará com o aplicativo REST via endpoints da API. Neste artigo, assumiremos que é baseado na web (desktop ou móvel) e, portanto, é escrito em JavaScript.
Etapas iniciais
Padrões de caminho
Uma das primeiras coisas que precisamos decidir é em qual caminho de URL os endpoints da API estarão disponíveis. Existem 2 formas populares:
- Crie um novo subdomínio, como api.example.com.
- Crie um caminho, como example.com/api.
À primeira vista, pode parecer que a primeira variante é mais popular e atraente. Na realidade, no entanto, se você estiver criando uma API específica do projeto, pode ser mais apropriado escolher a segunda variante.
Uma das razões mais importantes por trás da segunda abordagem é que isso permite que os cookies sejam usados como meio de transferir credenciais. Os clientes baseados em navegador enviarão automaticamente os cookies apropriados nas solicitações XHR, eliminando a necessidade de um cabeçalho de autorização adicional.
Outra razão importante é que você não precisa fazer nada em relação à configuração do subdomínio ou problemas de gerenciamento em que os cabeçalhos personalizados podem ser removidos por alguns servidores proxy. Isso pode ser uma provação tediosa em projetos legados.
O uso de cookies pode ser considerado uma prática “unRESTful”, pois as solicitações REST devem ser stateless. Nesse caso, podemos fazer um compromisso e passar o valor do token em um cookie em vez de passá-lo por meio de um cabeçalho personalizado. Efetivamente, estamos usando cookies apenas como uma maneira de passar o valor do token em vez do session_id diretamente. Essa abordagem pode ser considerada sem estado, mas podemos deixá-la a seu critério.
Os URLs de endpoint da API também podem ser versionados. Além disso, eles podem incluir o formato de resposta esperado como uma extensão no nome do caminho. Embora não sejam críticos, especialmente durante os estágios iniciais do desenvolvimento da API, a longo prazo esses detalhes certamente podem valer a pena. Especialmente quando você precisa implementar novos recursos. Verificar qual versão o cliente está esperando e fornecer o formato necessário para compatibilidade com versões anteriores pode ser a melhor solução.
A estrutura do URL do endpoint da API pode ter a seguinte aparência:
example.com/api/${version_code}/${actual_request_path}.${format}
E, um exemplo real:
example.com/api/v1.0/records.json
Roteamento
Depois de escolher uma URL base para os endpoints da API, a próxima coisa que precisamos fazer é pensar em nosso sistema de roteamento. Ele pode ser integrado a uma estrutura existente, mas se isso for muito complicado, uma possível solução alternativa é criar uma pasta chamada “api” na raiz do documento. Dessa forma, a API pode ter uma lógica completamente separada. Você pode estender essa abordagem colocando a lógica da API em seus próprios arquivos, como este:
Você pode pensar em “www/api/Apis/Users.php” como um “controlador” separado para um determinado endpoint de API. Seria ótimo reutilizar implementações da base de código existente, por exemplo, reutilizar modelos que já estão implementados no projeto para se comunicar com o banco de dados.
Finalmente, certifique-se de apontar todas as solicitações recebidas de “/api/*” para “/api/index.php”. Isso pode ser feito alterando a configuração do seu servidor web.
Classe de API
Versão e formato
Você deve sempre definir claramente quais versões e formatos seus endpoints de API aceitam e quais são os padrões. Isso permitirá que você crie novos recursos no futuro, mantendo as funcionalidades antigas. A versão da API pode ser basicamente uma string, mas você pode usar valores numéricos para melhor compreensão e comparabilidade. É bom ter dígitos sobressalentes para versões menores porque indicaria claramente que apenas algumas coisas são diferentes:
- v1.0 significaria primeira versão.
- v1.1 primeira versão com algumas pequenas alterações.
- v2.0 seria uma versão completamente nova.
O formato pode ser qualquer coisa que seu cliente precise, incluindo, entre outros, JSON, XML e até CSV. Ao fornecê-lo via URL como uma extensão de arquivo, o URL do endpoint da API garante a legibilidade e torna-se fácil para o consumidor da API saber qual formato pode esperar:
- “/api/v1.0/records.json” retornaria uma matriz de registros JSON
- “/api/v1.0/records.xml” retornaria arquivo XML de registros
Vale ressaltar que você também precisará enviar um cabeçalho Content-Type adequado na resposta para cada um desses formatos.
Ao receber uma solicitação de entrada, uma das primeiras coisas que você deve fazer é verificar se o servidor da API suporta a versão e o formato solicitados. Em seu método principal, que lida com a solicitação recebida, analise $_SERVER['PATH_INFO'] ou $_SERVER['REQUEST_URI'] para determinar se o formato e a versão solicitados são suportados. Em seguida, continue ou retorne uma resposta 4xx (por exemplo, 406 “Não Aceitável”). A parte mais crítica aqui é sempre devolver algo que o cliente espera. Uma alternativa para isso seria verificar o cabeçalho da solicitação “Aceitar” em vez da extensão do caminho da URL.
Rotas permitidas
Você pode encaminhar tudo de forma transparente para seus controladores de API, mas pode ser melhor usar um conjunto de rotas permitidas na lista de permissões. Isso reduziria um pouco a flexibilidade, mas forneceria uma visão muito clara da aparência dos URLs de endpoint da API na próxima vez que você retornar ao código.
private $public_routes = array( 'system' => array( 'regex' => 'system', ), 'records' => array( 'regex' => 'records(?:/?([0-9]+)?)', ), );
Você também pode movê-los para arquivos separados para tornar as coisas mais limpas. A configuração acima será usada para habilitar solicitações para estes URLs:
/api/v1.0/system.json /api/v1.0/records.json /api/v1.0/records/7.json
Manipulando dados PUT
O PHP manipula automaticamente os dados POST recebidos e os coloca em $_POST superglobal. No entanto, esse não é o caso das solicitações PUT. Todos os dados são “enterrados” em php://input . Não se esqueça de analisá-lo em uma estrutura ou matriz separada antes de invocar o método real da API. Um simples parse_str pode ser suficiente, mas se o cliente estiver enviando uma solicitação de várias partes, uma análise adicional pode ser necessária para lidar com os limites do formulário. O caso de uso típico de solicitações multipartes inclui uploads de arquivos. Detectar e lidar com solicitações de várias partes pode ser feito da seguinte maneira:
self::$input = file_get_contents('php://input'); // For PUT/DELETE there is input data instead of request variables if (!empty(self::$input)) { preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); if (isset($matches[1]) && strpos(self::$input, $matches[1]) !== false) { $this->parse_raw_request(self::$input, self::$input_data); } else { parse_str(self::$input, self::$input_data); } }
Aqui, parse_raw_request pode ser implementado como:
/** * Helper method to parse raw requests */ private function parse_raw_request($input, &$a_data) { // grab multipart boundary from content type header preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); $boundary = $matches[1]; // split content by boundary and get rid of last -- element $a_blocks = preg_split("/-+$boundary/", $input); array_pop($a_blocks); // loop data blocks foreach ($a_blocks as $id => $block) { if (empty($block)) { continue; } // parse uploaded files if (strpos($block, 'application/octet-stream') !== false) { // match "name", then everything after "stream" (optional) except for prepending newlines preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); // parse all other fields } else { // match "name" and optional value in between newline sequences preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); } $a_data[$matches[1]] = $matches[2]; } }
Com isso, podemos ter a carga útil da solicitação necessária em Api::$input como entrada bruta e Api::$input_data como um array associativo.

Fingindo PUT/DELETE
Às vezes, você pode se ver em uma situação em que o servidor não suporta nada além dos métodos HTTP GET/POST padrão. Uma solução comum para esse problema é “falsificar” PUT/DELETE ou qualquer outro método de solicitação personalizado. Para isso você pode usar um parâmetro “mágico”, como “_method”. Se você o vir em sua matriz $_REQUEST , simplesmente assuma que a solicitação é do tipo especificado. Frameworks modernos como o Laravel têm essa funcionalidade embutida neles. Ele oferece grande compatibilidade caso seu servidor ou cliente tenha limitações (por exemplo, uma pessoa está usando a rede Wi-Fi de seu trabalho por trás de um proxy corporativo que não permite solicitações PUT).
Encaminhando para API específica
Se você não tem o luxo de reutilizar autoloaders de projetos existentes, você pode criar seus próprios com a ajuda da função spl_autoload_register . Defina-o em sua página “api/index.php” e chame sua classe de API localizada em “api/Api.php”. A classe API atua como um middleware e chama o método real. Por exemplo, uma solicitação para “/api/v1.0/records/7.json” deve acabar invocando o método GET “Apis/Records.php” com o parâmetro 7. Isso garantiria a separação de interesses e forneceria uma maneira de manter o limpador de lógica. Claro, se for possível integrar isso mais profundamente na estrutura que você está usando e reutilizar seus controladores ou rotas específicas, você deve considerar essa possibilidade também.
Exemplo “api/index.php” com autoloader primitivo:
<?php // Let's define very primitive autoloader spl_autoload_register(function($classname){ $classname = str_replace('Api_', 'Apis/', $classname); if (file_exists(__DIR__.'/'.$classname.'.php')) { require __DIR__.'/'.$classname.'.php'; } }); // Our main method to handle request Api::serve();
Isso carregará nossa classe Api e começará a servi-la independentemente do projeto principal.
OPÇÕES Solicitações
Quando um cliente usa um cabeçalho personalizado para encaminhar seu token exclusivo, o navegador primeiro precisa verificar sempre que o servidor oferece suporte a esse cabeçalho. É aí que entra a solicitação OPTIONS. Seu objetivo é garantir que tudo esteja certo e seguro tanto para o cliente quanto para o servidor da API. Portanto, a solicitação OPTIONS pode ser disparada toda vez que um cliente tenta fazer alguma coisa. No entanto, quando um cliente está usando cookies para credenciais, isso evita que o navegador precise enviar essa solicitação OPTIONS adicional.
Se um cliente estiver solicitando POST /users/8.json com cookies, sua solicitação será bastante padrão:
- O aplicativo executa uma solicitação POST para /users/8.json.
- O navegador realiza a solicitação e recebe uma resposta.
Mas com autorização personalizada ou cabeçalho de token:
- O aplicativo executa uma solicitação POST para /users/8.json.
- O navegador para de processar a solicitação e inicia uma solicitação OPTIONS.
- A solicitação OPTIONS é enviada para /users/8.json.
- O navegador recebe uma resposta com uma lista de todos os métodos e cabeçalhos disponíveis, conforme definido pela API.
- O navegador continua com a solicitação POST original somente se o cabeçalho personalizado estiver presente na lista de cabeçalhos disponíveis.
No entanto, lembre-se de que, mesmo usando cookies, com PUT/DELETE você ainda pode receber essa solicitação OPTIONS adicional. Portanto, esteja preparado para responder a isso.
API de registros
Estrutura básica
Nosso exemplo de API de registros é bastante simples. Ele conterá todos os métodos de solicitação e retornará a saída para a mesma classe de API principal. Por exemplo:
<?php class Api_Records { public function __construct() { // In here you could initialize some shared logic between this API and rest of the project } /** * Get individual record or records list */ public function get($id = null) { if ($id) { return $this->getRecord(intval($id)); } else { return $this->getRecords(); } } /** * Update record */ public function put($record_id = null) { // In real world there would be call to model with validation and probably token checking // Use Api::$input_data to update return Api::responseOk('OK', array()); } // ...
Assim, definir cada método HTTP nos permitirá construir API no estilo REST com mais facilidade.
Saída de formatação
Responder ingenuamente com tudo recebido do banco de dados de volta ao cliente pode ter consequências catastróficas. Para evitar qualquer exposição acidental de dados, crie um método de formato específico que retornaria apenas as chaves da lista de permissões.
Outro benefício das chaves na lista de permissões é que você pode escrever documentação com base nelas e fazer todas as verificações de tipo, garantindo, por exemplo, que user_id sempre será um número inteiro, o sinalizador is_banned sempre será booleano verdadeiro ou falso e as datas terão um padrão formato de resposta.
Saída de resultados
Cabeçalhos
Métodos separados para saída de cabeçalhos garantirão que tudo enviado ao navegador esteja correto. Este método pode usar os benefícios de tornar a API acessível através do mesmo domínio, mantendo a possibilidade de receber um cabeçalho de autorização personalizado. A escolha entre o mesmo domínio ou de terceiros pode acontecer com a ajuda dos cabeçalhos de servidor HTTP_ORIGIN e HTTP_REFERER. Se o aplicativo estiver detectando que o cliente está usando autorização x (ou qualquer outro cabeçalho personalizado), ele deve permitir o acesso de todas as origens, permitir o cabeçalho personalizado. Então poderia ficar assim:
header('Access-Control-Allow-Origin: *'); header('Access-Control-Expose-Headers: x-authorization'); header('Access-Control-Allow-Headers: origin, content-type, accept, x-authorization'); header('X-Authorization: '.YOUR_TOKEN_HERE);
No entanto, se o cliente estiver usando credenciais baseadas em cookies, os cabeçalhos podem ser um pouco diferentes, permitindo apenas cabeçalhos relacionados ao host e cookie solicitados para credenciais:
header('Access-Control-Allow-Origin: '.$origin); header('Access-Control-Expose-Headers: set-cookie, cookie'); header('Access-Control-Allow-Headers: origin, content-type, accept, set-cookie, cookie'); // Allow cookie credentials because we're on the same domain header('Access-Control-Allow-Credentials: true'); if (strtolower($_SERVER['REQUEST_METHOD']) != 'options') { setcookie(TOKEN_COOKIE_NAME, YOUR_TOKEN_HERE, time()+86400*30, '/', '.'.$_SERVER['HTTP_HOST']); }
Lembre-se de que a solicitação OPTIONS não suporta cookies, portanto o aplicativo não os enviará com ela. E, finalmente, isso permite que todos os nossos métodos HTTP desejados tenham expiração do controle de acesso:
header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE'); header('Access-Control-Max-Age: 86400');
Corpo
O próprio corpo deve conter a resposta em um formato solicitado pelo seu cliente com status HTTP 2xx em caso de sucesso, status 4xx em caso de falha devido ao cliente e status 5xx em caso de falha devido a servidor. A estrutura da resposta pode variar, embora a especificação dos campos "status" e "resposta" também possa ser benéfica. Por exemplo, se o cliente estiver tentando registrar um novo usuário e o nome de usuário já estiver em uso, você poderá enviar uma resposta com status HTTP 200, mas um JSON no corpo semelhante a:
{“status”: “ERROR”, “response”: ”username already taken”}
… em vez do erro HTTP 4xx diretamente.
Conclusão
Não há dois projetos exatamente iguais. A estratégia descrita neste artigo pode ou não ser adequada para o seu caso, mas os conceitos principais devem ser semelhantes. Vale a pena notar que nem toda página pode ter a última tendência ou estrutura atualizada por trás dela, e às vezes a raiva sobre “por que meu pacote REST Symfony não funciona aqui” pode ser transformada em uma motivação para construir algo útil, algo que funciona. O resultado final pode não ser tão brilhante, pois sempre será alguma implementação personalizada e específica do projeto, mas no final das contas a solução será algo que realmente funciona; e em um cenário como esse esse deveria ser o objetivo de todo desenvolvedor de API.
Exemplos de implementações dos conceitos discutidos aqui foram carregados em um repositório GitHub por conveniência. Você pode não querer usar esses códigos de amostra diretamente na produção como estão, mas isso pode funcionar facilmente como um ponto de partida para seu próximo projeto de integração de API PHP legado.
Teve que implementar um servidor de API REST para algum projeto legado recentemente? Compartilhe sua experiência conosco na seção de comentários abaixo.