svb/service-factory-bundle
svb/service-factory-bundle is a PHP bundle that provides a centralized service factory for constructing and wiring services, helping organize object creation and dependency configuration in Symfony applications.
Reality check first. Despite being filed under "Laravel/PHP packages," this artifact is a Symfony bundle (Bundle suffix, config/bundles.php registration, DI compiler passes, "sf service tagging"). Laravel's container solves this differently (contextual binding, tagged() bindings, service providers). If you are on Laravel, stop at "read the source for the pattern" — nothing here is directly usable. The rest of this assumes a Symfony app.
Install and register
composer require svb/service-factory-bundle
There will be no Flex recipe (0 stars, 0 dependents, no third-party signal). Register it by hand:
// config/bundles.php
return [
// ...
SVB\ServiceFactoryBundle\ServiceFactoryBundle::class => ['all' => true],
];
Then look for a reference config in vendor/svb/service-factory-bundle/ — typically Resources/config/, config/, or a DependencyInjection/Configuration.php that tells you the exact config tree keys. Copy the stub into config/packages/service_factory.yaml yourself; nothing will generate it for you.
Where to look first (documentation is thin — the source is the docs):
README.md — usually the only worked example.src/DependencyInjection/*CompilerPass*.php — this is the whole product. Reading ~50 lines tells you exactly which services get collected, how they're keyed, and whether priority/ordering is supported.src/Attribute/ (and src/Annotation/ if it supports PHP 7) — the marker you put on services to enrol them.src/*Factory*.php / src/*Locator*.php — the service you actually inject and call.tests/ — if it exists, it doubles as usage documentation. If it doesn't, treat that as a strong signal.First real use case. The bundle's pitch is replacing repetitive tag registration for factory-resolved collaborators. Concretely, swap this:
# config/services.yaml
services:
App\Handler\FooHandler:
tags: [{ name: 'app.handler', priority: 10 }]
App\Handler\BarHandler:
tags: [{ name: 'app.handler' }]
for an attribute on each class (verify the exact FQCN against src/Attribute/):
use SVB\ServiceFactoryBundle\Attribute\ServiceFactoryItem; // confirm name
#[ServiceFactoryItem('app.handler', priority: 10)]
final class FooHandler implements HandlerInterface { /* ... */ }
and inject the generated factory where you'd previously have injected !tagged_iterator app.handler.
Do this before writing any feature code: boot the kernel and resolve one factory in a smoke test.
bin/console cache:clear
bin/console debug:container 'App\Factory\' --show-private
bin/console debug:autowiring handler
If the factory isn't there or is missing an item, you've learned the adoption cost before committing a sprint to it.
Factory-per-bounded-context. Don't create one global factory. Give each domain its own key (e.g. app.payment.methods, app.notification.channels) and its own typed factory interface. Day-to-day this keeps autowiring unambiguous and makes the compiler pass output debuggable by name.
Interface-first injection. Declare the contract yourself and let the bundle's factory implement or be aliased to it:
interface PaymentMethodFactory
{
/** @return PaymentMethod[] */
public function all(): iterable;
public function get(string $key): PaymentMethod;
}
services:
App\Payment\PaymentMethodFactory:
alias: svb.service_factory.app.payment.methods # confirm generated service id via debug:container
public: true
This is the single most valuable habit with an unaudited bundle: your application code depends on your interface, so swapping to !tagged_locator later is a one-line YAML change with zero PHP churn.
Lazy resolution via the factory, not constructor sprawl. The reason to use a factory over direct injection is that you rarely need all handlers — you need the one selected at runtime. Inject the factory, resolve inside the method:
public function handle(Envelope $e): void
{
$this->methods->get($e->channel())->process($e);
}
This keeps your own service's constructor small and its unit tests trivial (mock the interface, return one stub).
Override in tests by replacing the factory service. Because the factory is an ordinary container service, per-environment overrides are plain Symfony config:
# config/services_test.yaml
services:
App\Payment\PaymentMethodFactory:
class: App\Tests\Double\StubPaymentMethodFactory
public: true
No bundle-specific test hooks required — that's the one genuine ergonomic win here, and it's worth leaning on heavily.
Coexistence migration. Run the bundle alongside existing tag registration for a full release. Enrol services via attribute and keep the YAML tag. The factory resolves via the bundle, the old tagged iterator still exists for anything you haven't migrated. Delete the YAML tags once debug:container confirms the factory has every item. This is the only safe rollout for a package with no version guarantees — each step is independently revertible.
Per-environment gating. If the bundle misbehaves on a Symfony version bump, you want to disable it without a code change. Register the bundle conditionally or keep a feature-flag config key that flips your alias back to a native tagged iterator. Design the seam before you need it.
Ordering and priority. If the source compiler pass supports a priority/order attribute, verify it against debug:container — pass ordering across bundles is not guaranteed and "priority ignored" is a classic symptom of your bundle registering before the AddConsoleCommandPass-style passes it depends on.
Compiler passes are not stable API. The dominant failure mode is not runtime — it's bin/console cache:clear exploding after a Symfony minor/major upgrade because internal DI classes moved or method signatures changed (Definition::setArguments, ServiceLocatorTagPass, PriorityTaggedServiceTrait). Pin the exact version in composer.json ("svb/service-factory-bundle": "1.2.3", not ^1.2), and add a CI job that boots the kernel — compile-time regressions should fail the build, not production.
Failures are asymmetric. Ambiguous autowiring or a missing item shows up as a runtime ServiceNotFoundException deep in one code path, not at container compile time. Mitigate with a single test that asserts $factory->all() returns the expected count; a count assertion catches silent drops far better than a smoke test that only instantiates one item.
No recipe means no config scaffolding. Expect config/packages/service_factory.yaml to be absent. bin/console config:dump-reference <bundle_alias> will print the tree from Configuration.php — that's faster than reverse-engineering the compiler pass for key names.
Beware public: false in tests. Symfony's test container only exposes public services. Factories you want to fetch directly in a test (self::getContainer()->get(...)) must be public: true in services_test.yaml; otherwise you'll get "service not found" and waste an hour.
Always cache:clear after touching attributes or bundle config. Compiler-pass output is baked into the compiled container. In dev with debug: true it recompiles on change to some files, but attribute discovery is not reliably one of them.
Silent misinjection is the scary one. If two services share an interface and the factory keys by class name, you can get the wrong one and only notice on a specific request path. Name keys explicitly in the attribute rather than relying on auto-derived identifiers.
Zero community = you are the support channel. No Stack Overflow footprint, no dependents to learn from, no CVE response path. Before adopting, decide: vendor the code into your own namespace and own it, or don't adopt. MIT license makes forking legally trivial; the engineering decision is whether you want a named owner for a Symfony-upgrade treadmill. Budget an hour per Symfony major for this.
Ramp-up risk is cognitive, not technical. Wiring takes minutes. The cost is that new hires won't recognise the pattern — it duplicates first-party Symfony capability (ServiceSubscriberInterface, !tagged_locator / !tagged_iterator, #[AutowireLocator]). If you adopt, write a one-page convention doc and put a review date on it; if you skip it, the native features are strictly safer.
Ask "is this worth it?" honestly. The decision rule I'd use: count your factory classes and the tags on each. If you have fewer than ~5 factories or a handful of tags, native tagged locators already solve this with zero supply-chain surface. The 49.3 opportunity score reflects a real boilerplate pain point, not this package being the answer.
Extension point if you fork. The compiler pass is the whole product; reimplement it as ~50 lines of AbstractCompilerPass writing a ServiceLocatorTagPass into your factory definition. That gets you the same ergonomics, your own test matrix, and no orphaned-dependency risk on every Symfony upgrade.
How can I help you explore Laravel packages today?