Superpowers Symfony

repository·main·Indexed 41 days ago

https://github.com/makfly/superpowers-symfony

A specialized skill library providing AI coding agents with version-accurate expertise for Symfony 7.4 LTS/8.x, API Platform v4, and Doctrine ORM 3. It includes specialized agents (e.g., symfony-engineer, doctrine-architect), implementation skills for TDD, security, and async messaging, and a bin/symfony-context utility for runtime environment detection across Docker, DDEV, and host setups.

Tokens
120K
Snippets
301
Records
421
Agent score
52%

What's inside Superpowers Symfony

  1. Overview of Symfony Documentation in Superpowers Symfony

    main
    The docs/symfony/ directory provides long-form framework documentation for topics that are too broad to be contained within a single skill. For specific implementation details and technical references, users should look into the individual skills located in content/skills/<name>/, where each skill contains a reference.md file.
  2. Discover Symfony skills via the skills map

    main
    The superpowers-symfony project provides a categorized index of specialized skills designed to help coding agents and developers navigate Symfony development. Skills are grouped into functional domains such as API Platform, Doctrine ORM, Messenger, Security, and Architecture. Use this map to identify the specific skill name (e.g., api-platform-dto-resources) required to accomplish a task.
  3. Available Symfony Skills

    main

    The superpowers-symfony library provides a comprehensive set of specialized skills categorized into several domains. These skills are designed to guide coding agents through specific Symfony development tasks:

    API Platform

    • api-platform-dto-resources: Mapping entities to DTOs using #[Map] and stateOptions.
    • api-platform-filters: Implementing v4 Parameters API or legacy #[ApiFilter].
    • api-platform-resources: Configuring resources with operations, pagination, and OpenAPI.
    • api-platform-security: Securing resources with expressions, voters, and securityPostValidation.
    • api-platform-serialization: Controlling serialization via groups, #[Context], and IRI links.
    • api-platform-state-providers: Using ProviderInterface and ProcessorInterface to decouple data.
    • api-platform-tests: Testing with ApiTestCase.
    • api-platform-versioning: Managing API evolution via deprecation (RFC 8594/9745) or path/header strategies.

    Doctrine ORM

    • doctrine-batch-processing: Large dataset processing (ORM 3 toIterable, flush+clear).
    • doctrine-events: Lifecycle listeners (#[AsDoctrineListener], #[AsEntityListener]).
    • doctrine-fetch-modes: Optimizing via DTO hydration (SELECT NEW) and lazy loading.
    • doctrine-fixtures-foundry: Test data via Zenstruck Foundry v2 factories.
    • doctrine-migrations: Schema versioning with lib 4.x.
    • doctrine-relations: Managing relationships (OneToMany, ManyToMany, etc.) and preventing N+1 queries.
    • doctrine-transactions: Handling transactions (wrapInTransaction) and locking.

    Tests & TDD

    • e2e-panther-playwright: Browser automation with Symfony Panther 2.4 or Playwright.
    • functional-tests: Using WebTestCase, getContainer, and DAMA rollback.
    • tdd-with-pest: Pest v4 (PHP 8.3+) workflow.
    • tdd-with-phpunit: PHPUnit 10/11 workflow.
    • test-doubles-mocking: PHPUnit mocks for unit testing.

    Messenger & Async

    • cqrs-and-handlers: Implementing CQRS with Command/Query buses.
    • messenger-retry-failures: Handling failures with retry strategies and failure transports.
    • symfony-messenger: Async handling with RabbitMQ, Redis, or Doctrine transports.
    • symfony-scheduler: Scheduling recurring tasks (native since 7.x).

    Security

    • form-types-validation: Custom Form Types and validation constraints.
    • rate-limiting: Using #[RateLimit] and RateLimiter (sliding window, token bucket, etc.).
    • symfony-voters: Granular authorization logic.

    Architecture & Services

    • config-env-parameters: Managing .env, secrets vault, and parameters.
    • controller-cleanup: Refactoring logic into services or invokable commands.
    • interfaces-and-autowiring: Dependency Injection with #[Target] and decoration.
    • ports-and-adapters: Hexagonal Architecture implementation.
    • strategy-pattern: Using tagged services for runtime selection.
    • symfony-cache: Using the Cache component with tags and stampede prevention.
    • twig-components: Symfony UX Twig Components (props, slots, CVA).
    • value-objects-and-dtos: Designing immutable (readonly) domain objects.
  4. Best Practices for Doctrine Migrations

    main

    Follow these rules to maintain a healthy database schema:

    1. Review the diff: Always read every addSql() line to prevent destructive DDL.
    2. Schema only: Do not mix data migrations into schema migrations. Use dedicated, idempotent migrations for data changes.
    3. Always write down(): Ensure every change is reversible.
    4. One concern per migration: Keep migrations small and focused.
    5. Never edit applied migrations: If a mistake was made, create a new migration to fix it.
    6. Service migrations: If a migration requires a service, enable enable_service_migrations: true, tag the class doctrine_migrations.migration, and call parent::__construct($connection, $logger) in the constructor.
  5. Create Anonymous Twig Components

    main

    Anonymous components do not require a PHP class. The component name is derived from the template file path. To define properties for an anonymous component, use the {% props %} tag at the top of the template.

    {# templates/components/Button.html.twig #}
    {% props variant = 'primary', label %}
    <button class="btn btn-{{ variant }}" {{ attributes }}>{{ label }}</button>
    <twig:Button variant="danger" label="Delete" type="submit" />
  6. Use Slots in Twig Components

    main

    Slots allow you to inject content into specific areas of a component.

    • In the Template: Use {% block name %}{% endblock %} to define where content should go. You can also check if a block is filled using {% if block('name') is not empty %}.
    • In Usage: Use the <twig:block name="name"> tag inside the component tag to pass content to specific slots.
    {# templates/components/Card.html.twig #}
    <div class="card">
        <div class="card-body">
            {% block content %}{% endblock %}
        </div>
        {% if block('footer') is not empty %}
            <div class="card-footer">
                {% block footer %}{% endblock %}
            </div>
        {% endif %}
    </div>
    <twig:Card title="User Profile">
        <twig:block name="content">
            <p>Name: {{ user.name }}</p>
        </twig:block>
    
        <twig:block name="footer">
            <a href="#">Edit</a>
        </twig:block>
    </twig:Card>
  7. Choose the correct test base class

    main

    Select a base class depending on the scope of your test:

    • TestCase (PHPUnit): Use for pure unit tests with no Symfony kernel.
    • KernelTestCase: Use for integration tests. It boots the kernel and allows access to the real container and services.
    • WebTestCase: Use for functional/application tests. It allows you to simulate HTTP requests through the kernel.

    Important: When using WebTestCase, call static::createClient() to boot the kernel. Do not call bootKernel() manually beforehand, or you will encounter a "kernel already booted" error.

    use Symfonyundle\FrameworkBundle\Test\WebTestCase;
    
    final class HomeControllerTest extends WebTestCase
    {
        public function testHomepageIsSuccessful(): void
        {
            $client = static::createClient();
            $client->request('GET', '/');
    
            $this->assertResponseIsSuccessful();
            $this->assertSelectorTextContains('h1', 'Hello World');
        }
    }
  8. Handle EntityManager State After Exceptions

    main

    When a transaction fails and a rollback occurs, the EntityManager may be in an inconsistent state and should not be used for further operations in the same request.

    Best Practice:

    1. Catch the exception.
    2. Roll back the transaction.
    3. Call $em->clear() to reset the EntityManager and remove all managed entities. This prevents subsequent operations from using stale or broken data.
    4. Re-fetch entities if they are needed for the next step.
    try {
        $this->em->flush();
    } catch (\Exception $e) {
        $this->em->rollback();
    
        // Clear the EntityManager to reset state
        $this->em->clear();
    
        // Re-fetch entities if needed
        $user = $this->em->find(User::class, $userId);
    }
  9. Choosing a Doctrine event mechanism

    main

    Doctrine provides three ways to handle entity lifecycle events. Choose based on scope and whether you need access to Symfony services:

    1. Lifecycle callbacks: Fastest. Use for self-contained logic that only touches the entity's own state (e.g., setting a createdAt timestamp). Cannot access services.
    2. Entity listeners: Medium cost. Use when a specific entity needs to interact with a service (e.g., sending an email via a MailerInterface). Bound to a single entity class.
    3. Lifecycle listeners: Slowest. Use for cross-cutting concerns that apply to many or all entities (e.g., global audit logging). Must type-check the entity inside the handler because they fire for every entity.
  10. How to react to Doctrine entity lifecycles in Symfony

    main

    To implement side effects on entity persistence (such as timestamps, slugs, search indexing, or notifications), you should choose one of three mechanisms based on the required scope and dependencies:

    1. Lifecycle Callbacks: Use for simple logic that only affects a single entity and requires no external services. Requires the #[ORM\"HasLifecycleCallbacks] attribute on the entity class.
    2. Entity Listeners: Use when logic is specific to one entity but requires access to external services (dependency injection). Wire these using the #[AsEntityListener] attribute.
    3. Lifecycle Listeners: Use for cross-cutting concerns that apply to all entities or multiple entity types. Wire these using the #[AsDoctrineListener] attribute.

    Important Guardrails:

    • Never call flush() inside a Doctrine event handler; this corrupts the in-flight unit of work.
    • ORM 3.0 Compatibility: Do not use EventSubscriberInterface. Use attributes instead.
    • preUpdate restrictions: When using preUpdate events, you must change fields using $args->setNewValue(). Do not attempt to touch associations within this event.
    • Global Listener Safety: If using a global lifecycle listener (#[AsDoctrineListener]), always type-check the entity early, as the listener fires for every entity in the unit of work.
    // Example of choosing the narrowest mechanism:
    
    // 1. Callback (No deps, single entity)
    #[ORM\HasLifecycleCallbacks]
    class User {
        #[ORM\PrePersist]
        public function updateTimestamp(): void { ... }
    }
    
    // 2. Entity Listener (Needs services, single entity)
    #[AsEntityListener(event: 'postPersist', method: 'onPostPersist', entity: User::class)]
    class UserListener {
        public function onPostPersist(User $user, PostPersistEventArgs $args): void { ... }
    }
    
    // 3. Lifecycle Listener (Cross-cutting, all entities)
    #[AsDoctrineListener]
    class GlobalAuditListener {
        public function postPersist(PostPersistEventArgs $args): void {
            $entity = $args->getObject();
            if (!$entity instanceof User) return; // Type-check early!
            // ...
        }
    }
  11. Design idempotent handlers for safe retries

    main

    Because Messenger retries messages upon failure, handlers must be idempotent. An idempotent handler can be executed multiple times with the same message without causing unintended side effects (like double-charging a customer).

    Patterns for idempotency:

    1. State Check: Check if the entity associated with the message is already in the desired state (e.g., if ($order->getStatus() === OrderStatus::PROCESSED) return;).
    2. Idempotency Keys: When calling external APIs, pass a unique key (e.g., order_id + timestamp) so the external service can recognize and ignore duplicate requests.
    #[AsMessageHandler]
    class ProcessOrderHandler
    {
        public function __invoke(ProcessOrder $message): void
        {
            $order = $this->orders->find($message->orderId);
    
            // Idempotency check
            if ($order->getStatus() === OrderStatus::PROCESSED) {
                return;
            }
    
            $this->paymentGateway->charge(
                amount: $order->getTotal(),
                idempotencyKey: 'order_' . $order->getId()
            );
    
            $order->setStatus(OrderStatus::PROCESSED);
            $this->em->flush();
        }
    }
  12. Declare API Platform operations explicitly

    main

    In API Platform v4, operations are declared using classes from the ApiPlatform\Metadata namespace within the #[ApiResource] attribute.

    CRITICAL v4 behavior change: As soon as you manually declare any operation in the operations array, API Platform stops auto-registering the default CRUD routes. You must explicitly declare every operation you want to expose (e.g., Get, Post, Put, etc.).

    Default behaviors (when no operations key is provided):

    • Item operations: GET (mandatory), PATCH, DELETE. Note that PUT is never auto-registered.
    • Collection operations: GET (mandatory), POST.

    Disabling all routes: To expose a model for subrequests or IRIs only without any HTTP routes, pass an empty array to operations.

    Note: The v2-era collectionOperations and itemOperations arrays were removed in v3 and are fully gone in v4.

    <?php
    // src/Entity/Book.php
    
    namespace App\Entity;
    
    use ApiPlatform\Metadata\ApiResource;
    use ApiPlatform\Metadata\Get;
    use ApiPlatform\Metadata\GetCollection;
    use ApiPlatform\Metadata\Post;
    use ApiPlatform\Metadata\Put;
    use ApiPlatform\Metadata\Patch;
    use ApiPlatform\Metadata\Delete;
    
    #[ApiResource(
        operations: [
            new GetCollection(),
            new Get(),
            new Post(),
            new Put(),     // PUT is NOT registered automatically — declare it if you need it
            new Patch(),
            new Delete(),
        ],
    )]
    class Book
    {
        // ...
    }