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.