-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f1d1470
commit 330c708
Showing
2 changed files
with
179 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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']; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |