dvictorjhg / braidphp
An attribute-driven PHP framework with a router and lightweight single-process TCP HTTP runtime.
Requires
- php: ^8.4
- dvictorjhg/php-injector: ^1.0.1
- psr/http-message: ^2.0
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
- squizlabs/php_codesniffer: ^3.0
- zalas/phpunit-globals: ^3.0
Provides
README
BraidPHP is an attribute-driven PHP framework with a router and a lightweight
single-process TCP HTTP runtime. Dependency injection and PSR-11 storage are
provided by the standalone
dvictorjhg/php-injector package.
The supported platform is PHP ^8.4. The runtime is intentionally
single-process; scale it with a process manager, container platform, or reverse
proxy when the application needs more than one worker.
Documentation
The BraidPHP documentation page is the visual entry point for
installation, modules, attribute routing, HTTP messages, the server runtime,
and the public API. It is a static page with relative assets under
docs/assets, and includes light/dark themes plus English and Spanish
translations.
This README is the copy-and-paste reference for the smallest working application. The changelog, contribution guide, and security policy complete the release documentation.
Requirements
- PHP 8.4 or 8.5
- Composer
Installation
composer require dvictorjhg/braidphp
Quick Start
Create a module with a router provider and a controller, then start the server:
<?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\Router; use dvictorjhg\braidphp\Router\HttpModule; final class Greeter { public function greeting(string $name): string { return "Hello $name!"; } } #[Route(path: '/api')] final class GreetingController { public function __construct(private Greeter $greeter) { } #[Get('/hello/:name', pathMatch: 'full')] public function hello(Request $request): string { return $this->greeter->greeting($request->getRouteParam('name') ?? ''); } } #[Module( imports: [HttpModule::class], providers: [Greeter::class], controllers: [GreetingController::class] )] final class AppModule { } $app = new App(); $app->bootstrapModule(new AppModule()); $app->listen(address: '0.0.0.0', port: '8000');
With the server running, request http://127.0.0.1:8000/api/hello/Ada:
Hello Ada!
The controller is discovered from attributes, Greeter is resolved by the
injector, and the :name path segment is available on the immutable request
copy passed to the action.
The front controller in public/index.php reads
SERVER_ADDRESS and SERVER_PORT from the environment.
Run and Try the Example
From the repository root, install the dependencies and start the checked-in example:
composer install php public/index.php
Leave the server running, open a second terminal, and try the routes exposed by
Example/GreeterComponent.php:
curl http://127.0.0.1:8000/api/hello/Ada # Hello Ada! curl 'http://127.0.0.1:8000/api/hello?name=Ada' # Hello Ada! curl -X POST http://127.0.0.1:8000/api/hi/Ada # Hi Ada!
Stop the server with Ctrl+C when you are finished.
Modules
#[Module] is the composition boundary for an application. Each argument is
optional and accepts an array or a PHPInjector\Container\Container:
| Argument | Purpose |
|---|---|
imports |
Bootstrap other module classes or module objects first. |
providers |
Register classes, values, aliases, or factories with the injector. |
controllers |
Scan classes or objects for route attributes. |
bootstrap |
Resolve keyed classes after providers and controllers are ready. Existing objects are kept as-is. |
Import the built-in HTTP module when it is useful to keep router registration separate from application providers:
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 { }
HttpModule provides Router::class. Provider values and classes use the
same resolution rules as the standalone
php-injector package.
Routing
Routes are declared with #[Route] or one of the method-specific attributes:
#[Get], #[Head], #[Post], #[Put], #[Delete], #[Connect],
#[Options], #[Trace], and #[Patch].
- A class route is a prefix for its method routes.
- A
:namesegment captures a path parameter, available throughRequest::getRouteParam()orRequest::getRouteParams(). - Query strings are parsed into
Request::getQueryParams(). pathMatch: 'prefix'is the default for route nodes;pathMatch: 'full'requires the route path to consume the complete path at that level.- Programmatic
Routeobjects can use a custom matcher that returnsUrlMatcherResultornull. A path and matcher cannot be used together. HttpMethodvalues can be combined with bitwise OR when a route accepts more than one method, for exampleHttpMethod::GET->value | HttpMethod::POST->value.
Router::processRoutes() returns a RouteMatch containing the selected route
and captured parameters. App::handleRequest() applies those parameters to a
request copy before it invokes the action.
HTTP Messages
Request, Response, Uri, and Stream implement the relevant PSR message
contracts. Message and URI with*() methods return a new instance when a
value changes:
use dvictorjhg\braidphp\Router\Http\Request; $request = new Request(method: 'GET', uri: '/health'); $withTrace = $request->withHeader('X-Trace', 'request-1'); $withMoreTrace = $withTrace->withAddedHeader('X-Trace', 'request-2'); echo $request->hasHeader('X-Trace') ? 'changed' : 'original'; echo $withMoreTrace->getHeaderLine('x-trace');
Headers are case-insensitive and support multiple string values. Request bodies
and response bodies are streams; Stream::of() accepts scalar content,
resources, stringable objects, and existing PSR streams. Response supplies a
known reason phrase when one is available. During string serialization it adds
Content-Type: text/plain and calculates Content-Length when those headers
were not supplied.
Runtime and Errors
App::listen() opens a TCP socket and handles requests in a blocking,
single-process loop. The front controller reads SERVER_ADDRESS and
SERVER_PORT; both default to 0.0.0.0 and 8000 when they are not set.
App::handleRequest() returns a 404 Not Found response when no route matches.
String, scalar, null, and supported object results from actions become 200
responses; actions may return Response directly for full control. Unsupported
results or routing/application failures raise framework exceptions. The socket
loop catches uncaught throwables and writes a 500 response containing the
server error message.
Structure
bin/ Container launchers
docs/ Static documentation
docker/ PHP container definitions
public/ Application entry point
src/ Framework source
Example/ Example application code
tests/ Unit and integration tests
Quality
The main CI workflow in .github/workflows/ci.yml uses GitHub Actions on PHP 8.4 and 8.5 to validate Composer metadata and platform requirements, run PHPStan and PHP_CodeSniffer, execute the PHPUnit suite with Xdebug coverage enabled, publish the Clover report to Codecov, and fail the build if statement coverage drops below 50%.
Before the first upload, enable the repository in Codecov. Public pull requests
from forks can upload from an unprotected branch without a token, but uploads
for protected branches and all private repositories require a Codecov token
unless token authentication for public repositories has been disabled in
Codecov's Global Upload Token settings. For the reliable protected-branch
path, add the repository token as a GitHub Actions secret named
CODECOV_TOKEN under Settings > Secrets and variables > Actions. Keep it in
GitHub Secrets rather than committing it to the repository.
Tooling
- GitHub Actions for continuous integration, with actions/checkout and shivammathur/setup-php for supported PHP runtimes.
- Codecov for coverage reports and the README badge, uploaded through codecov/codecov-action.
- Composer for dependency management, package metadata validation, and project scripts.
- PHPStan for static analysis, executed through
composer analyse. - PHP_CodeSniffer for coding standards, executed through
composer check-style. - PHPUnit and Xdebug for tests and coverage reports.
- tools/check-coverage.php for enforcing the minimum 50% statement coverage threshold from Clover XML.
Development
composer install
composer validate --strict
composer check-platform-reqs
composer analyse
composer check-style
composer test
To run the same coverage checks enforced in CI:
composer test:coverage composer coverage:check
The Docker image uses PHP 8.5.9. To run the development container with Podman on Windows:
./bin/podman-run.ps1 -Environment development -Detach podman exec braidphp-development composer test ./bin/podman-run.ps1 -Action down
Use bin/podman-run.sh for Unix-like shells. Both launchers
read .env, build the selected image, expose SERVER_PORT, and mount source
directories in development mode.
Changelog
See CHANGELOG.md for the current release notes.
Contributing
See CONTRIBUTING.md and CODE_OF_CONDUCT.md for contribution guidelines. The AI use policy explains attribution expectations for generated material.
Security
See SECURITY.md for private vulnerability reporting. Do not open a public issue for a security vulnerability.
Release / Publishing
For a release, update CHANGELOG.md, validate the package with
composer validate --strict, composer analyse, composer test, and the
coverage checks, then create and push an annotated Git tag such as 1.0.1.
After the tag is on GitHub, publish a GitHub Release and refresh the package on
Packagist. Verify that users can install the package with
composer require dvictorjhg/braidphp:^1.0.
License And Attribution
BraidPHP is released under the Apache License 2.0. For redistribution, preserve the license text, repository copyright, and attribution notices. The supplemental NOTICE and CITATION.cff files record the project attribution and citation details.
For academic, professional, blog, package, or product reuse, keep attribution intact and link back to the original repository when reasonable.
AI Use Policy
The maintainer wants this project credited when it is reused and does not want it stripped of attribution or turned into low-quality AI-generated derivative spam. That expectation is documented in AI_USE_POLICY.md.
The policy is project guidance, not an additional open-source restriction. If you need enforceable no-AI or no-training terms, a source-available non-open-source license would be required.