NFC - Short array syntax - auto-convert Civi dir
[civicrm-core.git] / Civi / API / Provider / MagicFunctionProvider.php
CommitLineData
787604ff
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
787604ff 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
787604ff
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
d25dd0ee 26 */
787604ff
TO
27
28namespace Civi\API\Provider;
8882ff5c 29
787604ff
TO
30use Civi\API\Events;
31use Symfony\Component\EventDispatcher\EventSubscriberInterface;
32
33/**
34 * This class manages the loading of API's using strict file+function naming
35 * conventions.
36 */
37class MagicFunctionProvider implements EventSubscriberInterface, ProviderInterface {
6550386a
EM
38 /**
39 * @return array
40 */
787604ff 41 public static function getSubscribedEvents() {
c64f69d9
CW
42 return [
43 Events::RESOLVE => [
44 ['onApiResolve', Events::W_MIDDLE],
45 ],
46 ];
787604ff
TO
47 }
48
c65db512
TO
49 /**
50 * @var array (string $cachekey => array('function' => string, 'is_generic' => bool))
51 */
52 private $cache;
53
6550386a 54 /**
6550386a 55 */
8882ff5c 56 public function __construct() {
c64f69d9 57 $this->cache = [];
c65db512
TO
58 }
59
6550386a
EM
60 /**
61 * @param \Civi\API\Event\ResolveEvent $event
8882ff5c 62 * API resolution event.
6550386a 63 */
787604ff
TO
64 public function onApiResolve(\Civi\API\Event\ResolveEvent $event) {
65 $apiRequest = $event->getApiRequest();
c65db512 66 $resolved = $this->resolve($apiRequest);
787604ff
TO
67 if ($resolved['function']) {
68 $apiRequest += $resolved;
69 $event->setApiRequest($apiRequest);
70 $event->setApiProvider($this);
71 $event->stopPropagation();
72 }
73 }
74
82376c19 75 /**
3dde2c6c 76 * @inheritDoc
257e7666
EM
77 * @param array $apiRequest
78 * @return array
82376c19 79 */
787604ff
TO
80 public function invoke($apiRequest) {
81 $function = $apiRequest['function'];
82 if ($apiRequest['function'] && $apiRequest['is_generic']) {
83 // Unlike normal API implementations, generic implementations require explicit
84 // knowledge of the entity and action (as well as $params). Bundle up these bits
85 // into a convenient data structure.
d6f190fb 86 if ($apiRequest['action'] === 'getsingle') {
87 // strip any api nested parts here as otherwise chaining may happen twice
88 // see https://lab.civicrm.org/dev/core/issues/643
89 // testCreateBAODefaults fails without this.
90 foreach ($apiRequest['params'] as $key => $param) {
91 if ($key !== 'api.has_parent' && substr($key, 0, 4) === 'api.' || substr($key, 0, 4) === 'api_') {
92 unset($apiRequest['params'][$key]);
93 }
94 }
95 }
787604ff 96 $result = $function($apiRequest);
d6f190fb 97
787604ff
TO
98 }
99 elseif ($apiRequest['function'] && !$apiRequest['is_generic']) {
17617b96 100 $result = $function($apiRequest['params']);
787604ff
TO
101 }
102 return $result;
103 }
104
82376c19 105 /**
3dde2c6c 106 * @inheritDoc
257e7666
EM
107 * @param int $version
108 * @return array
82376c19 109 */
8882ff5c 110 public function getEntityNames($version) {
c64f69d9 111 $entities = [];
82376c19
TO
112 $include_dirs = array_unique(explode(PATH_SEPARATOR, get_include_path()));
113 #$include_dirs = array(dirname(__FILE__). '/../../');
114 foreach ($include_dirs as $include_dir) {
8882ff5c 115 $api_dir = implode(DIRECTORY_SEPARATOR,
c64f69d9 116 [$include_dir, 'api', 'v' . $version]);
8882ff5c 117 if (!is_dir($api_dir)) {
82376c19
TO
118 continue;
119 }
120 $iterator = new \DirectoryIterator($api_dir);
121 foreach ($iterator as $fileinfo) {
122 $file = $fileinfo->getFilename();
123
124 // Check for entities with a master file ("api/v3/MyEntity.php")
125 $parts = explode(".", $file);
8882ff5c 126 if (end($parts) == "php" && $file != "utils.php" && !preg_match('/Tests?.php$/', $file)) {
82376c19
TO
127 // without the ".php"
128 $entities[] = substr($file, 0, -4);
129 }
130
8882ff5c 131 // Check for entities with standalone action files (eg "api/v3/MyEntity/MyAction.php").
82376c19
TO
132 $action_dir = $api_dir . DIRECTORY_SEPARATOR . $file;
133 if (preg_match('/^[A-Z][A-Za-z0-9]*$/', $file) && is_dir($action_dir)) {
134 if (count(glob("$action_dir/[A-Z]*.php")) > 0) {
135 $entities[] = $file;
136 }
137 }
138 }
139 }
c64f69d9 140 $entities = array_diff($entities, ['Generic']);
82376c19
TO
141 $entities = array_unique($entities);
142 sort($entities);
143
144 return $entities;
145 }
146
147 /**
3dde2c6c 148 * @inheritDoc
257e7666
EM
149 * @param int $version
150 * @param string $entity
151 * @return array
82376c19
TO
152 */
153 public function getActionNames($version, $entity) {
92776611 154 $entity = _civicrm_api_get_camel_name($entity);
82376c19
TO
155 $entities = $this->getEntityNames($version);
156 if (!in_array($entity, $entities)) {
c64f69d9 157 return [];
c65db512
TO
158 }
159 $this->loadEntity($entity, $version);
160
161 $functions = get_defined_functions();
c64f69d9 162 $actions = [];
6cbff284 163 $prefix = 'civicrm_api' . $version . '_' . _civicrm_api_get_entity_name_from_camel($entity) . '_';
c65db512
TO
164 $prefixGeneric = 'civicrm_api' . $version . '_generic_';
165 foreach ($functions['user'] as $fct) {
166 if (strpos($fct, $prefix) === 0) {
167 $actions[] = substr($fct, strlen($prefix));
168 }
169 elseif (strpos($fct, $prefixGeneric) === 0) {
170 $actions[] = substr($fct, strlen($prefixGeneric));
171 }
172 }
173 return $actions;
174 }
175
176 /**
fe482240 177 * Look up the implementation for a given API request.
c65db512 178 *
8882ff5c
TO
179 * @param array $apiRequest
180 * Array with keys:
181 * - entity: string, required.
182 * - action: string, required.
183 * - params: array.
184 * - version: scalar, required.
c65db512 185 *
8882ff5c
TO
186 * @return array
187 * Array with keys:
188 * - function: callback (mixed)
189 * - is_generic: boolean
c65db512
TO
190 */
191 protected function resolve($apiRequest) {
192 $cachekey = strtolower($apiRequest['entity']) . ':' . strtolower($apiRequest['action']) . ':' . $apiRequest['version'];
193 if (isset($this->cache[$cachekey])) {
194 return $this->cache[$cachekey];
195 }
196
197 $camelName = _civicrm_api_get_camel_name($apiRequest['entity'], $apiRequest['version']);
198 $actionCamelName = _civicrm_api_get_camel_name($apiRequest['action']);
199
200 // Determine if there is an entity-specific implementation of the action
201 $stdFunction = $this->getFunctionName($apiRequest['entity'], $apiRequest['action'], $apiRequest['version']);
202 if (function_exists($stdFunction)) {
203 // someone already loaded the appropriate file
8882ff5c
TO
204 // FIXME: This has the affect of masking bugs in load order; this is
205 // included to provide bug-compatibility.
c64f69d9 206 $this->cache[$cachekey] = ['function' => $stdFunction, 'is_generic' => FALSE];
c65db512
TO
207 return $this->cache[$cachekey];
208 }
209
c64f69d9 210 $stdFiles = [
8882ff5c
TO
211 // By convention, the $camelName.php is more likely to contain the
212 // function, so test it first
c65db512
TO
213 'api/v' . $apiRequest['version'] . '/' . $camelName . '.php',
214 'api/v' . $apiRequest['version'] . '/' . $camelName . '/' . $actionCamelName . '.php',
c64f69d9 215 ];
c65db512
TO
216 foreach ($stdFiles as $stdFile) {
217 if (\CRM_Utils_File::isIncludable($stdFile)) {
218 require_once $stdFile;
219 if (function_exists($stdFunction)) {
c64f69d9 220 $this->cache[$cachekey] = ['function' => $stdFunction, 'is_generic' => FALSE];
c65db512
TO
221 return $this->cache[$cachekey];
222 }
223 }
224 }
225
226 // Determine if there is a generic implementation of the action
227 require_once 'api/v3/Generic.php';
228 # $genericFunction = 'civicrm_api3_generic_' . $apiRequest['action'];
229 $genericFunction = $this->getFunctionName('generic', $apiRequest['action'], $apiRequest['version']);
c64f69d9 230 $genericFiles = [
8882ff5c
TO
231 // By convention, the Generic.php is more likely to contain the
232 // function, so test it first
c65db512
TO
233 'api/v' . $apiRequest['version'] . '/Generic.php',
234 'api/v' . $apiRequest['version'] . '/Generic/' . $actionCamelName . '.php',
c64f69d9 235 ];
c65db512
TO
236 foreach ($genericFiles as $genericFile) {
237 if (\CRM_Utils_File::isIncludable($genericFile)) {
238 require_once $genericFile;
239 if (function_exists($genericFunction)) {
c64f69d9 240 $this->cache[$cachekey] = ['function' => $genericFunction, 'is_generic' => TRUE];
c65db512
TO
241 return $this->cache[$cachekey];
242 }
243 }
244 }
245
c64f69d9 246 $this->cache[$cachekey] = ['function' => FALSE, 'is_generic' => FALSE];
c65db512
TO
247 return $this->cache[$cachekey];
248 }
249
250 /**
8882ff5c
TO
251 * Determine the function name for a given API request.
252 *
c65db512 253 * @param string $entity
8882ff5c 254 * API entity name.
c65db512 255 * @param string $action
8882ff5c
TO
256 * API action name.
257 * @param int $version
258 * API version.
2a6da8d7 259 *
c65db512
TO
260 * @return string
261 */
262 protected function getFunctionName($entity, $action, $version) {
263 $entity = _civicrm_api_get_entity_name_from_camel($entity);
264 return 'civicrm_api' . $version . '_' . $entity . '_' . $action;
265 }
266
267 /**
268 * Load/require all files related to an entity.
269 *
270 * This should not normally be called because it's does a file-system scan; it's
271 * only appropriate when introspection is really required (eg for "getActions").
272 *
273 * @param string $entity
8882ff5c 274 * API entity name.
c65db512 275 * @param int $version
8882ff5c 276 * API version.
c65db512
TO
277 */
278 protected function loadEntity($entity, $version) {
279 $camelName = _civicrm_api_get_camel_name($entity, $version);
280
281 // Check for master entity file; to match _civicrm_api_resolve(), only load the first one
282 $stdFile = 'api/v' . $version . '/' . $camelName . '.php';
283 if (\CRM_Utils_File::isIncludable($stdFile)) {
284 require_once $stdFile;
285 }
286
287 // Check for standalone action files; to match _civicrm_api_resolve(), only load the first one
c64f69d9 288 $loaded_files = []; // array($relativeFilePath => TRUE)
c65db512
TO
289 $include_dirs = array_unique(explode(PATH_SEPARATOR, get_include_path()));
290 foreach ($include_dirs as $include_dir) {
c64f69d9 291 foreach ([$camelName, 'Generic'] as $name) {
8882ff5c 292 $action_dir = implode(DIRECTORY_SEPARATOR,
c64f69d9 293 [$include_dir, 'api', "v${version}", $name]);
07007b7a
CW
294 if (!is_dir($action_dir)) {
295 continue;
c65db512
TO
296 }
297
07007b7a
CW
298 $iterator = new \DirectoryIterator($action_dir);
299 foreach ($iterator as $fileinfo) {
300 $file = $fileinfo->getFilename();
301 if (array_key_exists($file, $loaded_files)) {
302 continue; // action provided by an earlier item on include_path
303 }
304
305 $parts = explode(".", $file);
306 if (end($parts) == "php" && !preg_match('/Tests?\.php$/', $file)) {
307 require_once $action_dir . DIRECTORY_SEPARATOR . $file;
308 $loaded_files[$file] = TRUE;
309 }
c65db512
TO
310 }
311 }
312 }
313 }
314
2a6da8d7 315}