Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refined ServiceManager factory, delegator and configuration types, to allow for easier introspection of mis-configuration at type level #98

Merged
merged 20 commits into from
Sep 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
323e362
feature: introduce plenty of psalm types
boesing Sep 17, 2021
b12525b
bugfix: widen `services` type as requested in code review
boesing Sep 14, 2021
3f0d8b2
qa: restore method description
boesing Sep 14, 2021
1cd5bed
bugfix: limit services to be either `array|object` as outlined in cod…
boesing Sep 14, 2021
bd1e9a9
feature: add `@psalm-require-extends` to ensure trait is only used wi…
boesing Sep 14, 2021
51f9cbe
refactor: optimize callable delegator assertion
boesing Sep 14, 2021
007ae06
qa: add return types to some functions
boesing Sep 14, 2021
8e16a2a
bugfix: remove `class-string` type from some config entries
boesing Sep 17, 2021
6ba9e4d
bugfix: allow `null` for the `$options` argument in `delegators` and …
boesing Sep 17, 2021
5820225
bugfix: allow `null` for `$options` in delegator callables
boesing Sep 17, 2021
d14e2eb
bugfix: re-use psalm type rather than re-declaring it
boesing Sep 17, 2021
9dd34d7
qa: remove configuration type for the deprecated `DelegatorFactoryInt…
boesing Sep 17, 2021
9d3b97f
qa: remove unnecessary var annotation
boesing Sep 17, 2021
5b8b860
refactor: make nested creation callback static
boesing Sep 17, 2021
c4866a7
qa: remove native return type from `ServiceManager#doCreate`
boesing Sep 17, 2021
2ab2756
qb: add detailed reason on why we suppress the invalid argument
boesing Sep 17, 2021
b906701
qa: restore `ServiceManager#configure` call
boesing Sep 17, 2021
10f04b5
qa: remove unused psalm type import
boesing Sep 18, 2021
9e9746d
refactor: avoid creating a new option array in pluginmanager if not n…
boesing Sep 18, 2021
883ff57
qa: remove leftover template type
boesing Sep 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
277 changes: 21 additions & 256 deletions psalm-baseline.xml

Large diffs are not rendered by default.

19 changes: 18 additions & 1 deletion psalm.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,34 @@
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
errorBaseline="psalm-baseline.xml"
findUnusedPsalmSuppress="true"
>
<projectFiles>
<directory name="bin"/>
<directory name="src"/>
<directory name="test"/>
<ignoreFiles>
<directory name="vendor"/>
<directory name="test/TestAsset/laminas-code"/>
<directory name="test/TestAsset"/>
<directory name="test/**/TestAsset"/>
</ignoreFiles>
</projectFiles>

<issueHandlers>
<DeprecatedClass>
<errorLevel type="suppress">
<referencedClass name="Laminas\ServiceManager\DelegatorFactoryInterface"/>
<referencedClass name="Laminas\ServiceManager\InitializerInterface"/>
<referencedClass name="Laminas\ServiceManager\FactoryInterface"/>
</errorLevel>
</DeprecatedClass>
<InvalidThrow>
<errorLevel type="suppress">
<referencedClass name="Interop\Container\Exception\ContainerException"/>
</errorLevel>
</InvalidThrow>
</issueHandlers>

<plugins>
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
</plugins>
Expand Down
27 changes: 17 additions & 10 deletions src/AbstractPluginManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Laminas\ServiceManager;

use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\Exception\ContainerModificationsNotAllowedException;
use Laminas\ServiceManager\Exception\InvalidServiceException;
use Psr\Container\ContainerInterface as PsrContainerInterface;

