Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
symfony-bundle-core-advanced-patterns
symfony-bundle-core-advanced-patterns
symkit/sitemap-bundle
claude-code
symfony-bundles
dependency-injection
compiler-passes
event-dispatcher
security-voter
service-tags
container-configuration

Advanced Symfony bundle patterns: compiler passes, event system, security voters, conditional service registration, configuration validation, deep merge pitfalls, cache warmers, and semantic versioning contracts. Referenced from the main symfony-bundle-core skill for deeper topics.

Install
php artisan boost:add-skill symkit/sitemap-bundle

Save this content to: .claude/skills/symfony-bundle-core-advanced-patterns/SKILL.md

# Advanced Bundle Patterns Reference

## Compiler Passes

Use compiler passes for DI manipulation that can't be expressed declaratively:

```php
<?php
// src/DependencyInjection/CompilerPass/PluginRegistrationPass.php
namespace Acme\BlogBundle\DependencyInjection\CompilerPass;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class PluginRegistrationPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        if (!$container->has('acme_blog.plugin_registry')) {
            return;
        }

$definition = $container->findDefinition('acme_blog.plugin_registry');
        $taggedServices = $container->findTaggedServiceIds('acme_blog.plugin');

        foreach ($taggedServices as $id => $tags) {
            $definition->addMethodCall('addPlugin', [new Reference($id)]);
        }
    }
}
```

Register in the bundle class:

```php
public function build(ContainerBuilder $container): void
{
    parent::build($container);
    $container->addCompilerPass(new PluginRegistrationPass());
}
```

## Event System

Expose bundle events for extensibility:

```php
<?php
// src/Event/ArticlePublishedEvent.php
namespace Acme\BlogBundle\Event;

use Acme\BlogBundle\Entity\Article;
use Symfony\Contracts\EventDispatcher\Event;

class ArticlePublishedEvent extends Event
{
    public const NAME = 'acme_blog.article.published';

    public function __construct(
        private readonly Article $article,
    ) {}

    public function getArticle(): Article
    {
        return $this->article;
    }
}
```

## Security Voter

If bundle needs authorization logic:

```php
<?php
// src/Security/ArticleVoter.php
namespace Acme\BlogBundle\Security;

use Acme\BlogBundle\Entity\Article;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class ArticleVoter extends Voter
{
    public const EDIT = 'ACME_BLOG_ARTICLE_EDIT';
    public const DELETE = 'ACME_BLOG_ARTICLE_DELETE';

protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::EDIT, self::DELETE])
            && $subject instanceof Article;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        // Bundle provides base logic; host app can decorate or replace
        return false;
    }
}
```

Prefix voter attributes with bundle name to avoid collisions.

## Conditional Service Registration

Handle optional dependencies gracefully:

```php
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
    $container->import('../config/services.xml');

// Only register Twig extension if Twig is available
    if (interface_exists(\Twig\Extension\ExtensionInterface::class)) {
        $container->import('../config/services_twig.xml');
    }

    // Only register Doctrine listeners if Doctrine is available
    if (interface_exists(\Doctrine\ORM\EntityManagerInterface::class)) {
        $container->import('../config/services_doctrine.xml');
    }
}
```

In `composer.json`, these go in `suggest`, not `require`:

```json
{
    "suggest": {
        "twig/twig": "Required for template rendering (^3.0)",
        "doctrine/orm": "Required for database persistence (^3.0)"
    }
}
```

## Configuration Validation with Callbacks

For complex validation beyond type checking:

```php
public function configure(DefinitionConfigurator $definition): void
{
    $definition->rootNode()
        ->children()
            ->arrayNode('connection')
                ->children()
                    ->scalarNode('host')->isRequired()->end()
                    ->integerNode('port')->defaultValue(443)->end()
                    ->enumNode('protocol')
                        ->values(['https', 'http', 'ws', 'wss'])
                        ->defaultValue('https')
                    ->end()
                ->end()
                ->validate()
                    ->ifTrue(function ($v) {
                        return $v['protocol'] === 'http' && $v['port'] === 443;
                    })
                    ->thenInvalid('Port 443 is typically used with HTTPS, not HTTP.')
                ->end()
            ->end()
        ->end()
    ;
}
```

## Deep Merge Pitfalls

When defining array nodes, explicitly control merge behavior:

```php
->arrayNode('widgets')
    ->useAttributeAsKey('name')
    ->normalizeKeys(false)          // Don't convert dashes to underscores
    ->arrayPrototype()
        ->children()
            ->scalarNode('type')->isRequired()->end()
            ->arrayNode('options')
                ->performNoDeepMerging()  // CRITICAL: prevent unpredictable deep merge
                ->variablePrototype()->end()
            ->end()
        ->end()
    ->end()
->end()
```

Without `performNoDeepMerging()`, if a user tries to override a default widget's options, the arrays merge unpredictably instead of replacing cleanly.

## Cache Warmers

If your bundle needs to pre-compute data at cache warmup:

```php
<?php
namespace Acme\BlogBundle\CacheWarmer;

use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;

class PluginCacheWarmer implements CacheWarmerInterface
{
    public function isOptional(): bool
    {
        return true; // Don't block deployment if this fails
    }

    public function warmUp(string $cacheDir, ?string $buildDir = null): array
    {
        // Pre-compute and cache plugin metadata
        return []; // Return list of files generated
    }
}
```

