laminas / laminas-servicemanager

Factory-Driven Dependency Injection Container

Home Page:https://docs.laminas.dev/laminas-servicemanager/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

FactoryCallable Psalm type is too narrow

InvisibleSmiley opened this issue · comments

Bug Report

Q A
Version(s) 3.9.x (then without Psalm type) till now

Summary

The FactoryCallable type, which is used for both setFactory and getFactory, is too narrow: It does not accept factories which require only one parameter, or only two.

Current behavior

callable(ContainerInterface,string,array|null):mixed suggests that all factories must accept three parameters where in reality most factories only require one (the container) or maybe two (container, requested name). Only factories to be used with build() require three parameters.
So effectively, getFactory is lying right now and setFactory does not accept many valid factories.

Funnily even the documentation is ambiguous here:

A factory is any callable or any class that implements the interface Laminas\ServiceManager\Factory\FactoryInterface.

(note "any callable")

MyObject::class => function(ContainerInterface $container, $requestedName) {

(note missing third parameter)

// or without implementing the interface:
class MyObjectFactory
{
public function __invoke(ContainerInterface $container, $requestedName)

(note missing third parameter)

How to reproduce

Create a ServiceManager instance with a config including factories that require only one parameter (e.g. \Psr\Container\ContainerInterface). Observe that this works without any issues, i.e. it's a perfectly valid use case (in fact, >90% of our factories are like that).

Now try to override a factory using setFactory or receive one using getFactory. Check what Psalm/PHPStan complain or which type they report against reality.

Expected behavior

The ServiceManager not only accepts simple DI factories via config but also via setFactory (as long as that method exists) and returns a correct (union?) type for getFactory., i.e. neither Psalm nor PHPStan complain when you get/set such factories after init.

We are already aware of this and fixed it in v4 as we reworked most of the configuration types there.
You will need a more recent phpstan and one of the latest psalm v5 releases starting with v5.9 (AFAIR).

Please double check if that type works for you and report back.
Maybe just copy & paste it to your local environment for now and re-run your static analysis.


Possible duplicate? #159

Please explain. I think I am referring to v4 here:
https://github.com/laminas/laminas-servicemanager/blob/4.0.x/src/ServiceManager.php#L76
I don't see anything there that allows the second or third parameter to be optional.

FTR I'm mainly concerned with factory lambdas, e.g. arrow functions like this:

[
  'factories' => [
    FooInterface::class => static fn (\Psr\Container\ContainerInterface $container) => new Foo(
      $container->get(SomeDependencyInterface::class)
    ),
  ],
]

Can you probably provide a psalm.dev link where I can understand the problem?

Just because the callable states that it gets 3 arguments passed does not mean that you have to implement all 3 arguments in your callable.
https://psalm.dev/r/3e968b61f8

But maybe I do miss the whole point, thats why I need a psalm.dev link so I get the underlying issue.

I found these snippets:

https://psalm.dev/r/3e968b61f8
<?php

interface ContainerInterface
{
    public function get(string $id): SomeDependencyInterface;
}

interface SomeDependencyInterface
{}

interface FooInterface
{}

class Foo implements FooInterface
{
    public function __construct(SomeDependencyInterface $dependency)
    {}
}

final class ConfigProvider
{
    public function __invoke(): array
    {
    	return [
            'dependencies' => $this->getServiceDependencies(),
        ];
    }
    
    /**
     * @return array{factories:array<non-empty-string,callable(ContainerInterface,string,array|null):mixed>}
     */
    public function getServiceDependencies(): array
    {
        return [
            'factories' => [
                FooInterface::class => static fn (ContainerInterface $container) => new Foo(
                  $container->get(SomeDependencyInterface::class)
               ),
          ],
        ];
    }
}
Psalm output (using commit 6c0a09a):

No issues!

Sorry I should actually have followed the advice you gave:

Please double check if that type works for you and report back.
Maybe just copy & paste it to your local environment for now and re-run your static analysis.

The v4 version is actually fine and fixes the issues we had (with a class factory actually) with v3.
https://phpstan.org/r/5ebb5141-8d48-4ba0-87c8-0cf66d9def21