Skip to content

Commit 11f44b9

Browse files
authored
Merge pull request #1 from kodedphp/refactor
2 parents 6aee2bd + 912843e commit 11f44b9

28 files changed

Lines changed: 480 additions & 252 deletions

.gitattributes

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,3 @@
77
/phpunit.* export-ignore
88
/infection.* export-ignore
99
/phpbench.* export-ignore
10-
/.* export-ignore

.scrutinizer.yml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@ build:
77
- php-scrutinizer-run
88
environment:
99
php:
10-
version: '7.2'
11-
dependencies:
12-
override:
13-
- composer install --no-interaction --prefer-source
10+
version: '7.3'
11+
12+
before_commands:
13+
- 'composer update -o --prefer-source --no-interaction'
1414

1515
filter:
1616
excluded_paths:
17-
- 'Tests/'
18-
- 'vendor/'
17+
- 'build/*'
18+
- 'vendor/*'
19+
- 'Tests/*'
1920

2021
tools:
21-
php_analyzer: true
2222
external_code_coverage: true
23+
php_analyzer: true
24+
php_code_sniffer: true

.travis.yml

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
sudo: false
21
language: php
2+
os: linux
3+
dist: xenial
34

45
notifications:
56
email: false
@@ -11,19 +12,21 @@ cache:
1112
php:
1213
- 7.2
1314
- 7.3
14-
- 7.4snapshot
15+
- 7.4
16+
- nightly
1517

16-
matrix:
18+
jobs:
1719
fast_finish: true
1820
allow_failures:
19-
- php: 7.4snapshot
21+
- php: nightly
2022

2123
install:
22-
- travis_retry composer update -o --no-interaction --prefer-source
24+
- composer update -o --no-interaction --prefer-source
25+
- wget https://scrutinizer-ci.com/ocular.phar
2326

2427
script:
25-
- vendor/bin/phpunit --coverage-clover=build/coverage/clover.xml
28+
- vendor/bin/phpunit --coverage-clover=build/clover.xml
2629

27-
after_success:
28-
- travis_retry vendor/bin/ocular code-coverage:upload --format=php-clover build/coverage/clover.xml
29-
- travis_retry vendor/bin/infection --threads=4 --min-msi=80 --min-covered-msi=80 --log-verbosity=none
30+
after_script:
31+
- php ocular.phar code-coverage:upload --format=php-clover build/clover.xml
32+
- vendor/bin/infection --threads=4 --min-msi=77 --min-covered-msi=77 --log-verbosity=none

DIContainer.php

Lines changed: 118 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,54 +12,74 @@
1212

1313
namespace Koded;
1414

15-
use Psr\Container\ContainerInterface;
16-
use Throwable;
15+
use Psr\Container\{ContainerExceptionInterface, ContainerInterface};
1716

17+
/**
18+
* Interface DIModule contributes the application configuration,
19+
* typically the interface binding which are used to inject the dependencies.
20+
*
21+
* The application is composed of a set of DIModules and some bootstrapping code.
22+
*/
1823
interface DIModule
1924
{
20-
public function configure(DIContainer $injector): void;
25+
/**
26+
* Provides bindings and other configurations for this app module.
27+
* Also reduces the repetition and results in a more readable configuration.
28+
* Implement the `configure()` method to bind your interfaces.
29+
*
30+
* ex: `$container->bind(MyInterface::class, MyImplementation::class);`
31+
*
32+
* @param DIContainer $container
33+
*/
34+
public function configure(DIContainer $container): void;
2135
}
2236

