Advice about PHPStan level 5 and container data type

I’m using Slim/4.11.0 and PHP-DI/7.0.2 (this one perhaps incorrectly, because I rushed to install it when I couldn’t make Pimple work after a migration). My application works smoothly. But when I run PHPStan with level 5 I get this message from my routing file:

Parameter #1 $config of class Alvaro\Paginas\Error constructor expects Alvaro\Config\Config, Psr\Container\ContainerInterface|null given.

I have a custom class for my settings, which is what my constructor expects and uses:

    public function __construct(Config $config)
    {
        $this->config = $config;
    }

But PHPStan doesn’t know the specific type because it comes from the container:

/** @var \Alvaro\Config\Config $config */
$config = require __DIR__ . '/../bootstrap.php';
AppFactory::setContainer($config);
$app = AppFactory::create();

$errorMiddleware->setErrorHandler(HttpNotFoundException::class, function (Request $request) use ($app) {
    return (new Paginas\Error($app->getContainer()))
        ->notFound404($request, $app->getResponseFactory()->createResponse());
});
    public static function setContainer(ContainerInterface $container): void
    {
        static::$container = $container;
    }
    public function getContainer(): ?ContainerInterface
    {
        return $this->container;
    }

I’ve a couple of workarounds, but they don’t serve any real purpose other than making PHPStan happy. For that, I could just ignore the error. After all, I’m using PHPStan to make my code better. :smile:

$errorMiddleware->setErrorHandler(HttpNotFoundException::class, function (Request $request) use ($app, $config) {
    return (new Paginas\Error($config))
        ->notFound404($request, $app->getResponseFactory()->createResponse());
});
$errorMiddleware->setErrorHandler(HttpNotFoundException::class, function (Request $request) use ($app) {
    /** @var Config $config */
    $config = $app->getContainer();
    return (new Paginas\Error($config))
        ->notFound404($request, $app->getResponseFactory()->createResponse());
});

I wonder if I’m missing something more straightforward, or if my app set up can be improved in some way.

Here’s what we do - not sure if it’s totally correct but it works for us - and it’s based on various ways that gathered from things like the slim skeleton etc.

in our public/index.php we build the container from a factory class

// Build DI Container instance
$container = ContainerFactory::createInstance();

and inside that method it loads the definitions

final class ContainerFactory {
    /**
     * Create a new container instance.
     *
     * @return \Psr\Container\ContainerInterface The container
     * @throws \Exception
     */
    public static function createInstance(): ContainerInterface {
        $containerBuilder = new ContainerBuilder();
        // Set up settings
        $containerBuilder->addDefinitions(__DIR__ . '/../../config/container.php');

        // Build PHP-DI Container instance
        return $containerBuilder->build();
    }
}

in the container definition we load everything we need for general operation including response interfaces and such

return [
    // Application settings
    'settings' => function () {
        return require __DIR__ . '/settings.php';
    },

    App::class => function (ContainerInterface $container) {
        $app = AppFactory::createFromContainer($container);

        // Register routes
        (require __DIR__ . '/routes/api.php')($app);
        (require __DIR__ . '/routes/web.php')($app);

        // Register middleware
        (require __DIR__ . '/middleware.php')($app);

        return $app;
    },

    // HTTP factories
    ResponseFactoryInterface::class => function (ContainerInterface $container) {
        return $container->get(Psr17Factory::class);
    },
etc
etc
etc

With things set up this way we let the DI handle the injection stuff and the config is already in the container so we only have to pass into other classes the classes we might need

public function __construct(User $user) {

much of this was based on recent changes in Odan’s slim4 skeleton - it really helped us clear up our index file and loading of settings and container.

1 Like

I have similar clean-ups in mind (for other reasons) but the code sample you’ve shared uses ContainerInterface as type all the way through. PHPStan wouldn’t complaint about my code if I did the same. I have a custom class that implements the interface because I prefer to have predictable and strongly typed settings ($container->froggleSurname as opposed to $container->get('froggleSurname')). It’s safe to extend the interface because it’s my custom application code, not a library or framework.