tonybogdanov/magic-services-bundle
A Symfony bundle that brings magic to service definitions, letting you register, configure, and autowire services with minimal boilerplate. It reduces container configuration overhead so you can focus on application logic while keeping dependency injection clean and predictable.
This is a Symfony bundle, not a Laravel package — the first thing to internalise is that it cannot be dropped into a Laravel app and expected to do anything. It hooks into symfony/dependency-injection via a bundle extension and (almost certainly) one or more compiler passes, and those only run inside a Symfony kernel. If your day-to-day is Laravel, treat this as something you'd evaluate for a companion Symfony service, a legacy app you've inherited, or as prior art for how to reduce service-definition boilerplate.
Minimal steps to get it running (in a Symfony 4/5 app):
composer.json first, before the README. With zero stars and zero dependents, the constraint set (php, symfony/*) tells you more about viability than the marketing copy does. Symfony 4.0/5.0 support means it likely predates the 5.4/6.x era — expect PHP 7.x-era constraints.composer require tonybogdanov/magic-services-bundle in a throwaway Symfony 5 project, not in anything production-facing yet.config/bundles.php (or AppKernel::registerBundles() on 4.0-era skeletons).config/packages/ and point it at a namespace/directory — start with a single, small App\Services\ namespace rather than all of src/.bin/console debug:container --show-private 'App\Services' and confirm the services appeared, then cache:clear and boot the kernel to prove nothing exploded.Where to look first: the bundle's DependencyInjection/*Extension.php and any CompilerPass classes. In a bundle of this kind the README will describe the intent ("auto-magic service definition generator") but the actual rules — which classes get registered, what's public, what gets tagged, how IDs are derived — live in those two files. The tests directory is the second stop; it's the only reliable spec for edge cases.
First use case: you have a src/ tree full of small collaborator classes and you're tired of hand-writing services.yaml entries. Instead of listing each one, you let the bundle scan the namespace and register them as autowired, private services. Validate that against Symfony's own resource: '../src/*' autoloading, which has done exactly this since 3.3 — if native resource loading already covers your case, this bundle's value is whatever extra it layers on top (tagging by interface, naming conventions, conditional registration).
Scan-and-register per namespace. The core workflow is: one config entry per source root. Keep the mapping explicit and small (e.g. one entry for App\Services\, another for App\Handler\) instead of a single App\ catch-all. This is the difference between a container you can reason about and one that swallows your entities, DTOs, form types and event subscribers into service definitions that then fail on construction.
Pair it with _defaults rather than replacing them. Symfony 4+ expects services to be private and autowired. If the bundle generates definitions, check whether it honours _defaults from services.yaml or emits its own flags. If it doesn't, set the defaults in the bundle config and treat services.yaml as the place for the handful of services that genuinely need to be public, decorated, or bound with scalar arguments.
Tag by interface, not by class name. For anything event-driven (handlers, subscribers, message consumers) the useful pattern is: let the scanner register the class, then attach tags based on instanceof checks against a marker interface. That keeps the "magic" confined to registration and leaves dispatch wiring declarative. If the bundle doesn't support this directly, a small compiler pass of your own that runs after the bundle's pass is the standard extension point.
Incremental adoption behind explicit definitions. Don't migrate a whole services.yaml in one commit. Leave the explicit definitions in place, add the bundle for one new namespace, and diff debug:container output before and after. Explicit definitions win over generated ones in Symfony when both exist — confirm that ordering, because if the generated ones override yours you'll get silently different constructor arguments.
Keep scalar/parameter wiring explicit. Auto-registration handles constructor-injected class dependencies well and handles %env()%, bindings, and scalars badly. Anything needing a string, a URL, or a feature flag should stay in services.yaml with bind: or explicit arguments:.
Integration with a Laravel codebase. The realistic pattern is a composer package shared between a Laravel app and a Symfony service — pure PHP domain classes with no container coupling. The Symfony side uses this bundle to wire them; the Laravel side uses its own container's constructor auto-resolution or a service provider. Do not attempt to bridge this bundle into Laravel's Illuminate\Container; the abstractions are not compatible and you'd be fighting both.
Testing. Add one container smoke test that boots the kernel and asserts a representative service resolves ($container->get(...) on a public alias, or debug:container in a functional test). With generated definitions, that test is your only guard against a rename quietly removing a service from the graph.
Vendor risk is the headline gotcha. Zero stars, zero dependents, no ecosystem means no Stack Overflow answers, no security advisories, no one else hitting your bug first. Pin the exact version in composer.json, commit composer.lock, and be prepared to fork and vendor it. Read every source file before it touches production — it's small enough to do in an hour, and that hour is cheaper than debugging a compiler pass that silently re-tags your handlers.
"Magic" is a debugging tax. When a service's arguments are wrong, there is no line of YAML to point at. Debug in this order: debug:container 'App\Foo' for the resolved definition, debug:autowiring for what's available, then read the dumped container in var/cache/dev/Container*/ (the XML/PHP dump shows the final compiled state, which is the only truth). Delete the cache directory if the dump looks stale — compiler passes run at compile time, not runtime.
Over-broad scans pick up non-services. Abstract classes, interfaces, traits, entities, enum-like constants holders, and classes with unresolvable constructor args will all get definitions if the scanner is naive. Symptoms: container compile errors about unresolvable arguments for classes you never meant to register. Fix by narrowing the namespace, excluding subdirectories, or marking classes abstract/adding a marker interface the bundle respects.
Service IDs collide. Generated IDs derived from FQCNs are safe; generated IDs derived from short class names are not, and will silently clobber services with the same basename in different namespaces. Verify the ID scheme early with debug:container | grep YourClassName.
Symfony 4+ private-by-default. Anything generated is almost certainly private. $container->get() in a controller will fail. Use constructor injection or a public alias (App\FooInterface: '@App\Foo') for the few entry points that need to be pulled, and remember the test container (test.service_container) is the sanctioned way to reach private services in tests.
Compiler pass ordering. If you extend this with your own pass, register it with the right priority and confirm it runs after the bundle's registration pass — otherwise you're tagging services that don't exist yet. PassConfig::TYPE_BEFORE_OPTIMIZATION vs TYPE_AFTER_REMOVING matters here.
Environment config quirks. Most bundle extensions only read config at compile time, so %env()% values that vary at runtime won't change which services are registered — only their arguments. If you expected per-environment service sets, you'll need separate config files per env, and even then the container cache must be rebuilt on deploy (it is on cache:clear, but not on a plain code push to a warm cache).
Extension points worth knowing. The bundle's config tree (usually the Configuration class), its compiler passes, and Symfony's _instanceof block in services.yaml — that last one lets you apply tags and method calls to everything matching an interface regardless of who registered it, which often removes the need to fight the bundle's own tagging logic.
Before adopting, benchmark against the built-in. Write the equivalent services.yaml using resource: '../src/*' with exclude: and _defaults: { autowire: true, autoconfigure: true }. If that's within a few lines of what the bundle saves you, prefer the built-in — it's maintained, documented, and every Symfony developer who inherits your code already understands it.
How can I help you explore Laravel packages today?