37+
/**
38+
* The entry point of the DIContainer that draws the lines between the
39+
* APIs, implementation of these APIs, modules that configure these
40+
* implementations and applications that consist of a collection of modules.
41+
*
42+
* ```
43+
* $container = new DIContainer(new ModuleA, new ModuleB, ... new ModuleZ);
44+
* ($container)([AppEntry::class, 'method']);
45+
* ```
46+
*/
2347
final class DIContainer implements ContainerInterface
2448
{
2549
public const SINGLETONS = 'singletons';
2650
public const BINDINGS = 'bindings';
51+
public const EXCLUDE = 'exclude';
2752
public const NAMED = 'named';
2853

2954
private $reflection;
3055
private $inProgress = [];
3156

3257
private $singletons = [];
3358
private $bindings = [];
59+
private $exclude = [];
3460
private $named = [];
3561

36-
private $interfaces = [];
37-
3862
public function __construct(DIModule ...$modules)
3963
{
4064
$this->reflection = new DIReflector;
41-
$this->interfaces = array_filter(get_declared_interfaces(), function(string $name) {
42-
return false === strpos($name, '\\');
43-
});
44-
$this->interfaces = array_flip($this->interfaces);
45-
4665
foreach ((array)$modules as $module) {
4766
$module->configure($this);
4867
}
4968
}
5069

5170
public function __clone()
5271
{
53-
throw DIException::forCloningNotAllowed();
72+
$this->inProgress = [];
73+
$this->singletons = [];
74+
$this->named = [];
5475
}
5576

5677
public function __destruct()
5778
{
5879
$this->reflection = null;
59-
6080
$this->singletons = [];
61-
$this->interfaces = [];
6281
$this->bindings = [];
82+
$this->exclude = [];
6383
$this->named = [];
6484
}
6585

@@ -70,14 +90,20 @@ public function __invoke(callable $callable, array $arguments = [])
7090
));
7191
}
7292

73-
public function inject(string $class, array $arguments = []): ?object
93+
/**
94+
* Creates a new instance of a class. Builds the graph of objects that make up the application.
95+
* It can also inject already created dependencies behind the scene (with singleton and share).
96+
*
97+
* @param string $class FQCN
98+
* @param array $arguments [optional] The arguments for the class constructor.
99+
* They have top precedence over the shared dependencies
100+
*
101+
* @return object|callable|null
102+
* @throws ContainerExceptionInterface
103+
*/
104+
public function new(string $class, array $arguments = []): ?object
74105
{
75-
$binding = $this->getFromBindings($class);
76-
77-
if (isset($this->singletons[$binding])) {
78-
return $this->singletons[$binding];
79-
}
80-
106+
$binding = $this->getNameFromBindings($class);
81107
if (isset($this->inProgress[$binding])) {
82108
throw DIException::forCircularDependency($binding);
83109
}
@@ -90,43 +116,89 @@ public function inject(string $class, array $arguments = []): ?object
90116
}
91117
}
92118

119+
/**
120+
* Create once and share an object throughout the application lifecycle.
121+
* Internally the object is immutable, but it can be replaced with share() method.
122+
*
123+
* @param string $class FQCN
124+
* @param array $arguments [optional] See new() description
125+
*
126+
* @return object
127+
*/
93128
public function singleton(string $class, array $arguments = []): object
94129
{
95-
return $this->singletons[$class] = $this->inject($class, $arguments);
130+
$binding = $this->getNameFromBindings($class);
131+
if (isset($this->singletons[$binding])) {
132+
return $this->singletons[$binding];
133+
}
134+
return $this->singletons[$class] = $this->new($class, $arguments);
96135
}
97136

98-
public function share(object $instance): DIContainer
137+
/**
138+
* Share already created instance of an object throughout the app lifecycle.
139+
*
140+
* @param object $instance The object that will be shared as dependency
141+
* @param array $exclude [optional] A list of FQCNs that should
142+
* be excluded from injecting this instance.
143+
* In this case, a new object will be created and
144+
* injected for these classes
145+
*
146+
* @return DIContainer
147+
*/
148+
public function share(object $instance, array $exclude = []): DIContainer
99149
{
100150
$class = get_class($instance);
101-
$this->mapInterfaces($class, $class);
151+
$this->bindInterfaces($instance, $class);
152+
102153
$this->singletons[$class] = $instance;
154+
$this->bindings[$class] = $class;
103155

156+
foreach ($exclude as $name) {
157+
$this->exclude[$name][$class] = $class;
158+
}
104159
return $this;
105160
}
106161

