Pular para o conteúdo

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.


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);

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 => true junto com CURLOPT_CUSTOMREQUEST. O CURLOPT_POST força o método pra POST internamente e pode causar conflito. Defina apenas CURLOPT_CUSTOMREQUEST e CURLOPT_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);
<?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á define Content-Type: application/json automaticamente. Não precisa setar na mão.


Laravel não tem helper nativo pra QUERY (tipo Route::get ou Route::post), mas Route::match() resolve em uma linha.

routes/api.php
use App\Http\Controllers\ContatoController;
use Illuminate\Support\Facades\Route;
Route::match(['QUERY'], '/contatos', [ContatoController::class, 'search']);
<?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(),
]);
}
}

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 $routeMiddleware no Kernel.


Symfony suporta métodos custom no atributo #[Route] via o parâmetro methods.

<?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,
],
]);
}
}
config/routes.yaml
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.


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.

<?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);
<?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();

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 adicionar QUERY à lista de métodos permitidos. Nginx não restringe métodos por padrão.


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.


<?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