Commit | Line | Data |
---|---|---|
0a946de2 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
41498ac5 | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
0a946de2 | 5 | | | |
41498ac5 TO |
6 | | This work is published under the GNU AGPLv3 license with some | |
7 | | permitted exceptions and without any warranty. For full license | | |
8 | | and copyright information, see https://civicrm.org/licensing | | |
0a946de2 | 9 | +--------------------------------------------------------------------+ |
d25dd0ee | 10 | */ |
0a946de2 TO |
11 | |
12 | namespace Civi\API\Subscriber; | |
46bcf597 | 13 | |
0a946de2 TO |
14 | use Civi\API\Events; |
15 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; | |
16 | ||
17 | /** | |
8882ff5c TO |
18 | * The ChainSubscriber looks for API parameters which specify a nested or |
19 | * chained API call. For example: | |
0a946de2 TO |
20 | * |
21 | * @code | |
22 | * $result = civicrm_api('Contact', 'create', array( | |
23 | * 'version' => 3, | |
24 | * 'first_name' => 'Amy', | |
25 | * 'api.Email.create' => array( | |
26 | * 'email' => 'amy@example.com', | |
27 | * 'location_type_id' => 123, | |
28 | * ), | |
29 | * )); | |
30 | * @endcode | |
31 | * | |
8882ff5c TO |
32 | * The ChainSubscriber looks for any parameters of the form "api.Email.create"; |
33 | * if found, it issues the nested API call (and passes some extra context -- | |
34 | * eg Amy's contact_id). | |
0a946de2 TO |
35 | */ |
36 | class ChainSubscriber implements EventSubscriberInterface { | |
34f3bbd9 | 37 | |
6550386a EM |
38 | /** |
39 | * @return array | |
40 | */ | |
0a946de2 | 41 | public static function getSubscribedEvents() { |
c64f69d9 CW |
42 | return [ |
43 | Events::RESPOND => ['onApiRespond', Events::W_EARLY], | |
44 | ]; | |
0a946de2 TO |
45 | } |
46 | ||
6550386a EM |
47 | /** |
48 | * @param \Civi\API\Event\RespondEvent $event | |
8882ff5c | 49 | * API response event. |
6550386a EM |
50 | * |
51 | * @throws \Exception | |
52 | */ | |
0a946de2 TO |
53 | public function onApiRespond(\Civi\API\Event\RespondEvent $event) { |
54 | $apiRequest = $event->getApiRequest(); | |
8bcc0d86 CW |
55 | if ($apiRequest['version'] < 4) { |
56 | $result = $event->getResponse(); | |
57 | if (\CRM_Utils_Array::value('is_error', $result, 0) == 0) { | |
58 | $this->callNestedApi($event->getApiKernel(), $apiRequest['params'], $result, $apiRequest['action'], $apiRequest['entity'], $apiRequest['version']); | |
59 | $event->setResponse($result); | |
60 | } | |
0a946de2 TO |
61 | } |
62 | } | |
63 | ||
64 | /** | |
fe482240 | 65 | * Call any nested api calls. |
0a946de2 TO |
66 | * |
67 | * TODO: We don't really need this to be a separate function. | |
c0e26341 | 68 | * @param \Civi\API\Kernel $apiKernel |
257e7666 EM |
69 | * @param $params |
70 | * @param $result | |
71 | * @param $action | |
72 | * @param $entity | |
73 | * @param $version | |
74 | * @throws \Exception | |
0a946de2 | 75 | */ |
c0e26341 | 76 | protected function callNestedApi($apiKernel, &$params, &$result, $action, $entity, $version) { |
4846df91 | 77 | $lowercase_entity = _civicrm_api_get_entity_name_from_camel($entity); |
0a946de2 | 78 | |
8882ff5c TO |
79 | // We don't need to worry about nested api in the getfields/getoptions |
80 | // actions, so just return immediately. | |
c64f69d9 | 81 | if (in_array($action, ['getfields', 'getfield', 'getoptions'])) { |
0a946de2 TO |
82 | return; |
83 | } | |
84 | ||
4846df91 | 85 | if ($action == 'getsingle') { |
0a946de2 TO |
86 | // I don't understand the protocol here, but we don't want |
87 | // $result to be a recursive array | |
88 | // $result['values'][0] = $result; | |
89 | $oldResult = $result; | |
c64f69d9 | 90 | $result = ['values' => [0 => $oldResult]]; |
0a946de2 TO |
91 | } |
92 | foreach ($params as $field => $newparams) { | |
93 | if ((is_array($newparams) || $newparams === 1) && $field <> 'api.has_parent' && substr($field, 0, 3) == 'api') { | |
94 | ||
8882ff5c TO |
95 | // 'api.participant.delete' => 1 is a valid options - handle 1 |
96 | // instead of an array | |
0a946de2 | 97 | if ($newparams === 1) { |
c64f69d9 | 98 | $newparams = ['version' => $version]; |
0a946de2 TO |
99 | } |
100 | // can be api_ or api. | |
101 | $separator = $field[3]; | |
102 | if (!($separator == '.' || $separator == '_')) { | |
103 | continue; | |
104 | } | |
105 | $subAPI = explode($separator, $field); | |
106 | ||
107 | $subaction = empty($subAPI[2]) ? $action : $subAPI[2]; | |
c64f69d9 | 108 | $subParams = [ |
0a946de2 | 109 | 'debug' => \CRM_Utils_Array::value('debug', $params), |
c64f69d9 | 110 | ]; |
4846df91 | 111 | $subEntity = _civicrm_api_get_entity_name_from_camel($subAPI[1]); |
0a946de2 | 112 | |
449fbe14 SL |
113 | // Hard coded list of entitys that have fields starting api_ and shouldn't be automatically |
114 | // deemed to be chained API calls | |
c64f69d9 CW |
115 | $skipList = [ |
116 | 'SmsProvider' => ['type', 'url', 'params'], | |
117 | 'Job' => ['prefix', 'entity', 'action'], | |
118 | 'Contact' => ['key'], | |
119 | ]; | |
5bc7d8e2 | 120 | if (isset($skipList[$entity]) && in_array($subEntity, $skipList[$entity])) { |
449fbe14 SL |
121 | continue; |
122 | } | |
123 | ||
0a946de2 TO |
124 | foreach ($result['values'] as $idIndex => $parentAPIValues) { |
125 | ||
4846df91 | 126 | if ($subEntity != 'contact') { |
0a946de2 | 127 | //contact spits the dummy at activity_id so what else won't it like? |
8882ff5c TO |
128 | //set entity_id & entity table based on the parent's id & entity. |
129 | //e.g for something like note if the parent call is contact | |
130 | //'entity_table' will be set to 'contact' & 'id' to the contact id | |
131 | //from the parent call. in this case 'contact_id' will also be | |
132 | //set to the parent's id | |
4a517906 PN |
133 | if (!($subEntity == 'line_item' && $lowercase_entity == 'contribution' && $action != 'create')) { |
134 | $subParams["entity_id"] = $parentAPIValues['id']; | |
135 | $subParams['entity_table'] = 'civicrm_' . $lowercase_entity; | |
136 | } | |
9960c0ef | 137 | |
602e7157 | 138 | $addEntityId = TRUE; |
9960c0ef JV |
139 | if ($subEntity == 'relationship' && $lowercase_entity == 'contact') { |
140 | // if a relationship call is chained to a contact call, we need | |
141 | // to check whether contact_id_a or contact_id_b for the | |
142 | // relationship is given. If so, don't add an extra subParam | |
143 | // "contact_id" => parent_id. | |
144 | // See CRM-16084. | |
145 | foreach (array_keys($newparams) as $key) { | |
146 | if (substr($key, 0, 11) == 'contact_id_') { | |
602e7157 | 147 | $addEntityId = FALSE; |
9960c0ef JV |
148 | break; |
149 | } | |
150 | } | |
151 | } | |
602e7157 | 152 | if ($addEntityId) { |
9960c0ef JV |
153 | $subParams[$lowercase_entity . "_id"] = $parentAPIValues['id']; |
154 | } | |
0a946de2 | 155 | } |
4846df91 | 156 | if ($entity != 'Contact' && \CRM_Utils_Array::value(strtolower($subEntity . "_id"), $parentAPIValues)) { |
8882ff5c TO |
157 | //e.g. if event_id is in the values returned & subentity is event |
158 | //then pass in event_id as 'id' don't do this for contact as it | |
e4f46be0 | 159 | //does some weird things like returning primary email & |
0a946de2 TO |
160 | //thus limiting the ability to chain email |
161 | //TODO - this might need the camel treatment | |
162 | $subParams['id'] = $parentAPIValues[$subEntity . "_id"]; | |
163 | } | |
164 | ||
165 | if (\CRM_Utils_Array::value('entity_table', $result['values'][$idIndex]) == $subEntity) { | |
166 | $subParams['id'] = $result['values'][$idIndex]['entity_id']; | |
167 | } | |
8882ff5c TO |
168 | // if we are dealing with the same entity pass 'id' through |
169 | // (useful for get + delete for example) | |
4846df91 | 170 | if ($lowercase_entity == $subEntity) { |
0a946de2 TO |
171 | $subParams['id'] = $result['values'][$idIndex]['id']; |
172 | } | |
173 | ||
0a946de2 TO |
174 | $subParams['version'] = $version; |
175 | if (!empty($params['check_permissions'])) { | |
176 | $subParams['check_permissions'] = $params['check_permissions']; | |
177 | } | |
178 | $subParams['sequential'] = 1; | |
179 | $subParams['api.has_parent'] = 1; | |
180 | if (array_key_exists(0, $newparams)) { | |
181 | $genericParams = $subParams; | |
182 | // it is a numerically indexed array - ie. multiple creates | |
183 | foreach ($newparams as $entityparams) { | |
184 | $subParams = array_merge($genericParams, $entityparams); | |
845d6d75 | 185 | _civicrm_api_replace_variables($subParams, $result['values'][$idIndex], $separator); |
5a3846f7 | 186 | $result['values'][$idIndex][$field][] = $apiKernel->runSafe($subEntity, $subaction, $subParams); |
0a946de2 TO |
187 | if ($result['is_error'] === 1) { |
188 | throw new \Exception($subEntity . ' ' . $subaction . 'call failed with' . $result['error_message']); | |
189 | } | |
190 | } | |
191 | } | |
192 | else { | |
193 | ||
194 | $subParams = array_merge($subParams, $newparams); | |
845d6d75 | 195 | _civicrm_api_replace_variables($subParams, $result['values'][$idIndex], $separator); |
5a3846f7 | 196 | $result['values'][$idIndex][$field] = $apiKernel->runSafe($subEntity, $subaction, $subParams); |
0a946de2 TO |
197 | if (!empty($result['is_error'])) { |
198 | throw new \Exception($subEntity . ' ' . $subaction . 'call failed with' . $result['error_message']); | |
199 | } | |
200 | } | |
201 | } | |
202 | } | |
203 | } | |
4846df91 | 204 | if ($action == 'getsingle') { |
0a946de2 TO |
205 | $result = $result['values'][0]; |
206 | } | |
207 | } | |
208 | ||
6550386a | 209 | } |