Как перехватить и обработать данные запроса перед сохранением в базу данных?

«Как перехватить и обработать данные запроса перед сохранением в базу данных?» — вопрос из категории Архитектура, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

1. Мутаторы (Mutators) в ORM (Eloquent/Doctrine): Идеально для простых преобразований полей модели (например, форматирование, хеширование).

  • В Laravel Eloquent:

    // app/Models/User.php
    class User extends Model
    {
        // Мутатор автоматически вызовется при установке $user->email
        public function setEmailAttribute(string $value): void
        {
            $this->attributes['email'] = strtolower(trim($value));
        }
    
        // Аксессор для получения данных
        public function getFullNameAttribute(): string
        {
            return $this->first_name . ' ' . $this->last_name;
        }
    }

2. События моделей (Model Events / Lifecycle Callbacks): Подходят для более сложной логики, которая может зависеть от нескольких полей или внешних сервисов.

  • В Laravel (наблюдатели или в модели):

    // В модели можно использовать методы типа saving(), creating()
    class Post extends Model
    {
        protected static function booted(): void
        {
            static::saving(function (Post $post) {
                // Генерация slug из заголовка перед сохранением
                $post->slug = Str::slug($post->title);
    
                // Подсчёт примерного времени чтения
                $post->read_time = ceil(str_word_count($post->body) / 200);
            });
        }
    }
  • В Doctrine (используя аннотации/атрибуты):

    // src/Entity/Product.php
    #[ORMEntity]
    #[ORMHasLifecycleCallbacks]
    class Product
    {
        // ...
        #[ORMPrePersist]
        #[ORMPreUpdate]
        public function updateTimestampsAndSlug(): void
        {
            $this->updatedAt = new DateTimeImmutable();
            if ($this->slug === null) {
                $this->slug = $this->generateSlug($this->name);
            }
        }
    }

3. Сервисный слой (Service Layer) или Действия (Actions): Наиболее контролируемый и явный способ. Вся бизнес-логика по обработке данных инкапсулирована в отдельном классе.

// src/Service/ProductService.php
class ProductService
{
    public function __construct(private ProductRepository $repository) {}

    public function createProduct(CreateProductDto $dto): Product
    {
        // 1. Валидация DTO уже пройдена на уровне контроллера
        // 2. Преобразование DTO в сущность с дополнительной логикой
        $product = new Product();
        $product->setName($dto->name);
        $product->setPrice($dto->price);

        // 3. Перехват и обработка данных ПЕРЕД сохранением
        $this->applyBusinessRules($product, $dto);

        // 4. Вызов репозитория для сохранения
        $this->repository->save($product);

        return $product;
    }

    private function applyBusinessRules(Product $product, CreateProductDto $dto): void
    {
        // Например, установка цены со скидкой для оптовиков
        if ($dto->customerType === 'wholesale') {
            $product->setFinalPrice($product->getPrice() * 0.9);
        }
        // Или логирование изменения цены
        if ($product->getPrice() !== $dto->price) {
            $this->logger->info('Price adjusted', ['product_id' => $product->getId()]);
        }
    }
}

Рекомендация по выбору:

  • Мутаторы — для простого форматирования данных одного поля.
  • События модели — для логики, тесно связанной с жизненным циклом сущности, но которая не должна «засорять» сервис.
  • Сервисный слой — для сложной бизнес-логики, требующей доступа к другим сервисам (логирование, внешние API), или когда нужно явно видеть все преобразования в одном месте.