Изображение создано TG ботом Kandinsky
Изображение создано TG ботом Kandinsky

Доброго времени суток, я Едифанов Виталий, являюсь CEO своей компании MediaRise, решил собрать распространённые вопросы для интервью PHP Senior разработчика.

Оглавление:

  1. Что такое Идемпотентность. Ключ идемпотентности. Какие HTTP-методы API являются идемпотентные?

  2. Жизненный цикл laravel

  3. Kafka

  4. Redis

  5. Микросервисы. Плюсы и минусы

  6. Core Domain in DDD

  7. Что такое сервис в DDD

  8. DDD: Entity, Value Object. Различие между ними

  9. DDD: Repository. Collection. Куда поместить логику приложения

  10. Event Sourcing

  11. Виды тестирования

  12. Создание библиотеки на PHP

  13. Как добиться отказоустойчивости между Service A и Service B

  14. Проведи рефакторинг данного кода на PHP. Code Review

  15. SELECT FOR UPDATE

Что такое Идемпотентность. Ключ идемпотентности. Какие HTTP-методы API являются идемпотентные?

  1. Что такое Идемпотентность. Ключ идемпотентности. Какие HTTP-методы API являются идемпотентные?

Идемпотентность — это свойство операций в вычислительных системах, которое означает, что многократное применение одной и той же операции к одной и той же системе приведет к тому же результату, что и однократное применение. Иными словами, если операция идемпотентна, её повторное выполнение не изменяет результат после первого выполнения.

Примеры идемпотентных операций:

1. HTTP-методы: GET, PUT, DELETE — все они идемпотентны, поскольку повторный запрос не изменяет состояние сервера.

2. Обнуление значения переменной в программе: присваивание `x = 0` всегда установит значение переменной x в 0, независимо от того, сколько раз эта операция выполняется.

Ключ идемпотентности — это механизм, используемый для обеспечения идемпотентности в распределенных системах и API. Ключ идемпотентности представляет собой уникальный идентификатор, который клиент предоставляет при выполнении запроса. Сервер использует этот ключ для отслеживания уже выполненных операций и предотвращения их повторного выполнения.

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

  • Клиент отправляет запрос на создание ресурса с уникальным ключом идемпотентности.

  • Сервер сохраняет этот ключ вместе с результатом выполнения запроса.

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

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

Жизненный цикл laravel

  1. Жизненный цикл laravel

Жизненный цикл запроса в Laravel включает несколько ключевых этапов, которые обеспечивают обработку HTTP-запросов и генерацию ответов. Вот основные этапы жизненного цикла запроса в Laravel:

1. Загрузка Composer Autoload:

   - Laravel использует Composer для управления зависимостями. Первым шагом является загрузка всех классов и файлов, необходимых для работы приложения.

2. Инициализация фреймворка:

   - Фреймворк Laravel загружается, инициализируя основные компоненты, включая конфигурацию, сервис-провайдеры и другие системные настройки.

3. Запуск HTTP-запроса:

   - Приходит HTTP-запрос, и приложение начинает обработку этого запроса.

4. Регистрация сервис-провайдеров:

   - Все сервис-провайдеры, зарегистрированные в приложении, инициализируются и регистрируют свои сервисы в контейнере инверсии зависимостей.

5. Загрузка маршрутов:

   - Laravel загружает маршруты, которые определены в приложении, и пытается сопоставить текущий запрос с одним из них.

6. Обработка запроса:

   - Если маршрут найден, контроллер или замыкание, связанное с этим маршрутом, выполняется. Здесь может происходить вызов моделей, логики обработки данных и т.д.

7. Формирование ответа:

   - После выполнения всех необходимых операций формируется HTTP-ответ, который будет отправлен обратно клиенту. Это может быть HTML-страница, JSON-данные или другой тип контента.

8. Отправка ответа клиенту:

   - Сформированный ответ отправляется обратно клиенту, завершая жизненный цикл запроса.

Рассмотрим пример жизненного цикла запроса на простом маршруте:

1. Маршрут в `routes/web.php`:

<?php

use Illuminate\Support\Facades\Route;

Route::get('/hello', function () {
    return 'Hello, World!';

});

Обработка запроса:

  • Клиент отправляет GET-запрос на URL `/hello`.

  • Laravel загружает и инициализирует все необходимые компоненты.

  • Запрос проходит через стек промежуточного ПО.

  • Laravel ищет маршрут, соответствующий `/hello`, и находит соответствующее замыкание.

  • Выполняется замыкание, которое возвращает строку "Hello, World!".

  • Строка "Hello, World!" упаковывается в HTTP-ответ.

  • Ответ отправляется обратно клиенту.

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

Kafka

  1. Kafka

Kafka — это распределённая потоковая платформа, разработанная LinkedIn и открытая под лицензией Apache. Она используется для создания высокопроизводительных, масштабируемых систем для передачи данных в реальном времени. Основные компоненты Kafka включают продюсеров (producers), которые отправляют данные в темы (topics), брокеров (brokers), которые хранят данные, и консумеров (consumers), которые читают данные из тем.

Основные концепции Kafka:

  1. Topics: Темы являются логическими каналами, через которые проходят данные. Данные записываются в темы и читаются из них.

  2. Producers: Процедуры отправляют данные в темы.

  3. Consumers: Консумеры читают данные из тем.

  4. Brokers: Брокеры хранят данные и управляют их распределением между продюсерами и консумерами.

  5. Partitions: Темы разделены на разделы (partitions) для обеспечения параллельной обработки данных.

  6. Replication: Данные реплицируются для повышения отказоустойчивости.

Пример использования Kafka на PHP:

1. Установите библиотеку `php-rdkafka`, которая является клиентом для Kafka. Вы можете установить её через Composer:

composer require edenhill/php-rdkafka

2. Пример кода для продюсера, отправляющего сообщения в Kafka:

<?php

// Подключаем автозагрузку Composer
require 'vendor/autoload.php';

// Настройка продюсера
$conf = new RdKafka\Conf();
$conf->set('bootstrap.servers', 'localhost:9092');
$producer = new RdKafka\Producer($conf);
$topic = $producer->newTopic("test");

// Отправка сообщения
$message = "Hello, Kafka!";
$topic->produce(RD_KAFKA_PARTITION_UA, 0, $message);

// Ожидание завершения отправки
$producer->flush(10000);

echo "Сообщение отправлено: $message";


3. Пример кода для консумера, читающего сообщения из Kafka:

// Подключаем автозагрузку Composer
require 'vendor/autoload.php';

// Настройка консумера
$conf = new RdKafka\Conf();
$conf->set('group.id', 'myConsumerGroup');
$conf->set('metadata.broker.list', 'localhost:9092');

$consumer = new RdKafka\KafkaConsumer($conf);

// Подписка на тему
$consumer->subscribe(['test']);

// Чтение сообщений
echo "Ожидание сообщений...\n";

while (true) {
    $message = $consumer->consume(120*1000);

    switch ($message->err) {
        case RD_KAFKA_RESP_ERR_NO_ERROR:
            echo "Получено сообщение: " . $message->payload . "\n";
            break;

        case RD_KAFKA_RESP_ERR__PARTITION_EOF:
            echo "Конец раздела, больше нет сообщений\n";
            break;

        case RD_KAFKA_RESP_ERR__TIMED_OUT:
            echo "Таймаут ожидания\n";
            break;

        default:
            echo "Ошибка: " . $message->errstr() . "\n";
            break;
    }
}

Этот пример показывает, как настроить продюсера и консумера для отправки и получения сообщений с использованием Apache Kafka в PHP. Не забудьте запустить Kafka сервер и создать тему перед запуском этих скриптов.

Redis

  1. Redis

Redis (Remote Dictionary Server) — это высокопроизводительная система хранения данных в оперативной памяти с открытым исходным кодом, которая используется как кэш, брокер сообщений и база данных. Вот основные аспекты его работы:

