1 | <?php |
||
2 | |||
3 | namespace Elgg\Di; |
||
4 | |||
5 | /** |
||
6 | * Container holding values which can be resolved upon reading and optionally stored and shared |
||
7 | * across reads. |
||
8 | * |
||
9 | * <code> |
||
10 | * $c = new \Elgg\Di\DiContainer(); |
||
11 | * |
||
12 | * $c->setFactory('foo', 'Foo_factory'); // $c will be passed to Foo_factory() |
||
13 | * $c->foo; // new Foo instance |
||
14 | * $c->foo; // same instance |
||
15 | * |
||
16 | * $c->setFactory('bar', 'Bar_factory', false); // non-shared |
||
17 | * $c->bar; // new Bar instance |
||
18 | * $c->bar; // different Bar instance |
||
19 | * |
||
20 | * $c->setValue('a_string', 'foo_factory'); // don't call this |
||
21 | * $c->a_string; // 'foo_factory' |
||
22 | * </code> |
||
23 | * |
||
24 | * @internal |
||
25 | * |
||
26 | * @package Elgg.Core |
||
27 | * @since 1.9 |
||
28 | */ |
||
29 | class DiContainer { |
||
30 | |||
31 | /** |
||
32 | * @var array each element is an array: ['callable' => mixed $factory, 'shared' => bool $isShared] |
||
33 | */ |
||
34 | private $factories_ = []; |
||
35 | |||
36 | const CLASS_NAME_PATTERN_53 = '/^(\\\\?[a-z_\x7f-\xff][a-z0-9_\x7f-\xff]*)+$/i'; |
||
37 | |||
38 | /** |
||
39 | * Fetch a value. |
||
40 | * |
||
41 | * @param string $name The name of the value to fetch |
||
42 | * @return mixed |
||
43 | * @throws \Elgg\Di\MissingValueException |
||
44 | */ |
||
45 | 5015 | public function __get($name) { |
|
46 | 5015 | if (!isset($this->factories_[$name])) { |
|
47 | 3 | throw new \Elgg\Di\MissingValueException("Value or factory was not set for: $name"); |
|
48 | } |
||
49 | 5015 | $value = $this->build($this->factories_[$name]['callable'], $name); |
|
50 | |||
51 | // Why check existence of factory here? A: the builder function may have set the value |
||
52 | // directly, in which case the factory will no longer exist. |
||
53 | 5015 | if (!empty($this->factories_[$name]) && $this->factories_[$name]['shared']) { |
|
54 | 5015 | $this->{$name} = $value; |
|
55 | } |
||
56 | 5015 | return $value; |
|
57 | } |
||
58 | |||
59 | /** |
||
60 | * Build a value |
||
61 | * |
||
62 | * @param mixed $factory The factory for the value |
||
63 | * @param string $name The name of the value |
||
64 | * @return mixed |
||
65 | * @throws \Elgg\Di\FactoryUncallableException |
||
66 | */ |
||
67 | 5015 | private function build($factory, $name) { |
|
0 ignored issues
–
show
Coding Style
introduced
by
Loading history...
|
|||
68 | 5015 | if (is_callable($factory)) { |
|
69 | 5015 | return call_user_func($factory, $this); |
|
70 | } |
||
71 | 3 | $msg = "Factory for '$name' was uncallable"; |
|
72 | 3 | if (is_string($factory)) { |
|
73 | 1 | $msg .= ": '$factory'"; |
|
74 | 2 | } elseif (is_array($factory)) { |
|
75 | 2 | if (is_string($factory[0])) { |
|
76 | 1 | $msg .= ": '{$factory[0]}::{$factory[1]}'"; |
|
77 | } else { |
||
78 | 1 | $msg .= ": " . get_class($factory[0]) . "->{$factory[1]}"; |
|
79 | } |
||
80 | } |
||
81 | 3 | throw new \Elgg\Di\FactoryUncallableException($msg); |
|
82 | } |
||
83 | |||
84 | /** |
||
85 | * Set a value to be returned without modification |
||
86 | * |
||
87 | * @param string $name The name of the value |
||
88 | * @param mixed $value The value |
||
89 | * @return \Elgg\Di\DiContainer |
||
90 | * @throws \InvalidArgumentException |
||
91 | */ |
||
92 | 5515 | public function setValue($name, $value) { |
|
93 | 5515 | if (substr($name, -1) === '_') { |
|
94 | 1 | throw new \InvalidArgumentException('$name cannot end with "_"'); |
|
95 | } |
||
96 | 5515 | $this->{$name} = $value; |
|
97 | 5515 | return $this; |
|
98 | } |
||
99 | |||
100 | /** |
||
101 | * Remove previously built service, so that it's rebuld from factory on next call |
||
102 | * |
||
103 | * @param string $name Name |
||
104 | * @return void |
||
105 | */ |
||
106 | 2 | public function reset($name) { |
|
107 | 2 | $this->{$name} = null; |
|
108 | 2 | unset($this->{$name}); |
|
109 | 2 | } |
|
110 | |||
111 | /** |
||
112 | * Set a factory to generate a value when the container is read. |
||
113 | * |
||
114 | * @param string $name The name of the value |
||
115 | * @param callable $callable Factory for the value |
||
116 | * @param bool $shared Whether the same value should be returned for every request |
||
117 | * @return \Elgg\Di\DiContainer |
||
118 | * @throws \InvalidArgumentException |
||
119 | */ |
||
120 | 4999 | public function setFactory($name, $callable, $shared = true) { |
|
121 | 4999 | if (substr($name, -1) === '_') { |
|
122 | 1 | throw new \InvalidArgumentException('$name cannot end with "_"'); |
|
123 | } |
||
124 | 4999 | if (!is_callable($callable, true)) { |
|
125 | 1 | throw new \InvalidArgumentException('$factory must appear callable'); |
|
126 | } |
||
127 | 4999 | $this->remove($name); |
|
128 | 4999 | $this->factories_[$name] = [ |
|
129 | 4999 | 'callable' => $callable, |
|
130 | 4999 | 'shared' => $shared, |
|
131 | ]; |
||
132 | 4999 | return $this; |
|
133 | } |
||
134 | |||
135 | /** |
||
136 | * Set a factory based on instantiating a class with no arguments. |
||
137 | * |
||
138 | * @param string $name Name of the value |
||
139 | * @param string $class_name Class name to be instantiated |
||
140 | * @param bool $shared Whether the same value should be returned for every request |
||
141 | * @return \Elgg\Di\DiContainer |
||
142 | * @throws \InvalidArgumentException |
||
143 | */ |
||
144 | 5004 | public function setClassName($name, $class_name, $shared = true) { |
|
145 | 4999 | if (substr($name, -1) === '_') { |
|
146 | throw new \InvalidArgumentException('$name cannot end with "_"'); |
||
147 | } |
||
148 | 4999 | $classname_pattern = self::CLASS_NAME_PATTERN_53; |
|
149 | 4999 | if (!is_string($class_name) || !preg_match($classname_pattern, $class_name)) { |
|
150 | 2 | throw new \InvalidArgumentException('Class names must be valid PHP class names'); |
|
151 | } |
||
152 | 5004 | $func = function () use ($class_name) { |
|
153 | 5004 | return new $class_name(); |
|
154 | 4999 | }; |
|
155 | 4999 | return $this->setFactory($name, $func, $shared); |
|
156 | } |
||
157 | |||
158 | /** |
||
159 | * Remove a value from the container |
||
160 | * |
||
161 | * @param string $name The name of the value |
||
162 | * @return \Elgg\Di\DiContainer |
||
163 | */ |
||
164 | 4999 | public function remove($name) { |
|
165 | 4999 | if (substr($name, -1) === '_') { |
|
166 | 1 | throw new \InvalidArgumentException('$name cannot end with "_"'); |
|
167 | } |
||
168 | 4999 | unset($this->{$name}); |
|
169 | 4999 | unset($this->factories_[$name]); |
|
170 | 4999 | return $this; |
|
171 | } |
||
172 | |||
173 | /** |
||
174 | * Does the container have this value |
||
175 | * |
||
176 | * @param string $name The name of the value |
||
177 | * @return bool |
||
178 | */ |
||
179 | 6 | public function has($name) { |
|
180 | 6 | if (isset($this->factories_[$name])) { |
|
181 | 2 | return true; |
|
182 | } |
||
183 | 4 | if (substr($name, -1) === '_') { |
|
184 | 1 | return false; |
|
185 | } |
||
186 | 3 | return (bool) property_exists($this, $name); |
|
187 | } |
||
188 | |||
189 | /** |
||
190 | * Get names for all values/factories |
||
191 | * |
||
192 | * @return string[] |
||
193 | * @internal For unit testing only, do not use |
||
194 | */ |
||
195 | 1 | public function getNames() { |
|
196 | 1 | $names = []; |
|
197 | |||
198 | 1 | $refl = new \ReflectionObject($this); |
|
199 | 1 | foreach ($refl->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) { |
|
200 | 1 | $names[] = $prop->name; |
|
201 | } |
||
202 | 1 | foreach (array_keys($this->factories_) as $name) { |
|
203 | 1 | $names[] = $name; |
|
204 | } |
||
205 | |||
206 | 1 | sort($names); |
|
207 | 1 | return $names; |
|
208 | } |
||
209 | } |
||
210 |