Completed
Push — 3.1 ( d59679...4b8741 )
by Jeroen
62:38 queued 13s
created

engine/classes/Elgg/Di/DiContainer.php (1 issue)

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
Private method name "DiContainer::build" must be prefixed with an underscore
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