Архитектура и Устройство Redis

  1. In-Memory Хранение: Redis хранит все данные в оперативной памяти, что обеспечивает очень высокую скорость чтения и записи. Данные могут быть периодически сохраняться на диск для обеспечения надежности (с использованием механизмов snapshotting и журналирования команд).

  2. Однопоточный Дизайн: Redis работает в однопоточном режиме, что упрощает проектирование и устранение состояния гонки, но требует от разработчика тщательной оптимизации команд и использования.

  3. Клиент-Серверная Модель: Redis использует модель клиент-сервер, где клиенты взаимодействуют с сервером Redis через сетевые соединения с использованием текстового протокола Redis.

Типы данных

Redis поддерживает различные структуры данных, что делает его очень гибким:

  1. Строки (Strings): Это базовый и самый простой тип данных в Redis. Строки могут содержать до 512 МБ данных. Их можно использовать для хранения текстов, чисел и бинарных данных.

  2. Списки (Lists): Упорядоченные коллекции строк, которые позволяют добавлять элементы в начало или конец списка. Они полезны для реализации очередей и стеков.

  3. Множества (Sets): Неупорядоченные коллекции уникальных строк. Поддерживают операции над множествами, такие как объединение, пересечение и разность.

  4. Упорядоченные множества (Sorted Sets): Множества, где каждый элемент имеет связанный с ним «вес» (score). Элементы упорядочены по этому весу. Это полезно для создания рейтинговых таблиц и временных шкал.

  5. Хэши (Hashes): Наборы пар ключ‑значение, часто используемые для представления объектов и их свойств. Очень полезны для хранения сущностей с множеством атрибутов.

  6. Битмапы (Bitmaps): Манипулирование отдельными битами в строках, что позволяет эффективно использовать память для хранения больших наборов данных.

  7. Гиперлоглоги (HyperLogLogs): Структура данных для приблизительного подсчета уникальных элементов в наборе, использующая очень мало памяти.

  8. Потоки (Streams): Новый тип данных, введенный в Redis 5.0, который позволяет работать с последовательностями записей. Полезно для обработки событий и сообщений.

Сохранение и Устойчивость данных

Redis предлагает два основных механизма для сохранения данных на диск:

  1. Snapshotting (RDB): Redis периодически создает моментальные снимки (снапшоты) данных и сохраняет их на диск. Это метод сохранения состояния базы данных на определенные моменты времени.

  2. Журналирование команд (AOF): Redis сохраняет каждую операцию записи в журнал (Append Only File). Это позволяет восстановить данные до последнего выполненного изменения.

Репликация и Кластеризация

  1. Репликация: Redis поддерживает асинхронную репликацию, где один сервер (мастер) может передавать данные нескольким серверам (репликам). Это улучшает отказоустойчивость и позволяет масштабировать чтение.

  2. Кластеры: Redis поддерживает распределение данных по нескольким узлам кластера, обеспечивая горизонтальное масштабирование. Кластеры используют шардинг для распределения данных и обеспечения их доступности даже при сбоях отдельных узлов.

Использование и Применения

Redis широко используется в различных сценариях:

  • Кэширование: Для уменьшения времени отклика приложений за счет хранения часто запрашиваемых данных в памяти.

  • Сессии: Для хранения пользовательских сессий и токенов аутентификации.

  • Очереди задач: Для реализации очередей задач с использованием списков.

  • Публикация/Подписка (Pub/Sub): Для передачи сообщений между приложениями в режиме реального времени.

  • Аналитика: Для быстрого анализа данных с использованием множеств и упорядоченных множеств.

Redis — это мощный инструмент для работы с данными в реальном времени, который предлагает богатый набор функций и высокую производительность.

Микросервисы. Плюсы и минусы

  1. Микросервисы. Плюсы и минусы

Плюсы микросервисов на PHP

1. Гибкость и масштабируемость:

   - Микросервисы позволяют независимо масштабировать отдельные компоненты системы, что помогает более эффективно управлять ресурсами.

2. Изоляция отказов:

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

3. Автономное развитие и развертывание:

   - Команды могут работать над разными микросервисами независимо друг от друга, что ускоряет разработку и развертывание новых функциональностей.

4. Технологическая независимость:

   - Можно использовать различные технологии и инструменты для каждого микросервиса, что позволяет выбрать наиболее подходящий стек для конкретной задачи.

5. Легкость в понимании и поддержке:

   - Небольшие и специализированные микросервисы легче понять и поддерживать, чем монолитное приложение.

Минусы микросервисов на PHP

1. Сложность инфраструктуры:

   - Микросервисная архитектура требует более сложной инфраструктуры для управления развертыванием, мониторингом и логированием.

2. Повышенные требования к DevOps:

   - Для эффективного управления микросервисами необходимы продвинутые навыки DevOps и использование таких инструментов, как Kubernetes, Docker и CI/CD.

3. Сетевые накладные расходы:

   - Взаимодействие между микросервисами происходит по сети, что может увеличивать задержки и требовать дополнительных ресурсов для обеспечения надежности и безопасности.

4. Управление согласованностью данных:

   - Распределенные данные требуют сложных механизмов для обеспечения согласованности и целостности данных между микросервисами.

5. Повышенные затраты на разработку и тестирование:

   - Разработка микросервисов требует больше времени на проектирование, написание и тестирование, особенно если необходимо обеспечить совместимость между различными микросервисами.

6. Сложности с управлением транзакциями:

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

Заключение

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

Core Domain in DDD

  1. Core Domain in DDD

Core Domain (ядро домена) в Domain-Driven Design (DDD) — это центральная часть бизнес-домена, которая имеет наибольшее стратегическое значение для организации. Основная цель Core Domain — быть основным источником конкурентного преимущества, определяя уникальные аспекты бизнеса, которые делают его успешным. 

В DDD Core Domain выделяется среди других доменов, таких как Supporting Domains (поддерживающие домены) и Generic (Common) Domains (общие домены):

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

  2. Supporting Domain: Эти домены не являются основными источниками конкурентного преимущества, но они поддерживают основной бизнес. Они необходимы для обеспечения бесперебойной работы Core Domain. Например, в том же интернет-магазине поддерживающий домен может включать систему управления складом.

  3. Generic Domain: Это области, которые можно стандартизировать и для которых можно использовать готовые решения. Они не приносят конкурентного преимущества и их лучше всего аутсорсить или использовать сторонние решения. Например, в интернет-магазине к таким доменам могут относиться системы управления платежами.

Основные характеристики Core Domain:

  • Высокая стратегическая значимость: Определяет уникальные возможности и конкурентные преимущества бизнеса.

  • Затраты на поддержку: Требует значительных усилий в разработке и поддержке.

  • Знание и экспертиза: Требует глубокого знания предметной области и тесного взаимодействия с экспертами домена.

Эффективная реализация Core Domain влечет за собой правильное проектирование и архитектуру, что требует тесного сотрудничества между разработчиками и бизнес-экспертами, использование подходов DDD и постоянное совершенствование модели домена.

Что такое сервис в DDD

  1. Что такое сервис в DDD

В контексте предметно-ориентированного проектирования (Domain-Driven Design, DDD), сервис представляет собой объект, который инкапсулирует доменную логику, которая не может быть естественным образом отнесена к отдельной сущности или значению (Value Object). Сервисы используются для реализации операций, которые связаны с несколькими сущностями или требующих сложных вычислений.

Пример на Symfony:

Предположим, у нас есть приложение для управления заказами, где нам нужно вычислить стоимость заказа с учетом различных скидок и налогов.

1. Создание сущностей

Во-первых, определим наши сущности:

Order.php

<?php

namespace App\Entity;

class Order

{

    private int $id;

    private float $subtotal;

    private float $tax;

    private float $discount;

    public function __construct(float $subtotal, float $tax, float $discount)
    {

        $this->subtotal = $subtotal;
        $this->tax = $tax;
        $this->discount = $discount;
    }

    public function getSubtotal(): float
    {
        return $this->subtotal;
    }

    public function getTax(): float
    {
        return $this->tax;
    }

    public function getDiscount(): float
    {
        return $this->discount;
    }

}

