Um dos mais recentes (tipo de) manias varrendo a rede é APIs, mais especificamente aqueles que RESTO alavancagem. É realmente nenhuma surpresa ou, como APIs REST consumir é tão incrivelmente fácil ... em qualquer idioma. É também incrivelmente fácil de criá-los como você usa essencialmente nada mais do que uma especificação HTTP que existe há séculos. Uma das poucas coisas que eu dou crédito para Rails é o seu suporte a REST bem pensada, tanto para fornecer e consumir essas APIs (como foi explicado por todos os fanboys Rails com quem trabalho).
Sério, se você nunca usou o REST, mas você já teve de trabalhar com (ou pior, criar) uma API SOAP, ou simplesmente abriu um WSDL e teve sua cabeça explodir, garoto, eu tenho uma boa notícia para você!
Então, o que na Terra é REST? Por que você deve cuidar?
Antes de chegarmos em escrever algum código, eu quero ter certeza todo mundo tem uma boa compreensão do que REST é e como seu grande para APIs. Em primeiro lugar, tecnicamente falando, REST não é específico para apenas APIs, é mais um conceito genérico. No entanto, obviamente, por causa deste artigo, vamos estar falando sobre isso no contexto de uma API. Então, vamos olhar para as necessidades básicas de uma API e como RESTO se dirige a eles.
Os pedidos
Todas as APIs precisam aceitar solicitações. Normalmente, com uma API RESTful, você terá um esquema de URL bem definida. Digamos que você deseja fornecer uma API para os usuários do seu site (eu sei, eu sempre uso o "users" conceito para meus exemplos). Bem, sua estrutura de URL provavelmente seria algo como: "API / usuários" e "api / users / [id]", dependendo do tipo de operação que está sendo solicitado contra a sua API. Você também precisa considerar como você deseja aceitar dados. Esses dias um monte de pessoas estão usando JSON ou XML, e eu, pessoalmente, prefiro JSON, porque ele funciona bem com JavaScript, PHP e tem uma funcionalidade fácil para codificação e decodificação. Se você quiser que seu API para ser realmente forte, você pode aceitar tanto por farejar o tipo de conteúdo do pedido (ou seja, application / json ou application / xml), mas é perfeitamente aceitável para restringir as coisas a um tipo de conteúdo. Heck, você poderia até usar simples pares chave / valor se você quisesse.
A outra peça de um pedido é o que é realmente queria fazer, como carga, salvar, etc Normalmente, você teria que vir com algum tipo de arquitetura que define a ação que o solicitante (consumidor) desejos, mas simplifica o REST que. Usando métodos de solicitação HTTP, ou verbos, não precisamos de definir qualquer coisa. Nós podemos apenas usar o GET, POST, PUT e DELETE métodos, e que abrange todos os pedidos que precisaríamos. Você pode equiparar os verbos para o seu padrão crud coisas estilo: GET = carga / recuperar, POST = criar, PUT = update, DELETE = bem, excluir. É importante notar que esses verbos não traduzir diretamente para CRUD, mas é uma boa maneira de pensar sobre elas. Então, voltando aos exemplos do URL acima, vamos dar uma olhada no que alguns pedidos possíveis pode significar:
- Solicitação GET para / api / users - Lista todos os usuários
- Solicitação GET para / api/users/1 - Informações da lista para o usuário com ID de 1
- POST pedido / API / usuários - Criar um novo usuário
- PUT pedido / api/users/1 - Atualização do usuário com ID de 1
- Solicitação de exclusão de / api/users/1 - Excluir usuário com ID de 1
Como você espera ver, REST já cuidou de muitas das grandes dores de cabeça de criar sua própria API através de algumas simples e bem compreendido padrões e protocolos, mas há uma outra peça para uma boa API ...
Respostas
Então, REST lida com as solicitações de forma muito fácil, mas também torna as respostas de geração fácil. Semelhante a solicitações, existem dois principais componentes de uma resposta RESTful: o corpo de resposta, e um código de estado. O corpo de resposta é muito fácil de lidar. Como os pedidos, a maioria das respostas em REST são geralmente ou JSON ou XML (talvez apenas texto simples no caso de postos, mas nós vamos cobrir isso mais tarde). E, como pedidos, o consumidor pode especificar o tipo de resposta que gostaria de através de uma outra parte da especificação solicitação HTTP, "Aceitar". Se o consumidor pretenda receber uma resposta em XML, que tinha acabado de enviar um cabeçalho Accept como uma parte do seu pedido dizendo que o máximo ("Aceitar: application / xml»). Evidentemente, este método não é tão amplamente adotado (ainda deve ser), então você tem também pode usar o conceito de uma extensão na URL. Por exemplo, / api / users.xml significa que o consumidor quer XML como resposta, à semelhança / api / users.json significa JSON (o mesmo para coisas como / api/users/1.json/xml). De qualquer maneira que você escolher (eu digo fazer as duas coisas), você deve escolher um tipo de resposta padrão como uma grande parte do tempo as pessoas 'não mesmo dizer-lhe o que eles querem. Novamente, eu diria que ir com JSON. Então, não cabeçalho Accept ou extensão (IE / api / usuários) não deve falhar, é necessário apenas fail-over para o padrão de resposta do tipo.
Mas o que sobre erros e outras mensagens de status importantes associados com os pedidos? Fácil, use códigos de status HTTP! Isto é muito e, acima de uma das minhas coisas favoritas sobre a criação de APIs RESTful. Ao usar códigos de status HTTP, você não precisa vir para cima com um erro de / para o seu esquema de sucesso API, que já foi feito para você. Por exemplo, se um consumidor para POSTS / api / usuários e pretende informar uma criação bem sucedida, basta enviar um código de status 201 (201 = Criado). Se falhasse, envie um 500 se ele falhou em sua extremidade (500 = erro interno do servidor), ou talvez um 400 se ferrou (400 = Solicitação inválida). Talvez estejam tentando postar contra um terminal API que não aceita mensagens ... enviar um 501 (não implementado). Talvez o seu servidor MySQL é para baixo, para que o seu API está temporariamente borked ... enviar um 503 (serviço indisponível). Felizmente, você começa a idéia. Se você gostaria de ler um pouco sobre os códigos de status, vê-los na wikipedia: Lista de códigos de status HTTP .
Eu estou esperando você ver todas as vantagens que você começa, aproveitando os conceitos de REST APIs para o seu. É realmente super-legal, e sua vergonha que não é mais amplamente falado na comunidade PHP (pelo menos tanto quanto eu posso dizer). Eu acho que isso é provavelmente devido à falta de uma boa documentação sobre como lidar com os pedidos que não estão GET ou POST, ou seja, PUT e DELETE. É certo que é um pateta pouco lidar com isso, mas certamente não é difícil. Eu também estou certo de que alguns dos quadros mais populares lá fora, provavelmente tem algum tipo de implementação de REST, mas eu não sou um fã enorme quadro (para uma série de razões que eu não vou entrar), e também é bom saber essas coisas, mesmo se alguém já criou a solução para você.
Se você ainda não está convencido de que este é um paradigma API útil, dê uma olhada no que resto tem feito para o Ruby on Rails. Uma de suas reivindicações principais para a fama é como é fácil criar APIs (através de algum tipo de voodoo RoR, tenho certeza), e com razão. Concedido eu sei muito pouco sobre RoR, mas os fanboys em torno do escritório de ter pregado este ponto para mim muitas vezes. Mas, eu discordo ... vamos escrever algum código!
Introdução ao REST e PHP
Um disclaimer últimos: o código que estamos prestes a passar por cima é de modo algum destinados a ser utilizados como um exemplo de uma solução robusta. Meu principal objetivo aqui é mostrar como lidar com os componentes individuais de REST no PHP, e deixar criar a solução final depende de você.
Então, vamos cavar! Acho que a melhor maneira de fazer algo prático é criar uma classe que irá fornecer todas as funções de utilidade que precisamos para criar uma API REST. Também vamos criar uma pequena classe para armazenar os dados. Você também pode pegar essa, estendê-la, e aplicá-lo às suas próprias necessidades. Então, vamos toco algumas coisas para fora:
RestUtils classe {processRequest public static function () {} SendResponse public static function ($ status = 200, $ corpo ='', 'text / html' $ content_type =) {} public static function getStatusCodeMessage ($ status) {/ / isso pode ser armazenado em um arquivo. ini e carregado / / via parse_ini_file () ... No entanto, esta será suficiente / / para um exemplo $ códigos = Array (100 => 'Continuar', 101 => 'Switching Protocols', 200 => 'OK', 201 => 'Criado', 202 => 'Aceito' , 203 => 'Informação não-autorizada', 204 => 'Conteúdo Não', 205 => 'Conteúdo Reset', 206 => 'Conteúdo parcial', 300 => 'Múltipla escolha', 301 => 'Movido permanentemente " , 302 => 'encontrado', 303 => 'Ver Outros', 304 => 'Não modificado', 305 => 'Usar Proxy', 306 => '(não utilizado)', 307 => 'Temporary Redirect', 400 => 'Bad Request ", 401 =>' não autorizado ', 402 =>' Pagamento obrigatório ', 403 =>' Proibido ', 404 =>' não encontrado ', 405 =>' Método não permitido ', 406 =>' Não Aceitável ', 407 =>' Autenticação de proxy necessária ", 408 => 'Pedido Timeout', 409 => 'Conflito', 410 => 'Gone', 411 => 'Comprimento Necessário', 412 => 'Pré-requisito Falha' , 413 => 'Entidade de solicitação muito grande', 414 => 'Pedido-URI Too Long', 415 => 'Tipo de Mídia não suportado', 416 => 'Intervalo solicitado não satisfatório', 417 => 'Expectation Failed', 500 => 'Internal Server Error ", 501 =>' não implementado", 502 => 'Bad Gateway ", 503 =>' Service Unavailable ', 504 =>' Gateway Timeout ', 505 =>' Versão HTTP não suportada") ; retorno (isset ($ códigos [$ status]))? $ Códigos [$ status]:'';}} class RestRequest {private $ request_vars; private $ dados; private $ HTTP_ACCEPT; método $ privado; public function __ construct () {$ this-> request_vars = array (); $ this- > dados =''; $ this-> HTTP_ACCEPT = (strpos ($ _SERVER ['HTTP_ACCEPT'], 'json'))? 'Json': 'xml'; $ this-> method = 'get';} public function setData ($ data) {$ this-> dados = $ dados;} setMethod função pública ($ method) {$ this-> método = método $;} setRequestVars função pública ($ request_vars) {$ this-> request_vars = $ request_vars;} getData função pública () {return $ this-> data;} getMethod função pública () {return $ this-> método; } função pública getHttpAccept () {return $ this-> HTTP_ACCEPT;} getRequestVars função pública () {return $ this-> request_vars;}}
OK, então o que temos é uma classe simples para armazenar algumas informações sobre o nosso pedido (RestRequest), e uma classe com algumas funções estáticas que podemos usar para lidar com os pedidos e respostas. Como você pode ver, nós realmente só tem duas funções para escrever ... o que é a beleza de tudo isso! Certo, vamos seguir em frente ...
O processamento do pedido
O processamento do pedido é bem simples, mas é aqui que pode funcionar em algumas capturas (ou seja, com PUT e DELETE ... principalmente PUT). Nós vamos passar por cima daqueles em um momento, mas vamos examinar a classe RestRequest um pouco. Se você olhar para o construtor, você vai ver que já estamos a interpretar o cabeçalho HTTP_ACCEPT, e falta de JSON se nenhum for fornecido. Com isso fora do caminho, nós só precisamos lidar com os dados recebidos.
Existem algumas maneiras nós poderíamos ir sobre fazer isto, mas vamos apenas supor que nós vamos sempre ter um par chave / valor em nosso pedido: 'dados' => dados reais. Vamos supor também que os dados reais serão JSON. Como referi na minha explicação anterior do REST, você pode olhar para o tipo de conteúdo do pedido e lidar com um JSON ou XML, mas vamos mantê-lo simples por agora. Então, a nossa função de pedido de processo vai acabar procurando algo parecido com isto:
processRequest public static function () { / / Obter o nosso verbo $ REQUEST_METHOD = strtolower ($ _SERVER ['REQUEST_METHOD']); $ Return_obj = new RestRequest (); / / Vamos armazenar os dados aqui $ Dados = array (); switch ($ REQUEST_METHOD) { / / Recebe são fáceis ... caso 'get': $ Data = $ _GET; quebrar; / / Por isso são mensagens 'post' caso: $ Data = $ _POST; quebrar; / / Aqui é a parte complicada ... caso 'put': / / Basicamente, lemos uma seqüência de localização do PHP entrada especial, / / E depois analisá-lo em uma matriz via parse_str ... por os documentos PHP: / / Analisa str como se fosse a seqüência de consulta passada via URL e define / / Variáveis no escopo atual. parse_str (file_get_contents (':/ php / input'), $ put_vars); $ Data = $ put_vars; quebrar; } / / Armazenar o método $ Return_obj-> setMethod ($ REQUEST_METHOD); / / Define os dados brutos, para que possamos acessá-lo, se necessário (pode haver Peças / / outros para os seus pedidos) $ Return_obj-> setRequestVars ($ dados); if (isset ($ dados ['data'])) { / / Traduzir o JSON a um objeto para uso como quiser $ Return_obj-> setData (json_decode ($ dados ['data'])); } return $ return_obj; }
Como eu disse, bem simples. Entretanto, algumas coisas a observar ... Primeiro, você normalmente não aceitar dados de pedidos DELETE, então não temos um caso para eles no switch. Segundo, você vai perceber que armazenar as variáveis de solicitação e os dados JSON analisados. Isso é útil porque você pode ter outras coisas como uma parte do seu pedido (por exemplo uma chave de API ou algo assim) que não é verdadeiramente os dados em si (como o nome de um novo usuário, e-mail, etc.)
Então, como poderíamos usar isso? Vamos voltar ao exemplo do usuário. Supondo que você tenha encaminhado o seu pedido para o controlador correto para os usuários, poderíamos ter algum código como este:
$ Dados = RestUtils :: processRequest (); interruptor ($ data-> getMethod) { caso 'get': / / Recuperar uma lista de usuários quebrar; 'post' caso: Usuário $ user = new (); $ User-> setFirstName ($ data-> getData () - first_name>) / / simplesmente por exemplo, isso deve ser feito mais limpo / / E assim por diante ... $ User-> save (); quebrar; / Etc /, etc, etc .. }
Por favor, não faça isso em um aplicativo real, este é apenas um exemplo rápido e rasteiro. Você gostaria de envolvê-lo em uma estrutura de controle legal com tudo abstraído corretamente, mas isso deve ajudá-lo a ter uma idéia de como usar este material. Mas estou divagando, vamos passar a enviar uma resposta.
Enviar a resposta
Agora que podemos interpretar o pedido, vamos passar a enviar a resposta. Nós já sabemos que tudo o que realmente precisa fazer é enviar o código de status correto, e talvez algum corpo (se este fosse um pedido GET, por exemplo), mas há uma captura importante para respostas que não têm corpo. Diga alguém fez um pedido contra a nossa API amostra usuário para um usuário que não existe (ou seja api/user/123). O código de status é apropriado para enviar um 404, neste caso, mas simplesmente enviando o código de status nos cabeçalhos não é suficiente. Se você viu essa página no seu navegador web, você poderia ter uma tela em branco. Isso ocorre porque o Apache (ou o que seu servidor web em um) não está enviando o código de status, então não há nenhuma página de status. Nós precisamos levar isso em conta quando nós construímos a nossa função. Manter tudo isso em mente, aqui está o que o código deve ser parecido:
SendResponse public static function ($ status = 200, $ corpo ='', 'text / html' $ content_type =) {status_header $ = 'HTTP/1.1'. $ Status. '. RestUtils :: getStatusCodeMessage ($ status); / / definir o cabeçalho de status ($ status_header) / / definir o cabeçalho tipo de conteúdo ('Content-type:'. $ Content_type) / / páginas com corpo são fáceis if ($ corpo ! ='') {/ / envia o corpo echo $ corpo; exit;} / / precisamos criar o corpo se não for aprovada else {/ / criar algumas mensagens do corpo $ mensagem =''; / / isso é puramente opcional , mas faz as páginas um pouco mais agradável de ler / / para seus usuários. Desde que você provavelmente não vai enviar um monte de códigos de status diferentes, / / isso também não deve ser muito pesado para manter o interruptor ($ status) {case 401: $ mensagem = '. Você deve ser autorizado a exibir esta página "; break; caso 404: $ mensagem = 'URL solicitada ". $ _SERVER ['REQUEST_URI']. 'Não foi encontrado.' Break; caso 500: $ mensagem = "O servidor encontrou um erro ao processar o pedido."; Break; caso 501: $ mensagem = '. O método solicitado não está implementado "; break;} / / servidores nem sempre têm uma assinatura ligado (esta é uma directiva apache "ServerSignature On") $ signature = ($ _SERVER ['SERVER_SIGNATURE'] =='')? $ _SERVER ['SERVER_SOFTWARE']. 'Servidor em'. $ _SERVER ['SERVER_NAME']. 'Porta'. $ _SERVER ['SERVER_PORT']: $ _SERVER ['SERVER_SIGNATURE'] / / isso deve ser templatized em uma solução do mundo real $ body = '<DOCTYPE HTML PUBLIC "- / / W3C / / DTD HTML 4.01 / / EN! "" http://www.w3.org/TR/html4/strict.dtd "> <html> <head> <meta http-equiv =" Content-Type "content =" text / html; charset = iso-8859 -1 "> title '. $ Status. '. RestUtils :: getStatusCodeMessage ($ status). '</ Title> </ head> <h1> <body>'. RestUtils :: getStatusCodeMessage ($ status). '</ H1> <p'. $ Mensagem. '</ P> <hr /> <endereço>'. $ Assinatura. '</ Endereço> </ body> </ html>'; echo $ corpo; exit;}}
Isso mesmo! Nós tecnicamente temos tudo que precisamos agora para processar os pedidos e enviar respostas. Vamos falar um pouco mais sobre por que precisamos ter uma resposta padrão de corpo ou um personalizado. Para solicitações GET, isso é bastante óbvio, é preciso enviar XML / JSON conteúdo em vez de uma página de status (desde que o pedido era válido). No entanto, há também POSTs para lidar com eles. Dentro de seus aplicativos, quando você cria uma nova entidade, provavelmente você buscar ID da nova entidade através de algo como mysql_insert_id (). Bem, se um usuário envia a sua API, que provavelmente vai querer que ID novo também. O que eu costumo fazer neste caso é simplesmente enviar a nova ID como o corpo (com um código de status 201), mas você também pode embrulhar que em XML ou JSON, se quiser.
Então, vamos estender a nossa implementação de exemplo um pouco:
switch ($ data-> getMethod) { / / Este é um pedido para todos os usuários, não uma em particular caso 'get': $ User_list = getUserList () / / assumir este retorna uma matriz if ($ data-> getHttpAccept == 'json') { RestUtils :: SendResponse (200, json_encode ($ user_list), "application / json '); } else if ($ data-> getHttpAccept == 'xml') { / / Usando o pacote XML_SERIALIZER Pear $ Options = array ( 'Travessão' => '", "AddDecl '=> false, "RootName '=> $ fc-> getAction (), XML_SERIALIZER_OPTION_RETURN_RESULT => true ); Serializador $ XML_Serializer = new ($ options); RestUtils :: SendResponse (200, $ serializador-> serialize ($ user_list), 'application / xml'); } quebrar; / / Criar novo usuário 'post' caso: Usuário $ user = new (); $ User-> setFirstName ($ data-> getData () - first_name>) / / simplesmente por exemplo, isso deve ser feito mais limpo / / E assim por diante ... $ User-> save (); / / Basta enviar a nova ID como o corpo RestUtils :: SendResponse (201, $ user-> getId ()); quebrar; }
Novamente, este é apenas um exemplo, mas não mostrar (eu acho, pelo menos) o quão pouco esforço é necessário para implementar coisas RESTful.
Resumindo
Então, é sobre isso. Estou bastante confiante de que eu tenho batido o ponto de que este deve ser muito fácil para o chão, então eu gostaria de fechar com a forma como você pode tirar essas coisas mais e talvez corretamente implementá-lo.
Em uma aplicação MVC no mundo real, o que você provavelmente quer fazer é configurar um controlador para o seu API que carrega controladores API individual. Por exemplo, usando o material acima, teremos, possivelmente, criar uma UserRestController que teve quatro métodos: get (), put (), post () e delete (). O controlador API iria olhar para o pedido e determinar o método a invocar na mesma controladora. Esse método, então, usar o utils para processar o pedido, fazer o que precisa para fazer dados sábio, então use o utils para enviar uma resposta.
Você também pode dar um passo adiante do que isso, e abstrair seu controlador API e modelos de dados um pouco mais. Ao invés de criar explicitamente um controlador para cada modelo de dados em seu aplicativo, você pode adicionar alguma lógica em seu controlador API para olhar primeiro para um controlador explicitamente definido, e se nada for encontrado, tente olhar para um modelo existente. Por exemplo, a url "api/user/1", que primeiro acionar uma pesquisa para um controlador de descanso "user". Se nada for encontrado, ele poderia, então, procurar um modelo chamado "usuário" em seu aplicativo. Se for encontrada, você poderia escrever um pouco de voodoo automatizado para processar automaticamente todos os pedidos contra os modelos.
Indo ainda mais longe, você poderia, então, fazer um genérico "lista-tudo" método que funciona de forma semelhante ao exemplo do parágrafo anterior. Diga o seu url era "API / usuários". O controlador API poderia verificar primeiro para um controlador de descanso "usuários" e, se nenhum foi encontrado, reconhecemos que os usuários se pluaralized, depluralize e, em seguida procurar um "usuário" do modelo. Se alguém for encontrado, carregar uma lista a lista de usuários e enviar o fora.
Finalmente, você pode adicionar a autenticação Digest para sua API muito facilmente também. Digamos que você só queria usuários devidamente autenticados para acessar seu API, bem, você poderia jogar algum código como este em sua funcionalidade pedido de processo (emprestado de um aplicativo existente da mina, por isso há algumas constantes e variáveis referenciadas que não estão definidos neste trecho ):
/ / Descobrir se precisamos desafiar o usuário if (empty ($ _SERVER ['PHP_AUTH_DIGEST'])) { header ("HTTP/1.1 401 Unauthorized"); header ('WWW-Authenticate: Digest realm = "".. AUTH_REALM "" ".. uniqid ()'", qop = "auth", nonce = ', opaco = "".. md5 (AUTH_REALM)' "') ; / / Mostrar o erro, se bater cancelar morrer (RestControllerLib :: erro (401, true)); } / / Agora, analayze o PHP_AUTH_DIGEST var if (($ dados = http_digest_parse ($ _SERVER ['PHP_AUTH_DIGEST'])) | |! auth_username $ = R $ dados ['username']) { / / Mostrar o erro devido ao mau auth morrer (RestUtils :: SendResponse (401)); } / / Até agora, tudo é bom, vamos agora marque a resposta um pouco mais ... $ A1 = md5 ($ data ['username'] ':' AUTH_REALM ':'.... $ Auth_pass); $ A2 = md5 ($ _SERVER ['REQUEST_METHOD'] ':'.. $ Dados ['uri']); $ Valid_response = md5 ($ A1. ':' $ Data ['nonce "].'.: '$ Data [' nc '].'.: '$ Data [' cnonce '].'.: '$ Dados. ['qop'] ':' $ A2);.. / / Última verificação .. if ($ data ['resposta']! = $ valid_response) { morrer (RestUtils :: SendResponse (401)); }
Coisa muito legal, hein? Com um pouco de código e uma lógica inteligente, você pode adicionar uma API REST totalmente funcional para seus aplicativos rapidamente. Eu não estou dizendo que a cheerlead o conceito seja, eu implementei este material em um dos meus quadros de pessoal em cerca de metade de um dia, e depois passou outro meio dia somando todos os tipos de magia legal para isso. Se você (o leitor) está interessado em ver minha implementação final, mande-me uma nota nos comentários e eu ficaria feliz em compartilhar com você! Além disso, se você tem todas as idéias legais que você gostaria de compartilhar, não se esqueça de deixar cair as nos comentários também ... se eu gosto bastante, eu mesmo deixar você autor convidado seu próprio artigo no assunto!
Até a próxima vez ...
UPDATE: O muito solicitado seguimento a este artigo foi publicado: Fazer solicitações RESTful em PHP
