stougeiro / base
Opinionated PHP 8.2 foundation for small HTTP applications: request kernel, routing, controllers, module middleware, error handling, configuration and logging.
Fund package maintenance!
Requires
- php: >=8.2
- ext-json: *
- stougeiro/base-contract: ^0.1.0
- stougeiro/container: ^4.0
- stougeiro/http: ^2.2
Requires (Dev)
- pestphp/pest: ^3.8
- phpstan/phpstan: ^2.2
- stougeiro/router: ^1.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-01 04:34:48 UTC
README
Base
An opinionated PHP 8.2 foundation for small HTTP and API applications.
Base supplies the request lifecycle, error handling, configuration, and logging, plus the FPM implementations that fulfill the shared module and controller contracts. Those contracts live in stougeiro/base-contract. Base deliberately stops at the HTTP boundary: concrete routing, cache, views, and application wiring belong to the application.
The goal is a small, explicit core that is easy to inspect, replace, and test.
✨ Features
- Per-request kernel: providers register and boot, modules configure themselves, the request is handled, and providers terminate.
- Self-contained modules: a module owns its routes, middleware, and controllers. Only its provider class is referenced globally.
- Routing and controllers: route variables become request attributes, middleware runs around controllers,
OPTIONSis answered automatically, and invalid verbs receive405with anAllowheader. - Consistent errors: HTML or JSON responses,
HttpException, request reference IDs, safe production responses, and configurable logging. - Secure bootstrap:
development,staging, andproductionmodes with production-safe defaults. - Configuration helpers: dot-notated
env.jsonandconfig/*.php.env()is strict and has no default;config()supports defaults. - Path registry:
Pathresolves real directories and fails loudly on invalid or duplicate registrations. - Small service layer: immutable
Resultimplementation; action/check contracts come fromstougeiro/base-contract. - Quality tooling: PHPStan Level 9 and a Pest suite.
📦 Installation
Install via Composer:
composer require stougeiro/base
Requirements
- PHP 8.2 or newer.
ext-json.
What Base Does Not Include
- Contract definitions; they live in
stougeiro/base-contract. - A concrete router or route repository.
- A cache implementation.
- A view engine or view registration.
- Application service providers.
- A project skeleton.
Those are application decisions. Reference wiring and expansion patterns live in stougeiro/app and stougeiro/skeleton.
⚡ Performance
Base targets the FPM request lifecycle. With OPcache enabled, and route caching when routing is used, framework overhead is small compared with database and network costs.
In a reference no-op benchmark with 80 FPM workers, Base sustained 500 req/s for 60 seconds with p95 between 3 ms and 14 ms and zero failed requests. This measures the Base/HTTP layer, not a complete application.
Real throughput and latency depend on database queries, external services, logging, serialization, and worker sizing. See BENCHMARK.md for methodology, environment, and caveats.
🚀 Usage Example
Base does not generate an application. The example below assumes the application has already bound its router and request pipeline providers.
<?php // public/index.php declare(strict_types=1); use Sourcerer\Http\Kernel; use Sourcerer\Path; use STDW\Http\Request; require dirname(__DIR__) . '/vendor/autoload.php'; Path::register('root', dirname(__DIR__)); Path::register('config', 'config/', 'root'); Path::register('logs', 'storage/log/', 'root'); $kernel = Kernel::create(); try { $kernel->handle(new Request())->send(); } finally { $kernel->terminate(); }
Path::register() requires the directory to exist. Duplicate keys and invalid paths throw immediately, catching configuration mistakes before dispatch.
Create env.json at the project root:
{
"environment": "development",
"app": {
"name": "My App"
}
}
environment is required. Invalid values, malformed JSON, or a missing file fall back to production:
developmentexposes error details and installs the development handler.stagingreturns production-style responses but logs full traces.productionlogs traces only for5xx.
Register providers and modules in config/app.php:
<?php use App\HelloWorld\HelloWorldModuleProvider; return [ 'providers' => [ // Application providers that bind the router and request pipeline. ], 'modules' => [ HelloWorldModuleProvider::class, ], ];
A module loads only its own surface:
<?php declare(strict_types=1); namespace App\HelloWorld; use Sourcerer\Contract\Module\ModuleProviderAbstract; final class HelloWorldModuleProvider extends ModuleProviderAbstract { public function configure(): void { $this->routes(__DIR__ . '/Config/routes.php'); $this->middlewares(__DIR__ . '/Config/middlewares.php'); } }
The routing example uses stougeiro/router. It is an application dependency, not a Base requirement.
For complete modules, controllers, middleware, and provider wiring, use stougeiro/app or stougeiro/skeleton.
🧠 Why?
API-first by default
Base has no view layer and no presentation dependency. Applications that need HTML can add their own integration without carrying view abstractions in the core.
Explicit modules
Modules are listed in configuration. Routes and middleware are declared in files. Only the module provider is global.
This avoids scanning, implicit registration, and hidden coupling while keeping modules self-contained and easy to inspect.
Safe error strategy
The error handler is installed before application configuration so a broken boot still gets a safe response.
| Behavior | development |
staging |
production |
|---|---|---|---|
| Display errors | yes | no | no |
| Details in response | yes | no | no |
| Trace in log | 5xx |
all errors | 5xx |
HttpException carries status 400–599 and validated headers. 5xx responses include an X-Error-Id; logs include the request path, redacted query, client IP, and user agent.
Fallback is the last line of defense. It can emit a minimal 500 even when the container, configuration, or autoloader is unavailable.
Why JSON for the environment?
env.json is hierarchical, which keeps related values grouped and readable. Flat files such as .env can represent hierarchy only through key-name conventions, which become harder to understand as the file grows.
JSON is also data-only. Base reads it with file_get_contents() and json_decode(), never with include, so the environment file cannot execute PHP code. It is practically a standard format, natively supported by PHP, editors, and tooling.
YAML, NEON, TOML, and similar formats would require an additional parser or translator, add a dependency, introduce implicit rules, and increase the learning curve without a proportional gain for a small environment file.
Strict environment, configurable application
env() is strict and throws on missing values because an absent environment value is usually a deployment error. config() accepts explicit defaults because application configuration often has a safe fallback.
Both fail fast instead of silently returning partial data.
FPM runtime model
Base targets PHP-FPM. Although workers are reused, PHP resets userland state for every request: static caches, the kernel singleton, the container, ini_set() changes, and error handlers do not carry over.
Request input and response emission are SAPI-based through stougeiro/http. The application must call terminate() after sending the response, ideally in a finally block.
Long-running runtimes that reuse the process across requests are not supported.
🤝 Contributions
Contributions are welcome. Feel free to open issues or submit pull requests.