Absolutely, you can achieve similar behavior in Laravel, even though there’s no out-of-the-box service locator as in Symfony. Laravel’s service container is very flexible, and you can mimic the same pattern using a combination of bindings, reflection, and maybe a bit of custom configuration.
Here’s one approach:
1. Tag your VoodooServices
You can tag all your services implementing VoodooInterface in a service provider:
// App\Providers\AppServiceProvider.php
public function register()
{
$this->app->tag([
FooVoodooService::class,
BarVoodooService::class,
// ...all your services
], 'voodoo');
}
Or, if you want to auto-discover, check out the laravel-auto-discovery package, but for most practical cases, a simple array suffices.
2. Mark your Messages
Laravel doesn’t natively handle PHP 8+ Attributes for DI discovery, but you can leverage them using Reflection.
#[Attribute]
class VoodooAttribute
{
public function __construct(public string $serviceClass) {}
}
#[VoodooAttribute(FooVoodooService::class)]
class FooMessage implements MessageInterface
{
// ...
}
3. Implement a VoodooLocator
You can inject all tagged Voodoo services and map them by class:
class VoodooLocator
{
protected array $servicesByClass;
public function __construct(iterable $voodooServices)
{
foreach ($voodooServices as $service) {
$this->servicesByClass[get_class($service)] = $service;
}
}
public function get($serviceClass)
{
if (!isset($this->servicesByClass[$serviceClass])) {
throw new \RuntimeException("Voodoo Service [$serviceClass] not found.");
}
return $this->servicesByClass[$serviceClass];
}
}
Register it:
$this->app->singleton(VoodooLocator::class, function ($app) {
return new VoodooLocator($app->tagged('voodoo'));
});
4. Use Attributes + Locator in Your Handler
class MessageHandler
{
public function __construct(protected VoodooLocator $voodooLocator) {}
public function handle(MessageInterface $message)
{
$voodooClass = $this->getVoodooAttributeClass($message);
$service = $this->voodooLocator->get($voodooClass);
$service->doVoodoo($message);
}
protected function getVoodooAttributeClass($message)
{
$reflection = new \ReflectionClass($message);
$attrs = $reflection->getAttributes(VoodooAttribute::class);
if (empty($attrs)) {
throw new \RuntimeException('No VoodooAttribute found.');
}
return $attrs[0]->newInstance()->serviceClass; // property from the attribute
}
}
5. Example Usage
$message = new FooMessage();
$app->make(MessageHandler::class)->handle($message);
Key Points
- This keeps your code flexible and highly decoupled, letting you assign services declaratively per message using Attributes.
- All your registered services are only those tagged as 'voodoo', not the entire container (so no real service locator anti-pattern risk).
- You might even utilize config files instead of attributes if you prefer, but PHP attributes give the same flavor as your Symfony example.
Let me know if you want a deeper dive into auto-discovery/tagging, or alternatives using configuration instead of attributes!