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

Feature: ServiceManager configuration validation

boesing opened this issue · comments

Feature Request

Q A
New Feature yes

Summary

Whenever we are working in projects with own plugin managers (like laminas-validator, etc.), we do probably ignore the configuration structure which is fetched from the container.

https://github.com/laminas/laminas-validator/blob/bdd503adc83d814a5c94e598ea0eb9fc7ca56339/src/ValidatorPluginManagerFactory.php#L49


So there will be some point in time where we have to validate the plugin managers configuration. To avoid having to do this manually, I'd prefer fetching a service from the container which is able to verify that the configuration is valid.

Such method/interface like I've created in the laminas-cache component would suffice.

https://github.com/laminas/laminas-cache/blob/130cc8db636d97b8287ff817515bb1c9ae0ef099/src/Service/StorageAdapterFactoryInterface.php#L45


WDYT? Feedback very welcome.

To avoid having to do this manually, I'd prefer fetching a service from the container which is able to verify that the configuration is valid.

Perhaps something like cuyz/valinor suffices? We already have most psalm types declared, AFAIK

Not sure how good valinor works with array shapes.
But yes, we do have psalm types around.

Do you have an example on how you would use valinor to instantiate a ServiceManager or a ValidatorPluginManager?

Would probably be something like:

$mapper->map(ServiceManager::class, ['config' => $config]);

The array shape should be validated this way, although I don't know how deep it can validate closures.

Maybe passing over validation to the Config instance would work better.

So one has to use:

$pluginManagerConfigFromProjectConfiguration = $config['validators'];
Assert::isMap($pluginManagerConfigFromProjectConfiguration);
$pluginManagerConfig = new \Laminas\ServiceManager\Config($pluginManagerConfigFromProjectConfiguration);
$pluginManager = new ValidatorPluginManager();
$pluginManagerConfig->configure($pluginManager);

In case of the laminas-validator package and all the other packages (which are plenty) which do provide a plugin manager, having to use valinor to verify that the config is valid would be a no-go for me tho.

We don't really have type-checking packages inside laminas: our validators are designed for checking HTTP input data (and similar), not in-memory object structures.

That's really the realm of more type-friendly tools (like in this case, valinor just being better at it).

I don't mind using other packages in general, but:

  1. no, please no more plugin managers: they hurt ;_;
  2. let's avoid adding dependency cycles wherever possible

A big if/else block of conditionals is better value than using laminas/laminas-validator in this scope.

Uhm, not sure if we are talking about the same.

  • I do not want to add more plugin managers
  • I do not want to use laminas-validator to validate a config

I just want to provide a proper flow for reading a config which can be extended by upstream projects (by Design) without the need of external tools. We do have 10+ existing plugin managers which will - at some time - use the latest servicemanager version and thus the typed array shape for the constructor.
At this point, psalm will bitch in the factory of that plugin manager, that the constructor expects an array with optional keys rather than just a map.

What I do want to achieve is that we do not have to use psalm-suppress at that LoC but instead some "safe" way to pass the plugin manager config from the application config to the plugin managers constructor.

Do you have a more practical example of a required @psalm-suppress? I thought this was about runtime validation, rather than compile-time validation :|

The example would be in the initial post. 1st link to an existing factory within laminas components.

Reading a config for the plugin manager from the application config and passing it to the constructor.

This actually only works because either there is no psalm in the validator component or the locked servicemanager version is below that version providing the array shape (or the error is already baselined).

Hmm, adding that at runtime to general use seems wasteful 🤔

Sure but how are we supposed to tell psalm that everything is correct then?

Because - the annotation is set. And thus something has to assert stuff. 🤷🏼‍♂️

Sure but how are we supposed to tell psalm that everything is correct then?

This is one of those cases where a leap of faith is required: adding runtime validation of a complex nested data structure has a huge performance impact (which I've already experienced with cuyz/valinor and azjezz/psl), so something like this should be opt-in.

I did have to do similar kind of "cuts" when dealing with ElasticSearch documents: the performance impact of checking the whole structure was having real impact on CPU usage.

Because - the annotation is set. And thus something has to assert stuff. 🤷🏼‍♂️

Usually /** @var */, as ugly as it is, is a pragmatic solution for these cases.

EDIT: here's an example from a project where I needed to do this sort of corner-cutting:

    /**
     * @param bool $validatePayload payload validation, at the time of this writing, is a really
     *                              CPU-intensive operation, since a lot of recursion happens
     *                              within type assertions. At the risk of production crashes,
     *                              we therefore conditionally enable it with this flag. It is
     *                              endorsed to use {@see true} when running integration tests,
     *                              as well as for local development.
     */
    public static function fromRawArray(array $raw, bool $validatePayload): self
    {
        if ($validatePayload) {
            $data = Type\shape([
                'hits' => Type\shape([
                    'total' => Type\shape(['value' => Type\union(Type\positive_int(), Type\literal_scalar(0))], true),
                    'hits' => Type\vec(
                        Type\shape([
                            '_score' => Type\float(),
                            '_source' => Product::rawArrayType(),
                        ], true)
                    ),
                ], true),
            ], true)->coerce($raw);

            return new self(
                array_map([Product::class, 'fromArray'], $data['hits']['hits']),
                $data['hits']['total']['value'],
            );
        }

Mhm. Maybe having some kind of runtime validation for non-production environments might work?
So providing some kind of validation which can be disabled in prod which then just early returns but still provides psalm-assert?

Maybe disabled by default (to reflect current behavior) and enable in development to have runtime validation?