Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

doe_doe_doe's avatar

Symfony Service Locator equivalent

Hi,

I'm relativly new to Laravel and currently I'm searching, which is the best way to design my app.

Let's say I have a MessageInterface and I have about 50x different implementations of this interface. And now I want to build different VoodooServices for these messages. Each message implementation could have a different VoodooService.

In Symfony there would be a nice solution for this: Service locator. I didn't find something similar in Laravel. What would be a good and flexible solution in Laravel?

In Symfony this would look like this:

Message class with attribute:

#[VoodooAttribute(FooVoodooService::class)]
class FooMessage() implements MessageInterface {
	...
}

VoodooInterface

#[AutoconfigureTag('voodoo')]
interface VoodooInterface {
  ...
}

Voodoo service

class FooVoodooService implements VoodooInterface {

  public function doVoodoo(MessageInterface $message): void {
    ...
  }

}

Service Locator

class VoodooLocator extends AbstractLocator {

  public function __construct(
    #[AutowireLocator('voodoo')]
    ServiceLocator $services
  ) {
    parent::__construct($services);
  }
  ...

MessageHandler

class MessageHandler {
  
  public function __constructor(
	private VoodooLocator $voodooLocator,
  )

  public function handle(MessageInterface $message) {
    $voodooClass = $this->getVoodooAttributeClass($message);
    $voodooService = $this->voodooLocator->getService($voodooClass);
    $voodooService->doVoodoo($message);
  }

}

So I have different message classes, where I can assign different voodoo services with an attribute. An easy and flexible design. Then I have a voodoo interface and a service locator. This locator contains all services, which have this voodoo interface. Then in the message handler I read out the voodoo class from the attribute, get the service from the locator and then do voodoo.

Service locators are considered an anti-pattern. But for me, the typical arguments against a service locator don't apply here, because this VoodooLocator contains only implementations of the VoodooInterface - not the whole container.

Now I search for a good and flexible solution in Laravel? Any tips?

0 likes
3 replies
LaryAI's avatar
Level 58

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!

doe_doe_doe's avatar

Is there a better solution for this in the VoodooLocator:

foreach ($voodooServices as $service) {
  $this->servicesByClass[get_class($service)] = $service;
}

Because all services get initialized - even if they are not used.

rodrigo.pedra's avatar

From your last snippet, you can use the container to instantiate the service class, without pre-configuring a locator

use Illuminate\Contracts\Container\Container;

class MessageHandler
{
    public function __construct(
        // 1. Inject the container
        private readonly Container $container,
    ) {}

    public function handle(MessageInterface $message) {
        $voodooClass = $this->getVoodooAttributeClass($message);
        
        // 2. resolve the service from the container
        $voodooService = $this->container->make($voodooClass);
        $voodooService->doVoodoo($message);
    }
}

If you don't want to inject the container, use the gloabl helper:

class MessageHandler
{
    public function handle(MessageInterface $message) {
        $voodooClass = $this->getVoodooAttributeClass($message);

        // Use the global helper to resolve the service
        $voodooService = app($voodooClass);
        $voodooService->doVoodoo($message);
    }
}
1 like

Please or to participate in this conversation.