Expand All @@ -31,6 +32,9 @@
*
* The implementation extends `ServiceManager`, thus providing the same set
* of capabilities as found in that implementation.
*
* @psalm-import-type ServiceManagerConfiguration from ServiceManager
* @psalm-suppress PropertyNotSetInConstructor
*/
abstract class AbstractPluginManager extends ServiceManager implements PluginManagerInterface
{
Expand All @@ -45,6 +49,7 @@ abstract class AbstractPluginManager extends ServiceManager implements PluginMan
* An object type that the created instance must be instanced of
*
* @var null|string
* @psalm-var null|class-string
*/
protected $instanceOf;

Expand All @@ -55,6 +60,7 @@ abstract class AbstractPluginManager extends ServiceManager implements PluginMan
*
* @param null|ConfigInterface|ContainerInterface|PsrContainerInterface $configInstanceOrParentLocator
* @param array $config
* @psalm-param ServiceManagerConfiguration $config
*/
public function __construct($configInstanceOrParentLocator = null, array $config = [])
{
Expand Down Expand Up @@ -110,16 +116,18 @@ public function __construct($configInstanceOrParentLocator = null, array $config
/**
* Override configure() to validate service instances.
boesing marked this conversation as resolved.
Show resolved Hide resolved
*
* If an instance passed in the `services` configuration is invalid for the
* plugin manager, this method will raise an InvalidServiceException.
*
* {@inheritDoc}
*
* @throws InvalidServiceException
* @param array $config
* @psalm-param ServiceManagerConfiguration $config
* @return self
* @throws InvalidServiceException If an instance passed in the `services` configuration is invalid for the
* plugin manager.
* @throws ContainerModificationsNotAllowedException If the allow override flag has been toggled off, and a
* service instanceexists for a given service.
*/
public function configure(array $config)
{
if (isset($config['services'])) {
/** @psalm-suppress MixedAssignment */
Ocramius marked this conversation as resolved.
Show resolved Hide resolved
foreach ($config['services'] as $service) {
$this->validate($service);
}
Expand All @@ -142,10 +150,8 @@ public function setService($name, $service)
}

/**
* {@inheritDoc}
*
* @param string $name Service name of plugin to retrieve.
* @param null|array $options Options to use when creating the instance.
* @param null|array<mixed> $options Options to use when creating the instance.
* @return mixed
* @throws Exception\ServiceNotFoundException If the manager does not have
* a service definition for the instance, and the service is not
Expand All @@ -167,7 +173,8 @@ public function get($name, ?array $options = null)
$this->setFactory($name, Factory\InvokableFactory::class);
}

$instance = empty($options) ? parent::get($name) : $this->build($name, $options);
/** @psalm-suppress MixedAssignment */
$instance = ! $options ? parent::get($name) : $this->build($name, $options);
$this->validate($instance);
return $instance;
}
Expand Down
51 changes: 18 additions & 33 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@

namespace Laminas\ServiceManager;

use Laminas\Stdlib\ArrayUtils\MergeRemoveKey;
use Laminas\Stdlib\ArrayUtils\MergeReplaceKeyInterface;
use Laminas\Stdlib\ArrayUtils;

use function array_key_exists;
use function array_keys;
use function is_array;
use function is_int;

/**
* Object for defining configuration and configuring an existing service manager instance.
Expand All @@ -26,10 +22,12 @@
*
* These features are advanced, and not typically used. If you wish to use them,
* you will need to require the laminas-stdlib package in your application.
*
* @psalm-import-type ServiceManagerConfigurationType from ConfigInterface
*/
class Config implements ConfigInterface
{
/** @var array */
/** @var array<string,bool> */
Ocramius marked this conversation as resolved.
Show resolved Hide resolved
private $allowedKeys = [
'abstract_factories' => true,
'aliases' => true,
Expand All @@ -42,7 +40,10 @@ class Config implements ConfigInterface
'shared' => true,
];

/** @var array */
/**
* @var array<string,array>
* @psalm-var ServiceManagerConfigurationType
*/
protected $config = [
'abstract_factories' => [],
'aliases' => [],
Expand All @@ -56,7 +57,7 @@ class Config implements ConfigInterface
];

/**
* @param array $config
* @psalm-param ServiceManagerConfigurationType $config
*/
public function __construct(array $config = [])
{
Expand All @@ -66,6 +67,8 @@ public function __construct(array $config = [])
unset($config[$key]);
}
}

/** @psalm-suppress ArgumentTypeCoercion */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This coercion shouldn't apply anymore: merge() has the correct types? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayUtils::merge returns array and thus, psalm loses track.

Not sure if something like this would help psalm to keep track:

/**
 * @template TArray1 of array
 * @template TArray2 of array
 * @param TArray1 $a
 * @param TArray2 $b
 * @return TArray1&TArray2
 */

But this wont properly work for nested arrays (as they are here). There is absolutely no way but to trust that ArrayUtils::merge will return the same types.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but you are not calling ArrayUtils::merge(), but rather $this->merge(), which is well typed, and has its own suppressions

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave this alone for now - I think it's not worth wasting effort on it after all the work you've already put in this, and it's in the internals anyway :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added reportUnusedPsalmSuppress in the config and thats not reported so something might still be odd :-/

$this->config = $this->merge($this->config, $config);
}

Expand All @@ -86,32 +89,14 @@ public function toArray()
}

/**
* Copy paste from https://github.com/laminas/laminas-stdlib/commit/26fcc32a358aa08de35625736095cb2fdaced090
* to keep compatibility with previous version
*
* @link https://github.com/zendframework/zend-servicemanager/pull/68
* @psalm-param ServiceManagerConfigurationType $a
* @psalm-param ServiceManagerConfigurationType $b
* @psalm-return ServiceManagerConfigurationType
* @psalm-suppress MixedReturnTypeCoercion
boesing marked this conversation as resolved.
Show resolved Hide resolved
*/
private function merge(array $a, array $b): array
private function merge(array $a, array $b)
{
foreach ($b as $key => $value) {
if ($value instanceof MergeReplaceKeyInterface) {
$a[$key] = $value->getData();
} elseif (isset($a[$key]) || array_key_exists($key, $a)) {
if ($value instanceof MergeRemoveKey) {
unset($a[$key]);
} elseif (is_int($key)) {
$a[] = $value;
} elseif (is_array($value) && is_array($a[$key])) {
$a[$key] = $this->merge($a[$key], $value);
} else {
$a[$key] = $value;
}
} else {
if (! $value instanceof MergeRemoveKey) {
$a[$key] = $value;
}
}
}
return $a;
/** @psalm-suppress MixedReturnTypeCoercion */
return ArrayUtils::merge($a, $b);
boesing marked this conversation as resolved.
Show resolved Hide resolved
}
}
48 changes: 48 additions & 0 deletions src/ConfigInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,53 @@

