Como projetar pesquisa / filtragem RESTful?

Atualmente estou projetando e implementando uma API RESTful em PHP. No entanto, tenho sido malsucedido implementando meu design inicial.

GET /users # list of users GET /user/1 # get user with id 1 POST /user # create new user PUT /user/1 # modify user with id 1 DELETE /user/1 # delete user with id 1 

Até agora bastante normal, certo?

Meu problema é com o primeiro GET /users . Eu estava pensando em enviar parâmetros no corpo da solicitação para filtrar a lista. Isso porque desejo especificar filtros complexos sem obter uma URL super longa, como:

 GET /users?parameter1=value1&parameter2=value2&parameter3=value3&parameter4=value4 

Em vez disso eu queria ter algo como:

 GET /users # Request body: { "parameter1": "value1", "parameter2": "value2", "parameter3": "value3", "parameter4": "value4" } 

que é muito mais legível e oferece grandes possibilidades para definir filtros complexos.

De qualquer forma, file_get_contents('php://input') não retornou o corpo da requisição para requisições GET . Eu também tentei http_get_request_body() , mas a hospedagem compartilhada que estou usando não tem pecl_http . Não tenho certeza que teria ajudado de qualquer maneira.

Eu encontrei esta pergunta e percebi que GET provavelmente não deveria ter um corpo de solicitação. Foi um pouco inconclusivo, mas eles desaconselharam.

Então agora não tenho certeza do que fazer. Como você cria uma function de pesquisa / filtragem RESTful?

Eu suponho que eu poderia usar o POST , mas isso não parece muito RESTful.

A melhor maneira de implementar uma pesquisa RESTful é considerar a pesquisa como um recurso. Então você pode usar o verbo POST porque está criando uma pesquisa. Você não precisa literalmente criar algo em um database para usar um POST.

Por exemplo:

 Accept: application/json Content-Type: application/json POST http://example.com/people/searches { "terms": { "ssn": "123456789" }, "order": { ... }, ... } 

Você está criando uma pesquisa do ponto de vista do usuário. Os detalhes da implementação são irrelevantes. Algumas APIs RESTful podem nem precisar de persistência. Esse é um detalhe de implementação.

Se você usar o corpo da solicitação em uma solicitação GET, estará quebrando o princípio REST, porque sua solicitação GET não poderá ser armazenada em cache, porque o sistema de cache usa apenas a URL.

E o que é pior, seu URL não pode ser marcado, porque o URL não contém todas as informações necessárias para redirect o usuário para esta página

Use parâmetros de URL ou Consulta em vez de parâmetros do corpo da solicitação.

por exemplo:

 /myapp?var1=xxxx&var2=xxxx /myapp;var1=xxxx/resource;var2=xxxx 

Na verdade, o HTTP RFC 7231 diz que:

Uma carga útil em uma mensagem de solicitação GET não tem semântica definida; O envio de um corpo de carga útil em uma solicitação GET pode fazer com que algumas implementações existentes rejeitem a solicitação.

Para mais informações, dê uma olhada aqui

Parece que a filtragem / pesquisa de resources pode ser implementada de maneira RESTful. A idéia é introduzir um novo endpoint chamado /filters/ ou /api/filters/ .

Usar este filtro de ponto de extremidade pode ser considerado como um recurso e, portanto, criado pelo método POST . Desta forma – é claro – o corpo pode ser usado para carregar todos os parâmetros, assim como estruturas complexas de pesquisa / filtragem podem ser criadas.

Depois de criar esse filtro, há duas possibilidades para obter o resultado da pesquisa / filtro.

  1. Um novo recurso com ID exclusivo será retornado junto com o código de status 201 Created . Então, usando este ID, um pedido GET pode ser feito para /api/users/ like:

     GET /api/users/?filterId=1234-abcd 
  2. Após o novo filtro ser criado via POST ele não responderá com 201 Created mas de uma só vez com 303 SeeOther junto com o header Location apontando para /api/users/?filterId=1234-abcd . Esse redirecionamento será tratado automaticamente pela biblioteca subjacente.

Em ambos os cenários, duas solicitações precisam ser feitas para obter os resultados filtrados – isso pode ser considerado uma desvantagem, especialmente para aplicativos móveis. Para aplicativos móveis, eu usaria uma única chamada POST para /api/users/filter/ .

Como manter filtros criados?

Eles podem ser armazenados no database e usados ​​posteriormente. Eles também podem ser armazenados em algum armazenamento temporário, por exemplo, redis e ter algum TTL após o qual eles irão expirar e serão removidos.

Quais são as vantagens dessa ideia?

Os filtros e os resultados filtrados podem ser armazenados em cache e podem até ser marcados como favoritos.

