We use annotations in PHP to the maximum
Being a back-end developer, I love microservice architectures with all the fibers of my soul, but even more, I like to develop microservices. When developing, whatever, I adhere to one simple principle - minimalism. By minimalism, I mean a simple truth: the code should be as transparent as possible, it should be at a minimum (the ideal code is the one that does not exist), and therefore I bet in favor of annotations.
In this article, I will bring to your attention a skeleton for future applications, in which annotations are used to solve the following tasks:
- Routing in controllers
- Dependency injection in controllers and not only
- Description of database schema in models
- Validation of model properties
Such a skeleton will be based on the following packages:
Also, such a skeleton will be based on packages adhering to the following PSR recommendations:
In this article, I will not tell you what a composer is, about a beaten "onion," and even more so about PSR, it is assumed that you already know all this in one degree or another.
composer create-project sunrise/awesome-skeleton app
Controllers
As a rule, regardless of what tools we use, we always have routing separately, controllers separately, I don’t take account of Symfony Routing right now, this is a little about something else.
Routing in controllers
Consider the example below:
declare(strict_types=1);
namespaceApp\Http\Controller;
usePsr\Http\Message\ResponseInterface;
usePsr\Http\Message\ServerRequestInterface;
usePsr\Http\Server\MiddlewareInterface;
usePsr\Http\Server\RequestHandlerInterface;
/**
* @Route(
* id="resource.update",
* path="/resource/{id<\d+>}",
* methods={"PATCH"},
* before={
* "App\Http\Middleware\FooMiddleware",
* "App\Http\Middleware\BarMiddleware"
* },
* after={
* "App\Http\Middleware\BazMiddleware",
* "App\Http\Middleware\QuxMiddleware"
* }
* )
*/classResourceUpdateControllerimplementsMiddlewareInterface{
/**
* {@inheritDoc}
*/publicfunctionprocess(
ServerRequestInterface $request,
RequestHandlerInterface $handler) : ResponseInterface{
$response = $handler->handle($request);
// some codereturn $response;
}
}
You probably noticed that the controller is middleware, as in Zend Expressive, moreover, through the annotations, you can specify which middleware will be launched before the launch of this controller, and which after.
The @Route annotation may contain the following properties:
- id - route ID
- path - route path rule
- methods - valid HTTP route methods
- before - middleware that will run before the controller starts
- after - middleware that will run after controller startup
The route path contains the usual for all {id} construct , however, to validate an attribute, you must specify a regular expression {id <\ d +>} , and if you need to make part of the path optional, you just have to put it in brackets / resource / {action} (/ {id }) .
It is worth noting that the properties of the annotation are strictly validated, you will be notified immediately if your regular expressions are not correct, or the middleware does not exist, etc.
Dependency Injection in Controllers
Consider the example below:
declare(strict_types=1);
namespaceApp\Http\Controller;
usePsr\Http\Message\ResponseInterface;
usePsr\Http\Message\ServerRequestInterface;
usePsr\Http\Server\MiddlewareInterface;
usePsr\Http\Server\RequestHandlerInterface;
usePsr\Log\LoggerInterface;
/**
* @Route(
* id="resource.update",
* path="/resource/{id<\d+>}",
* methods={"PATCH"}
* )
*/classResourceUpdateControllerimplementsMiddlewareInterface{
/**
* @Inject
* @var LoggerInterface
*/protected $logger;
/**
* {@inheritDoc}
*/publicfunctionprocess(
ServerRequestInterface $request,
RequestHandlerInterface $handler) : ResponseInterface{
$this->logger->debug('foo bar');
$response = $handler->handle($request);
// some codereturn $response;
}
}
There is nothing easier than getting some kind of dependency from a container ...
Registering the controller in the application
You just need to create a controller, the rest of the application will do for you, find such a controller, pass it to the router, which will launch it if necessary ... What could be simpler? The main thing that is required of you out of the box, create controllers in the src / Http / Controller directory .
Models
If you worked with Doctrine ORM and Symfony Validator, nothing interesting for you, except that everything is configured out of the box, for the rest I will present some examples. Immediately it is necessary to designate that the models should be created in the src / Entity directory and inherit by App \ Entity \ AbstractEntity .
A simple example of a model
Consider the example below:
declare(strict_types=1);
namespaceApp\Entity;
useDoctrine\ORM\MappingasORM;
useSymfony\Component\Validator\ConstraintsasAssert;
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
* @ORM\Table(name="resource")
*/classResourceextendsAbstractEntity{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*
* @var null|int
*/protected $id;
/**
* @ORM\Column(
* type="string",
* length=128,
* nullable=false
* )
*
* @Assert\NotBlank
* @Assert\Type("string")
* @Assert\Length(max=128)
*
* @var null|string
*/protected $title;
/**
* @ORM\Column(
* type="text",
* nullable=false
* )
*
* @Assert\NotBlank
* @Assert\Type("string")
*
* @var null|string
*/protected $content;
// setters and getters
}
Again, there is no need to register our model anywhere, there is no need to describe somewhere a validation, or a schema of tables, all in one place, simple and clear. The only thing that needs to be done in order for the resource table to be created is to run the following script from the application root:
composer db:update
Database connection settings are in the file: src / config / environment.php
A simple example of using the model in the controller
declare(strict_types=1);
namespaceApp\Http\Controller;
useApp\Entity\Resource;
useDoctrine\ORM\EntityManager;
usePsr\Http\Message\ResponseInterface;
usePsr\Http\Message\ServerRequestInterface;
usePsr\Http\Server\MiddlewareInterface;
usePsr\Http\Server\RequestHandlerInterface;
/**
* @Route(
* id="resource.create",
* path="/resource",
* methods={"POST"}
* )
*/classResourceCreateControllerimplementsMiddlewareInterface{
/**
* @Inject
*
* @var EntityManager
*/protected $entityManager;
/**
* {@inheritDoc}
*/publicfunctionprocess(
ServerRequestInterface $request,
RequestHandlerInterface $handler) : ResponseInterface{
$data = (array) $request->getParsedBody();
$response = $handler->handle($request);
$resource = new Resource();
$resource->setTitle($data['title'] ?? null);
$resource->setContent($data['content'] ?? null);
$violations = $resource->validate();
if ($violations->count() > 0) {
return $response->withStatus(400);
}
$this->entityManager->persist($resource);
$this->entityManager->flush();
return $response->withStatus(201);
}
}
The @Assert annotation is responsible for validation, the validation logic itself is described in the AbstractEntity class inherited by the model .
The implementation is of a demonstration nature, the purpose of the author is to reduce the number of lines of code ...
Application settings
File | Description |
---|---|
config / cli-config.php | Doctrine CLI |
config / container.php | Php di |
config / definitions.php | Application dependencies |
config / environment.php | Application Environment Configuration |
Your dependencies
To add a new dependency to the application, just open the file config / definitions.php and add a new dependency by analogy with the existing ones, after which it will be available through injections, as in the examples of this article.
Recommendations
After installing the skeleton, it is recommended to add the file config / environment.php to .gitignore , and the file itself as an example to duplicate with the new name:
cp config/environment.php config/environment.php.example
You can go the other way by filling out the environment settings from .env using the symfony / dotenv package .
The game “why write it when you have it” can be played endlessly, but be that as it may, open-source can endure everything ...
To install the skeleton, use the following command:
composer create-project sunrise/awesome-skeleton app
To explore the source code of the skeleton, use the link: sunrise / awesome-skeleton .
I would like to express special thanks to Oscar Otero for his contribution to open-source, especially for the excellent selection of Awesome PSR-15 Middleware , some of which are integrated into sunrise / awesome-skeleton .