https://github.com/sanderdlm/mono
Mono is a tiny, single-class PHP framework featuring FastRoute, PHP-DI, PSR-7 and Twig
https://github.com/sanderdlm/mono
Last synced: over 1 year ago
JSON representation
Mono is a tiny, single-class PHP framework featuring FastRoute, PHP-DI, PSR-7 and Twig
- Host: GitHub
- URL: https://github.com/sanderdlm/mono
- Owner: sanderdlm
- License: mit
- Created: 2023-11-21T17:20:03.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2024-05-14T19:59:34.000Z (about 2 years ago)
- Last Synced: 2025-03-20T15:34:25.665Z (over 1 year ago)
- Language: PHP
- Homepage:
- Size: 48.8 KB
- Stars: 7
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Mono
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:
1. Routing (using [nikic/FastRoute](https://github.com/nikic/FastRoute))
2. Dependency injection (using [php-di/php-di](https://github.com/PHP-DI/PHP-DI))
3. Middlewares (using [relay/relay](https://github.com/relayphp/Relay.Relay))
4. Templating (using [twigphp/wig](https://github.com/twigphp/Twig))
5. Data-to-object mapping (using [cuyz/valinor](https://github.com/CuyZ/Valinor))
This is the interface of the `Mono` class:
```php
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
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](https://github.com/sanderdlm/mono/blob/main/src/Mono.php) has comments and is easy to read.
## 1. Routing
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](https://github.com/nikic/FastRoute#defining-routes). 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](https://github.com/php-fig/http-message/blob/master/src/ServerRequestInterface.php) 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.
### 1.1 Example with closure
```php
addRoute('GET', '/books/{book}', function(ServerRequestInterface $request, string $book) {
return new TextResponse(200, 'Book: ' . $book);
});
$mono->run();
```
### 1.2 Example with controller
```php
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.
```php
$mono = new Mono(routeCacheFile: sys_get_temp_dir() . '/mono-route.cache');
```
## 2. Dependency injection
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
addRoute('GET', '/example', function() use ($mono) {
$result = $mono->get(SomeDependency::class)->doSomething();
return new JsonResponse($result);
});
$mono->run();
```
### Custom container
If you need to define custom definitions, you can pass a custom container to the Mono constructor. See [the PHP-DI documentation](https://php-di.org/doc/getting-started.html#2-create-the-container) for more information.
```php
... // 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();
```
## 3. Middleware
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
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](https://github.com/middlewares/psr15-middlewares) project. These can be plugged into Mono and used straight away.
## 4. Templating
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
addRoute('GET', '/example', function() use ($mono) {
$result = $mono->get(SomeDependency::class)->doSomething();
return new HtmlResponse($mono->render('example.twig', [
'result' => $result
]));
});
$mono->run();
````
## 5. Data-to-object mapping
This allows you to map data (for example, from the request POST body) to an object (for example, a DTO):
```php
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:
```php
$customMapper = (new MapperBuilder())
->enableFlexibleCasting()
->allowPermissiveTypes()
->mapper();
$containerBuilder = new ContainerBuilder();
$containerBuilder->useAutowiring(true);
$containerBuilder->addDefinitions([
TreeMapper::class => $customMapper
]);
$mono = new Mono(
container: $containerBuilder->build()
);
```
## Other
### Debug mode
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.
### Folder structure & project setup
Getting started with a new project is fast. Follow these steps:
1. Create a new folder for your project.
2. Run `composer require sanderdlm/mono`.
3. Create a `public` folder in the root of your project. Add an `index.php` file. There is a "Hello world" example below.
4. Optionally, create a `templates` folder in the root of your project. Add a `home.twig` file. There is an example below.
5. Run `php -S localhost:8000 -t public` to start the built-in PHP server.
6. Start developing your idea!
`public/index.php`:
```php
addRoute('GET', '/', function() {
return new HtmlResponse($mono->render('home.twig', [
'message' => 'Hello world!',
]));
});
$mono->run();
```
`templates/home.twig`:
```twig
Home
{{ message }}
```
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`:
```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!).
### Extra packages
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](https://github.com/vlucas/phpdotenv) for environment variables
```php
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
```
- [symfony/validator](https://symfony.com/doc/current/components/validator.html) for validation of the request DTOs
```php
// 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](https://github.com/brefphp/bref) for deploying to AWS Lambda
- [symfony/translation](https://symfony.com/doc/current/components/validator.html) for implementing i18n ([symfony/twig-bridge](https://github.com/symfony/twig-bridge) also recommended)
```php
// 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.
*/
```