Vue d'ensemble de l'architecture

OxPHP est un serveur HTTP mono-binaire qui remplace la traditionnelle pile nginx + PHP-FPM. Il prend en charge l'analyse HTTP, la terminaison TLS, le routage, l'exécution PHP, la compression et l'observabilité dans un seul processus, sans aucune dépendance externe à l'exécution.

Fonctionnement d'OxPHP

OxPHP combine deux couches d'exécution dans un même processus :

  1. Couche HTTP asynchrone. Une couche réseau événementielle accepte les connexions TCP, effectue les handshakes TLS, analyse les requêtes HTTP et envoie les réponses. Elle gère des milliers de connexions concurrentes grâce à des E/S non bloquantes, si bien qu'un client lent n'en bloque jamais un autre.
  2. Pool de workers PHP. Un pool de workers PHP dédiés exécute vos scripts PHP. En mode standard, chaque worker traite une requête à la fois. En mode worker avec le multiplexage par Fibers activé, un seul worker peut servir plusieurs requêtes concurrentes : lorsqu'un script appelle oxphp_sleep() ou oxphp_async_await(), la Fiber cède le thread et le worker passe à la requête suivante.
  3. Pool asynchrone (optionnel). Des threads OS distincts pour les tâches soumises via oxphp_async(). Activé en définissant ASYNC_WORKERS > 0. Isolé du pool de workers pour que les tâches d'arrière-plan ne bloquent pas le traitement des requêtes HTTP.

Les deux couches communiquent au travers d'une file bornée. Lorsqu'une requête HTTP nécessitant une exécution PHP arrive, la couche asynchrone la place dans la file. Un worker PHP disponible la récupère, exécute le script et renvoie la réponse à la couche asynchrone pour livraison au client.

Grâce à cette séparation, les E/S réseau (acceptation des connexions, lecture des en-têtes, compression des réponses, service des fichiers statiques) n'entrent jamais en concurrence avec l'exécution PHP pour les ressources. Chaque couche monte en charge indépendamment.

Pool de workers

Le pool de workers PHP détermine combien de scripts PHP peuvent s'exécuter simultanément. OxPHP prend en charge deux modes de pool.

Pool statique

Un nombre fixe de workers démarrent au boot et restent actifs pendant toute la durée de vie du serveur. C'est le mode par défaut.

bash
PHP_WORKERS=8 # exactly 8 workers PHP_WORKERS=0 # auto-detect (default): half of available CPU cores, minimum 1

Pool dynamique

Les workers montent et descendent en charge selon la demande. Indiquez un nombre minimal et maximal séparés par deux-points :

bash
PHP_WORKERS=2:16 # start with 2, scale up to 16 under load

Lorsque tous les workers courants sont occupés, OxPHP démarre de nouveaux workers jusqu'au maximum. Lorsqu'un worker est resté inactif plus longtemps que PHP_WORKERS_IDLE_SECONDS (par défaut : 30 secondes), il est retiré pour revenir vers le minimum.

File et contre-pression

Entre la couche HTTP asynchrone et le pool de workers se trouve une file bornée. Sa capacité vaut par défaut le nombre initial de workers multiplié par 128 et peut être remplacée par QUEUE_CAPACITY. Pour un pool statique, le nombre initial est le nombre de workers configuré. Pour un pool dynamique (MIN:MAX), le nombre initial est le minimum.

Lorsque la file est pleine (tous les workers sont occupés et la file a atteint sa capacité), OxPHP renvoie immédiatement au client une réponse 529 Site is Overloaded accompagnée d'un en-tête Retry-After. Le code de statut 529 (non standard, utilisé par Cloudflare et d'autres) distingue clairement la surcharge des erreurs applicatives (500) et de la maintenance (503), ce qui facilite la configuration des alertes et des répartiteurs de charge.

Flux des requêtes

Chaque requête traverse le même pipeline, qu'elle serve un fichier statique ou exécute du PHP :

