From the repository root, fetch the dependencies used by the example.
composer install
PHP 8.4+ / PSR-7 / TCP
An attribute-driven PHP framework for composing modules, matching routes, and serving small HTTP applications without hiding the request.
<?php
declare(strict_types=1);
use dvictorjhg\braidphp\Core\App;
use dvictorjhg\braidphp\Core\Attributes\Module;
use dvictorjhg\braidphp\Router\Attributes\Get;
use dvictorjhg\braidphp\Router\Attributes\Route;
use dvictorjhg\braidphp\Router\Http\Request;
use dvictorjhg\braidphp\Router\HttpModule;
use dvictorjhg\braidphp\Router\Router;
#[Route(path: '/api')]
final class GreetingController
{
#[Get('/hello/:name')]
public function hello(Request $request): string
{
return 'Hello ' . ($request->getRouteParam('name') ?? '') . '!';
}
}
#[Module(
imports: [HttpModule::class],
controllers: [GreetingController::class]
)]
final class AppModule {}
$app = new App();
$app->bootstrapModule(new AppModule());
$app->listen(port: '8000');
curl http://127.0.0.1:8000/api/hello/Ada
Hello Ada!
Install the repository dependencies, start its front controller, then call a real route from a second terminal.
From the repository root, fetch the dependencies used by the example.
composer install
Start the listener on port 8000 and keep this terminal open.
php public/index.php
In a second terminal, call the hello route and check its response.
curl http://127.0.0.1:8000/api/hello/Ada
# Hello Ada!
A module is the boundary where imported infrastructure, injectable providers, route controllers, and startup work become one graph.
<?php
use dvictorjhg\braidphp\Core\Attributes\Module;
use dvictorjhg\braidphp\Router\HttpModule;
#[Module(
imports: [HttpModule::class],
providers: [Greeter::class],
controllers: [GreetingController::class],
bootstrap: [CacheWarmup::class => ['prefix' => 'app']]
)]
final class AppModule
{
}
Bootstrap module classes or objects first.
Register classes, values, aliases, or factories with PHPInjector.
Scan classes and objects for route attributes.
Resolve keyed startup classes after the graph is ready.
Import HttpModule or register Router::class yourself before controllers are processed. The module scanner does not invent an application graph.
Class routes establish a prefix. Method attributes finish the path. Captures travel to the action on a request copy.
#[Route(path: '/api')]
final class GreetingController
{
public function __construct(private Greeter $greeter)
{
}
#[Get('/hello/:name', pathMatch: 'full')]
public function greet(Request $request): string
{
$name = $request->getRouteParam('name') ?? '';
return $this->greeter->greeting($name);
}
#[Post('/hello')]
public function create(Request $request): Response
{
return new Response(201, body: (string) $request->getBody());
}
}
GET /api/hello/Ada/api/hello:name = Ada/search?term=phpRead from getQueryParams().
prefix / fullUse full when a node must consume the remaining path.
Each shortcut fixes the HttpMethod value; Route accepts a combined integer mask for multi-method nodes.
#[Get]
HEAD#[Head]
POST#[Post]
PUT#[Put]
DELETE#[Delete]
CONNECT#[Connect]
OPTIONS#[Options]
TRACE#[Trace]
PATCH#[Patch]
Custom matchers receive path segments and the route. They return UrlMatcherResult with captured string parameters.
use dvictorjhg\braidphp\Router\Classes\Route;
use dvictorjhg\braidphp\Router\Classes\UrlMatcherResult;
use dvictorjhg\braidphp\Router\Http\HttpMethod;
$router->setRoutes(new Route(
httpMethod: HttpMethod::GET,
matcher: static function (array $parts, Route $route): ?UrlMatcherResult {
return ($parts[0] ?? null) === 'health'
? new UrlMatcherResult(['health'])
: null;
},
action: [HealthController::class, 'show'],
));
Request, response, URI, and stream objects follow PSR contracts. Header and URI changes are immutable and explicit.
Query parameters are parsed on construction; route parameters arrive through withRouteParams().
$request = new Request(
method: HttpMethod::GET,
uri: '/health?verbose=1',
);
$traced = $request
->withHeader('X-Trace', 'one')
->withAddedHeader('X-Trace', 'two')
->withRouteParams(['scope' => 'live']);
echo $traced->getQueryParams()['verbose'];
1Pass scalar content or a PSR stream. A reason phrase is inferred when the status code is known.
$response = new Response(
statusCode: 201,
headers: ['X-Request' => 'one'],
body: 'created',
);
echo $response->getReasonPhrase();
echo (string) $response;
Created
Content-Length: 7| Member | Behavior | Returns |
|---|---|---|
getHeader() | Case-insensitive lookup | list<string> |
withHeader() | Replace values on a copy | MessageInterface |
withAddedHeader() | Append values on a copy | MessageInterface |
getBody() / Stream::of() | Read or create stream content | StreamInterface |
withUri() | Replace URI and refresh query data | RequestInterface |
(string) $response | Serialize status, headers, and body | HTTP/1.1 ... |
The runtime uses PHP streams to accept a request, resolve its action, and write one HTTP response. There is no hidden worker pool.
$app->listen(
address: '0.0.0.0',
port: '8000',
);
Use a process manager, container platform, or reverse proxy when the application needs more than one worker.
handleRequest() returns Not Found.
The socket loop writes Server Error with the exception message.
Start with App and the attributes. Reach for the lower-level router and HTTP classes when you need precise control.
| Member | Role | Contract |
|---|---|---|
App | Bootstraps modules and handles requests | bootstrapModule() handleRequest() listen() |
#[Module] | Declares application composition | imports providers controllers bootstrap |
Router | Stores and processes route trees | setRoutes() processRoutes() |
RouteScanner | Turns attributes into route objects | scan() -> Route or RouteArray |
RouteMatch | Carries the selected route and captures | route params |
Request / Response | PSR HTTP request and response messages | headers, URI, body, status |
Uri / Stream | Immutable URI and PSR stream primitives | with*() Stream::of() |
UrlMatcherResult | Result returned by a custom matcher | consumed params |
The feedback loop is deliberately plain: Composer validation, platform checks, PHPStan, PHP_CodeSniffer, and PHPUnit.
composer install
composer validate --strict
composer check-platform-reqs
composer analyse
composer check-style
composer test
CI runs this suite on PHP 8.4 and 8.5. The production image is PHP 8.5.9.