Commit | Line | Data |
---|---|---|
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 | ||
28 | namespace Civi\API\Provider; | |
29 | use Civi\API\Events; | |
30 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; | |
31 | ||
32 | /** | |
33 | * This class manages the loading of API's using strict file+function naming | |
34 | * conventions. | |
35 | */ | |
36 | class 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 | } |