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 | 20 | * |
0b882a86 | 21 | * ``` |
0a946de2 TO |
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 | * )); | |
0b882a86 | 30 | * ``` |
0a946de2 | 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 | 42 | return [ |
39b870b8 | 43 | 'civi.api.respond' => ['onApiRespond', Events::W_EARLY], |
c64f69d9 | 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(); | |
baed13d5 | 57 | if (is_array($result) && empty($result['is_error'])) { |
8bcc0d86 CW |
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 | 91 | } |
0bb9a2e5 R |
92 | |
93 | // Scan the params for chain calls. | |
0a946de2 TO |
94 | foreach ($params as $field => $newparams) { |
95 | if ((is_array($newparams) || $newparams === 1) && $field <> 'api.has_parent' && substr($field, 0, 3) == 'api') { | |
0bb9a2e5 | 96 | // This param is a chain call, e.g. api.<entity>.<action> |
0a946de2 | 97 | |
8882ff5c TO |
98 | // 'api.participant.delete' => 1 is a valid options - handle 1 |
99 | // instead of an array | |
0a946de2 | 100 | if ($newparams === 1) { |
c64f69d9 | 101 | $newparams = ['version' => $version]; |
0a946de2 TO |
102 | } |
103 | // can be api_ or api. | |
104 | $separator = $field[3]; | |
105 | if (!($separator == '.' || $separator == '_')) { | |
106 | continue; | |
107 | } | |
108 | $subAPI = explode($separator, $field); | |
109 | ||
110 | $subaction = empty($subAPI[2]) ? $action : $subAPI[2]; | |
0bb9a2e5 R |
111 | /** @var array of parameters that will be applied to every chained request. */ |
112 | $enforcedSubParams = [ | |
9e10fb6b | 113 | 'debug' => $params['debug'] ?? NULL, |
c64f69d9 | 114 | ]; |
0bb9a2e5 R |
115 | /** @var array of parameters that provide defaults to every chained request, but which may be overridden by parameters in the chained request. */ |
116 | $defaultSubParams = []; | |
117 | ||
4846df91 | 118 | $subEntity = _civicrm_api_get_entity_name_from_camel($subAPI[1]); |
0a946de2 | 119 | |
449fbe14 SL |
120 | // Hard coded list of entitys that have fields starting api_ and shouldn't be automatically |
121 | // deemed to be chained API calls | |
c64f69d9 CW |
122 | $skipList = [ |
123 | 'SmsProvider' => ['type', 'url', 'params'], | |
124 | 'Job' => ['prefix', 'entity', 'action'], | |
125 | 'Contact' => ['key'], | |
126 | ]; | |
5bc7d8e2 | 127 | if (isset($skipList[$entity]) && in_array($subEntity, $skipList[$entity])) { |
449fbe14 SL |
128 | continue; |
129 | } | |
130 | ||
0a946de2 TO |
131 | foreach ($result['values'] as $idIndex => $parentAPIValues) { |
132 | ||
4846df91 | 133 | if ($subEntity != 'contact') { |
0a946de2 | 134 | //contact spits the dummy at activity_id so what else won't it like? |
8882ff5c TO |
135 | //set entity_id & entity table based on the parent's id & entity. |
136 | //e.g for something like note if the parent call is contact | |
137 | //'entity_table' will be set to 'contact' & 'id' to the contact id | |
138 | //from the parent call. in this case 'contact_id' will also be | |
139 | //set to the parent's id | |
4a517906 | 140 | if (!($subEntity == 'line_item' && $lowercase_entity == 'contribution' && $action != 'create')) { |
0bb9a2e5 R |
141 | $defaultSubParams["entity_id"] = $parentAPIValues['id']; |
142 | $defaultSubParams['entity_table'] = 'civicrm_' . $lowercase_entity; | |
4a517906 | 143 | } |
9960c0ef | 144 | |
602e7157 | 145 | $addEntityId = TRUE; |
9960c0ef JV |
146 | if ($subEntity == 'relationship' && $lowercase_entity == 'contact') { |
147 | // if a relationship call is chained to a contact call, we need | |
148 | // to check whether contact_id_a or contact_id_b for the | |
149 | // relationship is given. If so, don't add an extra subParam | |
150 | // "contact_id" => parent_id. | |
151 | // See CRM-16084. | |
152 | foreach (array_keys($newparams) as $key) { | |
153 | if (substr($key, 0, 11) == 'contact_id_') { | |
602e7157 | 154 | $addEntityId = FALSE; |
9960c0ef JV |
155 | break; |
156 | } | |
157 | } | |
158 | } | |
602e7157 | 159 | if ($addEntityId) { |
0bb9a2e5 | 160 | $defaultSubParams[$lowercase_entity . "_id"] = $parentAPIValues['id']; |
9960c0ef | 161 | } |
0a946de2 | 162 | } |
0bb9a2e5 | 163 | // @todo remove strtolower: $subEntity is already lower case |
4846df91 | 164 | if ($entity != 'Contact' && \CRM_Utils_Array::value(strtolower($subEntity . "_id"), $parentAPIValues)) { |
8882ff5c TO |
165 | //e.g. if event_id is in the values returned & subentity is event |
166 | //then pass in event_id as 'id' don't do this for contact as it | |
e4f46be0 | 167 | //does some weird things like returning primary email & |
0a946de2 TO |
168 | //thus limiting the ability to chain email |
169 | //TODO - this might need the camel treatment | |
0bb9a2e5 | 170 | $defaultSubParams['id'] = $parentAPIValues[$subEntity . "_id"]; |
0a946de2 TO |
171 | } |
172 | ||
173 | if (\CRM_Utils_Array::value('entity_table', $result['values'][$idIndex]) == $subEntity) { | |
0bb9a2e5 | 174 | $defaultSubParams['id'] = $result['values'][$idIndex]['entity_id']; |
0a946de2 | 175 | } |
8882ff5c TO |
176 | // if we are dealing with the same entity pass 'id' through |
177 | // (useful for get + delete for example) | |
4846df91 | 178 | if ($lowercase_entity == $subEntity) { |
0bb9a2e5 | 179 | $defaultSubParams['id'] = $result['values'][$idIndex]['id']; |
0a946de2 TO |
180 | } |
181 | ||
0bb9a2e5 R |
182 | $enforcedSubParams['version'] = $version; |
183 | // Copy check_permissions from parent. | |
184 | $enforcedSubParams['check_permissions'] = $params['check_permissions'] ?? NULL; | |
705f52bf | 185 | $defaultSubParams['sequential'] = 1; |
0bb9a2e5 R |
186 | $enforcedSubParams['api.has_parent'] = 1; |
187 | // Inspect $newparams, the passed in params for the chain call. | |
0a946de2 | 188 | if (array_key_exists(0, $newparams)) { |
0bb9a2e5 | 189 | // It is a numerically indexed array - ie. multiple creates |
0a946de2 | 190 | foreach ($newparams as $entityparams) { |
0bb9a2e5 R |
191 | // Defaults, overridden by request params, overridden by enforced params. |
192 | $subParams = array_merge($defaultSubParams, $entityparams, $enforcedSubParams); | |
845d6d75 | 193 | _civicrm_api_replace_variables($subParams, $result['values'][$idIndex], $separator); |
5a3846f7 | 194 | $result['values'][$idIndex][$field][] = $apiKernel->runSafe($subEntity, $subaction, $subParams); |
0a946de2 TO |
195 | if ($result['is_error'] === 1) { |
196 | throw new \Exception($subEntity . ' ' . $subaction . 'call failed with' . $result['error_message']); | |
197 | } | |
198 | } | |
199 | } | |
200 | else { | |
0bb9a2e5 R |
201 | // Defaults, overridden by request params, overridden by enforced params. |
202 | $subParams = array_merge($defaultSubParams, $newparams, $enforcedSubParams); | |
845d6d75 | 203 | _civicrm_api_replace_variables($subParams, $result['values'][$idIndex], $separator); |
5a3846f7 | 204 | $result['values'][$idIndex][$field] = $apiKernel->runSafe($subEntity, $subaction, $subParams); |
0a946de2 TO |
205 | if (!empty($result['is_error'])) { |
206 | throw new \Exception($subEntity . ' ' . $subaction . 'call failed with' . $result['error_message']); | |
207 | } | |
208 | } | |
209 | } | |
210 | } | |
211 | } | |
4846df91 | 212 | if ($action == 'getsingle') { |
0a946de2 TO |
213 | $result = $result['values'][0]; |
214 | } | |
215 | } | |
216 | ||
6550386a | 217 | } |