SyntaxConformanceTest - Accept some variation in capitalizaiton of errors
[civicrm-core.git] / Civi / API / Provider / MagicFunctionProvider.php
CommitLineData
787604ff
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.4 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2013 |
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 +--------------------------------------------------------------------+
26*/
27
28namespace Civi\API\Provider;
29use Civi\API\Events;
30use Symfony\Component\EventDispatcher\EventSubscriberInterface;
31
32/**
33 * This class manages the loading of API's using strict file+function naming
34 * conventions.
35 */
36class MagicFunctionProvider implements EventSubscriberInterface, ProviderInterface {
37 public static function getSubscribedEvents() {
38 return array(
39 Events::RESOLVE => array(
40 array('onApiResolve', Events::W_MIDDLE),
41 ),
42 );
43 }
44
c65db512
TO
45 /**
46 * @var array (string $cachekey => array('function' => string, 'is_generic' => bool))
47 */
48 private $cache;
49
50 function __construct() {
51 $this->cache = array();
52 }
53
787604ff
TO
54 public function onApiResolve(\Civi\API\Event\ResolveEvent $event) {
55 $apiRequest = $event->getApiRequest();
c65db512 56 $resolved = $this->resolve($apiRequest);
787604ff
TO
57 if ($resolved['function']) {
58 $apiRequest += $resolved;
59 $event->setApiRequest($apiRequest);
60 $event->setApiProvider($this);
61 $event->stopPropagation();
62 }
63 }
64
65 public function invoke($apiRequest) {
66 $function = $apiRequest['function'];
67 if ($apiRequest['function'] && $apiRequest['is_generic']) {
68 // Unlike normal API implementations, generic implementations require explicit
69 // knowledge of the entity and action (as well as $params). Bundle up these bits
70 // into a convenient data structure.
71 $result = $function($apiRequest);
72 }
73 elseif ($apiRequest['function'] && !$apiRequest['is_generic']) {
74 $result = isset($extra) ? $function($apiRequest['params'], $extra) : $function($apiRequest['params']);
75 }
76 return $result;
77 }
78
c65db512
TO
79 public function getActionNames($entity, $version) {
80 // TODO don't recurse into civicrm_api
81 $r = civicrm_api('Entity', 'Get', array('version' => $version));
82 if (!in_array($entity, $r['values'])) {
83 throw new \Civi\API\Exception\NotImplementedException("Entity " . $entity . " invalid. Use api.entity.get to have the list", array('entity' => $r['values']));
84 }
85 $this->loadEntity($entity, $version);
86
87 $functions = get_defined_functions();
88 $actions = array();
89 $prefix = 'civicrm_api' . $version . '_' . strtolower($entity) . '_';
90 $prefixGeneric = 'civicrm_api' . $version . '_generic_';
91 foreach ($functions['user'] as $fct) {
92 if (strpos($fct, $prefix) === 0) {
93 $actions[] = substr($fct, strlen($prefix));
94 }
95 elseif (strpos($fct, $prefixGeneric) === 0) {
96 $actions[] = substr($fct, strlen($prefixGeneric));
97 }
98 }
99 return $actions;
100 }
101
102 /**
103 * Look up the implementation for a given API request
104 *
105 * @param $apiRequest array with keys:
106 * - entity: string, required
107 * - action: string, required
108 * - params: array
109 * - version: scalar, required
110 *
111 * @return array with keys
112 * - function: callback (mixed)
113 * - is_generic: boolean
114 */
115 protected function resolve($apiRequest) {
116 $cachekey = strtolower($apiRequest['entity']) . ':' . strtolower($apiRequest['action']) . ':' . $apiRequest['version'];
117 if (isset($this->cache[$cachekey])) {
118 return $this->cache[$cachekey];
119 }
120
121 $camelName = _civicrm_api_get_camel_name($apiRequest['entity'], $apiRequest['version']);
122 $actionCamelName = _civicrm_api_get_camel_name($apiRequest['action']);
123
124 // Determine if there is an entity-specific implementation of the action
125 $stdFunction = $this->getFunctionName($apiRequest['entity'], $apiRequest['action'], $apiRequest['version']);
126 if (function_exists($stdFunction)) {
127 // someone already loaded the appropriate file
128 // FIXME: This has the affect of masking bugs in load order; this is included to provide bug-compatibility
129 $this->cache[$cachekey] = array('function' => $stdFunction, 'is_generic' => FALSE);
130 return $this->cache[$cachekey];
131 }
132
133 $stdFiles = array(
134 // By convention, the $camelName.php is more likely to contain the function, so test it first
135 'api/v' . $apiRequest['version'] . '/' . $camelName . '.php',
136 'api/v' . $apiRequest['version'] . '/' . $camelName . '/' . $actionCamelName . '.php',
137 );
138 foreach ($stdFiles as $stdFile) {
139 if (\CRM_Utils_File::isIncludable($stdFile)) {
140 require_once $stdFile;
141 if (function_exists($stdFunction)) {
142 $this->cache[$cachekey] = array('function' => $stdFunction, 'is_generic' => FALSE);
143 return $this->cache[$cachekey];
144 }
145 }
146 }
147
148 // Determine if there is a generic implementation of the action
149 require_once 'api/v3/Generic.php';
150 # $genericFunction = 'civicrm_api3_generic_' . $apiRequest['action'];
151 $genericFunction = $this->getFunctionName('generic', $apiRequest['action'], $apiRequest['version']);
152 $genericFiles = array(
153 // By convention, the Generic.php is more likely to contain the function, so test it first
154 'api/v' . $apiRequest['version'] . '/Generic.php',
155 'api/v' . $apiRequest['version'] . '/Generic/' . $actionCamelName . '.php',
156 );
157 foreach ($genericFiles as $genericFile) {
158 if (\CRM_Utils_File::isIncludable($genericFile)) {
159 require_once $genericFile;
160 if (function_exists($genericFunction)) {
161 $this->cache[$cachekey] = array('function' => $genericFunction, 'is_generic' => TRUE);
162 return $this->cache[$cachekey];
163 }
164 }
165 }
166
167 $this->cache[$cachekey] = array('function' => FALSE, 'is_generic' => FALSE);
168 return $this->cache[$cachekey];
169 }
170
171 /**
172 * @param string $entity
173 * @param string $action
174 * @return string
175 */
176 protected function getFunctionName($entity, $action, $version) {
177 $entity = _civicrm_api_get_entity_name_from_camel($entity);
178 return 'civicrm_api' . $version . '_' . $entity . '_' . $action;
179 }
180
181 /**
182 * Load/require all files related to an entity.
183 *
184 * This should not normally be called because it's does a file-system scan; it's
185 * only appropriate when introspection is really required (eg for "getActions").
186 *
187 * @param string $entity
188 * @param int $version
189 *
190 * @return void
191 */
192 protected function loadEntity($entity, $version) {
193 $camelName = _civicrm_api_get_camel_name($entity, $version);
194
195 // Check for master entity file; to match _civicrm_api_resolve(), only load the first one
196 $stdFile = 'api/v' . $version . '/' . $camelName . '.php';
197 if (\CRM_Utils_File::isIncludable($stdFile)) {
198 require_once $stdFile;
199 }
200
201 // Check for standalone action files; to match _civicrm_api_resolve(), only load the first one
202 $loaded_files = array(); // array($relativeFilePath => TRUE)
203 $include_dirs = array_unique(explode(PATH_SEPARATOR, get_include_path()));
204 foreach ($include_dirs as $include_dir) {
205 $action_dir = implode(DIRECTORY_SEPARATOR, array($include_dir, 'api', "v${version}", $camelName));
206 if (!is_dir($action_dir)) {
207 continue;
208 }
209
210 $iterator = new \DirectoryIterator($action_dir);
211 foreach ($iterator as $fileinfo) {
212 $file = $fileinfo->getFilename();
213 if (array_key_exists($file, $loaded_files)) {
214 continue; // action provided by an earlier item on include_path
215 }
216
217 $parts = explode(".", $file);
218 if (end($parts) == "php" && !preg_match('/Tests?\.php$/', $file)) {
219 require_once $action_dir . DIRECTORY_SEPARATOR . $file;
220 $loaded_files[$file] = TRUE;
221 }
222 }
223 }
224 }
225
787604ff 226}