Как навести порядок в PHP-проекте: Hexagonal архитектура без боли

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Когда ваш PHP-монолит начинает трещать по швам

Вы когда-нибудь оказывались в ситуации, когда смена базы данных с MySQL на PostgreSQL превращалась в недельный квест? Или когда нужно было добавить новый API-клиент, а код был так переплетён с фреймворком, что проще было написать всё с нуля? Знакомо?

Большинство PHP-разработчиков рано или поздно сталкиваются с «лапшой» в контроллерах и моделях. Фреймворки вроде Laravel или Symfony дают удобные инструменты, но они же и навязывают жёсткую связь с собой. Выход есть — Hexagonal архитектура (она же «порты и адаптеры», она же гексагональная архитектура).

В этой статье мы разберём, как построить приложение на PHP, которое не зависит от фреймворка, базы данных или внешних сервисов. Вы увидите примеры кода, которые можно сразу применить в своём проекте. Поехали!

Что такое Hexagonal архитектура и зачем она вам?

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

Главная идея: ядро приложения ничего не знает о внешнем мире. Оно не импортирует Laravel, не знает про PDO и не вызывает curl. Вся зависимость направлена внутрь — адаптеры зависят от портов, а не наоборот.

Плюсы такого подхода очевидны:

  • Тестируемость: бизнес-логику можно тестировать без базы данных и HTTP-запросов.
  • Гибкость: замена БД, очереди или фреймворка — это просто написание нового адаптера.
  • Чистота кода: вы не смешиваете «что делать» с «как делать».

Звучит как магия? Давайте посмотрим на код.

Структура проекта: базовый скелет

Для примера возьмём простой сценарий: регистрация пользователя. Нам нужно сохранить пользователя в БД и отправить приветственное письмо.

Вот как будет выглядеть типичная структура директорий:

src/
├── Domain/
│   ├── Entity/
│   │   └── User.php
│   ├── Repository/
│   │   └── UserRepositoryInterface.php
│   ├── Service/
│   │   └── UserRegistrationService.php
│   └── Event/
│       └── UserRegisteredEvent.php
├── Infrastructure/
│   ├── Persistence/
│   │   └── DoctrineUserRepository.php
│   ├── Mail/
│   │   └── SymfonyMailerAdapter.php
│   └── Controller/
│       └── RegisterUserController.php
└── Application/
    └── UseCase/
        └── RegisterUserUseCase.php

Обратите внимание: Domain — это чистая бизнес-логика. Infrastructure — адаптеры. Application — сценарии использования (use cases).

Шаг 1: Ядро — домен и порты

Сущность (Entity)

Начнём с самого простого — пользователя. Это просто класс с данными, без зависимостей.

<?php
namespace App\Domain\Entity;

class User
{
    public function __construct(
        private readonly string $id,
        private string $name,
        private string $email
    ) {}

    public function getId(): string
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getEmail(): string
    {
        return $this->email;
    }
}

Порт — интерфейс репозитория

Теперь определим, как мы будем сохранять пользователя. Это порт — интерфейс, который будет реализован адаптером.

<?php
namespace App\Domain\Repository;

use App\Domain\Entity\User;

interface UserRepositoryInterface
{
    public function save(User $user): void;
    public function findByEmail(string $email): ?User;
}

Порт — интерфейс почтового сервиса

Аналогично для отправки писем:

<?php
namespace App\Domain\Service;

use App\Domain\Entity\User;

interface MailerInterface
{
    public function sendWelcomeEmail(User $user): void;
}

Шаг 2: Use Case — сценарий регистрации

В центре архитектуры находится use case. Он оркестрирует бизнес-логику, используя порты. Обратите внимание: он не знает, какая именно БД или почтовик используется.

<?php
namespace App\Application\UseCase;

use App\Domain\Entity\User;
use App\Domain\Repository\UserRepositoryInterface;
use App\Domain\Service\MailerInterface;

class RegisterUserUseCase
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
        private MailerInterface $mailer
    ) {}

    public function execute(string $name, string $email): User
    {
        // Проверка на дубликат
        $existingUser = $this->userRepository->findByEmail($email);
        if ($existingUser !== null) {
            throw new \InvalidArgumentException('User already exists');
        }

        // Создаём пользователя
        $user = new User(uniqid(), $name, $email);
        $this->userRepository->save($user);

        // Отправляем письмо
        $this->mailer->sendWelcomeEmail($user);

        return $user;
    }
}

Вот это и есть сердце приложения. Код не зависит от фреймворка, его можно тестировать в изоляции, подменяя репозиторий и почтовик моками.

Шаг 3: Адаптеры — реализация портов

Репозиторий на Doctrine (ORM)

Теперь реализуем интерфейс. В этом примере используем Doctrine ORM, но вы можете использовать Eloquent или даже файловую систему.

<?php
namespace App\Infrastructure\Persistence;

use App\Domain\Entity\User;
use App\Domain\Repository\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;

class DoctrineUserRepository implements UserRepositoryInterface
{
    public function __construct(
        private EntityManagerInterface $em
    ) {}

    public function save(User $user): void
    {
        $this->em->persist($user);
        $this->em->flush();
    }

    public function findByEmail(string $email): ?User
    {
        return $this->em->getRepository(User::class)->findOneBy(['email' => $email]);
    }
}

Почтовый адаптер на Symfony Mailer

<?php
namespace App\Infrastructure\Mail;

use App\Domain\Entity\User;
use App\Domain\Service\MailerInterface;
use Symfony\Component\Mailer\MailerInterface as SymfonyMailer;
use Symfony\Component\Mime\Email;

class SymfonyMailerAdapter implements MailerInterface
{
    public function __construct(
        private SymfonyMailer $mailer
    ) {}

    public function sendWelcomeEmail(User $user): void
    {
        $email = (new Email())
            ->to($user->getEmail())
            ->subject('Welcome!')
            ->text                

Блоги

Рекомендаций по книгам