Skip to content

Commit a502cce

Browse files
committed
Add dependency injection package
1 parent 31cc0f1 commit a502cce

39 files changed

Lines changed: 1528 additions & 5 deletions

composer.json

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
},
1010
"require": {
1111
"php": "^8.2",
12-
"psr/clock": "^1.0"
12+
"psr/clock": "^1.0",
13+
"psr/container": "^2.0.x-dev"
1314
},
1415
"require-dev": {
1516
"friendsofphp/php-cs-fixer": "^3.64",
@@ -19,21 +20,25 @@
1920
"symplify/monorepo-builder": "^11.2"
2021
},
2122
"replace": {
22-
"aether/clock": "self.version"
23+
"aether/clock": "self.version",
24+
"aether/dependency-injection": "self.version"
2325
},
2426
"provide": {
25-
"psr/clock-implementation": "*"
27+
"psr/clock-implementation": "*",
28+
"psr/container-implementation": "2.0.2"
2629
},
2730
"minimum-stability": "dev",
2831
"prefer-stable": true,
2932
"autoload": {
3033
"psr-4": {
31-
"Aether\\Clock\\": "packages/Clock/src/"
34+
"Aether\\Clock\\": "packages/Clock/src/",
35+
"Aether\\DependencyInjection\\": "packages/DependencyInjection/src/"
3236
}
3337
},
3438
"autoload-dev": {
3539
"psr-4": {
36-
"Aether\\Tests\\Clock\\": "packages/Clock/tests/"
40+
"Aether\\Tests\\Clock\\": "packages/Clock/tests/",
41+
"Aether\\Tests\\DependencyInjection\\": "packages/DependencyInjection/tests/"
3742
}
3843
},
3944
"config": {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Close Pull Request
2+
3+
on:
4+
pull_request_target:
5+
types: [opened]
6+
7+
jobs:
8+
run:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: superbrothers/close-pull-request@v3
12+
with:
13+
comment: |
14+
Thanks for your Pull Request! We love contributions.
15+
16+
However, you should instead open your PR on the main repository:
17+
https://github.com/sxbrsky/aether
18+
19+
This repository is what we call a "subtree split": a read-only subset of that main repository.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2024 Dominik Szamburski
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Aether Dependency Injection Component
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "aether/dependency-injection",
3+
"description": "The Aether dependency injection package.",
4+
"license": "MIT",
5+
"type": "library",
6+
"support": {
7+
"issues": "https://github.com/sxbrsky/aether/issues",
8+
"source": "https://github.com/sxbrsky/aether"
9+
},
10+
"require": {
11+
"php": "^8.2",
12+
"psr/container": "^2.0.x-dev"
13+
},
14+
"provide": {
15+
"psr/container-implementation": "2.0.2"
16+
},
17+
"minimum-stability": "dev",
18+
"autoload": {
19+
"psr-4": {
20+
"Aether\\DependencyInjection\\": "src/"
21+
}
22+
},
23+
"autoload-dev": {
24+
"psr-4": {
25+
"Aether\\Tests\\DependencyInjection\\": "tests/"
26+
}
27+
},
28+
"config": {
29+
"sort-packages": true
30+
}
31+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the aether/aether.
5+
*
6+
* Copyright (C) 2024 Dominik Szamburski
7+
*
8+
* This software may be modified and distributed under the terms
9+
* of the MIT license. See the LICENSE file for details.
10+
*/
11+
12+
namespace Aether\DependencyInjection;
13+
14+
use function array_key_exists;
15+
16+
use Closure;
17+
use Exception;
18+
use InvalidArgumentException;
19+
20+
use function is_object;
21+
use function is_scalar;
22+
use function is_string;
23+
use function property_exists;
24+
25+
use Aether\DependencyInjection\Definition\Binding\Alias;
26+
use Aether\DependencyInjection\Definition\Binding\Factory;
27+
use Aether\DependencyInjection\Definition\Binding\Scalar;
28+
use Aether\DependencyInjection\Definition\Binding\Shared;
29+
use Aether\DependencyInjection\Definition\Binding\WeakReference;
30+
31+
use Aether\DependencyInjection\Definition\Exception\CircularDependencyException;
32+
use Aether\DependencyInjection\Definition\Resolver\DefinitionResolver;
33+
use Aether\DependencyInjection\Definition\State;
34+
use Aether\DependencyInjection\Exception\EntryNotFoundException;
35+
use Aether\DependencyInjection\Invoker\Invoker;
36+
37+
class Container implements ContainerInterface
38+
{
39+
private State $state;
40+
private FactoryInterface $factory;
41+
42+
public function __construct()
43+
{
44+
45+
$this->state = new State();
46+
$this->factory = new DefinitionResolver($this->state, $this);
47+
48+
$shared = new Alias(self::class);
49+
50+
$this->state->bindings = [
51+
self::class => new WeakReference(\WeakReference::create($this)),
52+
ContainerInterface::class => $shared,
53+
FactoryInterface::class => $shared,
54+
InvokerInterface::class => $shared
55+
];
56+
}
57+
58+
/**
59+
* @inheritDoc
60+
*/
61+
public function has(string $id): bool
62+
{
63+
return array_key_exists($id, $this->state->instances) || array_key_exists($id, $this->state->bindings);
64+
}
65+
66+
/**
67+
* @inheritDoc
68+
*/
69+
public function get(string $id): int|float|string|callable|object
70+
{
71+
try {
72+
return $this->make($id);
73+
} catch (Exception $e) {
74+
if ($this->has($id) || $e instanceof CircularDependencyException) {
75+
throw $e;
76+
}
77+
78+
throw new EntryNotFoundException($id);
79+
}
80+
}
81+
82+
/**
83+
* @inheritDoc
84+
*/
85+
public function instance(string $id, int|float|string|callable|object $instance): int|float|string|callable|object
86+
{
87+
$this->state->instances[$id] = $instance;
88+
return $instance;
89+
}
90+
91+
/**
92+
* @inheritDoc
93+
*/
94+
public function bind(string $abstract, int|float|string|callable|object $concrete, bool $shared = false): void
95+
{
96+
$concrete = match (true) {
97+
$concrete instanceof Closure => new Factory($concrete, $shared),
98+
$concrete instanceof \WeakReference => new WeakReference($concrete),
99+
is_string($concrete) => new Alias($concrete, $shared),
100+
is_scalar($concrete) => new Scalar($concrete),
101+
is_object($concrete) => new Shared($concrete),
102+
default => throw new InvalidArgumentException(self::class . '::bind() unknown binding type.')
103+
};
104+
105+
$this->state->bindings[$abstract] = $concrete;
106+
}
107+
108+
/**
109+
* @inheritDoc
110+
*/
111+
public function shared(string $abstract, int|float|string|callable|object $concrete): void
112+
{
113+
$this->bind($abstract, $concrete, true);
114+
}
115+
116+
/**
117+
* @inheritDoc
118+
*/
119+
public function isShared(string $abstract): bool
120+
{
121+
if (array_key_exists($abstract, $this->state->instances)) {
122+
return true;
123+
}
124+
125+
if (array_key_exists($abstract, $this->state->bindings)) {
126+
if (property_exists($this->state->bindings[$abstract], 'shared')) {
127+
return (bool) $this->state->bindings[$abstract]->shared;
128+
}
129+
}
130+
131+
return false;
132+
}
133+
134+
/**
135+
* @inheritDoc
136+
*/
137+
public function make(string $abstract, array $parameters = []): int|float|string|callable|object
138+
{
139+
return $this->factory->make($abstract, $parameters);
140+
}
141+
142+
/**
143+
* @inheritDoc
144+
*/
145+
public function call(callable|array|string $callable, array $parameters = []): mixed
146+
{
147+
return $this->getInvoker()->call($callable, $parameters);
148+
}
149+
150+
private function getInvoker(): InvokerInterface
151+
{
152+
return new Invoker($this);
153+
}
154+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the aether/aether.
5+
*
6+
* Copyright (C) 2024 Dominik Szamburski
7+
*
8+
* This software may be modified and distributed under the terms
9+
* of the MIT license. See the LICENSE file for details.
10+
*/
11+
12+
namespace Aether\DependencyInjection;
13+
14+
use Psr\Container\ContainerInterface as BaseContainerInterface;
15+
16+
interface ContainerInterface extends BaseContainerInterface, FactoryInterface, InvokerInterface
17+
{
18+
/**
19+
* Returns an entry of the container by its id.
20+
*
21+
* @param string|class-string<T> $id
22+
* The unique identifier for the entry.
23+
*
24+
* @return ($id is class-string ? T : int|float|string|callable|object)
25+
* The retrieved service.
26+
*
27+
* @throws \Psr\Container\NotFoundExceptionInterface
28+
*
29+
* @template T of object
30+
*/
31+
public function get(string $id): int|float|string|callable|object;
32+
33+
/**
34+
* Registers an instance as shared in the container.
35+
*
36+
* @param string $id
37+
* The unique identifier for the entry.
38+
* @param int|float|string|callable|object $instance
39+
* The instance to register.
40+
*
41+
* @return int|float|string|callable|object
42+
* Returns registered instance.
43+
*/
44+
public function instance(string $id, int|float|string|callable|object $instance): int|float|string|callable|object;
45+
46+
/**
47+
* Binds a given concrete with the container.
48+
*
49+
* @param class-string|string $abstract
50+
* The alias.
51+
* @param int|float|string|callable|object $concrete
52+
* The binding.
53+
* @param bool $shared
54+
* Sets a shared binding.
55+
*
56+
* @return void
57+
*/
58+
public function bind(string $abstract, int|float|string|callable|object $concrete, bool $shared = false): void;
59+
60+
/**
61+
* Binds a given concrete with the container as shared instance.
62+
*
63+
* @param class-string|string $abstract
64+
* The alias.
65+
* @param int|float|string|callable|object $concrete
66+
* The binding.
67+
*
68+
* @return void
69+
*
70+
* @throws \InvalidArgumentException
71+
*/
72+
public function shared(string $abstract, int|float|string|callable|object $concrete): void;
73+
74+
/**
75+
* Checks if a given concrete is shared.
76+
*
77+
* @param string $abstract
78+
* The alias.
79+
*
80+
* @return bool
81+
* Returns true if the given concrete is shared, otherwise false.
82+
*/
83+
public function isShared(string $abstract): bool;
84+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the aether/aether.
5+
*
6+
* Copyright (C) 2024 Dominik Szamburski
7+
*
8+
* This software may be modified and distributed under the terms
9+
* of the MIT license. See the LICENSE file for details.
10+
*/
11+
12+
namespace Aether\DependencyInjection\Definition\Binding;
13+
14+
use Aether\DependencyInjection\Definition\Definition;
15+
16+
final readonly class Alias implements Definition
17+
{
18+
public function __construct(
19+
public string $value,
20+
public bool $shared = false
21+
) {
22+
}
23+
}

0 commit comments

Comments
 (0)