graph TD
  Client(["Client"]) --> TLS["TLS termination<br/>(if configured)"]
  TLS --> Parse["HTTP parsing + Request ID"]
  Parse --> Proxy["Trusted proxy resolution<br/>(if TRUSTED_PROXIES set)"]
  Proxy --> Rate["Rate limiting check"]
  Rate --> Route{"Route resolution"}
  Route -->|Static file| Cache["File cache / disk read"]
  Cache --> Compress["Compression + Response headers"]
  Route -->|PHP request| Queue["Bounded queue<br/>(529 if full)"]
  Queue --> Worker["PHP worker executes script"]
  Worker --> Normal["Normal response"]
  Worker --> SSE["SSE streaming (chunked)"]
  Worker --> Early["Early response (finish_request)<br/>+ background work"]
  Compress --> Deliver(["Response to client"])
  Normal --> Deliver
  SSE --> Deliver
  Early --> Deliver
  1. Terminaison TLS. Si TLS_CERT et TLS_KEY sont configurés, OxPHP gère TLS directement. Aucun reverse proxy distinct n'est nécessaire.
  2. Analyse HTTP et ID de requête. La requête est analysée et un ID de requête unique est généré (ou un en-tête X-Request-ID entrant est préservé).
  3. Résolution des proxys de confiance. Si TRUSTED_PROXIES est défini et que l'IP de connexion est de confiance, OxPHP extrait l'IP réelle du client, le protocole et l'hôte à partir des en-têtes Forwarded (RFC 7239) ou X-Forwarded-*. L'IP résolue est utilisée pour toutes les étapes suivantes, y compris la limitation de débit et la journalisation des accès. Voir Proxys de confiance.
  4. Limitation de débit. Si RATE_LIMIT est défini, l'IP du client est vérifiée par rapport au compteur de requêtes par IP. Les requêtes qui dépassent la limite reçoivent immédiatement une réponse 429 Too Many Requests.
  5. Résolution de route. L'URL est mise en correspondance avec le mode de routage configuré (traditionnel, framework ou SPA). Le résultat est soit un fichier statique, soit un script PHP, soit une 404. Le mode worker, s'il est activé, change la façon dont PHP exécute le script résolu, mais ne modifie pas la résolution de route elle-même.
  6. Fichiers statiques. Servis directement depuis un cache en mémoire (pour les fichiers fréquemment consultés) ou diffusés depuis le disque. OxPHP ajoute automatiquement les en-têtes ETag, Last-Modified et Cache-Control.
  7. Exécution PHP. La requête est placée dans la file bornée et récupérée par un worker disponible. Si la file est pleine, le client reçoit immédiatement une 529.
  8. Compression. Les réponses textuelles sont compressées avec Brotli avant d'être envoyées lorsque le client envoie Accept-Encoding: br (configurable via COMPRESSION_LEVEL).
  9. Streaming SSE. Si le script définit Content-Type: text/event-stream ou appelle oxphp_stream_flush(), OxPHP bascule en mode streaming : chaque appel à flush() envoie immédiatement un fragment au client sans mettre en tampon l'intégralité de la réponse. En mode worker, le SSE fonctionne de façon coopérative avec le multiplexage par Fibers.
  10. Réponse anticipée. L'appel à oxphp_finish_request() envoie immédiatement la réponse HTTP au client. Le script continue de s'exécuter en arrière-plan (écriture de journaux, mise à jour de caches, envoi de notifications) sans maintenir la connexion ouverte.
  11. Livraison de la réponse. La réponse achevée est renvoyée sur la connexion et, si la journalisation des accès est activée, une entrée de journal est écrite.

Mode worker vs mode standard

OxPHP prend en charge deux modèles d'exécution PHP :

Mode standard (par défaut)

Crée un environnement PHP neuf pour chaque requête. Les autoloaders, la configuration et les connexions à la base de données sont initialisés à chaque requête puis démantelés ensuite. Ce modèle est compatible d'emblée avec toutes les applications PHP.

Mode worker

Maintient les processus PHP en vie d'une requête à l'autre. Votre application effectue son bootstrap une seule fois (chargement de l'autoloader, de la configuration et établissement des connexions à la base de données), puis entre dans une boucle de requêtes. Entre les requêtes, OxPHP réinitialise automatiquement les superglobales, les tampons de sortie et les en-têtes de réponse tout en préservant l'état amorcé.

Le mode worker élimine la surcharge de démarrage propre à chaque requête, ce qui peut réduire sensiblement les temps de réponse pour les applications basées sur un framework (Laravel, Symfony, etc.) où le bootstrap est coûteux.

Pour activer le mode worker, définissez WORKER_MODE_ENABLED=true et faites pointer ENTRY_FILE vers un script PHP qui appelle oxphp_worker() :

php
<?php require __DIR__ . '/../vendor/autoload.php'; $app = new MyApp\Application(); oxphp_worker(function () use ($app) { $app->handle(); });

Pour un guide détaillé, voir Mode worker.

Serveur interne

Si la variable INTERNAL_ADDR est définie, OxPHP démarre un serveur HTTP distinct sur le port spécifié. Il sert trois endpoints :

Endpoint Description
GET /health État de santé en JSON (uptime, compteurs de requêtes, connexions, état des workers). Renvoie 200 en fonctionnement normal, 503 en cas de dégradation.
GET /metrics Métriques au format Prometheus — compteurs de requêtes, temps de réponse, temps d'attente en file, statistiques des workers, gains de compression.
GET /config Instantané de la configuration active en JSON. Les chemins des fichiers TLS sont masqués.

Le serveur interne ne passe pas par le pool de workers PHP ni par la file bornée. Il répond directement depuis la couche HTTP asynchrone, ce qui le maintient accessible même lorsque le pool PHP est totalement chargé. Cela rend /health adapté aux sondes de liveness/readiness de Kubernetes.

Pour plus de détails, voir Serveur interne.

Sûreté

OxPHP fournit plusieurs garanties pour maintenir votre application en fonctionnement fiable en production :

  • Isolation des requêtes. Si un script PHP plante ou déclenche une erreur fatale, seule cette requête est affectée. Le serveur continue de traiter normalement toutes les autres requêtes. Le worker qui a planté est automatiquement remplacé par un worker neuf.
  • Redémarrage automatique des workers. OxPHP surveille la santé de tous les workers PHP. Si un worker meurt de façon inattendue, un nouveau worker est démarré à sa place sans intervention manuelle.
  • Protection par contre-pression. La file de requêtes bornée prévient la surcharge. Lorsque le serveur est à pleine capacité, les nouvelles requêtes reçoivent une réponse 529 assortie d'un en-tête Retry-After plutôt que d'être mises en file indéfiniment et de provoquer des délais d'expiration en cascade.
  • Protection contre la traversée de répertoires. Tous les chemins d'URL sont assainis avant tout accès au système de fichiers. Les tentatives de traversée encodées en pourcentage, les segments .. et les chemins qui s'échappent de la racine du document sont bloqués.
  • Arrêt gracieux. À la réception de SIGTERM ou SIGINT (Ctrl+C), OxPHP cesse d'accepter de nouvelles connexions et attend l'achèvement des requêtes en cours (jusqu'à un délai de drainage configurable) avant de se terminer.

Voir aussi