Skip to content

Commit

Permalink
Add TaggingServiceProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
XedinUnknown committed Nov 7, 2023
1 parent f1d1470 commit 330c708
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 0 deletions.
121 changes: 121 additions & 0 deletions src/TaggingServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

namespace Dhii\Container;

use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
use ReflectionException;
use ReflectionFunction;
use ReflectionObject;

/**
* A service provider that detects tags in factory docBlocks, and exposes them as services.
*
* A service may have a docBlock. The docBlock may contain various docBlock tags, such as `@param` or `@return`.
* This class will detect `@tag {tagname}` tags in service docBlocks. `tagname` may be anything that a service
* key may be - they exist in the same namespace. In fact, a `tagname` corresponds to a service
* that returns a list of tagged services. To retrieve them, just resolve the tagname as a service.
*
* For each unique `tagname` in factory docBlocks, this service provider will create an extension with
* an identical name. This extension at resolution time will resolve each tagged service by key,
* and add resulting services to the list it is extending. To ensure there's always a list to extend,
* this service provider will also add a service with an identical name, which resolves to an empty list.
* All such "tag" services are empty list in the beginning of their resolution, so it doesn't matter
* if it gets overwritten by another module's identical empty list.
*
* @psalm-import-type Factory from ServiceProvider
* @psalm-import-type Extension from ServiceProvider
*/
class TaggingServiceProvider implements ServiceProviderInterface
{
/** @var array<Factory> */
protected array $factories;
/** @var array<Extension> */
protected array $extensions;

public function __construct(ServiceProviderInterface $inner)
{
$this->factories = $inner->getFactories();
$this->extensions = $inner->getExtensions();
$this->indexTags();
}

/**
* @inheritDoc
*/
public function getFactories()
{
return $this->factories;
}

/**
* @inheritDoc
*/
public function getExtensions()
{
return $this->extensions;
}

/**
* Indexes tagged factories, and creates factories and extensions for tags.
*
* @throws ReflectionException If problem obtaining factory reflection.
*/
protected function indexTags(): void
{
$tags = [];

foreach ($this->factories as $serviceName => $factory) {
if (is_string($factory)) {
continue;
}

$reflection = is_object($factory) && get_class($factory) === 'Closure'
? new ReflectionFunction($factory)
: new ReflectionObject($factory);
$docBlock = $reflection->getDocComment();

// No docblock
if ($docBlock === false) {
continue;
}

$factoryTags = $this->getTagsFromDocBlock($docBlock);
foreach ($factoryTags as $tag) {
if (!isset($tags[$tag]) || !is_array($tags[$tag])) {
$tags[$tag] = [];
}
$tags[$tag][] = $serviceName;
}
}

foreach ($tags as $tag => $taggedServiceNames) {
$this->factories[$tag] = fn (): array => [];
$this->extensions[$tag] = function (ContainerInterface $c, array $prev) use ($taggedServiceNames): array {
return array_merge(
$prev,
array_map(fn (string $serviceName) => $c->get($serviceName), $taggedServiceNames)
);
};
}
}

/**
* Retrieves tags names that are part of a docBlock.
*
* @link https://www.php.net/manual/en/reflectionclass.getdoccomment.php#118606
*
* @param string $docBlock The docBlock.
*
* @return array<string> A list of tag names.
*/
protected function getTagsFromDocBlock(string $docBlock): array
{
$regex = '#(@tag\s*(?P<tags>[^\s]+))#';
preg_match_all($regex, $docBlock, $matches);

return $matches['tags'];
}
}
58 changes: 58 additions & 0 deletions tests/functional/TaggingServiceProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Dhii\Container\FuncTest;

use Dhii\Collection\ContainerInterface;
use Dhii\Container\DelegatingContainer;
use Dhii\Container\ServiceProvider;
use Dhii\Container\TaggingServiceProvider;
use Exception;
use PHPUnit\Framework\TestCase;

class TaggingServiceProviderTest extends TestCase
{
/**
* Tests that the extensions passed are correctly retrieved.
*
* @throws Exception If problem testing.
*/
public function testTagsRecognized()
{
$factories = [
'serviceX' =>
fn (): string => 'X',
'serviceA' =>
/**
* @tag my_tag
*/
fn (): string => 'A',
'serviceB' =>
/**
* @tag my_tag
*/
function (): string {
return 'B';
},
'serviceC' =>
/**
* @tag my_tag
*/
new class () {
public function __invoke(): string
{
return 'C';
}
},
'serviceD' => fn (ContainerInterface $c): string =>
implode('', array_merge($c->get('my_tag'), ['D'])),
];
$extensions = [];
$inner = new ServiceProvider($factories, $extensions);
$subject = new TaggingServiceProvider($inner);
$container = new DelegatingContainer($subject, null);

$result = $container->get('serviceD');
$this->assertEquals('ABCD', $result);
}

}

0 comments on commit 330c708

Please sign in to comment.