Skip to content

Commit 0e4b4b9

Browse files
committed
feat(singleton): support for set a singleton from an object
1 parent f2caabd commit 0e4b4b9

22 files changed

Lines changed: 580 additions & 313 deletions

.gitattributes

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
/.gitattributes export-ignore
44
/.gitignore export-ignore
55
/composer.lock export-ignore
6-
/LICENSE export-ignore
76
/phpstan.dist.neon export-ignore
8-
/phpunit.xml export-ignore
7+
/phpunit.xml.dist export-ignore
98
/rector.php export-ignore

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
vendor
33
phpstan.neon
44
.phpunit.cache
5+
phpunit.xml

composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
"flight\\": "src"
99
}
1010
},
11+
"autoload-dev": {
12+
"psr-4": {
13+
"flight\\tests\\": "tests"
14+
}
15+
},
1116
"authors": [
1217
{
1318
"name": "fadrian06",

phpunit.xml renamed to phpunit.xml.dist

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
bootstrap="vendor/autoload.php"
55
cacheResultFile=".phpunit.cache/test-results"
66
executionOrder="depends,defects"
7-
forceCoversAnnotation="true"
7+
forceCoversAnnotation="false"
88
beStrictAboutCoversAnnotation="true"
99
beStrictAboutOutputDuringTests="true"
1010
beStrictAboutTodoAnnotatedTests="true"
@@ -24,5 +24,8 @@
2424
<include>
2525
<directory suffix=".php">src</directory>
2626
</include>
27+
<exclude>
28+
<directory>src/Overloads</directory>
29+
</exclude>
2730
</coverage>
2831
</phpunit>

src/Container.php

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,39 +32,42 @@ final class Container implements ContainerInterface
3232
*/
3333
public function get(string $id): object
3434
{
35-
if ($this->has($id)) {
36-
[
37-
'concrete' => $concrete,
38-
'isSingleton' => $isSingleton
39-
] = $this->entries[$id];
35+
if (!$this->has($id)) {
36+
/** @var T */
37+
$object = $this->resolve($id);
4038

41-
if (is_callable($concrete)) {
42-
/** @var T */
43-
$object = $concrete($this);
39+
return $object;
40+
}
4441

45-
if ($isSingleton) {
46-
$this->singleton($id, $object);
47-
}
42+
[
43+
'concrete' => $concrete,
44+
'isSingleton' => $isSingleton
45+
] = $this->entries[$id];
4846

49-
return $object;
50-
}
47+
switch (true) {
48+
case is_callable($concrete):
49+
/** @var T */
50+
$object = $concrete($this);
51+
break;
5152

52-
if (is_string($concrete)) {
53+
case is_string($concrete):
5354
/** @var T */
5455
$object = $this->resolve($concrete);
56+
break;
5557

56-
$this->singleton($id, $object);
57-
58-
return $object;
59-
}
58+
case is_object($concrete) && $isSingleton:
59+
/** @var T */
60+
$object = $concrete;
61+
break;
6062

61-
if (is_object($concrete) && $isSingleton) {
62-
return $concrete;
63-
}
63+
default:
64+
/** @var T */
65+
$object = $this->resolve(get_class($concrete));
6466
}
6567

66-
/** @var T */
67-
$object = $this->resolve($concrete ?? $id);
68+
if ($isSingleton) {
69+
$this->singleton($id, $object);
70+
}
6871

6972
return $object;
7073
}
@@ -78,7 +81,7 @@ public function has(string $id): bool
7881
/**
7982
* @template T of object
8083
* @param class-string<T> $id
81-
* @param class-string<T>|callable(ContainerInterface $container): T $concrete
84+
* @param class-string<T>|T|callable(ContainerInterface $container): T $concrete
8285
*/
8386
public function set(string $id, $concrete): self
8487
{
@@ -95,6 +98,8 @@ public function set(string $id, $concrete): self
9598
public function singleton($id): self
9699
{
97100
$fqcn = is_object($id) ? get_class($id) : $id;
101+
102+
/** @var T|class-string<T>|callable */
98103
$concrete = func_num_args() === 2 ? func_get_arg(1) : $id;
99104

100105
$this->entries[$fqcn]['concrete'] = $concrete;

src/Overloads/Container.php

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,47 +8,49 @@
88
use Psr\Container\ContainerInterface;
99
use Psr\Container\NotFoundExceptionInterface;
1010

11-
final class Container implements ContainerInterface
12-
{
13-
/**
14-
* @template T of object
15-
* @param class-string<T> $id
16-
* @return T
17-
* @throws NotFoundExceptionInterface
18-
* @throws ContainerExceptionInterface
19-
*/
20-
public function get(string $id): object
11+
if (!class_exists(Container::class)) {
12+
final class Container implements ContainerInterface
2113
{
22-
if (!$this->has($id)) {
23-
throw new NotFoundException;
14+
/**
15+
* @template T of object
16+
* @param class-string<T> $id
17+
* @return T
18+
* @throws NotFoundExceptionInterface
19+
* @throws ContainerExceptionInterface
20+
*/
21+
public function get(string $id): object
22+
{
23+
if (!$this->has($id)) {
24+
throw new NotFoundException;
25+
}
26+
27+
return new $id;
2428
}
2529

26-
return new $id;
27-
}
28-
29-
/** @param class-string $id */
30-
public function has(string $id): bool
31-
{
32-
return false;
33-
}
30+
/** @param class-string $id */
31+
public function has(string $id): bool
32+
{
33+
return false;
34+
}
3435

35-
/**
36-
* @template T of object
37-
* @param class-string<T> $id
38-
* @param null|class-string<T>|callable(ContainerInterface $container): T $concrete
39-
*/
40-
public function singleton(string $id, $concrete = null): self
41-
{
42-
return $this;
43-
}
36+
/**
37+
* @template T of object
38+
* @param class-string<T>|T $id
39+
* @param null|class-string<T>|T|callable(ContainerInterface $container): T $concrete
40+
*/
41+
public function singleton($id, $concrete = null): self
42+
{
43+
return $this;
44+
}
4445

45-
/**
46-
* @template T of object
47-
* @param class-string<T> $id
48-
* @param class-string<T>|callable(ContainerInterface $container): T $concrete
49-
*/
50-
public function set(string $id, $concrete): self
51-
{
52-
return $this;
46+
/**
47+
* @template T of object
48+
* @param class-string<T> $id
49+
* @param class-string<T>|T|callable(ContainerInterface $container): T $concrete
50+
*/
51+
public function set(string $id, $concrete): self
52+
{
53+
return $this;
54+
}
5355
}
5456
}

tests/ContainerExceptionsTest.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace flight\tests;
6+
7+
use flight\Container;
8+
use flight\tests\examples\ExampleAbstractClass;
9+
use flight\tests\examples\ExampleClassWithBuiltinConstructorParameter;
10+
use flight\tests\examples\ExampleClassWithoutConstructorParameterTypeHint;
11+
use flight\tests\examples\ExampleClassWithUnionTypeHint;
12+
use flight\tests\examples\ExampleInterface;
13+
use flight\tests\examples\ExampleTrait;
14+
use PHPUnit\Framework\TestCase;
15+
use Psr\Container\ContainerExceptionInterface;
16+
use Psr\Container\NotFoundExceptionInterface;
17+
18+
final class ContainerExceptionsTest extends TestCase
19+
{
20+
public function test_it_throws_an_exception_when_the_class_does_not_exist(): void
21+
{
22+
$container = new Container;
23+
24+
self::expectException(NotFoundExceptionInterface::class);
25+
self::expectExceptionMessage('Class "NonExistentClass" does not exist');
26+
27+
$container->get('NonExistentClass'); // @phpstan-ignore-line
28+
}
29+
30+
/**
31+
* @dataProvider nonInstantiableClassStringsDataProvider
32+
* @param class-string $class
33+
*/
34+
public function test_it_throws_an_exception_when_the_class_is_not_instantiable(
35+
string $class
36+
): void {
37+
self::expectException(ContainerExceptionInterface::class);
38+
self::expectExceptionMessage("Class \"$class\" is not instantiable");
39+
40+
$container = new Container;
41+
$container->get($class);
42+
}
43+
44+
public function test_it_throws_an_exception_when_a_constructor_parameter_does_not_have_type_hint(): void
45+
{
46+
self::expectException(ContainerExceptionInterface::class);
47+
48+
self::expectExceptionMessage(
49+
'Failed to resolve class "'
50+
. ExampleClassWithoutConstructorParameterTypeHint::class
51+
. '" because param "parameter" is missing'
52+
);
53+
54+
$container = new Container;
55+
$container->get(ExampleClassWithoutConstructorParameterTypeHint::class);
56+
}
57+
58+
public function test_it_throws_an_exception_when_the_class_has_built_in_constructor_parameters(): void
59+
{
60+
self::expectException(ContainerExceptionInterface::class);
61+
62+
self::expectExceptionMessage(
63+
'Failed to resolve class "'
64+
. ExampleClassWithBuiltinConstructorParameter::class
65+
. '" because invalid param "parameter"'
66+
);
67+
68+
$container = new Container;
69+
$container->get(ExampleClassWithBuiltinConstructorParameter::class);
70+
}
71+
72+
public function test_it_throws_an_exception_when_the_class_has_a_union_type_hint(): void
73+
{
74+
if (PHP_VERSION < '8.0') {
75+
self::markTestSkipped('Union types are only available in PHP 8.0 and later');
76+
}
77+
78+
self::expectException(ContainerExceptionInterface::class);
79+
80+
self::expectExceptionMessage(
81+
'Failed to resolve class "'
82+
. ExampleClassWithUnionTypeHint::class
83+
. '" because of union type for param "dateTime"'
84+
);
85+
86+
$container = new Container;
87+
$container->get(ExampleClassWithUnionTypeHint::class);
88+
}
89+
90+
/** @return array{0: class-string}[] */
91+
public static function nonInstantiableClassStringsDataProvider(): array
92+
{
93+
return [
94+
[ExampleAbstractClass::class],
95+
[ExampleInterface::class],
96+
[ExampleTrait::class]
97+
];
98+
}
99+
}
100+
101+
if (PHP_VERSION >= '8.0') {
102+
require_once __DIR__ . '/examples/ExampleClassWithUnionTypeHint.php';
103+
}

tests/ContainerGetTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use flight\Container;
6+
use flight\tests\examples\ExampleClassWithAnNonBuiltinConstructorParameter;
7+
use flight\tests\examples\ExampleClassWithAnOptionalConstructorParameter;
8+
use flight\tests\examples\ExampleClassWithoutConstructorParameters;
9+
use flight\tests\examples\ExampleClassWithRandomDefaultProperty;
10+
use PHPUnit\Framework\TestCase;
11+
12+
final class ContainerGetTest extends TestCase
13+
{
14+
public function test_it_can_get_a_class(): void
15+
{
16+
$container = new Container;
17+
18+
self::assertInstanceOf(stdClass::class, $container->get(stdClass::class));
19+
}
20+
21+
public function test_can_get_a_class_without_constructor_parameters(): void
22+
{
23+
$container = new Container;
24+
25+
self::assertInstanceOf(
26+
ExampleClassWithoutConstructorParameters::class,
27+
$container->get(ExampleClassWithoutConstructorParameters::class)
28+
);
29+
}
30+
31+
public function test_it_can_get_a_class_with_a_constructor_with_default_values(): void
32+
{
33+
$container = new Container;
34+
35+
self::assertInstanceOf(
36+
ExampleClassWithRandomDefaultProperty::class,
37+
$container->get(ExampleClassWithRandomDefaultProperty::class)
38+
);
39+
}
40+
41+
public function test_can_get_a_class_with_a_non_builtin_constructor_parameter(): void
42+
{
43+
$container = new Container;
44+
45+
self::assertInstanceOf(
46+
ExampleClassWithAnNonBuiltinConstructorParameter::class,
47+
$container->get(ExampleClassWithAnNonBuiltinConstructorParameter::class)
48+
);
49+
}
50+
51+
public function test_it_get_optional_non_builtin_classes_constructor_parameters(): void
52+
{
53+
$container = new Container;
54+
55+
self::assertInstanceOf(
56+
ExampleClassWithAnOptionalConstructorParameter::class,
57+
$container->get(ExampleClassWithAnOptionalConstructorParameter::class)
58+
);
59+
}
60+
}

0 commit comments

Comments
 (0)