Register with tag `kernel.cache_warmer` in services.xml.

## Semantic Versioning Contract

Your bundle's public API (what constitutes a BC break) includes:
- Service IDs marked as `public`
- Configuration tree structure (removing/renaming keys)
- Event class names and properties
- Interface method signatures
- Entity field names (if using inheritance mapping)
- Twig function/filter names
- Stimulus controller names and their values/targets API

Changes to any of these require a major version bump.

Advanced Bundle Patterns Reference

Compiler Passes

Use compiler passes for DI manipulation that can't be expressed declaratively:

<?php
// src/DependencyInjection/CompilerPass/PluginRegistrationPass.php
namespace Acme\BlogBundle\DependencyInjection\CompilerPass;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class PluginRegistrationPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        if (!$container->has('acme_blog.plugin_registry')) {
            return;
        }

$definition = $container->findDefinition('acme_blog.plugin_registry');
        $taggedServices = $container->findTaggedServiceIds('acme_blog.plugin');

        foreach ($taggedServices as $id => $tags) {
            $definition->addMethodCall('addPlugin', [new Reference($id)]);
        }
    }
}

Register in the bundle class:

public function build(ContainerBuilder $container): void
{
    parent::build($container);
    $container->addCompilerPass(new PluginRegistrationPass());
}

Event System

Expose bundle events for extensibility:

<?php
// src/Event/ArticlePublishedEvent.php
namespace Acme\BlogBundle\Event;

use Acme\BlogBundle\Entity\Article;
use Symfony\Contracts\EventDispatcher\Event;

class ArticlePublishedEvent extends Event
{
    public const NAME = 'acme_blog.article.published';

    public function __construct(
        private readonly Article $article,
    ) {}

    public function getArticle(): Article
    {
        return $this->article;
    }
}

Security Voter

If bundle needs authorization logic:

<?php
// src/Security/ArticleVoter.php
namespace Acme\BlogBundle\Security;

use Acme\BlogBundle\Entity\Article;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class ArticleVoter extends Voter
{
    public const EDIT = 'ACME_BLOG_ARTICLE_EDIT';
    public const DELETE = 'ACME_BLOG_ARTICLE_DELETE';

protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::EDIT, self::DELETE])
            && $subject instanceof Article;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        // Bundle provides base logic; host app can decorate or replace
        return false;
    }
}

Prefix voter attributes with bundle name to avoid collisions.

Conditional Service Registration

Handle optional dependencies gracefully:

public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
    $container->import('../config/services.xml');

// Only register Twig extension if Twig is available
    if (interface_exists(\Twig\Extension\ExtensionInterface::class)) {
        $container->import('../config/services_twig.xml');
    }

    // Only register Doctrine listeners if Doctrine is available
    if (interface_exists(\Doctrine\ORM\EntityManagerInterface::class)) {
        $container->import('../config/services_doctrine.xml');
    }
}

In composer.json, these go in suggest, not require:

{
    "suggest": {
        "twig/twig": "Required for template rendering (^3.0)",
        "doctrine/orm": "Required for database persistence (^3.0)"
    }
}

Configuration Validation with Callbacks

For complex validation beyond type checking:

public function configure(DefinitionConfigurator $definition): void
{
    $definition->rootNode()
        ->children()
            ->arrayNode('connection')
                ->children()
                    ->scalarNode('host')->isRequired()->end()
                    ->integerNode('port')->defaultValue(443)->end()
                    ->enumNode('protocol')
                        ->values(['https', 'http', 'ws', 'wss'])
                        ->defaultValue('https')
                    ->end()
                ->end()
                ->validate()
                    ->ifTrue(function ($v) {
                        return $v['protocol'] === 'http' && $v['port'] === 443;
                    })
                    ->thenInvalid('Port 443 is typically used with HTTPS, not HTTP.')
                ->end()
            ->end()
        ->end()
    ;
}

Deep Merge Pitfalls

When defining array nodes, explicitly control merge behavior:

->arrayNode('widgets')
    ->useAttributeAsKey('name')
    ->normalizeKeys(false)          // Don't convert dashes to underscores
    ->arrayPrototype()
        ->children()
            ->scalarNode('type')->isRequired()->end()
            ->arrayNode('options')
                ->performNoDeepMerging()  // CRITICAL: prevent unpredictable deep merge
                ->variablePrototype()->end()
            ->end()
        ->end()
    ->end()
->end()

Without performNoDeepMerging(), if a user tries to override a default widget's options, the arrays merge unpredictably instead of replacing cleanly.

Cache Warmers

If your bundle needs to pre-compute data at cache warmup:

<?php
namespace Acme\BlogBundle\CacheWarmer;

use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;

class PluginCacheWarmer implements CacheWarmerInterface
{
    public function isOptional(): bool
    {
        return true; // Don't block deployment if this fails
    }

    public function warmUp(string $cacheDir, ?string $buildDir = null): array
    {
        // Pre-compute and cache plugin metadata
        return []; // Return list of files generated
    }
}

Register with tag kernel.cache_warmer in services.xml.

Semantic Versioning Contract

Your bundle's public API (what constitutes a BC break) includes:

  • Service IDs marked as public
  • Configuration tree structure (removing/renaming keys)
  • Event class names and properties
  • Interface method signatures
  • Entity field names (if using inheritance mapping)
  • Twig function/filter names
  • Stimulus controller names and their values/targets API

Changes to any of these require a major version bump.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route