5 * The resolver takes a string expression and returns an object or callable.
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
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 * - '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.
24 * - '0' or '1' - A dummy which returns the constant '0' or '1'.
26 * Note: To differentiate classes and functions, there is a hard requirement that
27 * class names begin with an uppercase letter.
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().
36 protected static $_singleton;
43 public static function singleton() {
44 if (self
::$_singleton === NULL) {
45 self
::$_singleton = new Resolver();
47 return self
::$_singleton;
51 * Convert a callback expression to a valid PHP callback.
53 * @param string|array $id
54 * A callback expression; any of the following.
56 * @return array|callable
57 * A PHP callback. Do not serialize (b/c it may include an object).
58 * @throws \RuntimeException
60 public function get($id) {
61 if (!is_string($id)) {
62 // An array or object does not need to be further resolved.
66 if (strpos($id, '::') !== FALSE) {
67 // Callback: Static method.
68 return explode('::', $id);
70 elseif (strpos($id, '://') !== FALSE) {
71 $url = parse_url($id);
72 switch ($url['scheme']) {
74 // Object: Lookup in container.
75 return \Civi
::service($url['host']);
78 // Callback: Object/method in container.
79 $obj = \Civi
::service($url['host']);
80 return [$obj, ltrim($url['path'], '/')];
84 return new ResolverApi($url);
87 // Lookup in a global variable.
88 return new ResolverGlobalCallback($url['query'], $url['host'] . (isset($url['path']) ?
rtrim($url['path'], '/') : ''));
91 throw new \
RuntimeException("Unsupported callback scheme: " . $url['scheme']);
94 elseif (in_array($id, ['0', '1'])) {
95 // Callback: Constant value.
96 return new ResolverConstantCallback((int) $id);
98 elseif ($id[0] >= 'A' && $id[0] <= 'Z') {
99 // Object: New/default instance.
103 // Callback: Function.
109 * Invoke a callback expression.
111 * @param string|callable $id
113 * Ordered parameters. To call-by-reference, set an array-parameter by reference.
117 public function call($id, $args) {
118 $cb = $this->get($id);
119 return $cb ?
call_user_func_array($cb, $args) : NULL;
125 * Private helper which produces a dummy callback.
129 class ResolverConstantCallback
{
138 * @param mixed $value
139 * The value to be returned by the dummy callback.
141 public function __construct($value) {
142 $this->value
= $value;
150 public function __invoke() {
157 * Private helper which treats an API as a callable function.
167 * - string query (optional)
175 * Parsed URL (e.g. "api3://EntityName/action?foo=bar").
179 public function __construct($url) {
186 public function __invoke() {
188 if (isset($this->url
['query'])) {
189 parse_str($this->url
['query'], $apiParams);
192 if (count($apiParams)) {
193 $args = func_get_args();
195 $this->interpolate($apiParams, $this->createPlaceholders('@', $args));
199 $result = civicrm_api3($this->url
['host'], ltrim($this->url
['path'], '/'), $apiParams);
200 return $result['values'] ??
NULL;
204 * Create placeholders.
206 * @param string $prefix
208 * Positional arguments.
211 * Named placeholders based on the positional arguments
212 * (e.g. "@1" => "firstValue").
214 protected function createPlaceholders($prefix, $args) {
216 foreach ($args as $offset => $arg) {
217 $result[$prefix . (1 +
$offset)] = $arg;
223 * Recursively interpolate values.
226 * $params = array('foo' => '@1');
227 * $this->interpolate($params, array('@1'=> $object))
228 * assert $data['foo'] == $object;
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.
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);
242 foreach ($replacements as $oldVal => $newVal) {
243 if ($array[$key] === $oldVal) {
244 $array[$key] = $newVal;
252 class ResolverGlobalCallback
{
259 * @param string $mode
260 * 'getter' or 'setter'.
261 * @param string $path
263 public function __construct($mode, $path) {
275 public function __invoke($arg1 = NULL) {
276 if ($this->mode
=== 'getter') {
277 return \CRM_Utils_Array
::pathGet($GLOBALS, explode('/', $this->path
));
279 elseif ($this->mode
=== 'setter') {
280 \CRM_Utils_Array
::pathSet($GLOBALS, explode('/', $this->path
), $arg1);
284 throw new \
RuntimeException("Resolver failed: global:// must specify getter or setter mode.");