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

Parameter name binding #87

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- New #90: Allow multiple method call in array definition (@vjik)
- Bug #86: Fix crash when intersection types are used (@vjik)
- Enh #87: Support parameter name bindings (@xepozz)

## 3.2.0 February 12, 2023

Expand Down
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ at the moment of obtaining a service instance or creating an object.

#### `ArrayDefinition`

Array definition allows describing a service or an object declaratively:
`ArrayDefinition` describes a class declaratively:

```php
use \Yiisoft\Definitions\ArrayDefinition;
Expand Down Expand Up @@ -206,6 +206,38 @@ ContentNegotiator::class => [
],
```

### Name binding

Name binding is a way to bind a name to a definition. It is used to resolve a definition not by its class name but by a name.

Set a definitions with a specific name. It may be typed or untyped reference like:
1. `'$serviceName' => $definition`
2. `Service::class . ' $serviceName' => $definition`

```php
return [
'$fileCache' => FileCache::class, // implements CacheInterface
'$redisCache' => RedisCache::class, // implements CacheInterface
CacheInterface::class . ' $memCache' => MemCache::class, // also implements CacheInterface
]
```

So now you can resolve a definition by its name:

```php
class MyService
{
public function __construct(
CacheInterface $memCache, // typed reference
$fileCache, // untyped reference
CacheInterface $redisCache, // typed reference to untyped definition
) {
// ...
}

}
```

### Definition storage

