PHP
PHP é provavelmente a linguagem que mais rápido adota métodos HTTP custom — curl aceita qualquer string em CURLOPT_CUSTOMREQUEST, Guzzle delega pra PSR-7, e os frameworks já suportam routing flexível. Aqui vai tudo que você precisa pra colocar QUERY em produção hoje.
PHP Nativo (curl)
Seção intitulada “PHP Nativo (curl)”A forma mais direta. CURLOPT_CUSTOMREQUEST define o método, e CURLOPT_POSTFIELDS carrega o body — exatamente como você já faz com DELETE ou PATCH customizados.
<?php
$payload = json_encode([ 'filter' => [ 'cidade' => 'São Paulo', 'ativo' => true, ], 'limit' => 25, 'offset' => 0,]);
$ch = curl_init('https://api.exemplo.com.br/contatos');
curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'QUERY', CURLOPT_POSTFIELDS => $payload, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Accept: application/json', 'Content-Length: ' . strlen($payload), ],]);
$response = curl_exec($ch);$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);curl_close($ch);
$data = json_decode($response, true);Por que funciona
Seção intitulada “Por que funciona”CURLOPT_CUSTOMREQUEST só troca a string do método na requisição — o libcurl não valida se é um método “oficial”. CURLOPT_POSTFIELDS envia o body independente do método. Esse combo funciona em qualquer PHP 7.4+.
Gotcha: Não use
CURLOPT_POST => truejunto comCURLOPT_CUSTOMREQUEST. OCURLOPT_POSTforça o método pra POST internamente e pode causar conflito. Defina apenasCURLOPT_CUSTOMREQUESTeCURLOPT_POSTFIELDS.
Guzzle aceita qualquer método HTTP no primeiro argumento de $client->request(). Sem gambiarras.
<?php
use GuzzleHttp\Client;
$client = new Client([ 'base_uri' => 'https://api.exemplo.com.br', 'timeout' => 10.0,]);
$response = $client->request('QUERY', '/contatos', [ 'json' => [ 'filter' => [ 'cidade' => 'Curitiba', 'cargo' => 'desenvolvedor', ], 'limit' => 50, ],]);
$status = $response->getStatusCode(); // 200$body = json_decode($response->getBody(), true);Com tratamento de erros
Seção intitulada “Com tratamento de erros”<?php
use GuzzleHttp\Client;use GuzzleHttp\Exception\ClientException;use GuzzleHttp\Exception\ServerException;
$client = new Client(['base_uri' => 'https://api.exemplo.com.br']);
try { $response = $client->request('QUERY', '/produtos', [ 'json' => [ 'filter' => ['categoria' => 'eletrônicos', 'preco_max' => 5000], 'sort' => ['preco' => 'asc'], 'limit' => 20, ], 'headers' => [ 'Accept' => 'application/json', ], ]);
$produtos = json_decode($response->getBody(), true);
} catch (ClientException $e) { // 4xx — requisição inválida (ex: Content-Type errado) $error = json_decode($e->getResponse()->getBody(), true);
} catch (ServerException $e) { // 5xx — problema no servidor error_log('API falhou: ' . $e->getMessage());}Nota: A opção
'json'do Guzzle já defineContent-Type: application/jsonautomaticamente. Não precisa setar na mão.
Laravel
Seção intitulada “Laravel”Laravel não tem helper nativo pra QUERY (tipo Route::get ou Route::post), mas Route::match() resolve em uma linha.
Definindo a rota
Seção intitulada “Definindo a rota”use App\Http\Controllers\ContatoController;use Illuminate\Support\Facades\Route;
Route::match(['QUERY'], '/contatos', [ContatoController::class, 'search']);Controller
Seção intitulada “Controller”<?php
namespace App\Http\Controllers;
use App\Models\Contato;use Illuminate\Http\JsonResponse;use Illuminate\Http\Request;
class ContatoController extends Controller{ public function search(Request $request): JsonResponse { // O body do QUERY vem pelo content — não por query string $filtros = $request->json()->all();
$query = Contato::query();
if ($cidade = data_get($filtros, 'filter.cidade')) { $query->where('cidade', $cidade); }
if (data_get($filtros, 'filter.ativo') !== null) { $query->where('ativo', data_get($filtros, 'filter.ativo')); }
$limit = data_get($filtros, 'limit', 25); $offset = data_get($filtros, 'offset', 0);
$contatos = $query->limit($limit)->offset($offset)->get();
return response()->json([ 'data' => $contatos, 'total' => $query->count(), ]); }}Middleware: validar Content-Type
Seção intitulada “Middleware: validar Content-Type”A RFC exige rejeitar requisições QUERY sem Content-Type. Um middleware simples resolve:
<?php
namespace App\Http\Middleware;
use Closure;use Illuminate\Http\Request;
class ValidateQueryContentType{ public function handle(Request $request, Closure $next) { if ($request->method() === 'QUERY' && !$request->hasHeader('Content-Type')) { return response()->json([ 'error' => 'Content-Type header é obrigatório para QUERY', ], 415); // Unsupported Media Type }
return $next($request); }}Dica: No Laravel 11+, registre o middleware direto no
bootstrap/app.php. Em versões anteriores, use o$routeMiddlewareno Kernel.
Symfony
Seção intitulada “Symfony”Symfony suporta métodos custom no atributo #[Route] via o parâmetro methods.
Controller
Seção intitulada “Controller”<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;use Symfony\Component\HttpFoundation\JsonResponse;use Symfony\Component\HttpFoundation\Request;use Symfony\Component\Routing\Attribute\Route;
class ProdutoController extends AbstractController{ #[Route('/api/produtos', name: 'produto_search', methods: ['QUERY'])] public function search(Request $request): JsonResponse { $filtros = json_decode($request->getContent(), true);
if (!$request->headers->has('Content-Type')) { return $this->json(['error' => 'Content-Type obrigatório'], 415); }
// Lógica de busca com os filtros... $categoria = $filtros['filter']['categoria'] ?? null; $precoMax = $filtros['filter']['preco_max'] ?? null;
$produtos = $this->buscarProdutos($categoria, $precoMax);
return $this->json([ 'data' => $produtos, 'meta' => [ 'total' => count($produtos), 'limit' => $filtros['limit'] ?? 25, ], ]); }}Configuração YAML (alternativa)
Seção intitulada “Configuração YAML (alternativa)”produto_search: path: /api/produtos controller: App\Controller\ProdutoController::search methods: [QUERY]Nota: O Symfony não filtra métodos HTTP por uma lista interna — qualquer string passa. O método chega no
$request->getMethod()exatamente como foi enviado.
PSR-7 (ServerRequest)
Seção intitulada “PSR-7 (ServerRequest)”Se você usa um framework PSR-7 (Slim, Mezzio, etc.) ou constrói requests manualmente pra testes, o método é tratado como string sem validação.
Criando uma ServerRequest QUERY
Seção intitulada “Criando uma ServerRequest QUERY”<?php
use GuzzleHttp\Psr7\ServerRequest;use GuzzleHttp\Psr7\Utils;
$body = json_encode([ 'filter' => ['status' => 'pendente'], 'limit' => 10,]);
$request = new ServerRequest('QUERY', 'https://api.exemplo.com.br/pedidos');$request = $request ->withHeader('Content-Type', 'application/json') ->withBody(Utils::streamFor($body));
// No handler:echo $request->getMethod(); // "QUERY"$filtros = json_decode((string) $request->getBody(), true);Slim Framework — roteamento
Seção intitulada “Slim Framework — roteamento”<?php
use Psr\Http\Message\ResponseInterface as Response;use Psr\Http\Message\ServerRequestInterface as Request;use Slim\Factory\AppFactory;
$app = AppFactory::create();
$app->map(['QUERY'], '/pedidos', function (Request $request, Response $response) { $filtros = json_decode((string) $request->getBody(), true);
// Processa a busca... $resultado = buscarPedidos($filtros);
$response->getBody()->write(json_encode($resultado)); return $response->withHeader('Content-Type', 'application/json');});
$app->run();Detectando QUERY no Servidor
Seção intitulada “Detectando QUERY no Servidor”Nem todo servidor PHP vai entregar o body de um método custom sem configuração. Aqui está como garantir:
<?php
// Lê o body independente do método — funciona em PHP-FPM, Apache mod_php, etc.$method = $_SERVER['REQUEST_METHOD']; // "QUERY"$body = file_get_contents('php://input');$data = json_decode($body, true);
if ($method === 'QUERY') { if (empty($_SERVER['CONTENT_TYPE'])) { http_response_code(415); echo json_encode(['error' => 'Content-Type obrigatório']); exit; }
// Processa a consulta... header('Content-Type: application/json'); echo json_encode(processarConsulta($data));}Apache: Se estiver com
mod_security, pode precisar adicionarQUERYà lista de métodos permitidos. Nginx não restringe métodos por padrão.
HTTP Client do Laravel (wrapper do Guzzle)
Seção intitulada “HTTP Client do Laravel (wrapper do Guzzle)”Se você já usa o Http facade do Laravel pra chamadas externas:
<?php
use Illuminate\Support\Facades\Http;
$response = Http::send('QUERY', 'https://api.exemplo.com.br/contatos', [ 'json' => [ 'filter' => ['ativo' => true, 'cidade' => 'Belo Horizonte'], 'limit' => 30, ],]);
if ($response->successful()) { $contatos = $response->json('data');}O método Http::send() aceita qualquer verbo — o Guzzle por baixo não valida.
Testes com PHPUnit
Seção intitulada “Testes com PHPUnit”<?php
namespace Tests\Feature;
use Tests\TestCase;
class ContatoQueryTest extends TestCase{ public function test_query_retorna_contatos_filtrados(): void { $response = $this->json('QUERY', '/api/contatos', [ 'filter' => ['cidade' => 'São Paulo', 'ativo' => true], 'limit' => 10, ]);
$response->assertStatus(200) ->assertJsonStructure(['data', 'total']) ->assertJsonCount(10, 'data'); }
public function test_query_sem_content_type_retorna_415(): void { $response = $this->call('QUERY', '/api/contatos', [], [], [], [ 'CONTENT_TYPE' => '', ], '{"filter":{}}');
$response->assertStatus(415); }}| Abordagem | Como usar QUERY |
|---|---|
| PHP nativo (curl) | CURLOPT_CUSTOMREQUEST => 'QUERY' + CURLOPT_POSTFIELDS |
| Guzzle | $client->request('QUERY', $uri, ['json' => ...]) |
| Laravel (rota) | Route::match(['QUERY'], ...) |
| Laravel (client) | Http::send('QUERY', $url, ['json' => ...]) |
| Symfony | #[Route('/path', methods: ['QUERY'])] |
| PSR-7 / Slim | $app->map(['QUERY'], ...) ou new ServerRequest('QUERY', ...) |
| Servidor | $_SERVER['REQUEST_METHOD'] + php://input |