MidПрактика4 min

Cache и Serializer

Cache component, cache pools, Serializer component, normalizers, encoders, groups

Cache Component

Symfony 8.0: Компонент Cache явно указан в темах экзамена.

Symfony Cache реализует PSR-6 (CacheItemPoolInterface) и PSR-16 (CacheInterface). Рекомендуемый подход -- использовать контрактный интерфейс CacheInterface.

Использование CacheInterface (рекомендуемый)

<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;

final class ProductCatalog
{
    public function __construct(
        private readonly CacheInterface $cache,
        private readonly ProductRepository $repository,
    ) {
    }

    public function getFeaturedProducts(): array
    {
        // get() handles cache miss automatically
        return $this->cache->get('featured_products', function (ItemInterface $item): array {
            // Set TTL
            $item->expiresAfter(3600); // 1 hour

            // Tag for invalidation
            $item->tag(['products', 'featured']);

            // This callback executes only on cache miss
            return $this->repository->findFeaturedProducts();
        });
    }

    public function getProductById(int $id): ?Product
    {
        return $this->cache->get(
            "product_{$id}",
            function (ItemInterface $item) use ($id): ?Product {
                $item->expiresAfter(1800); // 30 minutes
                $item->tag(['products', "product_{$id}"]);

                return $this->repository->find($id);
            }
        );
    }

    public function invalidateProduct(int $id): void
    {
        $this->cache->delete("product_{$id}");
    }
}

Cache Pools

# config/packages/cache.yaml
framework:
    cache:
        # Default adapter for all pools
        app: cache.adapter.redis
        system: cache.adapter.system

        # Custom pools
        pools:
            cache.products:
                adapter: cache.adapter.redis
                default_lifetime: 3600

            cache.api_responses:
                adapter: cache.adapter.filesystem
                default_lifetime: 600

            cache.sessions:
                adapter: cache.adapter.redis
                default_lifetime: 86400
<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\Cache\CacheInterface;

final class ApiService
{
    public function __construct(
        // Inject specific cache pool
        #[Autowire(service: 'cache.api_responses')]
        private readonly CacheInterface $cache,
    ) {
    }
}

Tag-based Invalidation

<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Contracts\Cache\TagAwareCacheInterface;

final class CacheManager
{
    public function __construct(
        private readonly TagAwareCacheInterface $cache,
    ) {
    }

    public function invalidateProductCache(): void
    {
        // Invalidate all cache items tagged with 'products'
        $this->cache->invalidateTags(['products']);
    }

    public function invalidateSpecificProduct(int $id): void
    {
        $this->cache->invalidateTags(["product_{$id}"]);
    }
}

Serializer Component

Symfony 8.0: Компонент Serializer явно указан в темах экзамена.

Serializer преобразует объекты в различные форматы (JSON, XML, CSV) и обратно.

Object -> Normalize -> Encode -> JSON/XML/CSV
JSON/XML/CSV -> Decode -> Denormalize -> Object

Базовое использование

<?php

declare(strict_types=1);

namespace App\Controller;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Serializer\SerializerInterface;

final class ApiController extends AbstractController
{
    public function __construct(
        private readonly SerializerInterface $serializer,
    ) {
    }

    public function getUser(User $user): JsonResponse
    {
        $json = $this->serializer->serialize($user, 'json', [
            'groups' => ['user:read'],
        ]);

        return new JsonResponse($json, json: true);
    }

    public function createUser(Request $request): JsonResponse
    {
        $user = $this->serializer->deserialize(
            $request->getContent(),
            User::class,
            'json',
        );

        // $user is now a User object populated from JSON

        return new JsonResponse(['id' => $user->getId()], 201);
    }
}

Serialization Groups

<?php

declare(strict_types=1);

namespace App\Entity;

use Symfony\Component\Serializer\Attribute\Groups;

final class User
{
    #[Groups(['user:read', 'user:write'])]
    private string $email;

    #[Groups(['user:read'])]
    private string $name;

    // Password -- only writable, never in response
    #[Groups(['user:write'])]
    private string $password;

