Requêtes asynchrones avec PHP
Et bien si, c'est possible, et sans aucun service tiers. Le principe est assez simple :
Ouverture d'un socket vers le serveur.
Construction de la requête HTTP avec les bons en-têtes.
Envoi de la requête via le socket.
Fermeture du socket, laissant le serveur traiter la requête en arrière-plan sans que l'on attende la réponse.
Et c'est finalement très simple à mettre en place.
Inconvénient principal :
- On ne peut pas exploiter la réponse (en tous cas pas directement)
Service de requêtes asynchrone
<?php namespace App\Service\Async; /** * this service allows you to set up asynchronous requests */ class Fetch { public function __construct() { } /** * @param string $addr * @param array|null $data * * @return int * * Fetch with GET method */ public function get(string $addr, array | null $data = null): int { return $this->fetch($addr, 'GET', $data); } /** * @param string $addr * @param array|null $data * * @return int * * Fetch with POST method */ public function post(string $addr, array | null $data = null): int { return $this->fetch($addr, 'POST', $data); } /** * @param string $addr * @param array|null $data * * @return int * * Fetch with PUT method */ public function put(string $addr, array | null $data = null): int { return $this->fetch($addr, 'PUT', $data); } /** * @param string $addr * @param array|null $data * * @return int * * Fetch with PATCH method */ public function patch(string $addr, array | null $data = null): int { return $this->fetch($addr, 'PATCH', $data); } /** * @param string $addr * @param array|null $data * * @return int * * Fetch with DELETE method */ public function delete(string $addr, array | null $data = null): int { return $this->fetch($addr, 'DELETE', $data); } /** * @param string $addr * @param string $method * @param array|null $data * * @return int */ private function fetch(string $url, string $method, array | null $data = null): int { $postString = $data ? json_encode($data): ''; $urlData = parse_url($url); if (!isset($urlData['host']) || !isset($urlData['path'])) { error_log("URL invalide : " . $url); return 0; } $errorCode = ''; $errorMessage = ''; $scheme = $urlData['scheme'] ?? 'http'; $host = $urlData['host']; $path = $urlData['path'] ?: '/'; $port = $urlData['port'] ?? ($scheme === 'https' ? 443 : 8000); $transport = $scheme === 'https' ? 'ssl://' : ''; $fp = stream_socket_client("$transport$host:$port", $errorCode, $errorMessage, 30); if (!$fp) { error_log(sprintf('Impossible to open socket whith %s %s %s %s', $url, $transport, $host, $port)); return 0; } $out = sprintf( "%s %s HTTP/1.1\r\n" . "Host: %s\r\n" . "Content-Type: application/json\r\n" . "Content-Length: %d\r\n" . "Connection: Close\r\n\r\n" . "%s", strtoupper($method), $path, $host, strlen($postString), $postString ); fwrite($fp, $out); fclose($fp); return 1; } }
Exemple d'utilisation dans un controller symfony
On crée une route normale
#[Route('/async', name: 'async')] public function index(Fetch $fetch) { $t1 = $this->shortTask(); /** * Ici on appelle la route asynchrone qui se charge des traitements lourds */ $fetch->post('localhost:8000/async/treatment'); /** * La réponse est renvoyée même si le traitement lourd n'est pas fini */ return new JsonResponse([ 't1' => $t1, ]); }
On a deux fonctions. L'une s’exécute instantanément, l'autre met 10 secondes à retourner un résultat. C'est cette dernière que l'on appelle dans la route /async/treatment
de manière asynchrone.
private function shortTask(): int { return 6 * 3; } private function longTask(int $a): int { sleep(10); return $a * 3; }
Cette route se charge donc de récupérer la requête envoyée en asynchrone et de faire les traitements voulus.
/** * ROUTE ASYNCHRONE */ #[Route('/async/treatment', name: 'async_treatment', methods: ['POST'])] public function async(KernelInterface $kernel): JsonResponse { /** * Méthode très longue à traiter (voir ci-dessus) */ $t = $this->longTask(4); $data = ['total' => $t]; $path = $kernel->getProjectDir() . '/public/test.json'; file_put_contents($path, json_encode($data)); return new JsonResponse(null); }
En résumé
On se rend sur la route /async
et on affiche instantanément la réponse JSON. Mais grâce à $fetch->post('route_asynchrone')
on a lancé une requête asynchrone qui fait un long calcul. Et 10 secondes plus tard, notre fichier JSON est mis à jour. Pour envoyer un mail par exemple, cela reste une méthode très pratique.