namespace Laminas\ServiceManager;

use ArrayAccess;
use Interop\Container\ContainerInterface;

/**
* @see ContainerInterface
* @see ArrayAccess
*
* @psalm-type AbstractFactoriesConfigurationType = array<
* array-key,
* (class-string<Factory\AbstractFactoryInterface>|Factory\AbstractFactoryInterface)
* >
* @psalm-type DelegatorsConfigurationType = array<
* string,
* array<
* array-key,
* (class-string<Factory\DelegatorFactoryInterface>|Factory\DelegatorFactoryInterface)
* |callable(ContainerInterface,string,callable():object,array<mixed>|null):object
* >
* >
* @psalm-type FactoriesConfigurationType = array<
* string,
* (class-string<Factory\FactoryInterface>|Factory\FactoryInterface)
* |callable(ContainerInterface,string,array<mixed>|null)
* >
* @psalm-type InitializersConfigurationType = array<
* array-key,
* (class-string<Initializer\InitializerInterface>|Initializer\InitializerInterface)
* |callable(ContainerInterface,object):void
* >
* @psalm-type LazyServicesConfigurationType = array{
* class_map?:array<string,class-string>,
* proxies_namespace?:non-empty-string,
* proxies_target_dir?:non-empty-string,
* write_proxy_files?:bool
* }
* @psalm-type ServiceManagerConfigurationType = array{
* abstract_factories?: AbstractFactoriesConfigurationType,
* aliases?: array<string,string>,
* delegators?: DelegatorsConfigurationType,
* factories?: FactoriesConfigurationType,
* initializers?: InitializersConfigurationType,
* invokables?: array<string,string>,
* lazy_services?: LazyServicesConfigurationType,
* services?: array<string,object|array>,
* shared?:array<string,bool>
* }
*/
interface ConfigInterface
{
/**
Expand Down Expand Up @@ -37,6 +84,7 @@ public function configureServiceManager(ServiceManager $serviceManager);
* a service manager or plugin manager, or pass to its `withConfig()` method.
*
* @return array
* @psalm-return ServiceManagerConfigurationType
*/
public function toArray();
}
3 changes: 0 additions & 3 deletions src/Exception/ContainerModificationsNotAllowedException.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@

use function sprintf;

/**
* @inheritDoc
*/
class ContainerModificationsNotAllowedException extends DomainException implements ExceptionInterface
{
/**
Expand Down
1 change: 1 addition & 0 deletions src/Factory/DelegatorFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface DelegatorFactoryInterface
* A factory that creates delegates of a given service
*
* @param string $name
* @psalm-param callable():mixed $callback
* @param null|array $options
* @return object
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is supposed to return object, then $callback cannot return mixed.

I feel like the DelegatorFactoryInterface may benefit from a templated type, in future (not here, due to BC)

Copy link
Member Author

@boesing boesing Sep 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since ContainerInterface#get can return mixed, this actually has to be mixed :-(

But yes, having a templated type would help for sure but until then, I've thought that mixed would be fine until the return type is object 🤷🏼‍♂️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but the commented line is about @return object (wrong)

Copy link
Member Author

@boesing boesing Sep 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I can follow.

Do you want me to change existing documentation that we end up having bc breaks? as of now, due to the lack of typing, callable factories can return mixed. When I enforce object now, this will be pain probably. Thats why I think that might be a BC break but I am not 100% sure tho.

Or do you want me to open up the return value of the delegator to be mixed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I am fine with both as I don't have that much experience with adding psalm types on such a widely used component. As always, I trust your experience. :-)

* @throws ServiceNotFoundException If unable to resolve the service.
Expand Down
2 changes: 1 addition & 1 deletion src/PluginManagerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface PluginManagerInterface extends ServiceLocatorInterface
/**
* Validate an instance
*
* @param object $instance
* @param mixed $instance
* @return void
* @throws InvalidServiceException If created instance does not respect the
* constraint on type imposed by the plugin manager.
Expand Down
2 changes: 1 addition & 1 deletion src/ServiceLocatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface ServiceLocatorInterface extends
* Build a service by its name, using optional options (such services are NEVER cached).
*
* @param string $name
* @param null|array $options
* @param null|array<mixed> $options
* @return mixed
* @throws Exception\ServiceNotFoundException If no factory/abstract
* factory could be found to create the instance.
Expand Down
Loading