2. Создание сервиса домена

Теперь создадим доменный сервис, который будет вычислять итоговую стоимость заказа:

OrderCalculatorService.php

<?php

namespace App\Service;

use App\Entity\Order;

class OrderCalculatorService

{

    public function calculateTotal(Order $order): float

    {
        $subtotal = $order->getSubtotal();
        $tax = $order->getTax();
        $discount = $order->getDiscount();
        $total = $subtotal + ($subtotal * $tax / 100) - $discount;

        return $total;

    }

}

3. Использование сервиса в контроллере

Теперь мы можем использовать наш сервис в контроллере для вычисления итоговой стоимости заказа.

OrderController.php

<?php

namespace App\Controller;

use App\Entity\Order;

use App\Service\OrderCalculatorService;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

use Symfony\Component\HttpFoundation\Response;

use Symfony\Component\Routing\Annotation\Route;

class OrderController extends AbstractController

{

    private OrderCalculatorService $orderCalculatorService;

    public function __construct(OrderCalculatorService $orderCalculatorService)
    {

        $this->orderCalculatorService = $orderCalculatorService;

    }

    /**
     * @Route("/order/total", name="order_total")
     */
    public function calculateOrderTotal(): Response
    {

        $order = new Order(100.0, 10.0, 5.0); // Пример данных для заказа
        $total = $this->orderCalculatorService->calculateTotal($order);
        return new Response('Total order amount: ' . $total);

    }

}

Регистрация сервиса в Symfony

Если вы используете автоконфигурацию, Symfony автоматически зарегистрирует сервис `OrderCalculatorService`. Однако, если вы хотите зарегистрировать его вручную, добавьте следующую строку в `config/services.yaml`:

services:
    App\Service\OrderCalculatorService: ~

Теперь у вас есть полностью функционирующий пример использования доменного сервиса в Symfony для вычисления итоговой стоимости заказа.

DDD: Entity, Value Object. Различие между ними

  1. DDD: Entity, Value Object. Различие между ними

В предметно-ориентированном проектировании (Domain-Driven Design, DDD) сущности (Entity) и объекты-значения (Value Object) играют ключевые роли. 

Отличия Entity и Value Object

- Entity (Сущность):

  - Имеет уникальный идентификатор (ID), который отличает одну сущность от другой.

  - Сохраняет свою идентичность на протяжении всего жизненного цикла, даже если ее свойства изменяются.

  - Пример: пользователь, заказ, продукт.

- Value Object (Объект-значение):

  - Не имеет уникального идентификатора.

  - Считается неизменяемым: любое изменение создает новый объект.

  - Сравнивается по значению, а не по идентификатору.

  - Пример: адрес, деньги, координаты.

Пример на Symfony

1. Создание Entity

Предположим, у нас есть сущность `User` с уникальным идентификатором и некоторыми полями.

User.php

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**

 * @ORM\Entity()

 */

class User
{

    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private int $id;

    /**
     * @ORM\Column(type="string", length=100)
     */
    private string $name;

    /**
     * @ORM\Column(type="string", length=100)
     */
    private string $email;

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

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

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

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

    public function setName(string $name): void
    {
        $this->name = $name;
    }

    public function setEmail(string $email): void
    {
        $this->email = $email;
    }

}

2. Создание Value Object

Теперь создадим объект-значение для адреса.

Address.php

<?php

namespace App\ValueObject;

class Address

{

    private string $street;

    private string $city;

    private string $postalCode;

    public function __construct(string $street, string $city, string $postalCode)

    {
        $this->street = $street;
        $this->city = $city;
        $this->postalCode = $postalCode;

    }

    public function getStreet(): string
    {
        return $this->street;
    }

    public function getCity(): string
    {
        return $this->city;
    }

    public function getPostalCode(): string
    {
        return $this->postalCode;
    }

    public function equals(Address $address): bool
    {
        return $this->street === $address->getStreet() &&
               $this->city === $address->getCity() &&
               $this->postalCode === $address->getPostalCode();

    }

}

3. Использование Value Object в Entity

Теперь добавим адрес к пользователю.

User.php (обновленный)

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

use App\ValueObject\Address;

/**
 * @ORM\Entity()
 */
class User

{
  
    // ...
  
    /**
     * @ORM\Embedded(class="App\ValueObject\Address")
     */
    private Address $address;

    public function __construct(string $name, string $email, Address $address)
    {
        $this->name = $name;
        $this->email = $email;
        $this->address = $address;
    }

    // ...

    public function getAddress(): Address
    {
        return $this->address;
    }

    public function setAddress(Address $address): void
    {
        $this->address = $address;
    }

}

Конфигурация Doctrine

Необходимо также настроить Doctrine для работы с объектами-значениями.

config/packages/doctrine.yaml

doctrine:
    orm:
        mappings:
            App:
                is_bundle: false
                type: annotation
                dir: '%kernel.project_dir%/src/Entity'
                prefix: 'App\Entity'
                alias: App

        types:
            address:
                class: App\ValueObject\Address

С этим примером вы теперь понимаете, как реализовать сущности и объекты-значения в Symfony с использованием DDD. Сущности представляют собой объекты с уникальной идентификацией и изменяемыми свойствами, тогда как объекты-значения представляют собой неизменяемые объекты, сравниваемые по значению.

DDD: Repository. Collection. Куда поместить логику приложения

  1. DDD: Repository. Collection. Куда поместить логику приложения

В Domain-Driven Design (DDD) на PHP репозиторий и коллекция выполняют важные роли в управлении доменными объектами и бизнес-логикой. Давайте рассмотрим их более подробно:

Репозиторий (Repository)

Репозиторий является паттерном для доступа к данным, который обеспечивает абстракцию над источниками данных (например, база данных). Репозиторий позволяет извлекать и сохранять доменные объекты, скрывая детали реализации хранилища данных.

Основные задачи репозитория:

  1. Извлечение доменных объектов: предоставление методов для получения объектов на основе различных критериев.

  2. Сохранение доменных объектов: предоставление методов для сохранения изменений в доменных объектах.

  3. Удаление доменных объектов: предоставление методов для удаления объектов.

Пример интерфейса репозитория:

<?php

interface UserRepository

{

    public function findById(int $id): ?User;

    public function findByEmail(string $email): ?User;

    public function save(User $user): void;

    public function delete(User $user): void;

}

Коллекция (Collection)

Коллекция – это набор объектов, который может использоваться для управления группой объектов как единым целым. В контексте DDD коллекции часто используются для управления связями между агрегатами.

Основные задачи коллекции:

  1. Управление набором объектов: добавление, удаление, итерация по объектам.

  2. Предоставление методов для работы с группой объектов: методы для фильтрации, сортировки и других операций над группой объектов.

Пример коллекции:

<?php

class UserCollection implements \IteratorAggregate

{

    private array $users = [];

    public function add(User $user): void
    {
        $this->users[] = $user;
    }

    public function remove(User $user): void
    {
        $this->users = array_filter($this->users, fn($u) => $u !== $user);
    }

    public function getIterator(): \ArrayIterator
    {
        return new \ArrayIterator($this->users);
    }

}

Куда поместить бизнес-логику

Бизнес-логика в DDD должна быть сосредоточена в доменной модели, то есть в сущностях, значимых объектах и агрегатах. Она не должна находиться в репозиториях или сервисах данных. Основные места для бизнес-логики:

  1. Сущности (Entities): объекты с уникальной идентичностью, которые изменяются с течением времени.

  2. Объекты значений (Value Objects): объекты без уникальной идентичности, которые неизменяемы.

  3. Агрегаты (Aggregates): группы связанных объектов, которые рассматриваются как единое целое для целей изменения данных.

  4. Доменные сервисы (Domain Services): объекты, содержащие бизнес-логику, которая не подходит ни для сущностей, ни для объектов значений.

Пример сущности с бизнес-логикой:

<?php

class User

{

    private int $id;

    private string $email;

    private string $passwordHash;

