Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laminas Servicemanager Laravel Package

laminas/laminas-servicemanager

Laminas ServiceManager is a PSR-11 compatible dependency injection container for PHP. It supports factory-based service creation, aliases, abstract factories, delegators, and lazy loading, making it a flexible foundation for managing object dependencies.

View on GitHub
Deep Wiki
Context7

Getting Started

Fit assessment up front. laminas/laminas-servicemanager is a PSR-11 service locator with a factory-driven configuration model. Laravel already ships Illuminate\Container\Container, which does the same job with auto-wiring, contextual binding, and a mature set of app()->singleton()/bind() helpers. There are zero Laravel packages depending on this library, so treat it as a deliberate, scoped choice, not a default. The realistic reasons a Laravel developer reaches for it are:

  1. You are pulling in a Laminas/Mezzio component (e.g. laminas-form, laminas-validator, laminas-db, laminas-hydrator) whose internals are built around AbstractPluginManager and Laminas\ServiceManager\ServiceManager.
  2. You have a legacy Zend Framework 2/3 or Laminas MVC module being migrated into a Laravel app piecemeal, and want to keep its module.config.php dependency wiring intact while the rest of the app is Laravel.
  3. You are writing a framework-agnostic package and want its internals to depend on PSR-11, with Laravel binding the package's container as a singleton in a service provider.

If none of those apply, stop — use Laravel's container.

Install and minimal bootstrap.

composer require laminas/laminas-servicemanager
use Laminas\ServiceManager\ServiceManager;
use Laminas\ServiceManager\Factory\InvokableFactory;

$container = new ServiceManager([
    'factories' => [
        \App\Support\Clock::class => InvokableFactory::class,
    ],
]);

$clock = $container->get(\App\Support\Clock::class);      // shared
$other = $container->build(\App\Support\Clock::class);    // fresh instance

Three config keys carry most of the weight: factories, aliases, and delegators. Everything else (abstract_factories, initializers, shared, shared_by_default) is refinement.

Where to look first, in order.

Doc Why you care
docs/book/v3/quick-start.md Constructor array, get() vs build(), sharing semantics.
docs/book/v3/configuring-the-service-manager.md The full config key reference; read this before inventing your own.
docs/book/v3/factories.md The FactoryInterface::__invoke($container, $requestedName, $options) contract.
docs/book/v3/delegators.md The closest analogue to Laravel's decorator/tap patterns.
docs/book/v3/plugin-managers.md Only if you're integrating Laminas components — this is the shape they arrive in.
docs/book/v3/cookbook/factories-vs-abstract-factories.md Performance vs. convenience tradeoff; explains why explicit factories win.

First concrete use case for a Laravel app. Bridge the two containers in one service provider and never look back:

// app/Providers/LaminasServiceProvider.php
use Laminas\ServiceManager\ServiceManager;
use Laminas\Validator\ValidatorPluginManager;

public function register(): void
{
    $this->app->singleton(ServiceManager::class, function ($app) {
        return new ServiceManager([
            'factories' => [
                // Laminas-side factory delegating back to Laravel
                \App\Domain\InvoiceRepository::class
                    => fn () => $app->make(\App\Domain\InvoiceRepository::class),
            ],
            'aliases' => [
                'InvoiceRepository' => \App\Domain\InvoiceRepository::class,
            ],
        ]);
    });

    $this->app->singleton(ValidatorPluginManager::class, function ($app) {
        return new ValidatorPluginManager($app->make(ServiceManager::class), [
            'factories' => [
                \Laminas\Validator\StringLength::class => \Laminas\ServiceManager\Factory\InvokableFactory::class,
            ],
        ]);
    });
}

Register the Laminas manager as a singleton and treat it as a leaf dependency. Do not let it become a second application container.

Implementation Patterns

1. Factories are the unit of wiring. Every service is produced by a callable with a fixed signature:

final class ReportFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): Report
    {
        return new Report(
            $container->get(ReportRepository::class),
            $options['format'] ?? 'pdf',
        );
    }
}

InvokableFactory::class is shorthand for "no-arg construct via new". For anything with dependencies, write a real factory class — it is the only form that survives config caching (see Gotchas).

