commiting uncommited changes on live site
[weblabels.fsf.org.git] / crm.fsf.org / 20131203 / files / sites / all / modules-new / civicrm / 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 * - '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'.
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 /**
39 * Singleton function.
40 *
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.
55 *
56 * @return array
57 * A PHP callback. Do not serialize (b/c it may include an object).
58 * @throws \RuntimeException
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.
75 return Container::singleton()->get($url['host']);
76
77 case 'call':
78 // Callback: Object/method in container.
79 $obj = Container::singleton()->get($url['host']);
80 return array($obj, ltrim($url['path'], '/'));
81
82 case 'api3':
83 // Callback: API.
84 return new ResolverApi($url);
85
86 case 'global':
87 // Lookup in a global variable.
88 return new ResolverGlobalCallback($url['query'], $url['host'] . (isset($url['path']) ? rtrim($url['path'], '/') : ''));
89
90 default:
91 throw new \RuntimeException("Unsupported callback scheme: " . $url['scheme']);
92 }
93 }
94 elseif (in_array($id, array('0', '1'))) {
95 // Callback: Constant value.
96 return new ResolverConstantCallback((int) $id);
97 }
98 elseif ($id{0} >= 'A' && $id{0} <= 'Z') {
99 // Object: New/default instance.
100 return new $id();
101 }
102 else {
103 // Callback: Function.
104 return $id;
105 }
106 }
107
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.
114 *
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
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 /**
136 * Class constructor.
137 *
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 /**
146 * Invoke function.
147 *
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 /**
172 * Class constructor.
173 *
174 * @param array $url
175 * Parsed URL (e.g. "api3://EntityName/action?foo=bar").
176 *
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() {
187 $apiParams = array();
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);
200 return isset($result['values']) ? $result['values'] : NULL;
201 }
202
203 /**
204 * Create placeholders.
205 *
206 * @param string $prefix
207 * @param array $args
208 * Positional arguments.
209 *
210 * @return array
211 * Named placeholders based on the positional arguments
212 * (e.g. "@1" => "firstValue").
213 */
214 protected function createPlaceholders($prefix, $args) {
215 $result = array();
216 foreach ($args as $offset => $arg) {
217 $result[$prefix . (1 + $offset)] = $arg;
218 }
219 return $result;
220 }
221
222 /**
223 * Recursively interpolate values.
224 *
225 * @code
226 * $params = array('foo' => '@1');
227 * $this->interpolate($params, array('@1'=> $object))
228 * assert $data['foo'] == $object;
229 * @endcode
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 }
251
252 class ResolverGlobalCallback {
253 private $mode, $path;
254
255 /**
256 * Class constructor.
257 *
258 * @param string $mode
259 * 'getter' or 'setter'.
260 * @param string $path
261 */
262 public function __construct($mode, $path) {
263 $this->mode = $mode;
264 $this->path = $path;
265 }
266
267 /**
268 * Invoke function.
269 *
270 * @return mixed
271 */
272 public function __invoke($arg1 = NULL) {
273 if ($this->mode === 'getter') {
274 return \CRM_Utils_Array::pathGet($GLOBALS, explode('/', $this->path));
275 }
276 elseif ($this->mode === 'setter') {
277 \CRM_Utils_Array::pathSet($GLOBALS, explode('/', $this->path), $arg1);
278 }
279 else {
280 throw new \RuntimeException("Resolver failed: global:// must specify getter or setter mode.");
281 }
282 }
283
284 }