    public function __construct(int $id, string $email, string $passwordHash)
    {
        $this->id = $id;
        $this->email = $email;
        $this->passwordHash = $passwordHash;
    }

    public function changeEmail(string $newEmail): void
    {
        if (!filter_var($newEmail, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException("Invalid email format");
        }
        $this->email = $newEmail;
    }

    public function checkPassword(string $password): bool
    {
        return password_verify($password, $this->passwordHash);
    }

}

Таким образом, бизнес-логика размещается внутри доменных моделей, а репозитории и коллекции используются для управления и хранения этих моделей.

Event Sourcing

  1. Event Sourcing

Event Sourcing (от англ. "источник событий") — это шаблон проектирования программного обеспечения, при котором изменения состояния системы представляются в виде последовательности событий. Основная идея заключается в том, что вместо хранения текущего состояния системы в базе данных, все изменения состояния сохраняются как последовательность неизменяемых событий. Текущее состояние системы в любой момент времени можно получить, применяя эти события последовательно.

Вот основные концепты Event Sourcing:

  1. События: Все изменения состояния системы записываются как события. Событие представляет собой неизменяемую запись, которая описывает изменение состояния.

  2. Хранение событий: События хранятся в специальном хранилище событий (Event Store). Это может быть база данных, специально предназначенная для хранения событий, или обычная база данных, адаптированная для этих целей.

  3. Восстановление состояния: Для получения текущего состояния системы события читаются и применяются последовательно. Это называется "реплеем" (replay) событий.

  4. CQRS (Command Query Responsibility Segregation): Event Sourcing часто используется вместе с CQRS, шаблоном, разделяющим операции на чтение и запись данных. Команды (commands) изменяют состояние системы, создавая новые события, а запросы (queries) читают состояние системы.

Преимущества Event Sourcing включают:

  • Возможность воспроизведения всех изменений состояния системы для аудита или восстановления.

  • Простота реализации функций, связанных с временными аспектами (например, просмотр состояния системы на конкретный момент времени).

  • Улучшенная масштабируемость за счет разделения операций на чтение и запись.

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

Виды тестирования

  1. Виды тестирования

В PHP, как и в любом другом языке программирования, существует множество видов тестирования, направленных на проверку различных аспектов кода и обеспечения его качества. Вот основные виды тестирования, применяемые в PHP:

1. Модульное тестирование (Unit Testing):

    - Проверяет отдельные модули или функции программы на корректность их работы.

    - В PHP для этого часто используются библиотеки, такие как PHPUnit, Codeception или PHPSpec.

2. Интеграционное тестирование (Integration Testing):

    - Проверяет, как различные модули или компоненты системы работают вместе.

    - Для интеграционного тестирования можно также использовать Codeception или Behat.

3. Функциональное тестирование (Functional Testing):

    - Проверяет функциональность системы с точки зрения пользователя.

    - Инструменты: Behat, Codeception.

4. Тестирование пользовательского интерфейса (UI Testing):

    - Проверяет интерфейс пользователя на корректность отображения и работы.

    - Инструменты: Selenium, Codeception с модулем WebDriver, Laravel Dusk.

5. Тестирование производительности (Performance Testing):

    - Проверяет производительность приложения под различными нагрузками.

    - Инструменты: Apache JMeter, Gatling, PHPBench.

6. Нагрузочное тестирование (Load Testing):

    - Проверяет, как система справляется с высокими нагрузками и каковы её пределы.

    - Инструменты: Apache JMeter, Gatling.

7. Тестирование безопасности (Security Testing):

    - Проверяет приложение на наличие уязвимостей.

    - Инструменты: OWASP ZAP, phpstan, Psalm.

8. Тестирование совместимости (Compatibility Testing):

    - Проверяет, как приложение работает в различных окружениях, браузерах, операционных системах и устройствах.

    - Инструменты: BrowserStack, Sauce Labs.

9. Приемочное тестирование (Acceptance Testing):

    - Проверяет, соответствует ли приложение требованиям и ожиданиям пользователя.

    - Инструменты: Behat, Codeception.

10. Тестирование регрессионное (Regression Testing):

    - Проверяет, что новые изменения в коде не нарушили существующую функциональность.

    - Инструменты: PHPUnit, Codeception.

Каждый из этих видов тестирования имеет свою цель и область применения, и использование их комбинации помогает обеспечить высокое качество и надежность PHP-приложения.

Создание библиотеки на PHP

  1. Создание библиотеки на PHP (Туториал)

Создание библиотеки в PHP включает несколько ключевых шагов. Вот пошаговое руководство, чтобы помочь вам начать:

1. Определите Цель и Функционал Библиотеки

Прежде чем писать код, четко определите, какие задачи будет выполнять ваша библиотека и какие функции она должна содержать.

2. Создайте Структуру Проекта

Создайте структуру директорий для вашей библиотеки. Это может выглядеть следующим образом:

my-library/
├── src/
│   ├── MyClass.php
├── tests/
│   ├── MyClassTest.php
├── vendor/
├── composer.json
├── README.md

3. Напишите Код Библиотеки

В директории `src/` создайте файлы с кодом вашей библиотеки.

Пример файла `src/MyClass.php`:

<?php

namespace MyLibrary;

class MyClass {

    public function sayHello() {
        return "Hello, World!";
    }

}

4. Настройте Composer

Composer — это стандартный менеджер пакетов для PHP, который упростит управление зависимостями и автозагрузку классов.

Создайте файл `composer.json` в корне вашего проекта:

{
    "name": "your-username/my-library",
    "description": "A brief description of your library",
    "type": "library",
    "autoload": {
        "psr-4": {
            "MyLibrary\\": "src/"
        }
    },
    "require": {
        "php": ">=7.4"
    },
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}

5. Установите Зависимости

Выполните команду `composer install`, чтобы установить зависимости и создать автозагрузчик.

composer install

6. Напишите Тесты

В директории `tests/` создайте тесты для вашей библиотеки. Используйте PHPUnit для тестирования.

Пример файла `tests/MyClassTest.php`:

<?php

use PHPUnit\Framework\TestCase;

use MyLibrary\MyClass;

class MyClassTest extends TestCase {

    public function testSayHello() {
        $myClass = new MyClass();
        $this->assertEquals("Hello, World!", $myClass->sayHello());

    }

}

7. Запустите Тесты

Запустите тесты, чтобы убедиться, что все работает корректно:

vendor/bin/phpunit tests

8. Напишите Документацию

Создайте файл `README.md` с инструкциями по установке и использованию вашей библиотеки.

Пример:

# My Library

## Installation
```bash
composer require your-username/my-library
```

## Usage
```php
use MyLibrary\MyClass;
$myClass = new MyClass();
echo $myClass->sayHello();
```

9. Опубликуйте Библиотеку

Если вы хотите, чтобы ваша библиотека была доступна для всех, опубликуйте ее на Packagist.

  1. Зарегистрируйтесь на Packagist.

  2. Создайте репозиторий на GitHub и загрузите туда ваш код.

  3. Добавьте ваш репозиторий на Packagist.

Дополнительные Рекомендации

  • Следуйте стандартам кодирования (например, PSR-12).

  • Используйте системы CI/CD для автоматического тестирования и развертывания.

  • Регулярно обновляйте библиотеку и следите за зависимостями.

Как добиться отказоустойчивости между Service A и Service B

  1. Как добиться отказоустойчивости между Service A и Service B

Ответ: Применить шаблон Outbox

Pattern Outbox (паттерн «Исходящий ящик») - это архитектурный шаблон, используемый для обеспечения гарантированной доставки сообщений из одного компонента системы в другой. Основная цель этого паттерна заключается в обеспечении надежной и упорядоченной передачи данных между сервисами, особенно в распределенных системах.

Основные идеи паттерна Outbox:

1. Запись в таблицу исходящих сообщений:

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

2. Транзакционная консистентность:

Запись в таблицу исходящих сообщений происходит в той же транзакции, что и изменение данных в основной таблице. Это обеспечивает согласованность данных: либо оба изменения происходят, либо не происходит ни одно.

3. Процессор исходящих сообщений:

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

4. Обработка дубликатов:

В случае сбоев возможны повторные отправки одного и того же сообщения. Поэтому целевой компонент должен быть идемпотентным (т.е. корректно обрабатывать повторные поступления одного и того же сообщения).

Пример работы:

Представим, что у нас есть два микросервиса: сервис заказов (Order Service) и сервис уведомлений (Notification Service).

