Skip to content
This repository has been archived by the owner on Jan 30, 2020. It is now read-only.

Commit

Permalink
Merge pull request #87 from weierophinney/feature/enabled-by-namespace
Browse files Browse the repository at this point in the history
Provide a standalone plugin manager implementation
  • Loading branch information
Ocramius committed Dec 8, 2018
2 parents 20a996c + 375386e commit f074aa3
Show file tree
Hide file tree
Showing 14 changed files with 648 additions and 6 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,17 @@ All notable changes to this project will be documented in this file, in reverse

### Added

- [#87](https://github.com/zendframework/zend-hydrator/pull/87) adds `Zend\Hydrator\HydratorPluginManagerInterface` to allow
type-hinting on plugin manager implementations. The interface simply extends
the [PSR-11 ContainerInterface](https://www.php-fig.org/psr/psr-11/).

- [#87](https://github.com/zendframework/zend-hydrator/pull/87) adds `Zend\Hydrator\StandaloneHydratorPluginManager` as an implementation
of each of `Psr\Container\ContainerInterface` and `Zend\Hydrator\HydratorPluginManagerInterface`,
along with a factory for creating it, `Zend\Hydrator\StandaloneHydratorPluginManagerFactory`.
It can act as a replacement for `Zend\Hydrator\HydratorPluginManager`, but
only supports the shipped hydrator implementations. See the [plugin manager documentation](https://docs.zendframework.com/zend-hydrator/v3/plugin-managers/)
for more details on usage.

- [#79](https://github.com/zendframework/zend-hydrator/pull/79) adds a third, optional parameter to the `DateTimeFormatterStrategy` constructor.
The parameter is a boolean, and, when enabled, a string that can be parsed by
the `DateTime` constructor will still result in a `DateTime` instance during
Expand All @@ -28,6 +39,15 @@ All notable changes to this project will be documented in this file, in reverse
Aliases resolving the original class name to the new class were also added to
the `HydratorPluginManager` to ensure you can still obtain instances.

- [#87](https://github.com/zendframework/zend-hydrator/pull/87) modifies `Zend\Hydrator\ConfigProvider` to add a factory entry for
`Zend\Hydrator\StandaloneHydratorPluginManager`.

- [#87](https://github.com/zendframework/zend-hydrator/pull/87) modifies `Zend\Hydrator\ConfigProvider` to change the target of the
`HydratorManager` alias based on the presence of the zend-servicemanager
package; if the package is not available, the target points to
`Zend\Hydrator\StandaloneHydratorPluginManager` instead of
`Zend\Hydrator\HydratorPluginManager`.

- [#83](https://github.com/zendframework/zend-hydrator/pull/83) renames `Zend\Hydrator\FilterEnabledInterface` to `Zend\Hydrator\Filter\FilterEnabledInterface` (new namespace).

- [#83](https://github.com/zendframework/zend-hydrator/pull/83) renames `Zend\Hydrator\NamingStrategyEnabledInterface` to `Zend\Hydrator\NamingStrategy\NamingStrategyEnabledInterface` (new namespace).
Expand Down
9 changes: 8 additions & 1 deletion docs/book/v3/migration.md
Expand Up @@ -222,4 +222,11 @@ is now marked `private`.
This version removes support for zend-servicemanager v2 service names. Under
zend-servicemanager v2, most special characters were removed, and the name
normalized to all lowercase. Now, only fully qualified class names are mapped to
factories, and short names (names omitting the namespace) are mapped as aliases.
factories, and short names (names omitting the namespace and/or "Hydrator"
suffix) are mapped as aliases.

Additionally, version 3 ships a standalone, PSR-11 compliant version,
`Zend\Hydrator\StandaloneHydratorPluginManager`. By default, the `HydratorManager`
service alias will point to the `StandaloneHydratorPluginManager` if
zend-servicemanager is not installed, and the `HydratorPluginManager` otherwise.
See the [plugin managers chapter](plugin-managers.md) for more details.
208 changes: 208 additions & 0 deletions docs/book/v3/plugin-managers.md
@@ -0,0 +1,208 @@
# Plugin Managers

It can be useful to compose a plugin manager from which you can retrieve
hydrators; in fact, `Zend\Hydrator\DelegatingHydrator` does exactly that!
With such a manager, you can retrieve instances using short names, or instances
that have dependencies on other services, without needing to know the details of
how that works.

Examples of Hydrator plugin managers in real-world scenarios include:

- [hydrating database result sets](https://docs.zendframework.com/zend-db/result-set/#zend92db92resultset92hydratingresultset)
- [preparing API payloads](https://docs.zendframework.com/zend-expressive-hal/resource-generator/#resourcegenerator)

## HydratorPluginManagerInterface

We provide two plugin manager implementations. Essentially, they only need to
implement the [PSR-11 ContainerInterface](https://www.php-fig.org/psr/psr-11/),
but plugin managers in current versions of [zend-servicemanager](https://docs.zendframework.com/zend-servicemanager/)
only implement it indirectly via the container-interop project.

As such, we ship `Zend\Hydrator\HydratorPluginManagerInterface`, which simply
extends the PSR-11 `Psr\Container\ContainerInterface`. Each of our
implementations implement it.

## HydratorPluginManager

If you have used zend-hydrator prior to version 3, you are likely already
familiar with this class, as it has been the implementation we have shipped from
initial versions. The `HydratorPluginManager` extends the zend-servicemanager
`AbstractPluginManager`, and has the following behaviors:

- It will only return `Zend\Hydrator\HydratorInterface` instances.
- It defines short-name aliases for all shipped hydrators (the class name minus
the namespace), in a variety of casing combinations.
- All but the `DelegatingHydrator` are defined as invokable services (meaning
they can be instantiated without any constructor arguments).
- The `DelegatingHydrator` is configured as a factory-based service, mapping to
the `Zend\Hydrator\DelegatingHydratorFactory`.
- No services are shared; a new instance is created each time you call `get()`.

### HydratorPluginManagerFactory

`Zend\Hydrator\HydratorPluginManager` is mapped to the factory
`Zend\Hydrator\HydratorPluginManagerFactory` when wired to the dependency
injection container.

The factory will look for the `config` service, and use the `hydrators`
configuration key to seed it with additional services. This configuration key
should map to an array that follows [standard zend-servicemanager configuration](https://docs.zendframework.com/zend-servicemanager/configuring-the-service-manager/).

## StandaloneHydratorPluginManager

`Zend\Hydrator\StandaloneHydratorPluginManager` provides an implementation that
has no dependencies on other libraries. **It can only load the hydrators shipped
with zend-hydrator**.

### StandardHydratorPluginManagerFactory

`Zend\Hydrator\StandardHydratorPluginManager` is mapped to the factory
`Zend\Hydrator\StandardHydratorPluginManagerFactory` when wired to the dependency
injection container.

## HydratorManager alias

`Zend\Hydrator\ConfigManager` defines an alias service, `HydratorManager`. That
service will point to `Zend\Hydrator\HydratorPluginManager` if
zend-servicemanager is installed, and `Zend\Hydrator\StandaloneHydratorPluginManager`
otherwise.

## Custom plugin managers

If you do not want to use zend-servicemanager, but want a plugin manager that is
customizable, or at least capable of loading the hydrators you have defined for
your application, you should write a custom implementation of
`Zend\Hydrator\HydratorPluginManagerInterface`, and wire it to the
`HydratorManager` service, and/or one of the existing service names.

As an example, if you want a configurable solution that uses factories, and want
those factories capable of pulling application-level dependencies, you might do
something like the following:

```php
// In src/YourApplication/CustomHydratorPluginManager.php:

namespace YourApplication;

use Psr\Container\NotFoundExceptionInterface;
use Psr\Container\ContainerInterface;
use RuntimeException;
use Zend\Hydrator\HydratorInterface;
use Zend\Hydrator\HydratorPluginManagerInterface;
use Zend\Hydrator\StandaloneHydratorPluginManager;

class CustomHydratorPluginManager implements HydratorPluginManagerInterface
{
/** @var ContainerInterface */
private $appContainer;

/** @var StandaloneHydratorPluginManager */
private $defaults;

/** @var array<string, string|callable> */
private $factories = [];

public function __construct(ContainerInterface $appContainer)
{
$this->appContainer = $appContainer;
$this->defaults = new StandaloneHydratorPluginManager();
}

/**
* {@inheritDoc}
*/
public function get($id) : HydratorInterface
{
if (! isset($this->factories[$id]) && ! $this->defaults->has($id)) {
$message = sprintf('Hydrator service %s not found', $id);
throw new class($message) extends RuntimeException implements NotFoundExceptionInterface {};
}

// Default was requested; fallback to standalone container
if (! isset($this->factories[$id])) {
return $this->defaults->get($id);
}

$factory = $this->factories[$id];
if (is_string($factory)) {
$this->factories[$id] = $factory = new $factory();
}

return $factory($this->appContainer, $id);
}

public function has($id) : bool
{
return isset($this->factories[$id]) || $this->defaults->has($id);
}

public function setFactoryClass(string $name, string $factory) : void
{
$this->factories[$name] = $factory;
}

public function setFactory(string $name, callable $factory) : void
{
$this->factories[$name] = $factory;
}
}
```

```php
// In src/YourApplication/CustomHydratorPluginManagerFactory.php:

namespace YourApplication;

use Psr\Container\ContainerInterface;

class CustomHydratorPluginManagerFactory
{
public function __invoke(ContainerInterface $container) : CustomHydratorPluginManager
{
$config = $container->has('config') ? $container->get('config') : [];
$config = $config['hydrators']['factories'] ?? [];

$manager = new CustomHydratorPluginManager($this);

if ([] !== $config) {
$this->configureManager($manager, $config);
}

return $manager;
}

/**
* @param array<string, string|callable> $config
*/
private function configureManager(CustomHydratorPluginManager $manager, array $config) : void
{
foreach ($config as $name => $factory) {
is_string($factory)
? $manager->setFactoryClass($name, $factory)
: $manager->setFactory($name, $factory);
}
}
}
```

```php
// in config/autoload/hydrators.global.php or similar:

return [
'dependencies' => [
'aliases' => [
'HydratorManager' => \YourApplication\CustomHydratorPluginManager::class,
],
'factories' => [
\YourApplication\CustomHydratorPluginManager::class => \YourApplication\CustomHydratorPluginManagerFactory::class
],
],
'hydrators' => [
'factories' => [
\Blog\PostHydrator::class => \Blog\PostHydratorFactory::class,
\News\ItemHydrator::class => \News\ItemHydratorFactory::class,
// etc.
],
],
];
```
1 change: 1 addition & 0 deletions mkdocs.yml
Expand Up @@ -13,6 +13,7 @@ pages:
- "Mapping": v3/naming-strategy/map-naming-strategy.md
- "Underscore Mapping": v3/naming-strategy/underscore-naming-strategy.md
- "Composite": v3/naming-strategy/composite-naming-strategy.md
- "Plugin Managers": v3/plugin-managers.md
- Migration: v3/migration.md
- v2:
- "Quick Start": v2/quick-start.md
Expand Down
15 changes: 13 additions & 2 deletions src/ConfigProvider.php
Expand Up @@ -9,6 +9,8 @@

namespace Zend\Hydrator;

use Zend\ServiceManager\ServiceManager;

class ConfigProvider
{
/**
Expand All @@ -26,16 +28,25 @@ public function __invoke() : array
/**
* Return dependency mappings for this component.
*
* If zend-servicemanager is installed, this will alias the HydratorPluginManager
* to the `HydratorManager` service; otherwise, it aliases the
* StandaloneHydratorPluginManager.
*
* @return string[][]
*/
public function getDependencyConfig() : array
{
$hydratorManagerTarget = class_exists(ServiceManager::class)
? HydratorPluginManager::class
: StandaloneHydratorPluginManager::class;

return [
'aliases' => [
'HydratorManager' => HydratorPluginManager::class,
'HydratorManager' => $hydratorManagerTarget,
],
'factories' => [
HydratorPluginManager::class => HydratorPluginManagerFactory::class,
HydratorPluginManager::class => HydratorPluginManagerFactory::class,
StandaloneHydratorPluginManager::class => StandaloneHydratorPluginManagerFactory::class,
],
];
}
Expand Down
4 changes: 2 additions & 2 deletions src/DelegatingHydratorFactory.php
Expand Up @@ -25,10 +25,10 @@ public function __invoke(ContainerInterface $container) : DelegatingHydrator
/**
* Locate and return a HydratorPluginManager instance.
*/
private function marshalHydratorPluginManager(ContainerInterface $container) : HydratorPluginManager
private function marshalHydratorPluginManager(ContainerInterface $container) : ContainerInterface
{
// Already one? Return it.
if ($container instanceof HydratorPluginManager) {
if ($container instanceof HydratorPluginManagerInterface) {
return $container;
}

Expand Down
25 changes: 25 additions & 0 deletions src/Exception/MissingHydratorServiceException.php
@@ -0,0 +1,25 @@
<?php
/**
* @see https://github.com/zendframework/zend-hydrator for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-hydrator/blob/master/LICENSE.md New BSD License
*/

declare(strict_types=1);

namespace Zend\Hydrator\Exception;

use Psr\Container\NotFoundExceptionInterface;

use function sprintf;

class MissingHydratorServiceException extends InvalidArgumentException implements NotFoundExceptionInterface
{
public static function forService(string $serviceName) : self
{
return new self(sprintf(
'Unable to resolve "%s" to a hydrator service.',
$serviceName
));
}
}
2 changes: 1 addition & 1 deletion src/HydratorPluginManager.php
Expand Up @@ -23,7 +23,7 @@
*
* Enforces that adapters retrieved are instances of HydratorInterface
*/
class HydratorPluginManager extends AbstractPluginManager
class HydratorPluginManager extends AbstractPluginManager implements HydratorPluginManagerInterface
{
/**
* Default aliases
Expand Down
15 changes: 15 additions & 0 deletions src/HydratorPluginManagerFactory.php
Expand Up @@ -12,7 +12,9 @@
use Psr\Container\ContainerInterface;
use Zend\ServiceManager\Config;

use function class_exists;
use function is_array;
use function sprintf;

class HydratorPluginManagerFactory
{
Expand All @@ -25,9 +27,22 @@ class HydratorPluginManagerFactory
* configuration.
*
* @see https://docs.zendframework.com/zend-expressive/v3/features/container/config/
* @throws Exception\DomainException if zend-servicemanager is not installed.
*/
public function __invoke(ContainerInterface $container, string $name, ?array $options = []) : HydratorPluginManager
{
if (! class_exists(Config::class)) {
throw new Exception\DomainException(sprintf(
'%s requires the zendframework/zend-servicemanager package, which is not installed.'
. ' If you do not want to install that package, you can use the %s instead;'
. ' however, that version does not have support for the "hydrators"'
. ' configuration outside of aliases, invokables, and factories. If you'
. ' need those features, please install zendframework/zend-servicemanager.',
HydratorPluginManager::class,
StandaloneHydratorPluginManager::class
));
}

$pluginManager = new HydratorPluginManager($container, $options ?: []);

// If this is in a zend-mvc application, the ServiceListener will inject
Expand Down

0 comments on commit f074aa3

Please sign in to comment.