How to access ServiceLocatorInterface features if we "only" get a ContainerInterface passed into FactoryInterface

Hi Laminas community,

I currently have the use-case that I want to pass options into a factory from within another factory.
I thought I would use the build method of the ServiceManager.

But now I’m facing the problem, that the ContainerInterface passed into my factories does not expose that method.
Well technically the $container object I’m receiving exposes it, because it’s the ServiceManager. But the contract does not guarantee it.

What am I missing?

Could you be a bit more specific, maybe a code snippet, so we can see what the idea behind it might be?

Okay. My use-case is pretty much analogue to the one described in the documentation

The __invoke method signature of the current FactoryInterface looks like this:

public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null);

I want to pass in the $options from another factory using the build() method of the ServiceManager.

So I would basically have two factories:

// ValidatorFactory
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null) {
    return new Validator($options['length'] ?? 0);
}
// FormFactory
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null) {
    $validator1 = $container->build(Validator::class, ['length' => 12]); // ContainerInterface only exposes get and has
    $validator2 = $container->build(Validator::class, ['length' => 20]); // ContainerInterface only exposes get and has
    $form = new Form();
    $form->addValidators([$validator1, $validator2]);
    return $form;
}

So essentially I want to use this build() method to pass $options to the factory like described in the documentation, but I seem to struggle with how this is supposed to work, since the contract of the factory does not guarantee, that I get a $container which implements the ServiceLocatorInterface I would need.

Thanks for listening. :slight_smile:

Was my added example sufficiently specific? (@guidofaecke)

Maybe some project maintainer/designer who could shed some light here? :sweat_smile:

Hi!
this is how I do it:

public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $serviceManager = $container->get(ServiceManager::class);
        return new FileSystem($serviceManager->build(Validator::class, $options));
    }

You were right that the $container does not expose build, the ServiceManager does.
This leads to me getting the ServiceManager first, and then calling the build on it instead of the ContainerInterface.

Hope this helps :slight_smile:

Hi @makxk,

thank you for your input. In fact I’m doing something similar currently:

public function __invoke(ContainerInterface $container, $requestedName, array $options = null) {
    // THIS HERE
    assert($container instanceof ServiceLocatorInterface, 'Only usable in ServiceLocator context.');

    $validator1 = $container->build(Validator::class, ['length' => 12]);
    // ...
}

While I find it reasonable that the ServiceManager implements the PSR-11 container interface, I find it strange, that the FactoryInterface bundled with the ServiceManager doesn’t expose all of its features or rather doesn’t make it intuitive as to what type of container you’ll most likely get in a Laminas environment.

I thought there is something that I missed.

I feel like the default FactoryInterface shipped with the component and also mentioned in the documentation should look more like this:

public function __invoke(ServiceLocatorInterface $container, $requestedName, array $options = null);

and maybe, for convenience reasons, should also ship with a Psr11FactoryInterface which then would just be:

public function __invoke(ContainerInterface $container, $requestedName, array $options = null);

I think I will leave this topic open for a while and hope that some core developer might also chime in. :sweat_smile:

Thank you so much.

I feel like I should emphasize that there’s no build method in ContainerInterface but in ServiceManager, which implements ServiceLocatorInterface. How about you use the ContainerInterface to get a ServiceManager and call its build method like I did?
All of those interfaces and classes are within the laminas-namespace, therefore are available to you too.

I’m very aware of that. It’s the sole reason I asked about the underlying concepts, and what the idiomatic way, a laminas user is “supposed to” use the features of the ServiceManager is.

Are we “supposed to” fetch it explicitly in every factory we would need the build method?
Or are we “supposed to” assert, that the $container passed into the factory posseses the feature we are actually interested in?

The ServiceManager is currently the concept used in the Laminas world to obtain services. I’m wondering why it is not passed into my Laminas factories, so that I can use all of its features without prior having to fetch it.
Maybe for context: In ZF2 the ServiceLocatorInterface was in fact passed into the factory. I’m worried that there was an oversight during the refactor.

Long story short: I’m interested in the idiomatic way. The way the designer intended this to be used.

I see, thanks for clarifying.
In this case, maybe @froschdesign is be able to answer?