  1. Клиент создает заказ через Order Service.

  2. Order Service записывает данные о новом заказе в свою базу данных и в той же транзакции добавляет запись в таблицу исходящих сообщений о необходимости уведомить пользователя о создании заказа.

  3. Процессор исходящих сообщений Order Service периодически проверяет таблицу исходящих сообщений, видит новое сообщение и отправляет его в Notification Service.

  4. Notification Service получает сообщение и отправляет уведомление пользователю.

  5. После успешной отправки уведомления, процессор исходящих сообщений Order Service помечает сообщение как отправленное или удаляет его из таблицы.

Преимущества:

  • Гарантированная доставка: Паттерн обеспечивает надежную доставку сообщений даже в случае сбоев системы.

  • Согласованность данных: Транзакционная запись в таблицу исходящих сообщений обеспечивает консистентность данных.

  • Простота восстановления после сбоя: В случае сбоя система может просто перечитать сообщения из таблицы исходящих сообщений и повторить попытку отправки.

Недостатки:

  • Сложность реализации: Необходимо реализовать дополнительный компонент (процессор исходящих сообщений) и обеспечить его надежную работу.

  • Увеличение задержек: Между записью сообщения и его фактической отправкой может быть задержка, зависящая от частоты опроса таблицы исходящих сообщений.

Заключение:

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

Проведи рефакторинг данного кода на PHP. Code Review

  1. Проведи рефакторинг данного кода на PHP. Code Review

Рефакторинг кода поможет улучшить его читаемость, поддерживаемость и масштабируемость. 

Вот подробное объяснение изменений и сам рефакторинг кода:

Основные изменения:

  1. Внедрение зависимостей (Dependency Injection): Репозитории и сервисы теперь передаются через конструктор. Это делает код более гибким и тестируемым.

  2. Интерфейсы: Использование интерфейсов для уменьшения связности кода и упрощения замены реализаций.

  3. Принцип единственной ответственности: Каждый класс выполняет только одну функцию (а именно у нас одна причина на изменение кода), что соответствует принципу единственной ответственности (SRP).

  4. Улучшение читаемости: Код стал более организованным и поддерживаемым благодаря разделению ответственности.

Рефакторинг:

<?php

// Интерфейсы для репозиториев и сервисов
interface ItemRepositoryInterface
{
    public function get($itemId);
    public function save($item);
}

interface OrderRepositoryInterface
{
    public function save($order);
}

interface SmsServiceInterface
{
    public function send($message);
}

// Реализация интерфейсов
class MySqlItemRepository implements ItemRepositoryInterface
{
    public function get($itemId)
    {
        // Реализация получения товара из MySQL
    }

    public function save($item)
    {
        // Реализация сохранения товара в MySQL
    }

}

class PgOrderRepository implements OrderRepositoryInterface
{
    public function save($order)
    {
        // Реализация сохранения заказа в PostgreSQL
    }

}

class SimpleSmsService implements SmsServiceInterface
{
  
    private $login;

    private $password;

    public function __construct($login, $password)
    {
        $this->login = $login;
        $this->password = $password;
    }

    public function send($message)
    {
        // Реализация отправки SMS
    }

}

// Основной контроллер
class OrderController
{

    private $itemRepository;

    private $orderRepository;

    private $smsService;

    public function __construct(ItemRepositoryInterface $itemRepository, OrderRepositoryInterface $orderRepository, SmsServiceInterface $smsService)
    {
        $this->itemRepository = $itemRepository;
        $this->orderRepository = $orderRepository;
        $this->smsService = $smsService;
    }

    public function processOrder(Request $request)
    {
        $itemId = $request->get('item_id');
        $quantity = $request->get('quantity');
        $item = $this->itemRepository->get($itemId);

        $item->sell($quantity);
        $this->itemRepository->save($item);

        $order = new Order('id-abc', $request->user()->id, $itemId, $quantity);
        $this->orderRepository->save($order);
        $this->smsService->send('An order placed');

        return 'Ok';
    }
}

// Пример использования
$request = new Request(); // Предполагаем, что $request является экземпляром класса Request
$itemRepository = new MySqlItemRepository();
$orderRepository = new PgOrderRepository();
$smsService = new SimpleSmsService('login', 'pass');
$orderController = new OrderController($itemRepository, $orderRepository, $smsService);

$response = $orderController->processOrder($request);

echo $response;

Пояснения:

- Интерфейсы: Созданы интерфейсы для репозиториев и сервисов (`ItemRepositoryInterface`, `OrderRepositoryInterface`, `SmsServiceInterface`), чтобы уменьшить зависимость от конкретных реализаций и упростить замену одного репозитория или сервиса на другой.

- Реализации интерфейсов: Реализованы классы `MySqlItemRepository`, `PgOrderRepository`, `SimpleSmsService`, которые реализуют соответствующие интерфейсы.

- Контроллер: Контроллер `OrderController` теперь принимает зависимости через конструктор, что позволяет легко заменять зависимости при тестировании или изменении логики.

- Пример использования: Показан пример создания экземпляров классов и использования контроллера.

Также логику из контроллера переносим в сервисный слой (создаем например OrderService)

SELECT FOR UPDATE

  1. SELECT FOR UPDATE

`SELECT FOR UPDATE` — это SQL-запрос, который используется для получения блокировки на строки, выбранные из базы данных, для предотвращения их изменения другими транзакциями до завершения текущей транзакции. Это полезно в ситуациях, когда нужно убедиться, что данные не изменяются другими пользователями во время выполнения определенных операций, таких как обновление или удаление записей.

Пример использования `SELECT FOR UPDATE`

Рассмотрим таблицу `accounts`:

CREATE TABLE accounts (
    id INT PRIMARY KEY,
    balance DECIMAL
);

Если нужно перевести деньги между двумя счетами, необходимо убедиться, что баланс обоих счетов не изменится другими транзакциями во время выполнения операции.

Пример 1: Перевод денег между счетами

BEGIN;

-- Блокировка строк для аккаунтов с id 1 и 2
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
SELECT balance FROM accounts WHERE id = 2 FOR UPDATE;

-- Получение текущих балансов (предположим, они хранятся в переменных)
SET @balance1 = (SELECT balance FROM accounts WHERE id = 1);
SET @balance2 = (SELECT balance FROM accounts WHERE id = 2);

-- Перевод 100 единиц с аккаунта 1 на аккаунт 2
UPDATE accounts SET balance = @balance1 - 100 WHERE id = 1;
UPDATE accounts SET balance = @balance2 + 100 WHERE id = 2;

COMMIT;

В этом примере транзакция начинает с получения блокировки на строки для счетов с id 1 и 2. Это гарантирует, что никто другой не сможет изменить эти строки, пока транзакция не завершится. После этого выполняются операции обновления балансов, и транзакция завершается.

Особенности и Поведение

1. Блокировка только на чтение: `SELECT FOR UPDATE` блокирует строки только на изменение, т.е. другие транзакции могут читать эти строки, но не могут их изменять или удалять.

2. Продолжительность блокировки: Блокировка удерживается до завершения текущей транзакции (commit или rollback).

3. Совместимость с другими блокировками: `SELECT FOR UPDATE` несовместим с другими блокировками на те же строки. Если другая транзакция пытается заблокировать те же строки, она будет ждать завершения текущей транзакции.

4. Оптимизация производительности: Использование `SELECT FOR UPDATE` следует тщательно продумывать, так как оно может привести к проблемам с производительностью, особенно если транзакция длится долго и блокирует много строк.

Расширенные возможности

В некоторых СУБД, таких как PostgreSQL, `SELECT FOR UPDATE` может быть дополнен разными режимами блокировки:

FOR NO KEY UPDATE: Блокирует строки для обновления, но позволяет вставку или удаление в индексе.

FOR SHARE: Разрешает другим транзакциям чтение строк и установку `FOR SHARE` блокировки, но запрещает `FOR UPDATE` и другие более строгие блокировки.

FOR KEY SHARE: Позволяет другим транзакциям ставить `FOR SHARE` или `FOR KEY SHARE` блокировки, но не позволяет `FOR UPDATE` или `FOR NO KEY UPDATE`.

SELECT FOR UPDATE — мощный инструмент для обеспечения целостности данных в многопользовательских системах. Однако его использование должно быть сбалансировано с учетом возможного влияния на производительность и блокировку других операций в базе данных.

Заключение

Спасибо за внимание! А если вам скучно, можете с друзьями поиграть в мой онлайн шутер (Alpha)

Комментарии (42)