Definition storage could be used to hold and obtain definitions and check if a certain definition could be instantiated.
Expand Down
34 changes: 30 additions & 4 deletions src/DefinitionStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,38 @@ public function set(string $id, mixed $definition): void
*
* @throws CircularReferenceException
*/
private function isResolvable(string $id, array $building): bool
private function isResolvable(string $id, array $building, ?string $parameterName = null): bool
{
if (isset($this->definitions[$id])) {
return true;
}

if (
Copy link
Member

@vjik vjik Mar 3, 2024

Choose a reason for hiding this comment

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

I assume that feature can degrade performance. Suggest to do it optional or need check performance.

Copy link
Member

Choose a reason for hiding this comment

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

@xepozz would you mind running a comparison benchmark for it?

$parameterName !== null
&& (
isset($this->definitions[$typedParameterName = $id . ' $' . $parameterName])
|| isset($this->definitions[$typedParameterName = '$' . $parameterName])
)
&& (!empty($buildingClass = array_key_last($building))) && class_exists($buildingClass)
) {
$definition = $this->definitions[$buildingClass] ?? null;
$temporaryDefinition = ArrayDefinition::fromConfig([
ArrayDefinition::CLASS_NAME => $buildingClass,
ArrayDefinition::CONSTRUCTOR => [
$parameterName => is_string($this->definitions[$typedParameterName])
? Reference::to($this->definitions[$typedParameterName])
: $this->definitions[$typedParameterName],
],
]);
if ($definition instanceof ArrayDefinition) {
$this->definitions[$buildingClass] = $definition->merge($temporaryDefinition);
} else {
$this->definitions[$buildingClass] = $temporaryDefinition;
}

return true;
}

if ($this->useStrictMode || !class_exists($id)) {
$this->buildStack += $building + [$id => 1];
return false;
Expand Down Expand Up @@ -172,7 +198,7 @@ private function isResolvable(string $id, array $building): bool
continue;
}
$unionTypes[] = $typeName;
if ($this->isResolvable($typeName, $building)) {
if ($this->isResolvable($typeName, $building, $parameter->getName())) {
$isUnionTypeResolvable = true;
/** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
break;
Expand Down Expand Up @@ -215,7 +241,7 @@ private function isResolvable(string $id, array $building): bool
}

if (
!$this->isResolvable($typeName, $building)
!$this->isResolvable($typeName, $building, $parameter->getName())
&& ($this->delegateContainer === null || !$this->delegateContainer->has($typeName))
) {
$isResolvable = false;
Expand All @@ -227,7 +253,7 @@ private function isResolvable(string $id, array $building): bool
$this->buildStack += $building;
}

if ($isResolvable) {
if ($isResolvable && !isset($this->definitions[$id])) {
$this->definitions[$id] = $id;
}

Expand Down
6 changes: 5 additions & 1 deletion src/Helpers/Normalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public static function normalize(mixed $definition, ?string $class = null): Defi
return $definition;
}

if ($definition instanceof DefinitionInterface) {
return $definition;
}

if (is_string($definition)) {
// Current class
if (
Expand Down Expand Up @@ -88,7 +92,7 @@ public static function normalize(mixed $definition, ?string $class = null): Defi
}

// Ready object
if (is_object($definition) && !($definition instanceof DefinitionInterface)) {
if (is_object($definition)) {
return new ValueDefinition($definition);
}

Expand Down
42 changes: 42 additions & 0 deletions tests/Support/Helper/ReferenceResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Definitions\Tests\Support\Helper;

use Psr\Container\ContainerInterface;
use stdClass;

final class ReferenceResolver implements ContainerInterface
{
private ?string $reference = null;
private array $references = [];

public function __construct(private ?object $mockObject = null)
{
$this->mockObject ??= new stdClass();
}

public function get(string $id)
{
$this->reference = $id;
$this->references[] = $id;

return $this->mockObject;
}

public function has(string $id): bool
{
return true;
}

public function getReference(): ?string
{
return $this->reference;
}

public function getReferences(): array
{
return $this->references;
}
}
110 changes: 110 additions & 0 deletions tests/Unit/ArrayDefinitionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Yiisoft\Definitions\ArrayDefinition;
use Yiisoft\Definitions\DefinitionStorage;
use Yiisoft\Definitions\Exception\InvalidConfigException;
use Yiisoft\Definitions\Reference;
use Yiisoft\Definitions\Tests\Support\Bike;
use Yiisoft\Definitions\Tests\Support\Car;
use Yiisoft\Definitions\Tests\Support\ColorInterface;
use Yiisoft\Definitions\Tests\Support\ColorPink;
use Yiisoft\Definitions\Tests\Support\EngineInterface;
use Yiisoft\Definitions\Tests\Support\EngineMarkOne;
use Yiisoft\Definitions\Tests\Support\EngineMarkTwo;
use Yiisoft\Definitions\Tests\Support\Helper\ReferenceResolver;
use Yiisoft\Definitions\Tests\Support\Mouse;
use Yiisoft\Definitions\Tests\Support\Phone;
use Yiisoft\Definitions\Tests\Support\Recorder;
Expand Down Expand Up @@ -524,6 +527,113 @@ public function testMagicMethods(): void
);
}

public static function dataParameterNameBindings(): iterable
{
yield 'untyped reference' => [
[
'$engine' => EngineMarkOne::class,
'$color' => 'red',
],
Bike::class,
];

yield 'typed reference' => [
[
EngineInterface::class . ' $engine' => EngineMarkOne::class,
ColorInterface::class . ' $color' => ColorPink::class,
],
Bike::class,
];

yield 'referenced reference' => [
[
EngineInterface::class . ' $engine' => EngineMarkOne::class,
ColorInterface::class . ' $color' => Reference::to(ColorPink::class),
ColorPink::class => ColorPink::class,
],
Bike::class,
];
}

/**
* @dataProvider dataParameterNameBindings
*/
public function testParameterNameBindingsFormat(array $definitions, string $class): void
{
$storage = new DefinitionStorage($definitions);
$this->assertInstanceOf(ArrayDefinition::class, $storage->get($class));
}

public function testParameterNameBindings(): void
{
$storage = new DefinitionStorage([
'$engine' => EngineMarkOne::class,
ColorInterface::class . ' $color' => ColorPink::class,
]);

$object = $storage->get(Bike::class);

$this->assertInstanceOf(ArrayDefinition::class, $object);
$this->assertEmpty($object->getMethodsAndProperties());
$this->assertCount(2, $object->getConstructorArguments());
$this->assertArrayHasKey('engine', $object->getConstructorArguments());
$this->assertArrayHasKey('color', $object->getConstructorArguments());

$this->assertInstanceOf(Reference::class, $object->getConstructorArguments()['engine']);
$this->assertInstanceOf(Reference::class, $object->getConstructorArguments()['color']);

$resolver = new ReferenceResolver();
$object->getConstructorArguments()['engine']->resolve($resolver);

$this->assertSame(EngineMarkOne::class, $resolver->getReference());

$object->getConstructorArguments()['color']->resolve($resolver);
$this->assertSame(ColorPink::class, $resolver->getReference());
}

public static function dataWrongParameterNameBindingsFormat(): iterable
{
yield 'reference without dollar' => [
[
'engine' => EngineMarkOne::class,
'$color' => 'red',
],
Bike::class,
];

yield 'missing whitespace between class and variable' => [
[
EngineInterface::class . '$engine' => EngineMarkOne::class,
ColorInterface::class . ' $color' => ColorPink::class,
],
Bike::class,
];

yield 'missing definition' => [
[
EngineInterface::class . ' $engine' => EngineMarkOne::class,
ColorPink::class => ColorPink::class,
],
Bike::class,
];
}

/**
* @dataProvider dataWrongParameterNameBindingsFormat
*/
public function testWrongParameterNameBindingsFormat(array $definitions, string $class): void
{
$storage = new DefinitionStorage($definitions);

$this->expectExceptionMessage(
sprintf(
'Service %s doesn\'t exist in DefinitionStorage.',
$class,
)
);
$storage->get($class);
}

public function testMultipleCall()
{
$definition = ArrayDefinition::fromConfig([
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/DefinitionStorageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
use Yiisoft\Definitions\Tests\Support\ColorInterface;
use Yiisoft\Definitions\Tests\Support\ColorPink;
use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithBuiltinTypeWithoutDefault;
use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithNonExistingSubDependency;
use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithNonExistingDependency;
use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithNonExistingSubDependency;
use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithNonResolvableUnionTypes;
use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithPrivateConstructor;
use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithPrivateConstructorSubDependency;
Expand Down
Loading