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
- 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: ^11.1 || ^12.0 || ^13.0
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/dolphin:^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.
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.