  1. FanatPHP
    12.07.2024 08:56
    +6

    Эта стена текста вызывает двоякое впечатление. Такое ощущение, что сначала статья писалась добротно, потом автор выгорел и начал делать "на отвали", потом присобачил какой-то копипасты, всё это перемешал в случайном порядке и отправил на Хабр. Или это просто запись ответов разных кандидатов, и, как в том анекдоте, "Иногда побеждает тореадор, а иногда - бык"? :D

    Раздел про микросервисы, например, написан очень добротно, а жизненный цикл Ларавля наоборот какой-то хлипкий. А у "создания библиотеки" даже и формального вопроса нет, сразу тупо инструкция. Явные глюки перевода, "Основные характеристики Core Domain: максимальная инвестиция", "Какие HTTP-методы API являются идемпотентные".

    В целом добротных разделов больше, но очень не хватает оглавления в начале.


    1. MediaRise Автор
      12.07.2024 08:56

      Разные темы, где то очень кратко как навигатор для темы для углубленного изучения. Темы из реальных интервью других компаний. Оглавление добавлю


    1. dimas846
      12.07.2024 08:56

      Главу про библиотеку 100% искусственный интеллект писал. Другие главы, видимо тоже.


  1. AlexeichD
    12.07.2024 08:56
    +1