    #[Groups(['user:read', 'admin:read'])]
    private array $roles = [];

    #[Groups(['admin:read'])]
    private \DateTimeImmutable $createdAt;

    // Not in any group -- excluded from serialization by default
    private string $internalToken;

    // ... getters/setters
}
<?php

declare(strict_types=1);

// Serialize with specific groups
$json = $serializer->serialize($user, 'json', [
    'groups' => ['user:read'],
]);
// Result: {"email":"[email protected]","name":"John","roles":["ROLE_USER"]}

$json = $serializer->serialize($user, 'json', [
    'groups' => ['admin:read'],
]);
// Result: {"roles":["ROLE_USER"],"createdAt":"2025-01-15T10:30:00+00:00"}

Normalizers

Normalizer преобразует объект в массив и обратно. Порядок normalizers важен -- используется первый подходящий.

Normalizer Описание
ObjectNormalizer Основной, через getters/setters/property access
GetSetMethodNormalizer Только через get/set методы
PropertyNormalizer Через рефлексию (public properties)
DateTimeNormalizer DateTime <-> string
ArrayDenormalizer Десериализация массивов объектов
BackedEnumNormalizer PHP backed enums
UidNormalizer UUID/ULID

Кастомный Normalizer

<?php

declare(strict_types=1);

namespace App\Serializer;

use App\Entity\Money;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

final class MoneyNormalizer implements NormalizerInterface
{
    public function normalize(
        mixed $object,
        ?string $format = null,
        array $context = [],
    ): array {
        /** @var Money $object */
        return [
            'amount' => $object->getAmount(),
            'currency' => $object->getCurrency()->value,
            'formatted' => $object->format(),
        ];
    }

    public function supportsNormalization(
        mixed $data,
        ?string $format = null,
        array $context = [],
    ): bool {
        return $data instanceof Money;
    }

    public function getSupportedTypes(?string $format): array
    {
        return [Money::class => true];
    }
}

Encoders

Encoder Формат Описание
JsonEncoder json JSON формат
XmlEncoder xml XML формат
CsvEncoder csv CSV формат
YamlEncoder yaml YAML формат

#[SerializedName] и #[SerializedPath]

<?php

declare(strict_types=1);

namespace App\DTO;

use Symfony\Component\Serializer\Attribute\SerializedName;
use Symfony\Component\Serializer\Attribute\SerializedPath;
use Symfony\Component\Serializer\Attribute\Ignore;

final readonly class ApiResponse
{
    public function __construct(
        // Rename field in JSON
        #[SerializedName('user_id')]
        public int $userId,

        // Access nested path
        #[SerializedPath('[data][attributes][name]')]
        public string $name,

        // Ignore this property
        #[Ignore]
        public string $internalField = '',
    ) {
    }
}

Circular Reference Handling

<?php

declare(strict_types=1);

// Configure max depth and circular reference
$json = $serializer->serialize($entity, 'json', [
    'circular_reference_handler' => fn(object $object) => $object->getId(),
    'max_depth_handler' => fn(
        mixed $innerObject,
        string $outerObject,
        string $attributeName,
        string $format,
        array $context,
    ) => null,
]);
<?php

declare(strict_types=1);

use Symfony\Component\Serializer\Attribute\MaxDepth;

final class Category
{
    private string $name;

    // Limit depth to prevent infinite recursion
    #[MaxDepth(2)]
    private ?Category $parent = null;

    /** @var list<Category> */
    #[MaxDepth(1)]
    private array $children = [];
}

Итоги

  • Cache: PSR-6/16, CacheInterface::get() с callback при miss
  • Cache pools для разных хранилищ, tag-based invalidation
  • Serializer: Object -> normalize -> encode -> format
  • Groups контролируют, какие поля сериализуются
  • #[Groups], #[SerializedName], #[Ignore], #[MaxDepth] -- атрибуты
  • Normalizers: ObjectNormalizer, DateTimeNormalizer, BackedEnumNormalizer
  • Encoders: JSON, XML, CSV, YAML