Skip to content

Service Map

Gacela resolves sibling pillars (Facade → Factory → Config → Provider) by convention. From 1.12.0 onwards, the #[ServiceMap] attribute is the preferred way to declare that a class should be able to resolve an additional Gacela service on demand. For example, accessing a Facade from inside a Symfony Command without constructor injection.

#[ServiceMap] replaces the older DocBlock @method dance. Both work; the attribute is more discoverable, survives PHPStan/Psalm out of the box (with the bundled configs), and is friendlier to static tooling.

Basic usage

php
use Gacela\Framework\ServiceResolver\ServiceMap;
use Gacela\Framework\ServiceResolverAwareTrait;

#[ServiceMap(method: 'getFacade', className: UserFacade::class)]
final class UserController
{
    use ServiceResolverAwareTrait;

    public function show(int $id): array
    {
        return $this->getFacade()->findUser($id);
    }
}

The attribute is repeatable. Declare every resolvable service the class needs:

php
#[ServiceMap(method: 'getFacade',     className: UserFacade::class)]
#[ServiceMap(method: 'getCatalog',    className: CatalogFacade::class)]
#[ServiceMap(method: 'getLogger',     className: LoggerInterface::class)]
final class DashboardController
{
    use ServiceResolverAwareTrait;

    public function index(): array
    {
        return [
            'user'    => $this->getFacade()->current(),
            'top'     => $this->getCatalog()->popular(),
        ];
    }
}

Each __call() dispatch is cached. The resolver pool is static across the process, so repeated calls are essentially free.

DocBlock alternative

The @method form is still fully supported and produces identical runtime behaviour. It's useful for legacy code bases and for PHP versions without attribute support:

php
/**
 * @method UserFacade getFacade()
 */
final class UserController
{
    use ServiceResolverAwareTrait;
}

The trait was renamed from DocBlockResolverAwareTrait to ServiceResolverAwareTrait in 1.12.0. The old name still works, but new code should use the new trait.

Relationship with the container

#[ServiceMap] is a thin sugar on top of the Locator. The service is ultimately resolved through the main container, respecting every binding, alias, contextual binding and AnonymousGlobal declaration registered in gacela.php.

If you are authoring a class managed by another container (Symfony, Laravel), prefer constructor injection with #[Inject] (see Container configuration). #[ServiceMap] is targeted at classes instantiated outside of Gacela where constructor injection is not practical.

Limitations

  • The __call() dispatch means IDEs need the attribute (or @method) to autocomplete. Both are read by PhpStorm's Symfony plugin out of the box.
  • Protected services (addProtected()) cannot be resolved through #[ServiceMap]. They are stored as raw closures and the container will not instantiate them.
  • Static analysis may still flag magic calls; use the PHPStan / Psalm configs shipped with Gacela to suppress them.