    И почем нынче специалист, кто знает все ответы?)


    1. MediaRise Автор
      12.07.2024 08:56

      Senior уже от 350-400


      1. ChizhM
        12.07.2024 08:56

        1. MediaRise Автор
          12.07.2024 08:56

          ну это для избранных


        1. FanatPHP
          12.07.2024 08:56
          +1

          Странная вакансия. Я вполне допускаю, что на прошлую им никто не смог написать простенький парсер запросов. Но так резко задирать ставки - это как-то слишком.


        1. Vamp
          12.07.2024 08:56
          +3

          Это фейковая вакансия, чтобы привлечь внимание к настоящей.


        1. AlexeichD
          12.07.2024 08:56

          да это ржака, уже перетерли тут))


    1. ChizhM
      12.07.2024 08:56

      100тр после испытательного, до вычета НДФЛ.


  1. ChizhM
    12.07.2024 08:56

    Как хорошо, что я этого всего не знаю... и 20 лет разрабатываю сайты сми и интернет-магазины на php. :-)


  1. kozlov_de
    12.07.2024 08:56
    +1

    Мой рефакторинг

    <?php
    
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\DB;
    use App\Http\Controllers\Controller;
    use App\Repositories\ItemRepository;
    use App\Repositories\OrderRepository;
    use App\Services\NotificationService;
    use Illuminate\Http\JsonResponse;
    
    class OrderController extends Controller
    {
        public function __construct(
            private ItemRepository $itemRepository,
            private OrderRepository $orderRepository,
            private NotificationService $notificationService
        ) {}
    
        public function processOrder(Request $request): JsonResponse
        {
            $validated = $request->validate([
                'item_id' => 'required|uuid',
                'quantity' => 'required|integer|min:1',
            ]);
    
            $userId = $request->user()->id;
    
            $item = $this->itemRepository->findOrFail($validated['item_id']);
    
            return match(true) {
                $item === null => response()->json(['error' => 'Item not found'], 404),
                $item->stock < $validated['quantity'] => response()->json(['error' => 'Not enough items in stock'], 400),
                default => $this->processValidOrder($item, $userId, $validated['quantity'])
            };
        }
    
        private function processValidOrder($item, $userId, $quantity): JsonResponse
        {
            try {
                DB::beginTransaction();
    
                $this->itemRepository->decrementStock($item->id, $quantity);
                $order = $this->orderRepository->createOrder($userId, $item->id, $quantity);
                $this->notificationService->notify($userId, 'Order placed successfully');
    
                DB::commit();
    
                return response()->json([
                    'order_id' => $order->id,
                    'message' => 'Order processed successfully'
                ], 200);
            } catch (\Exception $e) {
                DB::rollBack();
                return response()->json(['error' => 'An error occurred while processing the order'], 500);
            }
        }
    }
    

    © claude-3-5-sonnet-20240620 под моим присмотром.

    Написано в стиле .net

    А вот ревью на ваш рефракторинг

    Ваш код уже демонстрирует хорошие практики, такие как использование интерфейсов и внедрение зависимостей. Тем не менее, вот несколько предложений по улучшению:

    Типизация:

    Добавьте строгую типизацию для параметров и возвращаемых значений методов.

    Обработка ошибок:

    Добавьте обработку исключений в методе processOrder.

    Валидация входных данных:

    Реализуйте валидацию входных данных в начале метода processOrder.

    Использование современных возможностей PHP:

    Используйте типизированные свойства класса (PHP 7.4+).

    Применяйте return type declarations.

    Именование:

    Переименуйте метод sell в Item на что-то более описательное, например, decreaseStock.

    Безопасность:

    Не передавайте напрямую логин и пароль в конструктор SimpleSmsService. Лучше использовать конфигурацию или переменные окружения.

    Ответ контроллера:

    Вместо простого 'Ok' возвращайте более информативный ответ, например, JSON с деталями заказа.

    Генерация ID заказа:

    Вместо хардкода 'id-abc', используйте генерацию уникального ID.


    1. MediaRise Автор
      12.07.2024 08:56

      логику бы также из контроллера вынести


      1. FanatPHP
        12.07.2024 08:56

        Тут не худо бы в зеркало посмотреться :) Вся эта кухня

                $item = $this->itemRepository->get($itemId);
        
                $item->sell($quantity);
                $this->itemRepository->save($item);
        
                $order = new Order('id-abc', $request->user()->id, $itemId, $quantity);
                $this->orderRepository->save($order);

        тоже явно не для контроллера.


        1. MediaRise Автор
          12.07.2024 08:56

          В статье описано, что логику в сервисный слой вынести необходимо.


          1. FanatPHP
            12.07.2024 08:56
            +2

            В статье написано, но в коде-то нет. Если уж вы сами не вынесли, то тогда и комментатора за это не упрекайте :)


            1. MediaRise Автор
              12.07.2024 08:56

              Во первых я написал, это есть в статье
              Во вторых не упрекаю, а дополняю своим комментарием
              В третих, не вам указывать что мне делать


              1. FanatPHP
                12.07.2024 08:56
                +2

                Вы совершенно зря обижаетесь.
                Во-первых, "в статье" написано только на словах, "Также логику из контроллера переносим в сервисный слой (создаем например OrderService)", но в предложенном варианте отрефакторенного кода этот сервис не реализован. Кандидата за такой "рефакторинг" на словах, "Вот код! Ну там еще надо будет логику в сервис вынести" вы же сами первый попросите на выход.

                А во-вторых, кому ещё указывать-то, как не мне? Я пришел на Хабр, читаю статью, пишу комментарий (причем по грамматическим ошибкам культурно написал в личку, но вы даже не отреагировали). Комментарии не всегда бывают комплементарными. Если хотите получать только хвалебные отзывы на свои статьи, то и не размещайте их на Хабре. Пишите в личный блог тогда, и дайте доступ только своим друзьям.

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


                1. PeopleMax
                  12.07.2024 08:56

                  Вполне адекватный человек) поддерживаю


                1. MediaRise Автор
                  12.07.2024 08:56

                  Видимо аватарка наложило впечатление))
                  Спасибо за ответ, не обижаюсь!


    1. FanatPHP
      12.07.2024 08:56

      Вполне ожидаемо для AI, лишние уши пальцы торчат отовсюду. Контроллер по уши залез в бизнес-логику, findOrFail возвращает null, критическая ошибка не логируется и не рендерится, соединение с БД берется из воздуха непонятно какое


      1. kozlov_de
        12.07.2024 08:56

        Всем спасибо за замечания

        Тогда так

        <?php
        
        namespace App\Http\Controllers;
        
        use App\Http\Requests\CreateOrderRequest;
        use App\Services\OrderService;
        use Illuminate\Http\JsonResponse;
        
        class OrderController extends Controller
        {
            public function __construct(
                private OrderService $orderService
            ) {}
        
            public function store(CreateOrderRequest $request): JsonResponse
            {
                $order = $this->orderService->createOrder(
                    $request->user()->id,
                    $request->validated('item_id'),
                    $request->validated('quantity')
                );
        
                return response()->json([
                    'order_id' => $order->id,
                    'message' => 'Order processed successfully'
                ], 201);
            }
        }
        
        namespace App\Http\Requests;
        
        use Illuminate\Foundation\Http\FormRequest;
        
        class CreateOrderRequest extends FormRequest
        {
            public function rules(): array
            {
                return [
                    'item_id' => 'required|uuid',
                    'quantity' => 'required|integer|min:1',
                ];
            }
        }
        
        namespace App\Services;
        
        use App\Repositories\ItemRepository;
        use App\Repositories\OrderRepository;
        use App\Exceptions\InsufficientStockException;
        use App\Exceptions\ItemNotFoundException;
        use Illuminate\Support\Facades\DB;
        use Illuminate\Support\Facades\Log;
        
        class OrderService
        {
            public function __construct(
                private ItemRepository $itemRepository,
                private OrderRepository $orderRepository,
                private NotificationService $notificationService
            ) {}
        
            public function createOrder(string $userId, string $itemId, int $quantity)
            {
                try {
                    return DB::transaction(function () use ($userId, $itemId, $quantity) {
                        $item = $this->itemRepository->findOrFail($itemId);
        
                        if ($item->stock < $quantity) {
                            throw new InsufficientStockException("Not enough items in stock");
                        }
        
                        $this->itemRepository->decrementStock($item->id, $quantity);
                        $order = $this->orderRepository->createOrder($userId, $item->id, $quantity);
                        $this->notificationService->notify($userId, 'Order placed successfully');
        
                        Log::info('Order processed', ['order_id' => $order->id, 'user_id' => $userId]);
        
                        return $order;
                    });
                } catch (\Exception $e) {
                    Log::error('Order processing failed', [
                        'error' => $e->getMessage(),
                        'user_id' => $userId,
                        'item_id' => $itemId,
                        'quantity' => $quantity
                    ]);
        
                    throw $e;
                }
            }
        }
        
        namespace App\Exceptions;
        
        use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
        use Throwable;
        
        class Handler extends ExceptionHandler
        {
            public function render($request, Throwable $e)
            {
                if ($e instanceof ItemNotFoundException) {
                    return response()->json(['error' => $e->getMessage()], 404);
                }
        
                if ($e instanceof InsufficientStockException) {
                    return response()->json(['error' => $e->getMessage()], 400);
                }
        
                return parent::render($request, $e);
            }
        }

        Кстати, у автора тоже бизнес логика в контроллере

        Так вот плавно 25 строк кода превращается в 125

        Но красиво


        1. FanatPHP
          12.07.2024 08:56

          Да, это уже сильно лучше. Кода действительно стало больше, но он разнесен по разным модулям. А сам контроллер похудел и занимается ровно тем, чем должен. Придраться остается если только по мелочи.

          • соединение с БД для транзакции всё так же берется из воздуха (чем резко диссонирует с остальными зависимостями)

          • насколько я могу судить, по задаче товары и заказы вообще-то лежат в разных СУБД (но это совсем уже придирка, понятно что бедняжке GPT это совсем не осилить)

          • нотификакцию я бы пожалуй вынес из транзакции. Отменять успешный заказ только потому что лег гейт СМС как-то глупо.

          • собственно наворачивать еще один try-catch в дополнение к тому, который и так есть DB::transaction() я не вижу смысла. Уж чего-чего, а логировать ошибки Ларавель умеет и сам.


          1. kozlov_de
            12.07.2024 08:56

            Ещё вариант попроще и завязал )

            Задолбался это делать на телефоне

            Вся моя работа это сплошной рефакторинг. Правда, на c#. PHP только читаю и то не особо.

            распределённая транзакция...

            <?php
            
            namespace App\Http\Controllers;
            
            use App\Http\Requests\CreateOrderRequest;
            use App\Services\OrderService;
            use Illuminate\Http\JsonResponse;
            use Illuminate\Support\Facades\Log;
            
            class OrderController extends Controller
            {
                public function __construct(private OrderService $orderService) {}
            
                public function store(CreateOrderRequest $request): JsonResponse
                {
                    try {
                        $order = $this->orderService->createOrder(
                            $request->user()->id,
                            $request->input('item_id'),
                            $request->input('quantity')
                        );
            
                        return response()->json(['order_id' => $order->id], 201);
                    } catch (\Exception $e) {
                        Log::error('Failed to create order: ' . $e->getMessage(), [
                            'user_id' => $request->user()->id,
                            'item_id' => $request->input('item_id'),
                            'quantity' => $request->input('quantity'),
                        ]);
            
                        throw $e;
                    }
                }
            }
            
            namespace App\Services;
            
            use App\Repositories\ItemRepository;
            use App\Repositories\OrderRepository;
            use App\Exceptions\InsufficientStockException;
            use Illuminate\Support\Facades\DB;
            use Netsells\DistributedTransactions\Facades\DistributedTransaction;
            
            class OrderService
            {
                public function __construct(
                    private ItemRepository $items,
                    private OrderRepository $orders,
                    private NotificationService $notifications,
                    private DB $itemsDb,
                    private DB $ordersDb
                ) {}
            
                public function createOrder(string $userId, string $itemId, int $quantity)
                {
                    $item = $this->items->findOrFail($itemId, $this->itemsDb);
            
                    throw_if($item->stock < $quantity, InsufficientStockException::class);
            
                    return DistributedTransaction::run([$this->itemsDb, $this->ordersDb], function () use ($userId, $itemId, $quantity, $item) {
                        $this->items->decrementStock($item->id, $quantity, $this->itemsDb);
                        $orderId = $this->orders->create($userId, $itemId, $quantity, $this->ordersDb);
                        $this->notifications->notify($userId, 'Order placed successfully');
            
                        return $orderId;
                    });
                }
            }
            
            namespace App\Repositories;
            
            use Illuminate\Support\Facades\DB;
            
            class ItemRepository
            {
                public function findOrFail(string $itemId, DB $db)
                {
                    return $db->table('items')->where('id', $itemId)->firstOrFail();
                }
            
                public function decrementStock(string $itemId, int $quantity, DB $db)
                {
                    $db->table('items')->where('id', $itemId)->decrement('stock', $quantity);
                }
            }
            
            namespace App\Repositories;
            
            use Illuminate\Support\Facades\DB;
            
            class OrderRepository
            {
                public function create(string $userId, string $itemId, int $quantity, DB $db)
                {
                    return $db->table('orders')->insertGetId([
                        'user_id' => $userId,
                        'item_id' => $itemId,
                        'quantity' => $quantity,
                    ]);
                }
            }
            
            namespace App\Exceptions;
            
            use Exception;
            use Illuminate\Support\Facades\Log;
            
            class InsufficientStockException extends Exception
            {
                public function render($request)
                {
                    Log::warning('Insufficient stock: ' . $this->getMessage(), [
                        'user_id' => $request->user()->id,
                        'item_id' => $request->input('item_id'),
                        'quantity' => $request->input('quantity'),
                    ]);
            
                    return response()->json(['error' => 'Insufficient stock'], 400);
                }
            }


            1. kozlov_de
              12.07.2024 08:56

              Вот так код и раздувается в 5-7 раз

              Задумаешься прежде чем рефакторить - а надо ли это в данном случае...

              Улучшение 1: Разделение ответственности (Separation of Concerns)
              Стоимость улучшения 1: 20 строк кода
              Описание: Вынесение логики создания заказа из контроллера в отдельный сервис OrderService позволяет разделить ответственность между классами. Контроллер теперь отвечает только за обработку HTTP-запросов и возврат ответов, а сервис - за бизнес-логику создания заказа. Это делает код более модульным, облегчает тестирование и упрощает внесение изменений в будущем.

              Улучшение 2: Внедрение зависимостей (Dependency Injection)
              Стоимость улучшения 2: 10 строк кода
              Описание: Зависимости (OrderService, ItemRepository, OrderRepository, NotificationService) теперь внедряются через конструктор, что позволяет легко заменять их на другие реализации и упрощает тестирование. Это делает код более гибким и облегчает внесение изменений в будущем.

              Улучшение 3: Обработка ошибок
              Стоимость улучшения 3: 15 строк кода
              Описание: Добавлена обработка исключений и логирование ошибок. В случае недостаточного количества товара на складе выбрасывается исключение InsufficientStockException, которое обрабатывается и возвращается соответствующий ответ клиенту. Это делает код более устойчивым к ошибкам и облегчает отладку.

              Улучшение 4: Использование распределенных транзакций
              Стоимость улучшения 4: 10 строк кода
              Описание: Создание заказа теперь происходит в рамках распределенной транзакции с использованием пакета netsells/laravel-distributed-transactions. Это гарантирует согласованность данных между базами данных товаров и заказов. Если в процессе создания заказа возникнет ошибка, все изменения будут отменены, что предотвратит несогласованность данных.

              Улучшение 5: Валидация запроса
              Стоимость улучшения 5: 5 строк кода
              Описание: Добавлен класс CreateOrderRequest для валидации входных данных запроса на создание заказа. Это позволяет проверять корректность данных перед их обработкой и возвращать информативные сообщения об ошибках в случае некорректных данных.

              Улучшение 6: Логирование
              Стоимость улучшения 6: 7 строк кода
              Описание: Добавлено логирование ошибок и предупреждений с помощью фасада Log. Это позволяет отслеживать возникающие проблемы и упрощает отладку и мониторинг приложения.

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


            1. FanatPHP
              12.07.2024 08:56

              Ну тут уже совсем глюки пошли. Здесь try-catch уже совсем не пришей кобыле хвост, откуда-то выдумал DistributedTransaction, какой-то бессмысленный логгинг.
              Предыдущий вариант был сильно лучше.


  1. aleksejs1
    12.07.2024 08:56
    +4

    Это первая статья подобного рода, которую я решил глянуть...

    И... даже не знаю стоит ли это комментировать...

    Во-первых, ответы выглядят, как GPT.

    Во-вторых, нет связанности. Вторым вопросом идёт Laravel, а 7-ым примеры на symfony.

    В-третьих, странные вопросы, не о PHP, а о конкретном стэке: kafka, redis. Т.е. не вопросы типа какие бывают мэсэдж брокеры и кэши, и как они работают, а разбор конкретных технологий.

    В-четвёртых, в микросервисах вопросы про PHP, а ответы, не про PHP. "В чём плюсы микросервиса на PHP?" - "В технологической независимовти". Т.е. микросервис на PHP можно написать на GO что ли? :D :D

    В-пятых, какой-то странный сет из видов тестирования. Например, есть Security Testing, но нет penetration testing.

    Создание библиотеки на PHP - это вопрос? Описание вообще смешное.

    Паттерны даже читать не стал.


    1. PeopleMax
      12.07.2024 08:56

      Ну не все такие умные, как вы батенька


      1. aleksejs1
        12.07.2024 08:56
        +2

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


        1. FanatPHP
          12.07.2024 08:56

          Мне кажется, вы одновременно и правы, и неправы. Да, подборка бессистемная и не всегда логичная. Но некоторые вопросы вполне ничего. Я так понимаю, что никто не требует от соискателя зазубрить по пунктам жизненный цикл Ларавля. Но понимание общих принципов, умение представить жизненный цикл в голове будет довольно хорошим показателем, мне кажется. На собеседовании же важно видеть обладает ли человек базовыми знаниями и умеет ли мыслить, делать какие-то построения на их основе. И вопрос про этот пресловутый жизненный цикл в этом смысле вполне ничего?


          1. MediaRise Автор
            12.07.2024 08:56

            Спасибо добрый человек


          1. aleksejs1
            12.07.2024 08:56
            +2

            А вы читали ответ о циклах в этой статье? Ни слова про ООП, ядро, мидлвэр, шаблонизацию, orm. Зато целый абзац про композер. Серьер бы точно так не отвечал.

            https://laravel-docs.com/ru/docs/10.x/lifecycle - есть схожесть с нормальным обьяснением, но на уровне ChatGPT.

            И вопрос в целом тоже не хороший. Лучше спрашивать жизненный цикл MVC фреймворка, а не конкретно Laravel.

            Laravel оправдан только, если нужен именно человек на Laravel.


            1. MediaRise Автор
              12.07.2024 08:56

              ооп, ядро, мидлвэр, мда, собеседование джунов можете почитать в других статьях.


              1. FanatPHP
                12.07.2024 08:56

                Эээ... Давайте я вам повторю то, что писал в личку

                3.Запуск HTTP-запроса:

                Приходит HTTP-запрос, и приложение начинает обработку этого запроса

                Это какой-то бред. С какой стати фреймворк запускает какие-то запросы? И почему запрос приходит только сейчас? Это синхронный фреймворк, то есть все шаги, начиная с первого, начинают выполняться ПОСЛЕ того как пришел запрос. Я бы предположил, что тут имеется в виду запуск обработки запроса, но это вроде и так есть в 6 пункте.

                Давайте вы сначала попробуете объяснить, что здесь имеется в виду, а потом уже поговорим про собеседования джунов?


  1. koreychenko
    12.07.2024 08:56
    +3

    На личном опыте нащупал странную закономерность, что у кандидатов с опытом Laravel обычно все хуже с паттернами и пониманием как оно все должно работать по сравнению с симфонистами.

    Также, если говорить про фуллстэков, то обычно ларавельщики выбирают React, а симфонисты Vue.


    1. MediaRise Автор
      12.07.2024 08:56

      это сугубо ваше мнение, прошедшее через ваш опыт


      1. koreychenko
        12.07.2024 08:56
        +2

        Безусловно, я так и написал. Возможно мне просто такие разрабы на собеседованиях попадались.


    1. FanatPHP
      12.07.2024 08:56
      +1

      Мне кажется, это совершенно очевидный факт. Они буквально смотрят в противоположные стороны: симфони специально форсит разделение ответственности и прописывание зависимостей, а Ларавель делает ровно наоборот - все скрывает за удобненькими фасадиками и хелперами.


      1. koreychenko
        12.07.2024 08:56
        +1

        Вот да, кстати. В симфони, несмотря на всякую магию вроде автовайринга и аргумент резолвинга, бизнес логика старается жить отдельно.

        Даже если не хочешь сильно на доктрину завязываться, то можно юзать XML маппинг и в коде сущностей вообще не будет упоминания ORM. В Ларе же этот богомерзкий ActiveRecord. Оно для старта просто и удобно, но связность кратно выше.


  1. Stems
    12.07.2024 08:56
    +5

    Кому сдался жизненный цикл laravel? Это реально вопрос на собес? Как это знание помогает вообще?


    1. MediaRise Автор
      12.07.2024 08:56

      Конечно, laravel И Symfony два форварда в PHP