Control API output with serialization groups, custom normalizers, and context-aware serialization in API Platform
/plugin marketplace add MakFly/superpowers-symfony/plugin install makfly-superpowers-symfony@MakFly/superpowers-symfonyThis skill inherits all available tools. When active, it can use any tool Claude has access to.
<?php
// src/Entity/User.php
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(
normalizationContext: ['groups' => ['user:list']],
),
new Get(
normalizationContext: ['groups' => ['user:read']],
),
new Post(
denormalizationContext: ['groups' => ['user:create']],
),
new Put(
denormalizationContext: ['groups' => ['user:update']],
),
],
)]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['user:list', 'user:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['user:list', 'user:read', 'user:create', 'user:update'])]
private string $name;
#[ORM\Column(length: 255, unique: true)]
#[Groups(['user:read', 'user:create'])] // Not in list, not updatable
private string $email;
#[ORM\Column]
#[Groups(['user:create'])] // Write-only
private string $password;
#[ORM\Column]
#[Groups(['user:read'])] // Read-only, not in list
private \DateTimeImmutable $createdAt;
#[ORM\OneToMany(targetEntity: Post::class, mappedBy: 'author')]
#[Groups(['user:read'])] // Only on detail view
private Collection $posts;
}
<?php
// src/Entity/Post.php
#[ApiResource(
normalizationContext: ['groups' => ['post:read']],
)]
class Post
{
#[Groups(['post:read', 'user:read'])]
private ?int $id = null;
#[Groups(['post:read', 'user:read'])]
private string $title;
#[Groups(['post:read'])] // Full content only on post detail
private string $content;
#[ORM\ManyToOne(targetEntity: User::class)]
#[Groups(['post:read'])]
private User $author;
}
// src/Entity/User.php
class User
{
// When user:read includes posts, only id and title are shown
#[Groups(['user:read'])]
private Collection $posts;
}
<?php
// src/Serializer/Normalizer/UserNormalizer.php
namespace App\Serializer\Normalizer;
use App\Entity\User;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class UserNormalizer implements NormalizerInterface
{
public function __construct(
#[Autowire(service: 'serializer.normalizer.object')]
private NormalizerInterface $normalizer,
) {}
public function normalize(mixed $object, ?string $format = null, array $context = []): array
{
/** @var User $object */
$data = $this->normalizer->normalize($object, $format, $context);
// Add computed fields
$data['fullName'] = $object->getFirstName() . ' ' . $object->getLastName();
$data['postCount'] = $object->getPosts()->count();
$data['isVerified'] = $object->getVerifiedAt() !== null;
return $data;
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof User;
}
public function getSupportedTypes(?string $format): array
{
return [User::class => true];
}
}
<?php
// src/Serializer/Normalizer/PostNormalizer.php
namespace App\Serializer\Normalizer;
use App\Entity\Post;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class PostNormalizer implements NormalizerInterface
{
public function __construct(
private NormalizerInterface $normalizer,
private Security $security,
) {}
public function normalize(mixed $object, ?string $format = null, array $context = []): array
{
/** @var Post $object */
$data = $this->normalizer->normalize($object, $format, $context);
// Add admin-only fields
if ($this->security->isGranted('ROLE_ADMIN')) {
$data['internalNotes'] = $object->getInternalNotes();
$data['moderationStatus'] = $object->getModerationStatus();
}
// Add owner-only fields
if ($this->security->getUser() === $object->getAuthor()) {
$data['analytics'] = [
'views' => $object->getViewCount(),
'engagement' => $object->getEngagementRate(),
];
}
return $data;
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof Post;
}
public function getSupportedTypes(?string $format): array
{
return [Post::class => true];
}
}
<?php
// src/Serializer/UserContextBuilder.php
namespace App\Serializer;
use ApiPlatform\Serializer\SerializerContextBuilderInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
final class UserContextBuilder implements SerializerContextBuilderInterface
{
public function __construct(
private SerializerContextBuilderInterface $decorated,
private Security $security,
) {}
public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
// Add admin group for admin users
if ($this->security->isGranted('ROLE_ADMIN')) {
$context['groups'][] = 'admin:read';
}
// Add owner group when viewing own resources
$resourceClass = $context['resource_class'] ?? null;
if ($resourceClass && $this->isOwner($request, $resourceClass)) {
$context['groups'][] = 'owner:read';
}
return $context;
}
private function isOwner(Request $request, string $resourceClass): bool
{
// Implementation depends on your resource structure
return false;
}
}
Register as decorator:
# config/services.yaml
services:
App\Serializer\UserContextBuilder:
decorates: 'api_platform.serializer.context_builder'
Prevent circular references:
use Symfony\Component\Serializer\Attribute\MaxDepth;
class User
{
#[MaxDepth(1)]
#[Groups(['user:read'])]
private Collection $posts;
}
class Post
{
#[MaxDepth(1)]
#[Groups(['post:read'])]
private User $author;
}
Enable in configuration:
#[ApiResource(
normalizationContext: [
'groups' => ['post:read'],
'enable_max_depth' => true,
],
)]
class Post { /* ... */ }
use Symfony\Component\Serializer\Attribute\Ignore;
class User
{
#[Ignore]
private string $password;
#[Ignore]
private string $resetToken;
}
entity:operation naming conventionApplies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.