Mono is a minimal, modern PHP framework in a single file.
It contains no custom implementations, but rather acts as a wrapper around the following great projects:
- Routing (using nikic/FastRoute)
- Dependency injection (using php-di/php-di)
- Middlewares (using relay/relay)
- Templating (using twigphp/wig)
- Data-to-object mapping (using cuyz/valinor)
This is the interface of the Mono
class:
interface MonoInterface
{
// Render a Twig template
public function render(string $template, array $data): string;
// Add a middleware to the PSR-15 stack
public function addMiddleware(MiddlewareInterface|callable $middleware): void;
// Add a route to the FastRoute router
public function addRoute(string|array $method, string $path, callable $handler): void;
// Run the app
public function run(): void;
}
Using just these methods, we have all the tools to build our application:
<?php
$mono = new Mono(__DIR__ . '/templates');
$mono->addRoute('GET', '/example', function() {
$result = $mono->get(SomeDependency::class)->doSomething();
return new HtmlResponse($mono->render('example.twig', [
'result' => $result
]));
});
$mono->run();
Mono is not intended as a full-fledged framework, but rather as a proof-of-concept for small PHP apps. The goal is to show how far you can go by combining well-known libraries & PSR implementations.
The source code has comments and is easy to read.
You use $mono->addRoute()
to add all your routes. Same method signature as the underlying FastRoute method. Route handlers are closures by default, since this is mainly intended as a framework for small apps, but you can use invokable controllers as well.
Read about the route pattern in the FastRoute documentation. The entered path is passed directly to FastRoute.
The first argument to the closure is the always current request, which is a PSR-7 ServerRequestInterface object. After that, the next arguments are the route parameters.
When $mono->run()
is called, the current request is matched against the routes you added, the closure is invoked and the response is emitted.
<?php
$mono = new Mono();
$mono->addRoute('GET', '/books/{book}', function(ServerRequestInterface $request, string $book) {
return new TextResponse(200, 'Book: ' . $book);
});
$mono->run();
<?php
class BookController
{
public function __construct(
private readonly Mono $mono
) {
}
public function __invoke(ServerRequestInterface $request, string $book): ResponseInterface
{
return new TextResponse('Book: ' . $book');
}
}
<?php
$mono = new Mono();
// By fetching the controller from the container, it will autowire all constructor parameters.
$mono->addRoute('GET', '/books/{book}', $mono->get(BookController::class));
$mono->run();
Caching your routes in production is trivial. Pass a valid, writeable path for your cache file as a routeCacheFile
parameter to your Mono
object.
$mono = new Mono(routeCacheFile: sys_get_temp_dir() . '/mono-route.cache');
When a Mono object is created, it constructs a basic PHP-DI container with default configuration. This means that any loaded classes (for example through PSR-4) can be autowired or pulled from the container manually.
You can fetch instances from the container with the get()
method on your Mono object.
<?php
$mono = new Mono();
$mono->addRoute('GET', '/example', function() use ($mono) {
$result = $mono->get(SomeDependency::class)->doSomething();
return new JsonResponse($result);
});
$mono->run();
If you need to define custom definitions, you can pass a custom container to the Mono constructor. See the PHP-DI documentation for more information.
<?php
// Custom container
$builder = new DI\ContainerBuilder();
$builder->... // Add some custom definitions
$container = $builder->build();
$mono = new Mono(container: $container);
$mono->addRoute('GET', '/example', function() use ($mono) {
$result = $mono->get(SomeDependency::class)->doSomething();
return new JsonResponse($result);
});
$mono->run();
Mono is built as a middleware stack application. The default flow is:
- Error handling
- Routing (route is matched to a handler)
- Your custom middlewares
- Request handling (the route handler is invoked)
You can add middleware to the stack with the addMiddleware()
method. Middleware are either a callable or a class implementing the MiddlewareInterface
interface. The middleware are executed in the order they are added.
<?php
$mono = new Mono();
$mono->addMiddleware(function (ServerRequestInterface $request, callable $next) {
// Do something before the request is handled
if ($request->getUri()->getPath() === '/example') {
return new TextResponse('Forbidden', 403);
}
return $next($request);
});
$mono->addMiddleware(function (ServerRequestInterface $request, callable $next) {
$response = $next($request);
// Do something after the request is handled
return $response->withHeader('X-Test', 'Hello, world!');
});
You can find a bunch of great PSR-15 compatible middlewares already written in the middlewares/psr15-middlewares project. These can be plugged into Mono and used straight away.
If you want to use Twig, you have to pass the path to your templates folder in the Mono constructor.
Afterward, you can use the render()
method on your Mono object to render a Twig template from that folder.
<?php
$mono = new Mono(__DIR__ . '/templates');
$mono->addRoute('GET', '/example', function() use ($mono) {
$result = $mono->get(SomeDependency::class)->doSomething();
return new HtmlResponse($mono->render('example.twig', [
'result' => $result
]));
});
$mono->run();
This allows you to map data (for example, from the request POST body) to an object (for example, a DTO):
<?php
class BookDataTransferObject
{
public function __construct(
public string $title,
public ?int $rating,
) {
}
}
$_POST['title'] = 'Moby dick';
$_POST['rating'] = 10;
$mono = new Mono();
$mono->addRoute('POST', '/book', function (
ServerRequestInterface $request
) use ($mono) {
/*
* $bookDataTransferObject now holds
* all the data from the request body,
* mapped to the properties of the class.
*/
$bookDataTransferObject = $mono->get(TreeMapper::class)->map(
BookDataTransferObject::class,
$request->getParsedBody()
);
});
$mono->run();
If you want to override the default mapping behaviour, define a custom Treemapper
implementation and set it in the container you pass to the Mono
constructor.
Example of a custom mapper config:
$customMapper = (new MapperBuilder())
->enableFlexibleCasting()
->allowPermissiveTypes()
->mapper();
$containerBuilder = new ContainerBuilder();
$containerBuilder->useAutowiring(true);
$containerBuilder->addDefinitions([
TreeMapper::class => $customMapper
]);
$mono = new Mono(
container: $containerBuilder->build()
);
Mono has a debug mode that will catch all errors by default and show a generic 500 response.
When developing, you can disable this mode by passing false
as the second argument to the Mono constructor. This will show the actual error messages and allow you to use dump
inside your Twig templates.
Getting started with a new project is fast. Follow these steps:
- Create a new folder for your project.
- Run
composer require sanderdlm/mono
. - Create a
public
folder in the root of your project. Add anindex.php
file. There is a "Hello world" example below. - Optionally, create a
templates
folder in the root of your project. Add ahome.twig
file. There is an example below. - Run
php -S localhost:8000 -t public
to start the built-in PHP server. - Start developing your idea!
public/index.php
:
<?php
declare(strict_types=1);
use Mono\Mono;
require_once __DIR__ . '/../vendor/autoload.php';
$mono = new Mono(__DIR__.'/../templates');
$mono->addRoute('GET', '/', function() {
return new HtmlResponse($mono->render('home.twig', [
'message' => 'Hello world!',
]));
});
$mono->run();
templates/home.twig
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Home</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
{{ message }}
</body>
</html>
If you're planning to keep things simple, you can work straight in your index.php. If you need to define multiple files/classes, you can add a src
folder and add the following PSR-4 autoloading snippet to your composer.json
:
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
You can now access all of your classes in the src
folder from your DI container (and autowire them!).
The following packages work really well with Mono. Most of them are quickly installed through Composer and then configured by adding a definition to the container.
- vlucas/phpdotenv for environment variables
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
- symfony/validator for validation of the request DTOs
// Custom container
$containerBuilder = new \DI\ContainerBuilder();
$containerBuilder->useAutowiring(true);
$containerBuilder->addDefinitions([
// Build & pass a validator
ValidatorInterface::class => Validation::createValidatorBuilder()
->enableAttributeMapping()
->getValidator(),
]);
$mono = new Mono(
container: $containerBuilder->build()
);
// Generic sign-up route
$mono->addRoute('POST', '/sign-up', function(
ServerRequestInterface $request
) use ($mono) {
// Build the DTO
$personDataTransferObject = $mono->get(TreeMapper::class)->map(
PersonDataTransferObject::class,
$request->getParsedBody()
);
// Validate the DTO
$errors = $mono->get(ValidatorInterface::class)->validate($personDataTransferObject);
// Do something with the errors
if ($errors->count() > 0) {
}
});
- brefphp/bref for deploying to AWS Lambda
- symfony/translation for implementing i18n (symfony/twig-bridge also recommended)
// Create a new translator, with whatever config you like.
$translator = new Translator('en');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', [
'hello_world' => 'Hello world!',
], 'en');
// Create a custom container & add your translator to it
$containerBuilder = new \DI\ContainerBuilder();
$containerBuilder->useAutowiring(true);
$containerBuilder->addDefinitions([
TranslatorInterface::class => $translator,
]);
$mono = new Mono(
container: $containerBuilder->build()
);
/*
* Use the |trans filter in your Twig templates,
* or get the TranslatorInterface from the container
* and use it directly in your route handlers.
*/