107-
public function bind(string $interface, string $class): DIContainer
162+
/**
163+
* Binds the interface to concrete class implementation.
164+
* It does not create objects, but prepares the container for dependency injection.
165+
*
166+
* This method should be used in the app modules (DIModule).
167+
*
168+
* @param string $interface FQN of the interface
169+
* @param string $class FQCN of the concrete class implementation,
170+
* or empty value for deferred binding
171+
*
172+
* @return DIContainer
173+
*/
174+
public function bind(string $interface, string $class = ''): DIContainer
108175
{
109-
$this->assertEmpty($class, 'class');
110-
$this->assertEmpty($interface, 'interface');
176+
assert(false === empty($interface), 'Dependency name for bind() method');
111177

112-
if ('$' === $class[0]) {
178+
if ('$' === ($class[0] ?? null)) {
113179
$this->bindings[$interface] = $interface;
114-
$this->bindings[$class] = $interface;
180+
$class && $this->bindings[$class] = $interface;
115181
} else {
116-
$this->bindings[$interface] = $class;
117-
$this->bindings[$class] = $class;
182+
$this->bindings[$interface] = $class ?: $interface;
183+
$class && $this->bindings[$class] = $class;
118184
}
119-
120185
return $this;
121186
}
122187

188+
/**
189+
* Shares an object globally by argument name.
190+
*
191+
* @param string $name The name of the argument
192+
* @param mixed $value The actual value
193+
*
194+
* @return DIContainer
195+
*/
123196
public function named(string $name, $value): DIContainer
124197
{
125198
if (1 !== preg_match('/\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $name)) {
126-
throw DIException::forInvalidParameterName();
199+
throw DIException::forInvalidParameterName($name);
127200
}
128201
$this->named[$name] = $value;
129-
130202
return $this;
131203
}
132204

@@ -138,6 +210,7 @@ public function getStorage(): array
138210
return [
139211
self::SINGLETONS => $this->singletons,
140212
self::BINDINGS => $this->bindings,
213+
self::EXCLUDE => $this->exclude,
141214
self::NAMED => $this->named,
142215
];
143216
}
@@ -147,8 +220,7 @@ public function getStorage(): array
147220
*/
148221
public function has($id): bool
149222
{
150-
$this->assertEmpty($id, 'dependency');
151-
223+
assert(false === empty($id), 'Dependency name for has() method');
152224
return isset($this->bindings[$id]) || isset($this->named[$id]);
153225
}
154226

@@ -161,42 +233,30 @@ public function get($id)
161233
throw DIInstanceNotFound::for($id);
162234
}
163235

164-
$dependency = $this->getFromBindings($id);
165-
236+
$dependency = $this->getNameFromBindings($id);
166237
return $this->singletons[$dependency]
167238
?? $this->named[$dependency]
168-
?? $this->inject($dependency);
239+
?? $this->new($dependency);
169240
}
170241

171242
private function newInstance(string $class, array $arguments): object
172243
{
173-
try {
174-
$this->bindings[$class] = $class;
175-
return $this->reflection->newInstance($this, $class, $arguments);
176-
} catch (Throwable $e) {
177-
throw $e;
178-
}
244+
$this->bindings[$class] = $class;
245+
return $this->reflection->newInstance($this, $class, $arguments);
179246
}
180247

181-
private function getFromBindings(string $dependency): string
248+
private function getNameFromBindings(string $dependency): string
182249
{
183-
$this->assertEmpty($dependency, 'class/interface');
184-
250+
assert(false === empty($dependency), 'Dependency name for class/interface');
185251
return $this->bindings[$dependency] ?? $dependency;
186252
}
187253

188-
private function assertEmpty(string $value, string $type): void
189-
{
190-
if (empty($value)) {
191-
throw DIException::forEmptyName($type);
192-
}
193-
}
194-
195-
private function mapInterfaces(string $dependency, string $class): void
254+
private function bindInterfaces(object $dependency, string $class): void
196255
{
197-
foreach ((@class_implements($dependency, false) ?: []) as $implements) {
198-
if (false === isset($this->interfaces[$implements])) {
199-
$this->bindings[$implements] = $class;
256+
foreach (class_implements($dependency) as $interface) {
257+
if (isset($this->bindings[$interface])) {
258+
$this->bindings[$interface] = $class;
259+
break;
200260
}
201261
}
202262
}

0 commit comments

Comments
 (0)