Commit | Line | Data |
---|---|---|
c8074a93 TO |
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). | |
36781411 TO |
22 | * - 'global://Variable/Key2/Key3?getter' - A dummy which looks up a global variable. |
23 | * - 'global://Variable/Key2/Key3?setter' - A dummy which updates a global variable. | |
c8074a93 TO |
24 | * - '0' or '1' - A dummy which returns the constant '0' or '1'. |
25 | * | |
26 | * Note: To differentiate classes and functions, there is a hard requirement that | |
27 | * class names begin with an uppercase letter. | |
28 | * | |
29 | * Note: If you are working in a context which requires a callable, it is legitimate to use | |
30 | * an object notation ("obj://objectName" or "ClassName") if the object supports __invoke(). | |
31 | * | |
32 | * @package Civi\Core | |
33 | */ | |
34 | class Resolver { | |
35 | ||
36 | protected static $_singleton; | |
37 | ||
38 | /** | |
c2b5a0af EM |
39 | * Singleton function. |
40 | * | |
c8074a93 TO |
41 | * @return Resolver |
42 | */ | |
43 | public static function singleton() { | |
44 | if (self::$_singleton === NULL) { | |
45 | self::$_singleton = new Resolver(); | |
46 | } | |
47 | return self::$_singleton; | |
48 | } | |
49 | ||
50 | /** | |
51 | * Convert a callback expression to a valid PHP callback. | |
52 | * | |
53 | * @param string|array $id | |
54 | * A callback expression; any of the following. | |
c2b5a0af | 55 | * |
3c250b10 | 56 | * @return array|callable |
c8074a93 | 57 | * A PHP callback. Do not serialize (b/c it may include an object). |
83af6f09 | 58 | * @throws \RuntimeException |
c8074a93 TO |
59 | */ |
60 | public function get($id) { | |
61 | if (!is_string($id)) { | |
62 | // An array or object does not need to be further resolved. | |
63 | return $id; | |
64 | } | |
65 | ||
66 | if (strpos($id, '::') !== FALSE) { | |
67 | // Callback: Static method. | |
68 | return explode('::', $id); | |
69 | } | |
70 | elseif (strpos($id, '://') !== FALSE) { | |
71 | $url = parse_url($id); | |
72 | switch ($url['scheme']) { | |
73 | case 'obj': | |
74 | // Object: Lookup in container. | |
048222df | 75 | return \Civi::service($url['host']); |
c8074a93 TO |
76 | |
77 | case 'call': | |
78 | // Callback: Object/method in container. | |
048222df | 79 | $obj = \Civi::service($url['host']); |
c64f69d9 | 80 | return [$obj, ltrim($url['path'], '/')]; |
c8074a93 TO |
81 | |
82 | case 'api3': | |
83 | // Callback: API. | |
84 | return new ResolverApi($url); | |
85 | ||
36781411 TO |
86 | case 'global': |
87 | // Lookup in a global variable. | |
88 | return new ResolverGlobalCallback($url['query'], $url['host'] . (isset($url['path']) ? rtrim($url['path'], '/') : '')); | |
89 | ||
c8074a93 TO |
90 | default: |
91 | throw new \RuntimeException("Unsupported callback scheme: " . $url['scheme']); | |
92 | } | |
93 | } | |
c64f69d9 | 94 | elseif (in_array($id, ['0', '1'])) { |
c8074a93 TO |
95 | // Callback: Constant value. |
96 | return new ResolverConstantCallback((int) $id); | |
97 | } | |
7be441d1 | 98 | elseif ($id[0] >= 'A' && $id[0] <= 'Z') { |
c8074a93 TO |
99 | // Object: New/default instance. |
100 | return new $id(); | |
101 | } | |
102 | else { | |
103 | // Callback: Function. | |
104 | return $id; | |
105 | } | |
106 | } | |
107 | ||
83af6f09 TO |
108 | /** |
109 | * Invoke a callback expression. | |
110 | * | |
111 | * @param string|callable $id | |
112 | * @param array $args | |
113 | * Ordered parameters. To call-by-reference, set an array-parameter by reference. | |
c2b5a0af | 114 | * |
83af6f09 TO |
115 | * @return mixed |
116 | */ | |
117 | public function call($id, $args) { | |
118 | $cb = $this->get($id); | |
119 | return $cb ? call_user_func_array($cb, $args) : NULL; | |
120 | } | |
121 | ||
c8074a93 TO |
122 | } |
123 | ||
124 | /** | |
125 | * Private helper which produces a dummy callback. | |
126 | * | |
127 | * @package Civi\Core | |
128 | */ | |
129 | class ResolverConstantCallback { | |
130 | /** | |
131 | * @var mixed | |
132 | */ | |
133 | private $value; | |
134 | ||
135 | /** | |
c2b5a0af EM |
136 | * Class constructor. |
137 | * | |
c8074a93 TO |
138 | * @param mixed $value |
139 | * The value to be returned by the dummy callback. | |
140 | */ | |
141 | public function __construct($value) { | |
142 | $this->value = $value; | |
143 | } | |
144 | ||
145 | /** | |
c2b5a0af EM |
146 | * Invoke function. |
147 | * | |
c8074a93 TO |
148 | * @return mixed |
149 | */ | |
150 | public function __invoke() { | |
151 | return $this->value; | |
152 | } | |
153 | ||
154 | } | |
155 | ||
156 | /** | |
157 | * Private helper which treats an API as a callable function. | |
158 | * | |
159 | * @package Civi\Core | |
160 | */ | |
161 | class ResolverApi { | |
162 | /** | |
163 | * @var array | |
164 | * - string scheme | |
165 | * - string host | |
166 | * - string path | |
167 | * - string query (optional) | |
168 | */ | |
169 | private $url; | |
170 | ||
171 | /** | |
c2b5a0af EM |
172 | * Class constructor. |
173 | * | |
c8074a93 TO |
174 | * @param array $url |
175 | * Parsed URL (e.g. "api3://EntityName/action?foo=bar"). | |
c2b5a0af | 176 | * |
c8074a93 TO |
177 | * @see parse_url |
178 | */ | |
179 | public function __construct($url) { | |
180 | $this->url = $url; | |
181 | } | |
182 | ||
183 | /** | |
184 | * Fire an API call. | |
185 | */ | |
186 | public function __invoke() { | |
c64f69d9 | 187 | $apiParams = []; |
c8074a93 TO |
188 | if (isset($this->url['query'])) { |
189 | parse_str($this->url['query'], $apiParams); | |
190 | } | |
191 | ||
192 | if (count($apiParams)) { | |
193 | $args = func_get_args(); | |
194 | if (count($args)) { | |
195 | $this->interpolate($apiParams, $this->createPlaceholders('@', $args)); | |
196 | } | |
197 | } | |
198 | ||
199 | $result = civicrm_api3($this->url['host'], ltrim($this->url['path'], '/'), $apiParams); | |
2e1f50d6 | 200 | return $result['values'] ?? NULL; |
c8074a93 TO |
201 | } |
202 | ||
203 | /** | |
67f947ac EM |
204 | * Create placeholders. |
205 | * | |
206 | * @param string $prefix | |
c8074a93 TO |
207 | * @param array $args |
208 | * Positional arguments. | |
67f947ac | 209 | * |
c8074a93 TO |
210 | * @return array |
211 | * Named placeholders based on the positional arguments | |
c2b5a0af | 212 | * (e.g. "@1" => "firstValue"). |
c8074a93 TO |
213 | */ |
214 | protected function createPlaceholders($prefix, $args) { | |
c64f69d9 | 215 | $result = []; |
c8074a93 TO |
216 | foreach ($args as $offset => $arg) { |
217 | $result[$prefix . (1 + $offset)] = $arg; | |
218 | } | |
219 | return $result; | |
220 | } | |
221 | ||
222 | /** | |
223 | * Recursively interpolate values. | |
224 | * | |
0b882a86 | 225 | * ``` |
c8074a93 TO |
226 | * $params = array('foo' => '@1'); |
227 | * $this->interpolate($params, array('@1'=> $object)) | |
228 | * assert $data['foo'] == $object; | |
0b882a86 | 229 | * ``` |
c8074a93 TO |
230 | * |
231 | * @param array $array | |
232 | * Array which may or many not contain a mix of tokens. | |
233 | * @param array $replacements | |
234 | * A list of tokens to substitute. | |
235 | */ | |
236 | protected function interpolate(&$array, $replacements) { | |
237 | foreach (array_keys($array) as $key) { | |
238 | if (is_array($array[$key])) { | |
239 | $this->interpolate($array[$key], $replacements); | |
240 | continue; | |
241 | } | |
242 | foreach ($replacements as $oldVal => $newVal) { | |
243 | if ($array[$key] === $oldVal) { | |
244 | $array[$key] = $newVal; | |
245 | } | |
246 | } | |
247 | } | |
248 | } | |
249 | ||
250 | } | |
36781411 TO |
251 | |
252 | class ResolverGlobalCallback { | |
34f3bbd9 SL |
253 | private $mode; |
254 | private $path; | |
36781411 TO |
255 | |
256 | /** | |
257 | * Class constructor. | |
258 | * | |
259 | * @param string $mode | |
260 | * 'getter' or 'setter'. | |
261 | * @param string $path | |
262 | */ | |
263 | public function __construct($mode, $path) { | |
264 | $this->mode = $mode; | |
265 | $this->path = $path; | |
266 | } | |
267 | ||
268 | /** | |
269 | * Invoke function. | |
270 | * | |
54957108 | 271 | * @param mixed $arg1 |
272 | * | |
36781411 TO |
273 | * @return mixed |
274 | */ | |
275 | public function __invoke($arg1 = NULL) { | |
276 | if ($this->mode === 'getter') { | |
277 | return \CRM_Utils_Array::pathGet($GLOBALS, explode('/', $this->path)); | |
278 | } | |
279 | elseif ($this->mode === 'setter') { | |
280 | \CRM_Utils_Array::pathSet($GLOBALS, explode('/', $this->path), $arg1); | |
3c250b10 | 281 | return NULL; |
36781411 TO |
282 | } |
283 | else { | |
284 | throw new \RuntimeException("Resolver failed: global:// must specify getter or setter mode."); | |
285 | } | |
286 | } | |
287 | ||
288 | } |