2. Mirror Laravel's container with a thin adapter layer. When both containers are in play, pick one direction and stay consistent. The pragmatic rule:

  • Laravel owns infrastructure (DB, cache, queue, HTTP client, config). Register these into Laminas as delegating factories that call $app->make(...).
  • Laminas owns the third-party component graph (its plugin managers, its validators, its hydrators). Nobody outside that component asks Laminas for ReportRepository.
'factories' => [
    \Laminas\Db\Adapter\Adapter::class => function ($sm) {
        $cfg = app('config')->get('database.connections.' . config('database.default'));
        return new \Laminas\Db\Adapter\Adapter([
            'driver'   => 'Pdo_Mysql',
            'hostname' => $cfg['host'],
            'username' => $cfg['username'],
            'password' => $cfg['password'],
            'database' => $cfg['database'],
        ]);
    },
],

3. Use ConfigAbstractFactory to kill boilerplate, then graduate. This is the Laminas equivalent of Laravel's auto-wiring, and it's the sweet spot when mapping a dozen classes:

use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;

'dependencies' => [
    'factories' => [
        Report::class => ConfigAbstractFactory::class,
    ],
],
'config_abstract_factory' => [
    Report::class => [
        ReportRepository::class,   // resolved from the container
        'reports.default_format',  // or a config key
    ],
],

ReflectionBasedAbstractFactory goes further: it reflects the constructor and resolves every type-hinted parameter, with zero per-service config. Excellent for a first pass during a migration; terrible as a permanent production strategy because every get() loops all abstract factories (see the factories-vs-abstract-factories cookbook). The documented evolution — reflection "catch-all" → ConfigAbstractFactory → generated explicit factories via vendor/bin/generate-factory-for-class — is genuinely good advice and maps directly onto how you'd migrate a legacy module incrementally.

4. Delegators are your decoration/middleware seam. A delegator wraps the factory callback and returns the service, so you can inject logging, caching, or proxying without touching the factory:

use Laminas\ServiceManager\Factory\DelegatorFactoryInterface;

final class LoggingDelegator implements DelegatorFactoryInterface
{
    public function __invoke(ContainerInterface $container, string $name, callable $callback, ?array $options = null): object
    {
        $service = $callback();
        return new TimedDecorator($service, $container->get(LoggerInterface::class));
    }
}

// config
'delegators' => [
    InvoiceRepository::class => [
        LoggingDelegator::class,
        CachingDelegator::class,   // applied in array order, outermost last
    ],
],

This is the pattern to reuse most heavily — it's how you add cross-cutting concerns to services baked inside a Laminas component you don't control.

5. Lazy services for expensive dependencies. laminas-servicemanager ships LazyServiceFactory/LazyServiceAggregator that build ProxyManager proxies, so a DB connection or mailer that is injected into many services is only actually constructed on first method call. Setup requires friendsofphp/proxy-manager-lts and registering the aggregator as a delegator:

'delegators' => [
    \Doctrine\DBAL\Connection::class => [
        \Laminas\ServiceManager\Proxy\LazyServiceFactory::class,
    ],
],
'lazy_services' => [
    'class_map' => [\Doctrine\DBAL\Connection::class => \Doctrine\DBAL\Connection::class],
],

Use this in Laravel only where you have measured a boot-cost problem (typically long-running Octane or queue workers with many injected clients). Otherwise the extra dependency and proxy indirection isn't worth it.

6. Plugin managers when integrating Laminas components. A plugin manager is a nested container with instance validation:

final class FilterPluginManager extends AbstractPluginManager
{
    protected $instanceOf = \Laminas\Filter\FilterInterface::class;
}

Register it as a service in the parent container, and inside its factories remember the v3 rule: the $container passed to a plugin factory is the parent service manager, not the plugin manager. Fetch the plugin manager from it if you need sibling plugins:

function ($container, $name, array $options = []) {
    $filters = $container->get(FilterPluginManager::class);
    // ...
}

7. Runtime configuration for tests. ServiceManager exposes mutators that make it trivially swappable in feature tests, which is often nicer than fighting Laravel's container:

$sm = new ServiceManager($config);
$sm->setService(Mailer::class, $fakeMailer);   // pre-built instance wins over any factory
$sm->setFactory(Clock::class, fn () => new FrozenClock('2026-01-01'));
$sm->configure(['aliases' => ['clock' => Clock::class]]);

