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