Rxn (from "reaction") is built around a single opinion: strict backend/frontend decoupling. The backend is API-only, responds in JSON, and rolls up every uncaught exception into a JSON error envelope. Frontends — web, mobile, whatever — build against the versioned contracts and stay decoupled.
The framework aims, in order, to be fast, small, and
easy to use. The ORM / query builder lives in a separate
package — davidwyly/rxn-orm
— pulled in automatically via Composer.
flowchart TB
Req["HTTP request"] --> Index["public/index.php"]
Index --> App["App::run"]
App --> Boot["Startup<br/>.env + autoload + DBs"]
App --> Dispatch{"route?"}
Dispatch -->|convention| ConvCtrl["Versioned controller"]
Dispatch -->|explicit| Router["Http/Router"]
Router --> Pipeline["Middleware pipeline"]
Pipeline --> ExplCtrl["Route handler"]
ConvCtrl --> Resp["Response"]
ExplCtrl --> Resp
Resp -->|success| OK["application/json<br/>{data, meta}"]
Resp -. uncaught exception .-> Fail["Response::getFailure"]
Fail --> PD["application/problem+json<br/>RFC 7807"]
See docs/index.md for the full request sequence
and per-subsystem deep dives.
Five motives drive every decision in the framework: novelty, simplicity, interoperability, speed, and strict JSON.
Every exit point — including uncaught exceptions — is a JSON
response. Slim / Lumen / Mezzio / API Platform all default to
JSON but still let controllers return HTML, XML, streams; that
flexibility forces a content-negotiation layer you can't opt out
of, and every app on top has to remember a surprising exception
can leak an HTML stack trace. Rxn removes the choice. Success
lands on {data, meta}; errors land on
application/problem+json. Two shapes, both machine-readable,
zero negotiation code.
Errors are RFC 7807 Problem Details, not a bespoke envelope.
API gateways, Problem Details-aware client libraries, and error
aggregators already understand the shape; Rxn just emits what the
ecosystem expects. OpenAPI 3 specs generate from reflection
(bin/rxn openapi), so the contract is always in sync with the
code — hand the spec to any OpenAPI consumer (Redocly, client
generators). Drop in Http\OpenApi\SwaggerUi::html($specUrl)
from a route handler for instant interactive docs. PSR-7 / PSR-15
bridge lets any ecosystem middleware drop in via Psr15Pipeline.
Opinionated pieces worth naming:
- Typed DTO binding + attribute-driven validation. Declare
public function create_v1(CreateProduct $input): array, giveCreateProductpublic typed properties with#[Required],#[Min(0)],#[Length(min: 1, max: 100)], etc., and the framework hydrates, casts, validates, and hands your action a populated instance — or fails the whole request with a 422 Problem Details listing every field error at once. The same FastAPI-class ergonomic move that almost nothing in the PHP ecosystem ships natively, in ~250 LoC with no DSL. - Attribute routing + middleware on the controller method:
#[Route('GET', '/products/{id:int}')]and#[Middleware(Auth::class)]are the route table. No separateroutes.phpto drift out of sync. - Typed route constraints (
{id:int},{slug:slug},{id:uuid}, custom) so/users/foofalls through to 404 instead of reaching a controller that has to validate and throw. - Reflection-driven OpenAPI — the framework knows your controllers; why duplicate that in a YAML file? DTO validation attributes map one-to-one to JSON Schema keywords, so the spec can't drift from the runtime behaviour — both sides read the same class.
- Production-safe by default — stack traces never ship outside
dev, boundary input sanitisation is one env flag, session
cookies auto-flip to
Securebehind an HTTPS proxy.
Small enough to read end to end. Dependency-free, injectable-for-test
middlewares for the common defensive layers: CORS with preflight,
request-id correlation, JSON-body decoding with size caps,
conditional GET via weak ETags. DI container supports
interface-to-implementation binding ($c->bind(UserRepo::class, PostgresUserRepo::class)) and factory closures, so serious apps
aren't stuck with autowire-only. An in-process TestClient
(Rxn\Framework\Testing\TestClient) fires requests at your Router
- middleware stack and returns a
TestResponsewith PHPUnit- integrated fluent assertions — no web server, no curl, no process boundary. The ORM lives in a separate package (davidwyly/rxn-orm) so the framework itself stays narrow.
- PSR-4 autoloading; no reflection on the hot path once classes load.
- File-backed query caching (
Database::setCache()) and object file caching with atomic writes for reflection-derived data. - ETag middleware drops 304s for unchanged GETs before your controller serializes a byte of response.
- No content-negotiation layer to walk on every request.
- Sync-first, process-per-request, predictable. We deliberately
don't chase async — PHP-FPM's process pool already gives you
concurrent-requests concurrency without the Fibers + event-loop
- non-blocking-driver tax, and every "why async" benchmark you see is really an "I'm IO-bound and never cached anything" story. Stack RoadRunner or Swoole under Rxn if you need in-request concurrency; the framework doesn't change shape for it.
composer install
vendor/bin/phpunit # run the test suite
composer validate --strict # sanity-check composer.json
bin/rxn help # list CLI subcommandsFull Docker stack (PHP 8.3-fpm + nginx 1.27 + MySQL 8):
cp docker-compose.env.example .env
# edit .env: set MYSQL_PASSWORD and MYSQL_ROOT_PASSWORD
docker compose up --buildSet INSTALL_XDEBUG=1 in .env to build the PHP image with Xdebug 3.
CI runs lint + phpunit against PHP 8.2, 8.3, and 8.4 plus an
end-to-end HTTP smoke job against MySQL 8
(.github/workflows/ci.yml).
Current test counts:
- Rxn framework: 230 tests / 521 assertions (
vendor/bin/phpunit). davidwyly/rxn-orm(query builder): 68 tests / 132 assertions, run in that repo.
| Topic | Where |
|---|---|
| Routing (convention + explicit patterns) | docs/routing.md |
| Dependency injection | docs/dependency-injection.md |
| Request binding + validation | docs/request-binding.md |
| Scaffolded CRUD | docs/scaffolding.md |
| Error handling | docs/error-handling.md |
| Building blocks (Logger, RateLimiter, Scheduler, Auth, Pipeline, Router, Validator, Migration, Chain, query cache, PSR-7 bridge) | docs/building-blocks.md |
CLI (bin/rxn) |
docs/cli.md |
Benchmarks (bin/bench) |
docs/benchmarks.md |
| Contribution / style guide | CONTRIBUTING.md |
[X] = implemented and shipped, [ ] = on the roadmap.
- 80%+ unit test code coverage (currently minimal; see
src/Rxn/**/Tests/for what's covered) - Gentle learning curve
- Installation through Composer
- Simple workflow with an existing database schema
- Code generation
- CLI utility to create controllers and models
(
bin/rxn make:controller,bin/rxn make:record)
- CLI utility to create controllers and models
(
- Code generation
- Database abstraction
- PDO for multiple database support
- Support for multiple database connections
- Security
- Prepared statements everywhere — user values flow only through PDO bindings; identifiers come from schema reflection, never from request data
- Session cookies set with HttpOnly + SameSite=Lax; Secure
flag flips on automatically when the request is HTTPS
(including behind a trusted
X-Forwarded-Protoproxy) - Stack traces never leave the server in production —
Response::getFailurestrips file / line / trace fields whenENVIRONMENT=production - Boundary input sanitization — control-character
stripping on every GET / POST / header param when
APP_USE_IO_SANITIZATION=true(JSON is the output format, so HTML-escaping stays in the frontend) - CSRF synchronizer tokens (
Session::token()/Session::validateToken()) with constant-time compare - Bearer-token authentication (
Service\Auth): the framework extracts + verifies, the app supplies the token → principal resolver — by design, not a gap - Rate limiting (
Utility\RateLimiter, file-backed withflock)
- Exception-driven error handling
- RFC 7807 Problem Details (
application/problem+json) is the error shape, period — uncaught exceptions included. Dev-mode file/line/trace carry asx-rxn-*extension members
- RFC 7807 Problem Details (
- Versioning (versioned controllers + actions)
- Scaffolding (version-less CRUD against a live schema)
- URI Routing
- Convention-based (
/v{N}/{controller}/{action}/key/value/...) - Explicit pattern routing (
Rxn\Framework\Http\Router) - Typed route constraints (
{id:int},{slug:slug},{id:uuid}, custom) — a non-matching URL just falls through to 404 instead of bubbling up as a controller-level validation error - Attribute-based routing —
#[Route('GET', '/products/{id:int}')]+#[Middleware(Auth::class)]directly on controller methods viaRxn\Framework\Http\Attribute\Scanner; no separate route table - Apache 2 (.htaccess)
- NGINX (see
docker/nginx)
- Convention-based (
- Dependency Injection container
- Controller method injection
- DI autowiring via constructor type hints
- Interface → implementation binding
(
$container->bind($abstract, $concrete)) and factory closures ($container->bind($abstract, fn ($c) => ...)) - Circular-dependency detection
- Object-Relational Mapping
- Query builder (SELECT / INSERT / UPDATE / DELETE with
subqueries, upsert, RETURNING) — ships as
davidwyly/rxn-orm - ActiveRecord hydration + hasMany / hasOne / belongsTo
relationships (
Rxn\Framework\Model\ActiveRecord) - Scaffolded CRUD on a record (
CrudController+Record) - FK relationship graph (
Data\Chain+Link)
- Query builder (SELECT / INSERT / UPDATE / DELETE with
subqueries, upsert, RETURNING) — ships as
- HTTP middleware pipeline (both Rxn-native and PSR-15; see
Http\Pipeline/Http\Psr15Pipeline)- Shipped middlewares: CORS w/ preflight, request-id
correlation, JSON-body decoding with size caps, conditional
GET via weak ETags + 304 short-circuit (see
Http\Middleware\{Cors,RequestId,JsonBody,ETag})
- Shipped middlewares: CORS w/ preflight, request-id
correlation, JSON-body decoding with size caps, conditional
GET via weak ETags + 304 short-circuit (see
- PSR-7 bridge (
Http\PsrAdapter::serverRequestFromGlobals()/::emit(); ecosystem middleware drops in via Psr15Pipeline) - Speed and performance
- PSR-4 autoloading
- File-backed query caching
- Object file caching (atomic writes)
- Event logging (JSON-lines)
- Scheduler (interval / predicate based)
- Database migrations (
*.sqlrunner) - Mailer (out of scope; use symfony/mailer or phpmailer)
- Request validation (rule-based
Validator::assert; seeRxn\Framework\Utility\Validator)- Typed DTO binding with attribute-driven validation —
Http\Binding\RequestDto+Http\Binding\Binder+#[Required],#[Min],#[Max],#[Length],#[Pattern],#[InSet]. All errors surface at once as a 7807errorsextension member.
- Typed DTO binding with attribute-driven validation —
- OpenAPI 3 spec generation from reflected controllers
(
bin/rxn openapi;Http\OpenApi\Generator+Discoverer)- One-line interactive docs via
Http\OpenApi\SwaggerUi::html($specUrl) - DTO parameters emit as
requestBodywith validation attributes mapped to JSON Schema keywords (#[Min]→minimum,#[Length]→minLength/maxLength,#[Pattern]→pattern,#[InSet]→enum) — the same class drives both validation and the spec, so they can't drift
- One-line interactive docs via
- In-process HTTP test client + fluent response assertions
(
Testing\TestClient+TestResponse) — no web server, no curl, PHPUnit-integrated failures - Automated API request validation from contracts
- Optional, modular plug-ins
Rxn is released under the permissive MIT license.
