strictlyphp / dolphpin
Requires
- php: >=8.2
- ext-bcmath: *
- ext-curl: *
- ext-intl: *
- ext-mbstring: *
- ext-simplexml: *
- fig/http-message-util: ^1.1
- haydenpierce/class-finder: ^0.5.3 || ^0.6.0
- league/route: ^6.2
- monolog/monolog: ^3.9
- nikic/php-parser: ^5
- php-di/php-di: ^7.0
- psr/http-server-handler: ^1.0
- psr/log: ^2.0 || ^3.0
- slim/psr7: ^1.7
Requires (Dev)
- php-coveralls/php-coveralls: ^2.7
- phpstan/phpstan: ^1.8
- phpstan/phpstan-phpunit: ^1.1
- phpunit/phpunit: ^9.5
- symplify/easy-coding-standard: 13.1.5
This package is auto-updated.
Last update: 2026-06-11 12:59:20 UTC
README
Dolphin is a lightweight PHP framework designed for running serverless functions on DigitalOcean. It provides attribute-based routing, automatic DTO mapping, role-based access control, and dependency injection out of the box.
For a detailed look at the internals, see ARCHITECTURE.md.
Requirements
- PHP >= 8.2
- Extensions: intl, bcmath, simplexml, curl, mbstring
Installation
composer require strictlyphp/dolphpin:^3.0
Quick Start
1. Define a Controller
Controllers are invokable classes annotated with #[Route]. The framework automatically deserializes the JSON request body into typed DTOs:
<?php declare(strict_types=1); namespace App\Controllers; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use StrictlyPHP\Dolphin\Attributes\Route; use StrictlyPHP\Dolphin\Request\Method; use StrictlyPHP\Dolphin\Response\JsonResponse; #[Route(Method::POST, '/users')] class CreateUserController { public function __invoke(CreateUserDto $dto, ServerRequestInterface $request): ResponseInterface { // $dto is automatically mapped from the JSON body return new JsonResponse(['id' => '123', 'name' => $dto->name], 201); } }
2. Define a DTO
DTOs are plain readonly classes. The framework maps JSON fields to constructor parameters, supporting scalars, value objects, nested DTOs, backed enums, and typed arrays:
<?php declare(strict_types=1); namespace App\Controllers; readonly class CreateUserDto { public function __construct( public string $name, public EmailAddress $email, ) { } }
3. Bootstrap the Application
Use App::build() in your DigitalOcean function entry point. Pass the namespace(s) containing your controllers — routes are discovered automatically from #[Route] attributes:
<?php use StrictlyPHP\Dolphin\App; function main(array $event, object $context): array { $app = App::build( controllers: ['App\Controllers'], ); return $app->run($event, $context); }
Features
Attribute-Based Routing
Routes are declared directly on controller classes using #[Route]:
#[Route(Method::GET, '/users/{id}')] class GetUserController { /* ... */ } #[Route(Method::DELETE, '/users/{id}')] class DeleteUserController { /* ... */ }
Supported HTTP methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD.
Automatic DTO Mapping
Controller parameters that are class types are automatically deserialized from the JSON request body. The mapper supports:
- Scalar types —
string,int,float,bool - Value objects — Single-constructor-argument classes (e.g.
new EmailAddress($value)) - Nested DTOs — Recursively mapped from nested JSON objects
- Backed enums — Resolved via
::tryFrom() - Typed arrays — Element type declared via
@param array<Type>docblock annotations - Nullable parameters — Mapped to
nullwhen absent
Role-Based Access Control
Protect controllers with #[RequiresRoles]. The framework checks the authenticated user's roles before invoking the controller:
#[Route(Method::POST, '/admin/settings')] #[RequiresRoles(['ADMIN'])] class UpdateSettingsController { /* ... */ }
This requires middleware that sets a user attribute on the request implementing AuthenticatedUserInterface:
use StrictlyPHP\Dolphin\Authentication\AuthenticatedUserInterface; class AuthMiddleware implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $user = // ... resolve authenticated user $request = $request->withAttribute('user', $user); return $handler->handle($request); } }
The AuthenticatedUserInterface requires getId(): string and getRoles(): array.
#[RequiresRoles] also accepts enum cases implementing RoleInterface (alongside plain strings) — they are normalised to their backing string values:
#[RequiresRoles([UserRole::ADMIN, 'SUPPORT'])] class UpdateSettingsController { /* ... */ }
#[RequiresRole] is an enum-only, repeatable alternative. Each instance contributes one role, and multiple instances combine with ANY-of (logical OR) semantics — equivalent to the array form above but without the brackets:
#[RequiresRole(UserRole::ADMIN)] #[RequiresRole(UserRole::SUPPORT)] // ADMIN or SUPPORT class UpdateSettingsController { /* ... */ }
Permission-Based Access Control
For finer-grained authorisation, protect controllers with #[RequiresPermission]. Dolphin owns the vocabulary and the attribute-driven enforcement; the authorisation policy itself lives in your app.
- Define enums for your user families and permissions using the marker interfaces:
use StrictlyPHP\Dolphin\Authorization\PermissionInterface; use StrictlyPHP\Dolphin\Authorization\RoleInterface; enum UserKind: string implements RoleInterface { case USER = 'USER'; case ADMIN = 'ADMIN'; } enum AdminPermission: string implements PermissionInterface { case CREATE_REPORT = 'CREATE_REPORT'; case DELETE_REPORT = 'DELETE_REPORT'; }
- Implement
AuthorizationServiceInterfacewith your app's policy (matrix lookups, bypass rules for back-office roles, etc.) and bind it in the container:
use StrictlyPHP\Dolphin\Authorization\AuthorizationServiceInterface; $app = App::build( controllers: ['App\Controllers'], containerDefinitions: [ AuthorizationServiceInterface::class => fn() => new MyAuthorizationService(), ], middlewares: [AuthMiddleware::class], // sets the 'user' request attribute );
- Decorate route handlers:
#[Route(Method::POST, '/reports')] #[RequiresPermission(UserKind::ADMIN, AdminPermission::CREATE_REPORT)] class CreateReportController { /* ... */ }
The framework calls isAllowed($user, $userKind, $permission) before invoking the controller and returns 403 Forbidden when it returns false (or 401 Unauthorized when no user is on the request).
The attribute is repeatable with ANY-of (logical OR) semantics — the user needs at least one of the listed permissions:
#[RequiresPermission(UserKind::ADMIN, AdminPermission::DELETE_REPORT)] #[RequiresPermission(UserKind::USER, UserPermission::DELETE_REPORT)] class DeleteReportController { /* ... */ }
If a controller declares #[RequiresPermission] but no AuthorizationServiceInterface is bound, the framework throws a RuntimeException — a misconfigured app fails loudly rather than silently allowing or denying.
Allowing a role through a permission gate
#[AllowsRole] widens access: a user holding one of the listed roles passes regardless of the #[RequiresPermission] (or #[RequiresRole]) gates on the same controller, and the permission check — including the AuthorizationServiceInterface call — is skipped for them. Use it to gate a route on a fine-grained permission while letting a back-office role straight through:
#[Route(Method::POST, '/reports')] #[RequiresPermission(UserKind::USER, UserPermission::CREATE_REPORT)] #[AllowsRole(UserKind::ADMIN)] class CreateReportController { /* ... */ } // ADMIN -> allowed (no permission check) // USER with CREATE_REPORT -> allowed // USER without CREATE_REPORT -> 403
It is repeatable with ANY-of semantics. #[AllowsRole] is purely additive — it grants, it never restricts. A controller carrying only #[AllowsRole] declares no gate and is therefore effectively open; to restrict access to a role, use #[RequiresRole] or #[RequiresRoles].
Dependency Injection
Dolphin uses PHP-DI for dependency injection. Pass container definitions to App::build():
$app = App::build( controllers: ['App\Controllers'], containerDefinitions: [ UserRepository::class => fn() => new UserRepository($db), ], );
Controllers are resolved through the container, so constructor dependencies are injected automatically.
Middleware
Register PSR-15 middleware globally via App::build():
$app = App::build( controllers: ['App\Controllers'], middlewares: [AuthMiddleware::class, CorsMiddleware::class], );
Debug Mode
Enable debug mode to include exception details (message, request body, stack trace) in error responses:
$app = App::build( controllers: ['App\Controllers'], debugMode: true, );
Custom Throwable Handler
By default, Dolphin catches all exceptions and returns JSON error responses with appropriate status codes. You can provide your own throwable handler middleware to customize this behavior:
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; class CustomErrorHandler implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { try { return $handler->handle($request); } catch (\Throwable $e) { // Your custom error handling logic $response = (new \Slim\Psr7\Factory\ResponseFactory())->createResponse(500); $response->getBody()->write(json_encode(['error' => $e->getMessage()])); return $response->withHeader('Content-Type', 'application/json'); } } } $app = App::build( controllers: ['App\Controllers'], throwableHandler: new CustomErrorHandler(), );
Custom handlers are PSR-15 middleware implementing MiddlewareInterface. They are responsible for their own logging, error formatting, and configuration.
App-Level Exception Handler
In addition to the route-level throwableHandler, you can provide an exceptionHandler closure to customize error handling at the application level (e.g. for errors that occur outside the middleware stack). This is useful for integrating error reporting services like Sentry, Bugsnag, or Datadog:
$app = App::build( controllers: ['App\Controllers'], throwableHandler: new CustomErrorHandler(), // route-level errors (PSR-15) exceptionHandler: function (\Throwable $e): ?array { // app-level safety net \Sentry\captureException($e); return null; // use default error response }, );
The exceptionHandler closure receives the \Throwable and can:
- Return an
array(['statusCode' => ..., 'body' => ..., 'headers' => ...]) to fully control the response - Return
nullto use the default 500 error response (logging is suppressed to avoid duplicates)
When no exceptionHandler is provided, the existing default behavior is preserved.
JSON Responses
Use JsonResponse for convenience:
use StrictlyPHP\Dolphin\Response\JsonResponse; return new JsonResponse(['key' => 'value']); // 200 return new JsonResponse(['created' => true], 201); // 201
Development
The project uses Docker for a consistent development environment. Available Make commands:
make install # Install dependencies make analyze # Run PHPStan static analysis (level 6) make style # Check coding style (ECS / PSR-12) make style-fix # Auto-fix coding style issues make coveralls # Run tests with coverage make check-coverage # Check test coverage of changed files
License
This project is licensed under the MIT License.