vanio/vanio-di-extra-bundle
Symfony bundle that extends the framework's dependency injection container with extra configuration options and conveniences, helping PHP developers define, wire, and organize services with less boilerplate in their applications.
This bundle is a dependency-injection add-on for Symfony: expect it to reduce boilerplate around service discovery, interface-based tagging, and compiler passes rather than to replace Symfony's DI container. Treat it as sugar on top of services.yaml, not as a framework of its own.
Minimal setup
composer require vanio/di-extra-bundle
If there is no Flex recipe (likely, given the package's size), register it manually:
// config/bundles.php
return [
// ...
Vanio\DIExtraBundle\VanioDIExtraBundle::class => ['all' => true],
];
Then clear the cache and confirm the extension loads:
bin/console cache:clear
bin/console debug:container --parameters | grep -i vanio
bin/console debug:config vanio_di_extra # if the bundle exposes a config tree
Where to look first
src/DependencyInjection/Configuration.php and .../VanioDIExtraExtension.php — the actual config keys and defaults. Do not trust README examples over this file.src/DependencyInjection/Compiler/ — this is where the value usually is. Read every pass and note whether it runs beforeOptimization or beforeRemoval.src/Resources/config/ — the service definitions the bundle registers for itself; these tell you what tags and aliases are expected.composer.json — check the Symfony version constraint before adopting. A 0-star, 0-dependent package is unlikely to track Symfony 7 deprecations.First use case
The realistic first win is eliminating hand-written service definitions for an application namespace while auto-tagging services by implemented interface:
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude: '../src/{Entity,Kernel.php}'
If the bundle provides namespace-based registration or interface-to-tag mapping, wire one namespace and one marker interface first, run debug:container --tag=<your_tag>, and only then expand. Start with a single bounded context (e.g. App\Payment\) rather than App\ so a misconfiguration does not silently register entities or DTOs as services.
Convention-based registration
The dominant pattern is: define the namespace once, let the bundle (or _defaults) handle the rest, and rely on constructor autowiring. Keep exclusions explicit and narrow.
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\Domain\:
resource: '../src/Domain/'
exclude: '../src/Domain/{Entity,ValueObject}/'
Prefer this over per-class attributes when you have many small services; prefer attributes when you need per-service control (lazy, decorates, tags) on a minority of classes.
Interface-driven tagging
Marker interfaces plus a single tag mapping scale much better than tagging each class:
interface PaymentMethodInterface
{
public function supports(Order $order): bool;
}
Configure the mapping once (key names depend on the bundle's Configuration.php — verify before committing):
vanio_di_extra:
tags:
App\Payment\PaymentMethodInterface: app.payment_method
Consume it with a tagged iterator/locator rather than injecting the container:
final class PaymentMethodRegistry
{
public function __construct(
#[TaggedIterator('app.payment_method')]
private readonly iterable $methods,
) {}
}
For lookups by key, use a tagged locator and index services with #[AsTaggedItem(index: 'stripe')] or an explicit priority/index in the tag attributes.
Compiler passes as the extension point
Anything the bundle does not cover, do in your own pass. This is also how you customize what the bundle's passes produce:
final class HandlerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
foreach ($container->findTaggedServiceIds('app.handler') as $id => $tags) {
$definition = $container->getDefinition($id);
$definition->setArgument('$bus', new Reference('app.command_bus'));
$definition->addTag('monolog.logger', ['channel' => 'handler']);
}
}
}
Register it in your kernel's build():
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new HandlerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10);
}
If the bundle exposes a base pass class or a pass-priority convention, use it so your pass runs in a predictable order relative to theirs.
Integration with autoconfigure
Use _instanceof for global behavior and let the bundle handle discovery:
services:
_instanceof:
App\Shared\Domain\EventHandlerInterface:
tags: ['app.event_handler']
Watch for double-tagging: if both _instanceof and the bundle's interface mapping add the same tag, you can end up with duplicated entries in !tagged_iterator or unexpected priorities. Pick one mechanism per interface.
Testing
Use the test container rather than the real one:
final class PaymentMethodRegistryTest extends KernelTestCase
{
public function testMethodsAreDiscovered(): void
{
self::bootKernel();
$registry = self::getContainer()->get(PaymentMethodRegistry::class);
self::assertNotEmpty(iterator_to_array($registry->all()));
}
}
Because the bundle's value is container shape, a cheap smoke test that boots the kernel and asserts a few tagged collections are non-empty catches most misconfigurations.
composer.json, and be prepared to vendor/fork it. MIT license makes that easy; read the passes before you rely on them.References after RemoveUnusedDefinitionsPass may be ignored or leave services private. Use TYPE_BEFORE_OPTIMIZATION for wiring and TYPE_BEFORE_REMOVAL only when you intentionally react to the final graph. When debugging, re-run with bin/console debug:container --tag=<tag> and check the compiled container in var/cache/dev/App_KernelDevDebugContainer.php._instanceof + bundle mapping. This is the most common silent failure: the service is discovered twice so it appears twice in a tagged iterator. Audit with debug:container --tag.public: false and test access. In tests, use self::getContainer() (test service container), not $kernel->getContainer()->get() for private services. If you must fetch directly, alias it in config/services_test.yaml.resource: '../src/' will pick up entities, migrations, and DTOs unless excluded. Always exclude Entity, Migrations, Kernel.php, and data-transfer/value objects. A mis-registered entity surfaces later as odd Doctrine errors, not as a DI error.doctrine/annotations, which is abandoned and removed in newer Doctrine versions. Check whether the bundle uses ReflectionAttribute (PHP 8) or the annotation reader; if the former, annotations in docblocks will be silently ignored.vanio_di_extra) and every key in Configuration.php. Symfony silently ignores unknown keys only if the extension is not strict; if it is strict you get a clear error, if not you get silent no-ops.bin/console cache:clear in dev and a container rebuild in prod. debug:container reads the compiled container, so a stale cache will show stale tags and send you down the wrong path.debug:autowiring <type> for missing aliases, debug:container --show-private --tag=<tag> for discovery, lint:container for broken references, and debug:config <alias> for config shape. Set container.dumper.inline_factories: false locally if you need to read the dumped container.#[AutoconfigureTag], _instanceof, !tagged_iterator, #[AsTaggedItem], and #[AutowireLocator] cover most of what DI extra bundles historically provided. Use the bundle only for the specific convenience it adds; otherwise you take on maintenance risk for behavior already in core.composer update and this package will not give you a deprecation window.composer why-not symfony/framework-bundle 7.* against the installed package to see whether it constrains you. A DI bundle that pins Symfony 5/6 will block an upgrade of the whole app.How can I help you explore Laravel packages today?