[Из песочницы] Используем аннотации в PHP по максимуму

Будучи back-end разработчиком, я всеми фибрами своей души люблю микросервисные архитектуры, но еще больше, люблю разрабатывать микросервисы. При разработке, чего бы то ни было, я придерживаюсь одного простого принципа — минимализм. Под минимализмом я подразумеваю простую истину: код должен быть максимально «прозрачным», его должно быть минимум (идеальный код — код которого нет), а посему, я делаю ставку в пользу аннотаций.

В настоящей статье, я представлю вашему вниманию скелет для будущих приложений, в котором аннотации используются для решения следующих задач:

Такой скелет будет основан на следующих пакетах:

Также, такой скелет, будет основан на пакетах придерживающихся следующих PSR-рекомендаций:

В настоящей статье, я не стану рассказывать, что такое composer, про избитую «луковицу», и уж тем более о PSR, предполагается, что вам все это уже знакомо в той или иной степени.


Лень читать, что там?
composer create-project sunrise/awesome-skeleton app

wheel bike


Контроллеры

Как правило, вне зависимости от того, какие инструменты мы используем, у нас всегда маршрутизация отдельно, контроллеры отдельно, я сейчас не беру в счет Symfony Routing, это немного про другое.


Маршрутизация в контроллерах

Рассмотрим пример ниже:

declare(strict_types=1);
namespace App\Http\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\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"
 *   }
 * )
 */
class ResourceUpdateController implements MiddlewareInterface
{
    /**
     * {@inheritDoc}
     */
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler) : ResponseInterface
    {
        $response = $handler->handle($request);

        // some code

        return $response;
    }
}

Вы наверняка обратили внимание, что контроллер является промежуточным ПО, как и в Zend Expressive, более того, через аннотации можно указать какие промежуточные ПО будут запущены перед запуском настоящего контроллера, а какие после.

Аннотация @­Route может содержать следующие свойства:


  • id — ID маршрута
  • path — правило пути маршрута
  • methods — допустимые HTTP методы маршрута
  • before — промежуточные ПО которые запустятся перед запуском контроллера
  • after — промежуточные ПО которые запустятся после запуска контроллера

Путь маршрута содержит привычную для всех конструкцию {id}, однако для валидации атрибута необходимо задать регулярное выражение {id<\d+>}, а если необходимо сделать часть пути необязательной, ее достаточно взять в скобки /resource/{action}(/{id}).

Стоит отметить, что свойства аннотации проходят строгую валидацию, вы сразу будете уведомлены, если ваши регулярные выражения не корректны, или промежуточное ПО не существует, и т.п.


Инъекция зависимостей в контроллерах

Рассмотрим пример ниже:

declare(strict_types=1);
namespace App\Http\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;

/**
 * @Route(
 *   id="resource.update",
 *   path="/resource/{id<\d+>}",
 *   methods={"PATCH"}
 * )
 */
class ResourceUpdateController implements MiddlewareInterface
{
    /**
     * @Inject
     * @var LoggerInterface
     */
    protected $logger;

    /**
     * {@inheritDoc}
     */
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler) : ResponseInterface
    {
        $this->logger->debug('foo bar');

        $response = $handler->handle($request);

        // some code

        return $response;
    }
}

Нет ничего проще, чем получить из контейнера ту или иную зависимость…


Регистрация контроллера в приложении

Вам достаточно просто создать контроллер, остальное приложение сделает за вас, обнаружит такой контроллер, передаст его маршрутизатору, который запустит его при необходимости… Что может быть проще? Главное, что от вас требуется из коробки, создавать контроллеры в директории src/Http/Controller.

wheel bike


Модели

Если вы работали с Doctrine ORM и Symfony Validator, для вас ничего интересного, за исключением того, что все настроено из коробки, для остальных представлю некоторые примеры. Сразу необходимо обозначить, что модели должны создаваться в директории src/Entity и наследовать App\Entity\AbstractEntity.


Простой пример модели

Рассмотрим пример ниже:

declare(strict_types=1);
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 * @ORM\Table(name="resource")
 */
class Resource extends AbstractEntity
{
    /**
     * @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
}

Опять же, нет необходимости где бы то ни было регистрировать нашу модель, нет необходимости описывать где-то валидацию, или схему таблиц, все в одном месте, просто и понятно. Единственное, что необходимо сделать, чтобы таблица resource создалась, запустить из корня приложения следующий скрипт:

composer db:update

Настройки подключения к базе данных находятся в файле: src/config/environment.php


Простой пример использования модели в контроллере

declare(strict_types=1);
namespace App\Http\Controller;

use App\Entity\Resource;
use Doctrine\ORM\EntityManager;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
 * @Route(
 *   id="resource.create",
 *   path="/resource",
 *   methods={"POST"}
 * )
 */
class ResourceCreateController implements MiddlewareInterface
{
    /**
     * @Inject
     *
     * @var EntityManager
     */
    protected $entityManager;

    /**
     * {@inheritDoc}
     */
    public function process(
        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);
    }
}

Аннотация @­Assert отвечает за валидацию, сама логика валидации описана в наследуемом моделью классе AbstractEntity.

Реализация несет демонстрационный характер, целью автора является сократить кол-во строк кода…

wheel bike


Настройки приложения


Файл Описание
config/cli-config.php Doctrine CLI
config/container.php PHP-DI
config/definitions.php Зависимости приложения
config/environment.php Конфигурация окружения приложения


Свои зависимости

Для добавления новой зависимости в приложение, достаточно открыть файл config/definitions.php, и добавить новую зависимость по аналогии с уже существующими, после чего она станет доступна через инъекции, как в примерах настоящей статьи.


Рекомендации

После установки скелета, рекомендуется добавить файл config/environment.php в .gitignore, а сам файл как пример продублировать с новым именем:

cp config/environment.php config/environment.php.example

Можно пойти другим путем, заполнив настройки окружения из .env задействовав пакет symfony/dotenv.

wheel bike

В игру «зачем писать это, когда есть это» можно играть бесконечно, но как бы там ни было, open-source все стерпит…

Для установки скелета воспользуйтесь следующей командой:

composer create-project sunrise/awesome-skeleton app

Для изучения исходного кода скелета воспользуйтесь ссылкой: sunrise/awesome-skeleton.


Отдельное спасибо хочется выразить Оскару Отеро за его вклад в open-source, в особенности за прекрасную подборку Awesome PSR-15 Middleware, часть из которой интегрирована в sunrise/awesome-skeleton.

© Habrahabr.ru