Микросервисы на PHP: архитектура и практика продвинутого уровня

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

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

Начать курс

Микросервисы на PHP: от теории к продакшену

Переход от монолита к микросервисной архитектуре на PHP — это не просто технологический апгрейд, а фундаментальное изменение мышления разработчика. На продвинутом уровне мы сталкиваемся с реальными вызовами: распределенные транзакции, консистентность данных, латентность сети и сложность отладки. В этой статье мы разберем ключевые паттерны и практические приемы, которые помогут построить надежную и масштабируемую систему на PHP.

1. Коммуникация между сервисами: синхронная vs асинхронная

Выбор протокола общения — первое, с чем сталкивается архитектор. В PHP-экосистеме есть два основных подхода.

Синхронная связь через REST/gRPC

Идеально для операций, требующих немедленного ответа (например, проверка баланса пользователя). Однако создает жесткие зависимости: если сервис B упал, сервис A тоже ляжет. Рекомендуется использовать паттерн Circuit Breaker.

// Пример Circuit Breaker на PHP с использованием библиотеки Guzzle
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;

class CircuitBreaker {
    private int $failureCount = 0;
    private int $threshold = 3;
    private int $timeout = 30; // секунды
    private bool $open = false;
    private int $lastFailureTime = 0;

    public function call(string $url): ?array {
        if ($this->isOpen()) {
            return null; // Быстрое замыкание
        }

        try {
            $client = new Client(['timeout' => 2.0]);
            $response = $client->get($url);
            $this->reset();
            return json_decode($response->getBody(), true);
        } catch (ConnectException $e) {
            $this->recordFailure();
            return null;
        }
    }

    private function isOpen(): bool {
        if (!$this->open) return false;
        if (time() - $this->lastFailureTime > $this->timeout) {
            $this->open = false; // Полуоткрытое состояние
            return false;
        }
        return true;
    }

    private function recordFailure(): void {
        $this->failureCount++;
        $this->lastFailureTime = time();
        if ($this->failureCount >= $this->threshold) {
            $this->open = true;
        }
    }

    private function reset(): void {
        $this->failureCount = 0;
        $this->open = false;
    }
}

Асинхронная связь через очереди (RabbitMQ, Kafka)

Для операций, которые не требуют мгновенного ответа (отправка email, генерация отчетов). Используйте библиотеку php-amqplib.

// Публикация события в RabbitMQ
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();

$channel->queue_declare('order_created', false, true, false, false);

$data = json_encode([
    'order_id' => 12345,
    'user_id' => 678,
    'total' => 1500.00
]);
$msg = new AMQPMessage($data, ['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]);
$channel->basic_publish($msg, '', 'order_created');

$channel->close();
$connection->close();

2. Управление данными: CQRS и Event Sourcing

В микросервисах каждая служба должна владеть своими данными. Но что делать, если нужно объединить данные из разных сервисов для отчета? На помощь приходит паттерн CQRS (Command Query Responsibility Segregation).

Реализация простого CQRS на PHP

Разделяем команды (запись) и запросы (чтение). Для записи используем Doctrine ORM, для чтения — прямые SQL-запросы к read-модели.

// Команда: создание заказа
class CreateOrderCommand {
    public function __construct(
        public readonly int $userId,
        public readonly array $items,
        public readonly float $total
    ) {}
}

// Обработчик команды
class CreateOrderHandler {
    public function __construct(
        private EntityManagerInterface $em,
        private EventBus $eventBus
    ) {}

    public function handle(CreateOrderCommand $command): void {
        $order = new Order($command->userId, $command->items, $command->total);
        $this->em->persist($order);
        $this->em->flush();

        // Публикуем событие для синхронизации read-модели
        $this->eventBus->publish(new OrderCreatedEvent($order->getId()));
    }
}

// Запрос: получение агрегированных данных
class OrderQueryService {
    public function __construct(private Connection $readDb) {}

    public function getMonthlyRevenue(int $year, int $month): float {
        $sql = "SELECT SUM(total) FROM orders WHERE YEAR(created_at) = :year AND MONTH(created_at) = :month";
        return (float) $this->readDb->fetchOne($sql, ['year' => $year, 'month' => $month]);
    }
}

Event Sourcing — это хранение не текущего состояния, а последовательности событий. В PHP для этого удобно использовать библиотеку prooph/event-sourcing.

// Пример агрегата с Event Sourcing
use Prooph\EventSourcing\AggregateChanged;
use Prooph\EventSourcing\AggregateRoot;

class Account extends AggregateRoot {
    private string $accountId;
    private float $balance = 0;

    public static function open(string $accountId, float $initialBalance): self {
        $self = new self();
        $self->recordThat(AccountOpened::occur($accountId, [
            'initial_balance' => $initialBalance
        ]));
        return $self;
    }

    public function withdraw(float $amount): void {
        if ($this->balance < $amount) {
            throw new \DomainException('Insufficient funds');
        }
        $this->recordThat(MoneyWithdrawn::occur($this->accountId, [
            'amount' => $amount
        ]));
    }

    protected function apply(AggregateChanged $event): void {
        match (get_class($event)) {
            AccountOpened::class => $this->applyAccountOpened($event),
            MoneyWithdrawn::class => $this->applyMoneyWithdrawn($event),
            default => null
        };
    }

    private function applyAccountOpened(AccountOpened $event): void {
        $this->accountId = $event->aggregateId();
        $this->balance = $event->                

Блоги

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