Merge remote-tracking branch 'upstream/4.6' into 4.6-master-2015-07-19-17-34-09
[civicrm-core.git] / Civi / Core / Resolver.php
1 <?php
2 namespace Civi\Core;
3
4 /**
5 * The resolver takes a string expression and returns an object or callable.
6 *
7 * The following patterns will resolve to objects:
8 * - 'obj://objectName' - An object from Civi\Core\Container
9 * - 'ClassName' - An instance of ClassName (with default constructor).
10 * If you need more control over construction, then register with the
11 * container.
12 *
13 * The following patterns will resolve to callables:
14 * - 'function_name' - A function(callable).
15 * - 'ClassName::methodName" - A static method of a class.
16 * - 'call://objectName/method' - A method on an object from Civi\Core\Container.
17 * - 'api3://EntityName/action' - A method call on an API.
18 * (Performance note: Requires full setup/teardown of API subsystem.)
19 * - 'api3://EntityName/action?first=@1&second=@2' - Call an API method, mapping the
20 * first & second args to named parameters.
21 * (Performance note: Requires parsing/interpolating arguments).
22 * - '0' or '1' - A dummy which returns the constant '0' or '1'.
23 *
24 * Note: To differentiate classes and functions, there is a hard requirement that
25 * class names begin with an uppercase letter.
26 *
27 * Note: If you are working in a context which requires a callable, it is legitimate to use
28 * an object notation ("obj://objectName" or "ClassName") if the object supports __invoke().
29 *
30 * @package Civi\Core
31 */
32 class Resolver {
33
34 protected static $_singleton;
35
36 /**
37 * Singleton function.
38 *
39 * @return Resolver
40 */
41 public static function singleton() {
42 if (self::$_singleton === NULL) {
43 self::$_singleton = new Resolver();
44 }
45 return self::$_singleton;
46 }
47
48 /**
49 * Convert a callback expression to a valid PHP callback.
50 *
51 * @param string|array $id
52 * A callback expression; any of the following.
53 *
54 * @return array
55 * A PHP callback. Do not serialize (b/c it may include an object).
56 * @throws \RuntimeException
57 */
58 public function get($id) {
59 if (!is_string($id)) {
60 // An array or object does not need to be further resolved.
61 return $id;
62 }
63
64 if (strpos($id, '::') !== FALSE) {
65 // Callback: Static method.
66 return explode('::', $id);
67 }
68 elseif (strpos($id, '://') !== FALSE) {
69 $url = parse_url($id);
70 switch ($url['scheme']) {
71 case 'obj':
72 // Object: Lookup in container.
73 return Container::singleton()->get($url['host']);
74
75 case 'call':
76 // Callback: Object/method in container.
77 $obj = Container::singleton()->get($url['host']);
78 return array($obj, ltrim($url['path'], '/'));
79
80 case 'api3':
81 // Callback: API.
82 return new ResolverApi($url);
83
84 default:
85 throw new \RuntimeException("Unsupported callback scheme: " . $url['scheme']);
86 }
87 }
88 elseif (in_array($id, array('0', '1'))) {
89 // Callback: Constant value.
90 return new ResolverConstantCallback((int) $id);
91 }
92 elseif ($id{0} >= 'A' && $id{0} <= 'Z') {
93 // Object: New/default instance.
94 return new $id();
95 }
96 else {
97 // Callback: Function.
98 return $id;
99 }
100 }
101
102 /**
103 * Invoke a callback expression.
104 *
105 * @param string|callable $id
106 * @param array $args
107 * Ordered parameters. To call-by-reference, set an array-parameter by reference.
108 *
109 * @return mixed
110 */
111 public function call($id, $args) {
112 $cb = $this->get($id);
113 return $cb ? call_user_func_array($cb, $args) : NULL;
114 }
115
116 }
117
118 /**
119 * Private helper which produces a dummy callback.
120 *
121 * @package Civi\Core
122 */
123 class ResolverConstantCallback {
124 /**
125 * @var mixed
126 */
127 private $value;
128
129 /**
130 * Class constructor.
131 *
132 * @param mixed $value
133 * The value to be returned by the dummy callback.
134 */
135 public function __construct($value) {
136 $this->value = $value;
137 }
138
139 /**
140 * Invoke function.
141 *
142 * @return mixed
143 */
144 public function __invoke() {
145 return $this->value;
146 }
147
148 }
149
150 /**
151 * Private helper which treats an API as a callable function.
152 *
153 * @package Civi\Core
154 */
155 class ResolverApi {
156 /**
157 * @var array
158 * - string scheme
159 * - string host
160 * - string path
161 * - string query (optional)
162 */
163 private $url;
164
165 /**
166 * Class constructor.
167 *
168 * @param array $url
169 * Parsed URL (e.g. "api3://EntityName/action?foo=bar").
170 *
171 * @see parse_url
172 */
173 public function __construct($url) {
174 $this->url = $url;
175 }
176
177 /**
178 * Fire an API call.
179 */
180 public function __invoke() {
181 $apiParams = array();
182 if (isset($this->url['query'])) {
183 parse_str($this->url['query'], $apiParams);
184 }
185
186 if (count($apiParams)) {
187 $args = func_get_args();
188 if (count($args)) {
189 $this->interpolate($apiParams, $this->createPlaceholders('@', $args));
190 }
191 }
192
193 $result = civicrm_api3($this->url['host'], ltrim($this->url['path'], '/'), $apiParams);
194 return isset($result['values']) ? $result['values'] : NULL;
195 }
196
197 /**
198 * Create placeholders.
199 *
200 * @param string $prefix
201 * @param array $args
202 * Positional arguments.
203 *
204 * @return array
205 * Named placeholders based on the positional arguments
206 * (e.g. "@1" => "firstValue").
207 */
208 protected function createPlaceholders($prefix, $args) {
209 $result = array();
210 foreach ($args as $offset => $arg) {
211 $result[$prefix . (1 + $offset)] = $arg;
212 }
213 return $result;
214 }
215
216 /**
217 * Recursively interpolate values.
218 *
219 * @code
220 * $params = array('foo' => '@1');
221 * $this->interpolate($params, array('@1'=> $object))
222 * assert $data['foo'] == $object;
223 * @endcode
224 *
225 * @param array $array
226 * Array which may or many not contain a mix of tokens.
227 * @param array $replacements
228 * A list of tokens to substitute.
229 */
230 protected function interpolate(&$array, $replacements) {
231 foreach (array_keys($array) as $key) {
232 if (is_array($array[$key])) {
233 $this->interpolate($array[$key], $replacements);
234 continue;
235 }
236 foreach ($replacements as $oldVal => $newVal) {
237 if ($array[$key] === $oldVal) {
238 $array[$key] = $newVal;
239 }
240 }
241 }
242 }
243
244 }