PHPackages                             phpdot/server-sapi - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. phpdot/server-sapi

ActiveLibrary[Framework](/categories/framework)

phpdot/server-sapi
==================

Classic-SAPI runtime adapter for PSR-15 (PHP-FPM, Apache mod\_php, php -S). Framework-agnostic.

00PHP

Since Jun 15Pushed 1mo agoCompare

[ Source](https://github.com/phpdot/server-sapi)[ Packagist](https://packagist.org/packages/phpdot/server-sapi)[ RSS](/packages/phpdot-server-sapi/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependenciesVersions (1)Used By (0)

phpdot/server-sapi
==================

[](#phpdotserver-sapi)

Classic-SAPI runtime adapter for **PSR-15** — the counterpart to [`phpdot/server-swoole`](https://github.com/phpdot/server-swoole) for **PHP-FPM**, **Apache `mod_php`**, and **`php -S`**. It captures the PHP request environment as a PSR-7 `ServerRequestInterface`, runs it through your PSR-15 `RequestHandlerInterface`, and emits the PSR-7 `ResponseInterface` back to the SAPI **correctly**. It's runtime glue — nothing more.

Request capture is built **in-house** from your own injected PSR-17 factories, so the only dependencies are **interface-only packages** (PSR + `phpdot/contracts`) — no third-party server-request creator to drag in. Bring any PSR-7/PSR-17 implementation (the framework uses [`phpdot/http`](https://github.com/phpdot/http)); the concrete objects are whatever your factories produce. PHP 8.3+.

Install
-------

[](#install)

```
composer require phpdot/server-sapi
```

RequirementVersionNotesPHP&gt;= 8.3psr/http-message^2.0PSR-7 interfacespsr/http-factory^1.0PSR-17 factory interfacespsr/http-server-handler^1.0PSR-15 `RequestHandlerInterface`phpdot/contracts^1.8shared `ServerInterface` (interface-only)A PSR-7/PSR-17 implementation—e.g. `phpdot/http`; supplies the concrete objects via your factoriesQuick Start
-----------

[](#quick-start)

A standalone front controller is a handful of lines:

```
use PHPdot\Http\ResponseFactory;        // any PSR-7 / PSR-17 implementation
use PHPdot\Server\Sapi\RequestFactory;
use PHPdot\Server\Sapi\SapiEmitter;
use PHPdot\Server\Sapi\SapiServer;

$http = new ResponseFactory();          // phpdot/http satisfies all five PSR-17 factories

$server = new SapiServer(
    new RequestFactory($http, $http, $http, $http),
    new SapiEmitter(),
);

// $handler is any PSR-15 RequestHandlerInterface — your router / middleware pipeline.
$server->serve($handler);
```

Inside the phpdot framework you never wire this by hand — every class autowires, so the front controller collapses to `$container->get(SapiServer::class)->serve($router)`. See [DI Wiring](#di-wiring).

Why This Package
----------------

[](#why-this-package)

- **Interface-only dependencies.** Beyond the PSR interface packages and `phpdot/contracts` (pure interfaces too), there is nothing. `fromGlobals()` is built in-house (~one focused class), so the package is genuinely usable standalone and never forces a specific PSR-7 implementation on you.
- **Correct emission.** A `headers_sent()` guard runs *first* and fails fast — you get an exception with the offending `file:line`, never a half-written body. Repeated `Set-Cookie` headers survive, the body streams in fixed-size chunks, and there is no body for `204`, `304`, or `HEAD`.
- **The fiddly corner is isolated.** PHP's transposed nested `$_FILES` shape (`name[a][b][]`) lives behind `UploadedFileNormalizer` with its own tests — the one historically error-prone part of request capture, contained.
- **Deterministically testable.** `fromArrays()` and injectable sinks mean every branch is exercised without touching a superglobal or calling native `header()`.
- **Swappable runtime.** `SapiServer` implements the shared `ServerInterface` (from `phpdot/contracts`) — the same interface `phpdot/server-swoole`'s `SwooleServer`implements — so the same app moves between SAPI and Swoole by changing one binding.
- **Framework-agnostic.** The `#[Singleton]`/`#[Binds]` attributes are inert at runtime; `phpdot/container` is a dev-only dependency.
- **Strict.** `declare(strict_types=1)` everywhere, `final` classes, PHPStan **level 10** with strict rules, zero ignored errors.

Architecture
------------

[](#architecture)

```
src/
  SapiServer.php               #[Singleton] #[Binds(ServerInterface)] — inject this; serve(handler) is the API
  RequestFactory.php           #[Singleton] #[Binds(RequestFactoryInterface)] — environment → PSR-7
  SapiEmitter.php              #[Singleton] #[Binds(EmitterInterface)] — PSR-7 → SAPI
  UploadedFileNormalizer.php   flattens PHP's nested $_FILES into PSR-7 uploaded files
  Contract/
    RequestFactoryInterface.php   capture swap point (fromGlobals)
    EmitterInterface.php          emission swap point (emit)
  Exception/
    ServerSapiException.php         base exception
    EmitterException.php           output already started

```

`SapiServer` implements `PHPdot\Contracts\Server\ServerInterface::serve()`, the contract shared with `phpdot/server-swoole`.

Flow — one request, start to finish:

```
$_SERVER · $_GET · $_POST · $_COOKIE · $_FILES · php://input
      │
      ▼  RequestFactory::fromGlobals()
ServerRequestInterface ─► PSR-15 RequestHandlerInterface (your app)
                                  │
                                  ▼  ResponseInterface
                          SapiEmitter::emit()
                                  │
                                  ▼
                   status line · headers · body ─► client

```

`SapiServer` ties the three together and wraps them in a last-resort `catch` that emits a minimal raw `500` so a misconfiguration never white-screens. It does not render or log — real error handling belongs in your middleware or `phpdot/error-handler`. The catch is a floor, not a feature.

API Reference
-------------

[](#api-reference)

### SapiServer · implements ServerInterface (inject this)

[](#sapiserver--implements-serverinterface-inject-this)

`#[Singleton]` `#[Binds(ServerInterface::class)]`

MethodReturnsNotes`__construct(RequestFactoryInterface $requests, EmitterInterface $emitter, ?callable $statusSink = null, ?callable $outputSink = null, ?callable $headersSentProbe = null)`—The sinks/probe override the floor's native `http_response_code()`/`echo`/`headers_sent()` — for tests; production leaves them null`serve(RequestHandlerInterface $handler)``void``ServerInterface::serve` — `fromGlobals()` → `handle()` → `emit()`, with a last-resort raw `500` floor### RequestFactory · implements RequestFactoryInterface

[](#requestfactory--implements-requestfactoryinterface)

`#[Singleton]` `#[Binds(RequestFactoryInterface::class)]`

MethodReturnsNotes`__construct(ServerRequestFactoryInterface, UriFactoryInterface, UploadedFileFactoryInterface, StreamFactoryInterface, ?UploadedFileNormalizer = null)`—All four PSR-17 factories; in the framework they resolve to one `phpdot/http` instance`fromGlobals()``ServerRequestInterface`Reads the superglobals once and delegates to `fromArrays()``fromArrays(array $server, array $headers = [], array $cookies = [], array $query = [], ?array $parsedBody = null, array $files = [], mixed $rawBody = null)``ServerRequestInterface`The deterministic seam — every branch testable without a superglobalCapture covers: method, URI (scheme/host/port/path/query), protocol version, headers (via `getallheaders()` with an `HTTP_*` fallback), cookies, query params, parsed body, the `php://input` body stream, and uploaded files.

### SapiEmitter · implements EmitterInterface

[](#sapiemitter--implements-emitterinterface)

`#[Singleton]` `#[Binds(EmitterInterface::class)]`

MethodReturnsNotes`__construct(int $bufferSize = 8192, ?callable $headerSink = null, ?callable $statusSink = null, ?callable $headersSentProbe = null, ?string $requestMethod = null)`—Sinks default to native `header()`/`headers_sent()`; override for testing`emit(ResponseInterface $response)``void`Throws `EmitterException` if output has already startedGuarantees: fail-fast `headers_sent()` guard before any output · status line `HTTP/{proto} {code} {reason}` · multi-value headers (repeated `Set-Cookie`) · chunked body streaming · no body for `204`/`304`/`HEAD`.

### UploadedFileNormalizer

[](#uploadedfilenormalizer)

MethodReturnsNotes`normalize(array $files, UploadedFileFactoryInterface $factory, StreamFactoryInterface $streams)``array``$_FILES` (single, flat `name[]`, or nested `name[a][b][]`) → a tree of PSR-7 `UploadedFileInterface`Working Examples
----------------

[](#working-examples)

### A controller-style PSR-15 handler

[](#a-controller-style-psr-15-handler)

```
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class HelloHandler implements RequestHandlerInterface
{
    public function __construct(private readonly ResponseFactoryInterface $responses) {}

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $name = $request->getQueryParams()['name'] ?? 'world';

        return $this->responses->createResponse(200)
            ->withHeader('Content-Type', 'text/plain')
            ->withBody($this->responses->createStream("Hello, {$name}"));
    }
}
```

### Capturing without the superglobals (testing, CLI, custom SAPIs)

[](#capturing-without-the-superglobals-testing-cli-custom-sapis)

```
$request = $requestFactory->fromArrays(
    server: ['REQUEST_METHOD' => 'POST', 'HTTP_HOST' => 'api.test', 'REQUEST_URI' => '/users?page=2'],
    query: ['page' => '2'],
    parsedBody: ['email' => 'omar@phpdot.com'],
    rawBody: '{"email":"omar@phpdot.com"}',
);
```

DI Wiring
---------

[](#di-wiring)

Everything autowires — there is nothing to register. `RequestFactory`, `SapiEmitter`, and `SapiServer` carry `#[Binds(...)]`, so `RequestFactoryInterface`, `EmitterInterface`, and `ServerInterface` resolve to them; `SapiServer` depends only on the first two. The four PSR-17 factory parameters on `RequestFactory` all resolve to `phpdot/http`'s `ResponseFactory` (it binds all five PSR-17 interfaces), so the graph wires itself.

Type-hint the shared `ServerInterface` and your front controller stays portable — swapping `phpdot/server-sapi` for `phpdot/server-swoole` needs no change here:

```
use PHPdot\Contracts\Server\ServerInterface;

final class FrontController
{
    public function __construct(private readonly ServerInterface $server) {}

    public function handle(RequestHandlerInterface $router): void
    {
        $this->server->serve($router);
    }
}
```

The attributes are inert without a container — standalone consumers just `new` the classes as shown in [Quick Start](#quick-start).

Development
-----------

[](#development)

```
composer test        # PHPUnit
composer analyse     # PHPStan level 10
composer cs-fix      # PHP-CS-Fixer
composer check       # all three (test + analyse + cs-check)
```

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

19

—

LowBetter than 9% of packages

Maintenance60

Regular maintenance activity

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/62e82421bda4b5d6ba9a47ba6d88caca060dcd0d1a2862f351f3a97657385db0?d=identicon)[phpdot](/maintainers/phpdot)

---

Top Contributors

[![phpdot](https://avatars.githubusercontent.com/u/252500?v=4)](https://github.com/phpdot "phpdot (1 commits)")

### Embed Badge

![Health badge](/badges/phpdot-server-sapi/health.svg)

```
[![Health](https://phpackages.com/badges/phpdot-server-sapi/health.svg)](https://phpackages.com/packages/phpdot-server-sapi)
```

###  Alternatives

[nineinchnick/edatatables

Grid widget for the Yii Framework, wrapper for the DataTables jQuery plugin

173.2k](/packages/nineinchnick-edatatables)[alizharb/laravel-modular

A professional, framework-agnostic modular architecture for Laravel 11+. Features zero-config autoloading, 29+ Artisan command overrides, and seamless Vite integration.

211.6k9](/packages/alizharb-laravel-modular)[link-cloud/fast-hyperf

LinkCloud Fast Hyperf

241.2k1](/packages/link-cloud-fast-hyperf)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