Gotchas and Tips

Laravel's config:cache will explode on closures. Laravel serialises config with var_export, so any Closure inside a config array is uncacheable. If your Laminas config lives in config/laminas.php, the factory entries must be class-strings (ReportFactory::class), never fn ($sm) => .... Register closures imperatively in a service provider instead, and keep the config file purely declarative. This is the single most common way a working dev setup breaks in production.

Two containers means two singletons for the same class. If ReportRepository is bound in Laravel as a singleton and also registered in Laminas with its own factory, you get two instances with divergent state. Always bridge by delegation (fn () => app(Repo::class)) rather than re-instantiating. Conversely, do not call app() from inside a Laminas factory at a point where the app hasn't finished booting — you'll get a partially-wired instance and a very confusing bug. Resolve lazily, inside the closure body, not at registration time.

Don't inject the container into your services. ServiceManager happily lets you do $container->get() from anywhere; that's how you end up with a service locator anti-pattern that no static analyser can trace. The factory signature gives you the container for free, so the temptation is real. Restrict $container to factory bodies only.

invokables was removed in v3. Old ZF2 config that says 'invokables' => [...] will silently(ish) fail. Convert to 'factories' => ['Name' => InvokableFactory::class].

Shared by default. Every get() returns the same instance. If you need a discrete object (a request-scoped DTO, a per-call validator), use build(). You can flip the global default with 'shared_by_default' => false and then opt individual services back in via 'shared' => [Name::class => true] — but this is a big behavioural switch and rarely correct outside specialised plugin managers.

Plugin manager get() throws InvalidServiceException, not ServiceNotFoundException, when a factory returns the wrong type. If you're asserting on exceptions in tests, catch the right one. Also note $instanceOf validation is skipped for setService()? — no, it is enforced; but it is skipped when the service is retrieved via an alias pointing to an already-validated entry.

Circular dependencies surface as CircularDependencyFoundException, and the message names the cycle. The usual cause in a mixed setup is a Laminas factory that calls app() which resolves something that calls back into Laminas. The fix is almost always to move the dependency behind a delegator or a lazy proxy.

Aliases don't cascade. 'aliases' => ['a' => 'b', 'b' => C::class] is not a reliable chain — alias resolution is meant to be a single hop to a real service name. Keep aliases flat.

Performance: prefer explicit factories, but measure before you refactor. Abstract factories cost a hash lookup plus 1:N canCreate() invocations per get(). With a handful of abstract factories this is noise; with fifteen it isn't. benchmarks/ and vendor/bin/phpbench run --report=aggregate in the package repo let you quantify it, and the console tools (generate-deps-for-config-factory, generate-factory-for-class) let you convert cheaply. Note also that resolution cost is per-service-first-request when services are shared, so the real cost concentrates at boot — which matters a lot under Octane.

Version and namespace hygiene. This is the maintained successor to zendframework/zend-servicemanager; if you're porting old code, rewrite Zend\ServiceManager\* → Laminas\ServiceManager\*. Current major versions require modern PHP (8.1+), so check compatibility before adding it to an older Laravel app. Also confirm which major you installed: v3 and v4 differ in the factory $options parameter nullability and in some deprecations around AbstractPluginManager.

Debugging recipe. When a service won't resolve, work down this list: (1) is the requested name an alias pointing at a real entry? (2) does a factory exist under factories for the resolved name? (3) if relying on an abstract factory, does canCreate() actually return true for that name — call it directly to check? (4) is a delegator swallowing or replacing the instance? Wrap resolution in a try/catch on Laminas\ServiceManager\Exception\ServiceNotCreatedException and inspect getPrevious() — the underlying cause (missing dependency, constructor type error) is almost always there.

Extension points worth knowing. AbstractPluginManager (nested containers with type validation), DelegatorFactoryInterface (decoration), InitializerInterface (post-construction initialize($instance, $container) hook — useful for injecting a logger into every service implementing a marker interface), AbstractFactoryInterface (convention-based resolution), and the mutable ServiceManager::configure()/setFactory()/setService() API (test doubles). The initializer is the one Laravel developers underuse — it's the closest thing to a global "booted model" hook.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin