Merge pull request #19165 from eileenmcnaughton/pdf
[civicrm-core.git] / Civi / Core / Resolver.php
CommitLineData
c8074a93
TO
1<?php
2namespace 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 */
34class 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 */
129class 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 */
161class 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
252class 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}