Eu acho que você deve ir com parâmetros de solicitação, mas apenas enquanto não houver um header HTTP apropriado para realizar o que você deseja fazer. A especificação HTTP não diz explicitamente que GET não pode ter um corpo. No entanto este artigo declara:

Por convenção, quando o método GET é usado, todas as informações necessárias para identificar o recurso são codificadas no URI. Não há nenhuma convenção no HTTP / 1.1 para uma interação segura (por exemplo, recuperação) em que o cliente fornece dados para o servidor em um corpo de entidade HTTP em vez de na parte de consulta de um URI. Isso significa que, para operações seguras, os URIs podem ser longos.

Não se preocupe muito se sua API inicial é totalmente RESTful ou não (especialmente quando você está apenas nos estágios alfa). Faça o encanamento de back-end funcionar primeiro. Você sempre pode fazer algum tipo de transformação / reescrita de URL para mapear as coisas, refinando iterativamente até obter algo estável o suficiente para testes generalizados (“beta”).

Você pode definir URIs cujos parâmetros são codificados por posição e convenção nos próprios URIs, prefixados por um caminho que você sabe que sempre mapeará para alguma coisa. Eu não sei PHP, mas eu diria que tal recurso existe (como existe em outras linguagens com frameworks web):

.ie Faça um tipo de pesquisa “usuário” com param [i] = valor [i] para i = 1..4 na loja # 1 (com valor1, valor2, valor3, … como um atalho para os parâmetros de consulta URI):

 1) GET /store1/search/user/value1,value2,value3,value4 

ou

 2) GET /store1/search/user,value1,value2,value3,value4 

ou como segue (embora eu não recomendaria, mais sobre isso mais tarde)

 3) GET /search/store1,user,value1,value2,value3,value4 

Com a opção 1, você mapeia todos os URIs prefixados com /store1/search/user para o manipulador de pesquisa (ou seja, qual for a designação do PHP) padronizando as buscas por resources em store1 (equivalente a /search?location=store1&type=user .

Por convenção documentada e aplicada pela API, os valores de parâmetros de 1 a 4 são separados por vírgulas e apresentados nessa ordem.

A opção 2 adiciona o tipo de pesquisa (neste caso, user ) como o parâmetro posicional # 1. Qualquer opção é apenas uma escolha cosmética.

A opção 3 também é possível, mas acho que não gostaria disso. Eu acho que a capacidade de pesquisa dentro de certos resources deve ser apresentada na própria URI antes da busca em si (como se estivesse indicando claramente no URI que a busca é específica dentro do recurso).

A vantagem disso sobre passar parâmetros no URI é que a busca faz parte do URI (tratando assim uma busca como um recurso, um recurso cujo conteúdo pode – e irá – mudar com o tempo). A desvantagem é que a ordem dos parâmetros é obrigatória. .

Depois de fazer algo assim, você pode usar GET, e seria um recurso somente leitura (desde que você não pode POST ou PUT para ele – ele é atualizado quando é GET’ed). Também seria um recurso que só existe quando é invocado.

Também é possível adicionar mais semânticas ao armazenar em cache os resultados por um período de tempo ou com um DELETE fazendo com que o cache seja excluído. Isso, no entanto, pode ser contrário ao que as pessoas normalmente usam DELETE (e porque as pessoas geralmente controlam o armazenamento em cache com headers de armazenamento em cache).

Como você iria, seria uma decisão de design, mas seria assim que eu iria. Não é perfeito, e tenho certeza de que haverá casos em que isso não é a melhor coisa a fazer (especialmente para critérios de pesquisa muito complexos).

Como estou usando um backend laravel / php, eu costumo seguir algo assim:

 /resource?filters[status_id]=1&filters[city]=Sydney&page=2&include=relatedResource 

O PHP automaticamente transforma [] params em um array, então neste exemplo eu vou acabar com uma variável de $filter que contém um array / object de filtros, junto com uma página e quaisquer resources relacionados que eu quiser carregar.

Se você usar outro idioma, isso ainda pode ser uma boa convenção e você pode criar um analisador para converter [] em um array.

FYI: Eu sei que isso é um pouco tarde, mas para quem está interessado. Depende de como RESTful você quer ser, você terá que implementar suas próprias estratégias de filtragem, pois a especificação HTTP não é muito clara sobre isso. Eu gostaria de sugerir codificação de URL para todos os parâmetros de filtro, por exemplo

 GET api/users?filter=param1%3Dvalue1%26param2%3Dvalue2 

Eu sei que é feio, mas eu acho que é a maneira mais RESTful de fazê-lo e deve ser fácil de analisar no lado do servidor 🙂