,

21 января, 2025

Автоматическое добавление времени создания в сущность Symfony

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

Вся реализация сводится к созданию слушателя, который будет реагировать на событие «prePersist»


События Symfony — это механизм, который позволяет реагировать на определённые действия (например, пользователь вошёл в систему или запрос обработан). Они создаются и передаются через EventDispatcher.

Слушатели Symfony — это специальные методы или классы, которые «слушают» события и выполняют определённые действия, когда событие происходит.

prePersist — это событие Doctrine, которое вызывается перед сохранением новой записи в базу данных (до вызова метода persist). Его можно использовать для подготовки данных или выполнения действий перед сохранением.

Все что нужно создать новый слушатель

<?php

namespace App\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;

class ConfigEventListener
{
    public function prePersist(LifecycleEventArgs $args): void
    {
        $object = $args->getObject();
        $object->setCreateAt(new \DateTime('now'));
    }
}

Теперь регистрируем слушатель в service.yaml

    App\EventListener\ConfigEventListener:
        tags:
            - { name: doctrine.event_listener, event: prePersist,  connection: default }

Пусть наша сущность будет такой ( для примера )

<?php

namespace App\Entity;

use App\Repository\ProductRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
    private ?\DateTimeInterface $createAt = null;

    #[ORM\Column(length: 255)]
    private ?string $title = null;

    #[ORM\Column(length: 10)]
    private ?string $shop = null;

    #[ORM\Column]
    private ?int $price = null;

    #[ORM\Column]
    private ?int $pricePiece = null;

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

    public function getCreateAt(): ?\DateTimeInterface
    {
        return $this->createAt;
    }

    public function setCreateAt(\DateTimeInterface $createAt): static
    {
        $this->createAt = $createAt;

        return $this;
    }

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(string $title): static
    {
        $this->title = $title;

        return $this;
    }

    public function getShop(): ?string
    {
        return $this->shop;
    }

    public function setShop(string $shop): static
    {
        $this->shop = $shop;

        return $this;
    }

    public function getPrice(): ?int
    {
        return $this->price;
    }

    public function setPrice(int $price): static
    {
        $this->price = $price;

        return $this;
    }

    public function getPricePiece(): ?int
    {
        return $this->pricePiece;
    }

    public function setPricePiece(int $pricePiece): static
    {
        $this->pricePiece = $pricePiece;

        return $this;
    }
}

Теперь при добавлении сущности в любом месте проекта, будет автоматически подставляться текущая дата и время в свойство createAt

class ProductRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Product::class);
    }

    public function add(string $shop, ProductDto $productDto): void
    {
        $product = (new Product())
            ->setShop($shop)
            ->setPrice($productDto->priceRub)
            ->setTitle($productDto->name);
        $this->_em->persist($product);
        $this->_em->flush();
    }
}

, ,

18 января, 2025

Маскировка HttpClient Symfony под браузер

Всем привет! Иногда в проекте на Symfony удобно задать один единственный заголовок по-умолчанию чтобы везде где требуется вызвать HttpClient не нужно было бы каждый раз прописывать заголовок.

Делается это достаточно просто. Например часто нужно прикинутся браузером в своем скрипте, для этого достаточно задать верный заголовок User-Agent и никто не догадается, что запрос пришел не от браузера, а от вашего скрипта на PHP

Я представлю несколько вариантов.

Статичный вариант представляет из себя возможность добавить заголовок в настройки Symfony для

Добавляем в config/packages/framework.yaml следующий код:

framework:
    http_client:
        default_options:
            headers:
                User-Agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'

Таким образом где бы вы не вызвали в коде http client в Symfony в запросе будет передаваться заголовок User-Agent c данными из настройки

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

<?php

namespace App\Service;

use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;

class RandomUserAgentMiddleware implements HttpClientInterface
{
    private HttpClientInterface $httpClient;
    private array $userAgents = [
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7; rv:118.0) Gecko/20100101 Firefox/118.0',
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67',
        'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1',
        'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 OPR/97.0.0.0'
    ];

    public function __construct(HttpClientInterface $httpClient)
    {
        $this->httpClient = $httpClient;
    }

    public function request(string $method, string $url, array $options = []): ResponseInterface
    {
        $options['headers']['User-Agent'] = $this->userAgents[array_rand($this->userAgents)];
        return $this->httpClient->request($method, $url, $options);
    }

    // Прокси для всех остальных методов
    public function stream($responses, float $timeout = null): ResponseStreamInterface
    {
        return $this->httpClient->stream($responses, $timeout);
    }

    public function withOptions(array $options): static
    {
        $new = clone $this;
        $new->httpClient = $this->httpClient->withOptions($options);
        return $new;
    }
}

И прописываем для прослойки декоратор http_client в services.yaml

services:
    App\Service\RandomUserAgentMiddleware:
        decorates: 'http_client'

Вызываем в коде проекта как обычно

class MySuperClass
{
    public function __construct(
        protected HttpClientInterface   $client,
        private readonly string $url
    )
    {
    }

    public function getData(): void
    {
        // Вот тут уже будет запрос с заголовком User-Agent реального браузера
        $response = $this->client->request('GET', $this->url);
        $data = $response->getContent();
        // ...
    }
}