Passed
Push — master ( ef8c69...a86aba )
by Alexey
07:15 queued 03:20
created

MutableContainer::decorate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.2109

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 2
dl 0
loc 11
ccs 5
cts 8
cp 0.625
crap 2.2109
rs 9.4285
c 0
b 0
f 0
1
<?php declare(strict_types = 1);
2
3
namespace Venta\Container;
4
5
use Closure;
6
use InvalidArgumentException;
7
use Venta\Contracts\Container\Invoker as InvokerContract;
8
use Venta\Contracts\Container\MutableContainer as MutableContainerContract;
9
use Venta\Contracts\Container\ServiceDecorator as ServiceDecoratorContract;
10
use Venta\Contracts\Container\ServiceInflector as ServiceInflectorContract;
11
12
/**
13
 * Class MutableContainer
14
 *
15
 * @package Venta\Container
16
 */
17
class MutableContainer extends AbstractContainer implements MutableContainerContract
18
{
19
20
    /**
21
     * @var ServiceDecoratorContract
22
     */
23
    private $decorator;
24
25
    /**
26
     * @var ServiceInflectorContract
27
     */
28
    private $inflector;
29
30
    /**
31
     * @inheritDoc
32
     */
33 31
    public function __construct()
34
    {
35 31
        $resolver = new ArgumentResolver($this);
36 31
        parent::__construct($resolver);
37 31
        $this->setInflector(new ServiceInflector($resolver));
38 31
        $this->setDecorator(new ServiceDecorator($this, $this->inflector, $this->invoker()));
39
40 31
        $this->bindInstance(InvokerContract::class, $this->invoker());
41 31
    }
42
43
    /**
44
     * @inheritDoc
45
     */
46 10
    public function bind(string $id, $service)
47
    {
48 10
        if (is_string($service)) {
49 8
            $this->bindClass($id, $service);
50 2
        } elseif (is_object($service)) {
51 2
            $this->bindInstance($id, $service);
52
        } else {
53
            throw new InvalidArgumentException('Invalid service provided. Class name or instance expected.');
54
        }
55 8
    }
56
57
    /**
58
     * @inheritDoc
59
     */
60 4
    public function decorate(string $id, $decorator)
61
    {
62 4
        $id = $this->normalize($id);
63
64
        // Check if correct id is provided.
65 4
        if (!$this->isResolvableService($id)) {
66
            throw new InvalidArgumentException('Invalid id provided.');
67
        }
68
69 4
        $this->decorator->add($id, $decorator);
70 4
    }
71
72
    /**
73
     * @inheritDoc
74
     */
75 12
    public function factory(string $id, $callable, $shared = false)
76
    {
77 12
        $reflectedCallable = new Invokable($callable);
78 12
        if (!$this->isResolvableCallable($reflectedCallable)) {
79
            throw new InvalidArgumentException('Invalid callable provided.');
80
        }
81
82
        $this->register($id, $shared, function ($id) use ($reflectedCallable) {
83 12
            $this->callableDefinitions[$id] = $reflectedCallable;
84 12
        });
85 12
    }
86
87
    /**
88
     * @inheritDoc
89
     */
90 4
    public function inflect(string $id, string $method, array $arguments = [])
91
    {
92 4
        $this->inflector->add($id, $method, $arguments);
93 3
    }
94
95
    /**
96
     * @inheritDoc
97
     */
98 27
    protected function instantiateService(string $id, array $arguments)
99
    {
100 27
        $object = parent::instantiateService($id, $arguments);
101 22
        $this->inflector->apply($object);
102
103 21
        return $this->decorator->apply($id, $object, $this->isShared($id));
104
    }
105
106
    /**
107
     * @param ServiceDecoratorContract $decorator
108
     */
109 31
    protected function setDecorator(ServiceDecoratorContract $decorator)
110
    {
111 31
        $this->decorator = $decorator;
112 31
    }
113
114
    /**
115
     * @param ServiceInflectorContract $inflector
116
     */
117 31
    protected function setInflector(ServiceInflectorContract $inflector)
118
    {
119 31
        $this->inflector = $inflector;
120 31
    }
121
122
    /**
123
     * @param string $id
124
     * @param string $class
125
     * @return void
126
     * @throws InvalidArgumentException
127
     */
128 8
    private function bindClass(string $id, string $class)
129
    {
130 8
        if (!$this->isResolvableService($class)) {
131 1
            throw new InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
132
        }
133
        $this->register($id, true, function ($id) use ($class) {
134 6
                $this->classDefinitions[$id] = $class;
135 7
        });
136 6
    }
137
138
    /**
139
     * @param string $id
140
     * @param object $instance
141
     * @return void
142
     * @throws InvalidArgumentException
143
     */
144 31
    private function bindInstance(string $id, $instance)
145
    {
146 31
        if (!$this->isConcrete($instance)) {
147
            throw new InvalidArgumentException('Invalid instance provided.');
148
        }
149 31
        $this->register($id, true, function ($id) use ($instance) {
150 31
            $this->instances[$id] = $instance;
151 31
        });
152 31
    }
153
154
    /**
155
     * Check if subject service is an object instance.
156
     *
157
     * @param mixed $service
158
     * @return bool
159
     */
160 31
    private function isConcrete($service): bool
161
    {
162 31
        return is_object($service) && !$service instanceof Closure;
163
    }
164
165
    /**
166
     * Verifies that provided callable can be called by service container.
167
     *
168
     * @param Invokable $reflectedCallable
169
     * @return bool
170
     */
171 12
    private function isResolvableCallable(Invokable $reflectedCallable): bool
172
    {
173
        // If array represents callable we need to be sure it's an object or a resolvable service id.
174 12
        $callable = $reflectedCallable->callable();
175
176 12
        return $reflectedCallable->isFunction()
177 6
               || is_object($callable[0])
178 12
               || $this->isResolvableService($callable[0]);
179
    }
180
181
    /**
182
     * Registers binding.
183
     * After this method call binding can be resolved by container.
184
     *
185
     * @param string $id
186
     * @param bool $shared
187
     * @param Closure $registrationCallback
188
     * @return void
189
     */
190 31
    private function register(string $id, bool $shared, Closure $registrationCallback)
191
    {
192
        // Check if correct service is provided.
193 31
        $this->validateId($id);
194 31
        $id = $this->normalize($id);
195
196
        // Clean up previous bindings, if any.
197 31
        unset($this->instances[$id], $this->shared[$id], $this->keys[$id]);
198
199
        // Register service with provided callback.
200 31
        $registrationCallback($id);
201
202
        // Mark service as shared when needed.
203 31
        $this->shared[$id] = $shared ?: null;
204
205
        // Save service key to make it recognizable by container.
206 31
        $this->keys[$id] = true;
207 31
    }
208
209
    /**
210
     * Validate service identifier. Throw an Exception in case of invalid value.
211
     *
212
     * @param string $id
213
     * @return void
214
     * @throws InvalidArgumentException
215
     */
216 31
    private function validateId(string $id)
217
    {
218 31
        if (!interface_exists($id) && !class_exists($id)) {
219 1
            throw new InvalidArgumentException(
220 1
                sprintf('Invalid service id "%s". Service id must be an existing interface or class name.', $id)
221
            );
222
        }
223 31
    }
224
225
}