1
|
|
|
<?php |
2
|
|
|
namespace Communibase; |
3
|
|
|
|
4
|
|
|
use Communibase\Logging\QueryLogger; |
5
|
|
|
use GuzzleHttp\ClientInterface; |
6
|
|
|
use GuzzleHttp\Exception\ClientException; |
7
|
|
|
use GuzzleHttp\Promise\Promise; |
8
|
|
|
use Psr\Http\Message\ResponseInterface; |
9
|
|
|
use Psr\Http\Message\StreamInterface; |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* Communibase (https://communibase.nl) data Connector for PHP |
13
|
|
|
* |
14
|
|
|
* For more information see https://communibase.nl |
15
|
|
|
* |
16
|
|
|
* Following are IDE hints for sync method versions: |
17
|
|
|
* |
18
|
|
|
* @method string getTemplateSync(string $entityType) Returns all the fields according to the definition. |
19
|
|
|
* @method array getByIdSync(string $entityType, string $id) Get an entity by id |
20
|
|
|
* @method array getByIdsSync(string $entityType, array $ids, array $params = []) Get an array of entities by their ids |
21
|
|
|
* @method array getAllSync(string $entityType, array $params) Get all entities of a certain type |
22
|
|
|
* @method array getIdSync(string $entityType, array $selector) Get the id of an entity based on a search |
23
|
|
|
* @method array getHistorySync(string $entityType, string $id) Returns an array of the history for the entity |
24
|
|
|
* @method array destroySync(string $entityType, string $id) Delete something from Communibase |
25
|
|
|
* |
26
|
|
|
* @package Communibase |
27
|
|
|
* @author Kingsquare ([email protected]) |
28
|
|
|
* @copyright Copyright (c) Kingsquare BV (http://www.kingsquare.nl) |
29
|
|
|
* @license http://opensource.org/licenses/MIT The MIT License (MIT) |
30
|
|
|
*/ |
31
|
|
|
class Connector implements ConnectorInterface |
32
|
|
|
{ |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* The official service URI; can be overridden via the constructor |
36
|
|
|
* |
37
|
|
|
* @var string |
38
|
|
|
*/ |
39
|
|
|
const SERVICE_PRODUCTION_URL = 'https://api.communibase.nl/0.1/'; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* The API key which is to be used for the api. |
43
|
|
|
* Is required to be set via the constructor. |
44
|
|
|
* |
45
|
|
|
* @var string |
46
|
|
|
*/ |
47
|
|
|
private $apiKey; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* The url which is to be used for this connector. Defaults to the production url. |
51
|
|
|
* Can be set via the constructor. |
52
|
|
|
* |
53
|
|
|
* @var string |
54
|
|
|
*/ |
55
|
|
|
private $serviceUrl; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* @var array of extra headers to send with each request |
59
|
|
|
*/ |
60
|
|
|
private $extraHeaders = []; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* @var QueryLogger |
64
|
|
|
*/ |
65
|
|
|
private $logger; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* @var ClientInterface |
69
|
|
|
*/ |
70
|
|
|
private $client; |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Create a new Communibase Connector instance based on the given api-key and possible serviceUrl |
74
|
|
|
* |
75
|
|
|
* @param string $apiKey The API key for Communibase |
76
|
|
|
* @param string $serviceUrl The Communibase API endpoint; defaults to self::SERVICE_PRODUCTION_URL |
77
|
|
|
* @param ClientInterface $client An optional GuzzleHttp Client (or Interface for mocking) |
78
|
|
|
*/ |
79
|
|
|
public function __construct( |
80
|
|
|
$apiKey, |
81
|
|
|
$serviceUrl = self::SERVICE_PRODUCTION_URL, |
82
|
|
|
ClientInterface $client = null |
83
|
|
|
) { |
84
|
|
|
$this->apiKey = $apiKey; |
85
|
|
|
$this->serviceUrl = $serviceUrl; |
86
|
|
|
$this->client = $client; |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Returns an array that has all the fields according to the definition in Communibase. |
91
|
|
|
* |
92
|
|
|
* @param string $entityType |
93
|
|
|
* |
94
|
|
|
* @return Promise of result |
95
|
|
|
* |
96
|
|
|
* @throws Exception |
97
|
|
|
*/ |
98
|
|
|
public function getTemplate($entityType) |
99
|
|
|
{ |
100
|
|
|
$params = [ |
101
|
|
|
'fields' => 'attributes.title', |
102
|
|
|
'limit' => 1, |
103
|
|
|
]; |
104
|
|
|
|
105
|
|
|
return $this->search('EntityType', ['title' => $entityType], $params)->then(function ($definition) { |
|
|
|
|
106
|
|
|
return array_fill_keys(array_merge(['_id'], array_column($definition[0]['attributes'], 'title')), null); |
107
|
|
|
}); |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* Get a single Entity by its id |
112
|
|
|
* |
113
|
|
|
* @param string $entityType |
114
|
|
|
* @param string $id |
115
|
|
|
* @param array $params (optional) |
116
|
|
|
* @param string|null $version |
117
|
|
|
* |
118
|
|
|
* @return Promise of result |
119
|
|
|
* |
120
|
|
|
* @return array entity |
121
|
|
|
* |
122
|
|
|
* @throws Exception |
123
|
|
|
*/ |
124
|
|
|
public function getById($entityType, $id, array $params = [], $version = null) |
125
|
|
|
{ |
126
|
|
|
if (empty($id)) { |
127
|
|
|
throw new Exception('Id is empty'); |
128
|
|
|
} |
129
|
|
|
if (!$this->isIdValid($id)) { |
130
|
|
|
throw new Exception('Id is invalid, please use a correctly formatted id'); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
return ($version === null) |
|
|
|
|
134
|
|
|
? $this->doGet($entityType . '.json/crud/' . $id, $params) |
135
|
|
|
: $this->doGet($entityType . '.json/history/' . $id . '/' . $version, $params); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* NOTE not yet async |
140
|
|
|
* |
141
|
|
|
* Get a single Entity by a ref-string |
142
|
|
|
* |
143
|
|
|
* @param string $ref |
144
|
|
|
* @param array $parentEntity (optional) |
145
|
|
|
* |
146
|
|
|
* @return array the referred Entity data |
147
|
|
|
* |
148
|
|
|
* @throws Exception |
149
|
|
|
*/ |
150
|
|
|
public function getByRef($ref, array $parentEntity = []) |
151
|
|
|
{ |
152
|
|
|
$refParts = explode('.', $ref); |
153
|
|
|
if ($refParts[0] !== 'parent') { |
154
|
|
|
$entityParts = explode('|', $refParts[0]); |
155
|
|
|
$parentEntity = $this->getById($entityParts[0], $entityParts[1]); |
|
|
|
|
156
|
|
|
} |
157
|
|
|
if (empty($refParts[1])) { |
158
|
|
|
return $parentEntity; |
159
|
|
|
} |
160
|
|
|
$propertyParts = explode('|', $refParts[1]); |
161
|
|
|
foreach ($parentEntity[$propertyParts[0]] as $subEntity) { |
162
|
|
|
if ($subEntity['_id'] === $propertyParts[1]) { |
163
|
|
|
return $subEntity; |
164
|
|
|
} |
165
|
|
|
} |
166
|
|
|
throw new Exception('Could not find the referred Entity'); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* Get an array of entities by their ids |
171
|
|
|
* |
172
|
|
|
* @param string $entityType |
173
|
|
|
* @param array $ids |
174
|
|
|
* @param array $params (optional) |
175
|
|
|
* |
176
|
|
|
* @return Promise of result |
177
|
|
|
*/ |
178
|
|
|
public function getByIds($entityType, array $ids, array $params = []) |
179
|
|
|
{ |
180
|
|
|
$validIds = array_values(array_unique(array_filter($ids, [$this, 'isIdValid']))); |
181
|
|
|
|
182
|
|
|
if (empty($validIds)) { |
183
|
|
|
return []; |
|
|
|
|
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
$doSortByIds = empty($params['sort']); |
187
|
|
|
|
188
|
|
|
return $this->search($entityType, ['_id' => ['$in' => $validIds]], $params)->then(function ($results) use ($doSortByIds, $validIds) { |
|
|
|
|
189
|
|
|
if (!$doSortByIds) { |
190
|
|
|
return $results; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
$flipped = array_flip($validIds); |
194
|
|
|
foreach ($results as $result) { |
195
|
|
|
$flipped[$result['_id']] = $result; |
196
|
|
|
} |
197
|
|
|
return array_filter(array_values($flipped), function ($result) { |
198
|
|
|
return is_array($result) && !empty($result); |
199
|
|
|
}); |
200
|
|
|
}); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* Get all entities of a certain type |
205
|
|
|
* |
206
|
|
|
* @param string $entityType |
207
|
|
|
* @param array $params (optional) |
208
|
|
|
* |
209
|
|
|
* @return Promise of result |
210
|
|
|
*/ |
211
|
|
|
public function getAll($entityType, array $params = []) |
212
|
|
|
{ |
213
|
|
|
return $this->doGet($entityType . '.json/crud/', $params); |
|
|
|
|
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* Get result entityIds of a certain search |
218
|
|
|
* |
219
|
|
|
* @param string $entityType |
220
|
|
|
* @param array $selector (optional) |
221
|
|
|
* @param array $params (optional) |
222
|
|
|
* |
223
|
|
|
* @return Promise of result |
224
|
|
|
*/ |
225
|
|
|
public function getIds($entityType, array $selector = [], array $params = []) |
226
|
|
|
{ |
227
|
|
|
$params['fields'] = '_id'; |
228
|
|
|
|
229
|
|
|
return $this->search($entityType, $selector, $params)->then(function ($results) { |
|
|
|
|
230
|
|
|
return array_column($results, '_id'); |
231
|
|
|
}); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Get the id of an entity based on a search |
236
|
|
|
* |
237
|
|
|
* @param string $entityType i.e. Person |
238
|
|
|
* @param array $selector (optional) i.e. ['firstName' => 'Henk'] |
239
|
|
|
* |
240
|
|
|
* @return Promise of result |
241
|
|
|
*/ |
242
|
|
|
public function getId($entityType, array $selector = []) |
243
|
|
|
{ |
244
|
|
|
$params = ['limit' => 1]; |
245
|
|
|
$ids = (array)$this->getIds($entityType, $selector, $params); |
246
|
|
|
|
247
|
|
|
return array_shift($ids); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* Returns an array of the history for the entity with the following format: |
252
|
|
|
* |
253
|
|
|
* <code> |
254
|
|
|
* [ |
255
|
|
|
* [ |
256
|
|
|
* 'updatedBy' => '', // name of the user |
257
|
|
|
* 'updatedAt' => '', // a string according to the DateTime::ISO8601 format |
258
|
|
|
* '_id' => '', // the ID of the entity which can ge fetched seperately |
259
|
|
|
* ], |
260
|
|
|
* ... |
261
|
|
|
* ] |
262
|
|
|
* </code> |
263
|
|
|
* |
264
|
|
|
* @param string $entityType |
265
|
|
|
* @param string $id |
266
|
|
|
* |
267
|
|
|
* @return Promise of result |
268
|
|
|
* |
269
|
|
|
* @throws Exception |
270
|
|
|
*/ |
271
|
|
|
public function getHistory($entityType, $id) |
272
|
|
|
{ |
273
|
|
|
return $this->doGet($entityType . '.json/history/' . $id); |
|
|
|
|
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* Search for the given entity by optional passed selector/params |
278
|
|
|
* |
279
|
|
|
* @param string $entityType |
280
|
|
|
* @param array $querySelector |
281
|
|
|
* @param array $params (optional) |
282
|
|
|
* |
283
|
|
|
* @return Promise of result |
284
|
|
|
* |
285
|
|
|
* @throws Exception |
286
|
|
|
*/ |
287
|
|
|
public function search($entityType, array $querySelector, array $params = []) |
288
|
|
|
{ |
289
|
|
|
return $this->doPost($entityType . '.json/search', $params, $querySelector); |
|
|
|
|
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* This will save an entity in Communibase. When a _id-field is found, this entity will be updated |
294
|
|
|
* |
295
|
|
|
* NOTE: When updating, depending on the Entity, you may need to include all fields. |
296
|
|
|
* |
297
|
|
|
* @param string $entityType |
298
|
|
|
* @param array $properties - the to-be-saved entity data |
299
|
|
|
* |
300
|
|
|
* @returns Promise of result |
301
|
|
|
* |
302
|
|
|
* @throws Exception |
303
|
|
|
*/ |
304
|
|
|
public function update($entityType, array $properties) |
305
|
|
|
{ |
306
|
|
|
$isNew = empty($properties['_id']); |
307
|
|
|
|
308
|
|
|
return $this->{$isNew ? 'doPost' : 'doPut'}( |
309
|
|
|
$entityType . '.json/crud/' . ($isNew ? '' : $properties['_id']), |
310
|
|
|
[], |
311
|
|
|
$properties |
312
|
|
|
); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Finalize an invoice by adding an invoiceNumber to it. |
317
|
|
|
* Besides, invoice items will receive a "generalLedgerAccountNumber". |
318
|
|
|
* This number will be unique and sequential within the "daybook" of the invoice. |
319
|
|
|
* |
320
|
|
|
* NOTE: this is Invoice specific |
321
|
|
|
* |
322
|
|
|
* @param string $entityType |
323
|
|
|
* @param string $id |
324
|
|
|
* |
325
|
|
|
* @return Promise of result |
326
|
|
|
* |
327
|
|
|
* @throws Exception |
328
|
|
|
*/ |
329
|
|
|
public function finalize($entityType, $id) |
330
|
|
|
{ |
331
|
|
|
if ($entityType !== 'Invoice') { |
332
|
|
|
throw new Exception('Cannot call finalize on ' . $entityType); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
return $this->doPost($entityType . '.json/finalize/' . $id); |
|
|
|
|
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* Delete something from Communibase |
340
|
|
|
* |
341
|
|
|
* @param string $entityType |
342
|
|
|
* @param string $id |
343
|
|
|
* |
344
|
|
|
* @return Promise of result |
345
|
|
|
*/ |
346
|
|
|
public function destroy($entityType, $id) |
347
|
|
|
{ |
348
|
|
|
return $this->doDelete($entityType . '.json/crud/' . $id); |
|
|
|
|
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* Get the binary contents of a file by its ID |
353
|
|
|
* |
354
|
|
|
* NOTE: for meta-data like filesize and mimetype, one can use the getById()-method. |
355
|
|
|
* |
356
|
|
|
* @param string $id id string for the file-entity |
357
|
|
|
* |
358
|
|
|
* @return StreamInterface Binary contents of the file. Since the stream can be made a string this works like a charm! |
359
|
|
|
* |
360
|
|
|
* @throws Exception |
361
|
|
|
*/ |
362
|
|
|
public function getBinary($id) |
363
|
|
|
{ |
364
|
|
|
if (!$this->isIdValid($id)) { |
365
|
|
|
throw new Exception('Invalid $id passed. Please provide one.'); |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
return $this->call('get', ['File.json/binary/' . $id])->then(function (ResponseInterface $response) { |
|
|
|
|
369
|
|
|
return $response->getBody(); |
370
|
|
|
}); |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* Uploads the contents of the resource (this could be a file handle) to Communibase |
375
|
|
|
* |
376
|
|
|
* @param StreamInterface $resource |
377
|
|
|
* @param string $name |
378
|
|
|
* @param string $destinationPath |
379
|
|
|
* @param string $id |
380
|
|
|
* |
381
|
|
|
* @return array|mixed |
382
|
|
|
* @throws Exception |
383
|
|
|
*/ |
384
|
|
|
public function updateBinary(StreamInterface $resource, $name, $destinationPath, $id = '') |
385
|
|
|
{ |
386
|
|
|
$metaData = ['path' => $destinationPath]; |
387
|
|
|
if (empty($id)) { |
388
|
|
|
$options = [ |
389
|
|
|
'multipart' => [ |
390
|
|
|
[ |
391
|
|
|
'name' => 'File', |
392
|
|
|
'filename' => $name, |
393
|
|
|
'contents' => $resource |
394
|
|
|
], |
395
|
|
|
[ |
396
|
|
|
'name' => 'metadata', |
397
|
|
|
'contents' => json_encode($metaData), |
398
|
|
|
] |
399
|
|
|
] |
400
|
|
|
]; |
401
|
|
|
|
402
|
|
|
return $this->call('post', ['File.json/binary', $options])->then(function (ResponseInterface $response) { |
403
|
|
|
return $this->parseResult($response->getBody(), $response->getStatusCode()); |
404
|
|
|
}); |
405
|
|
|
|
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
return $this->doPut('File.json/crud/' . $id, [], [ |
409
|
|
|
'filename' => $name, |
410
|
|
|
'length' => $resource->getSize(), |
411
|
|
|
'uploadDate' => date('c'), |
412
|
|
|
'metadata' => $metaData, |
413
|
|
|
'content' => base64_encode($resource->getContents()), |
414
|
|
|
]); |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
/** |
418
|
|
|
* MAGIC for making sync requests |
419
|
|
|
* |
420
|
|
|
* @param string $name |
421
|
|
|
* @param array $arguments |
422
|
|
|
* |
423
|
|
|
* @return mixed |
424
|
|
|
*/ |
425
|
|
|
public function __call($name, $arguments) |
426
|
|
|
{ |
427
|
|
|
if (preg_match('#(.*)Sync$#', $name, $matches) && is_callable([$this, $matches[1]])) { |
428
|
|
|
$promise = call_user_func_array([$this, $matches[1]], $arguments); |
429
|
|
|
|
430
|
|
|
/* @var Promise $promise */ |
431
|
|
|
return $promise->wait(); // wait for response |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
// fallback to known methods |
435
|
|
|
return null; |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
/** |
439
|
|
|
* @param string $path |
440
|
|
|
* @param array $params |
441
|
|
|
* @param array $data |
442
|
|
|
* |
443
|
|
|
* @return Promise |
444
|
|
|
* |
445
|
|
|
* @throws Exception |
446
|
|
|
*/ |
447
|
|
|
protected function doGet($path, array $params = null, array $data = null) |
448
|
|
|
{ |
449
|
|
|
return $this->getResult('GET', $path, $params, $data); |
450
|
|
|
} |
451
|
|
|
|
452
|
|
|
/** |
453
|
|
|
* @param string $path |
454
|
|
|
* @param array $params |
455
|
|
|
* @param array $data |
456
|
|
|
* |
457
|
|
|
* @return Promise |
458
|
|
|
* |
459
|
|
|
* @throws Exception |
460
|
|
|
*/ |
461
|
|
|
protected function doPost($path, array $params = null, array $data = null) |
462
|
|
|
{ |
463
|
|
|
return $this->getResult('POST', $path, $params, $data); |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
/** |
467
|
|
|
* @param string $path |
468
|
|
|
* @param array $params |
469
|
|
|
* @param array $data |
470
|
|
|
* |
471
|
|
|
* @return Promise |
472
|
|
|
* |
473
|
|
|
* @throws Exception |
474
|
|
|
*/ |
475
|
|
|
protected function doPut($path, array $params = null, array $data = null) |
476
|
|
|
{ |
477
|
|
|
return $this->getResult('PUT', $path, $params, $data); |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
/** |
481
|
|
|
* @param string $path |
482
|
|
|
* @param array $params |
483
|
|
|
* @param array $data |
484
|
|
|
* |
485
|
|
|
* @return Promise |
486
|
|
|
* |
487
|
|
|
* @throws Exception |
488
|
|
|
*/ |
489
|
|
|
protected function doDelete($path, array $params = null, array $data = null) |
490
|
|
|
{ |
491
|
|
|
return $this->getResult('DELETE', $path, $params, $data); |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
/** |
495
|
|
|
* Process the request |
496
|
|
|
* |
497
|
|
|
* @param string $method |
498
|
|
|
* @param string $path |
499
|
|
|
* @param array $params |
500
|
|
|
* @param array $data |
501
|
|
|
* |
502
|
|
|
* @return Promise array i.e. [success => true|false, [errors => ['message' => 'this is broken', ..]]] |
503
|
|
|
* |
504
|
|
|
* @throws Exception |
505
|
|
|
*/ |
506
|
|
|
protected function getResult($method, $path, array $params = null, array $data = null) |
507
|
|
|
{ |
508
|
|
|
if ($params === null) { |
509
|
|
|
$params = []; |
510
|
|
|
} |
511
|
|
|
$options = [ |
512
|
|
|
'query' => $this->preParseParams($params), |
513
|
|
|
]; |
514
|
|
|
if (!empty($data)) { |
515
|
|
|
$options['json'] = $data; |
516
|
|
|
} |
517
|
|
|
|
518
|
|
|
return $this->call($method, [$path, $options])->then(function (ResponseInterface $response) { |
519
|
|
|
|
520
|
|
|
return $this->parseResult($response->getBody(), $response->getStatusCode()); |
521
|
|
|
|
522
|
|
|
})->otherwise(function (\Exception $ex) { |
523
|
|
|
|
524
|
|
|
// GuzzleHttp\Exception\ClientException |
525
|
|
|
// Communibase\Exception |
526
|
|
|
|
527
|
|
|
if ($ex instanceof ClientException) { |
528
|
|
|
|
529
|
|
|
if ($ex->getResponse()->getStatusCode() !== 200) { |
530
|
|
|
throw new Exception( |
531
|
|
|
$ex->getMessage(), |
532
|
|
|
$ex->getResponse()->getStatusCode(), |
533
|
|
|
null, |
534
|
|
|
[] |
535
|
|
|
); |
536
|
|
|
} |
537
|
|
|
|
538
|
|
|
} |
539
|
|
|
|
540
|
|
|
throw $ex; |
541
|
|
|
|
542
|
|
|
}); |
543
|
|
|
|
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
/** |
547
|
|
|
* @param array $params |
548
|
|
|
* |
549
|
|
|
* @return mixed |
550
|
|
|
*/ |
551
|
|
|
private function preParseParams(array $params) |
552
|
|
|
{ |
553
|
|
|
if (!array_key_exists('fields', $params) || !is_array($params['fields'])) { |
554
|
|
|
return $params; |
555
|
|
|
} |
556
|
|
|
|
557
|
|
|
$fields = []; |
558
|
|
|
foreach ($params['fields'] as $index => $field) { |
559
|
|
|
if (!is_numeric($index)) { |
560
|
|
|
$fields[$index] = $field; |
561
|
|
|
continue; |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
$modifier = 1; |
565
|
|
|
$firstChar = substr($field, 0, 1); |
566
|
|
|
if ($firstChar == '+' || $firstChar == '-') { |
567
|
|
|
$modifier = $firstChar == '+' ? 1 : 0; |
568
|
|
|
$field = substr($field, 1); |
569
|
|
|
} |
570
|
|
|
$fields[$field] = $modifier; |
571
|
|
|
} |
572
|
|
|
$params['fields'] = $fields; |
573
|
|
|
|
574
|
|
|
return $params; |
575
|
|
|
} |
576
|
|
|
|
577
|
|
|
/** |
578
|
|
|
* Parse the Communibase result and if necessary throw an exception |
579
|
|
|
* |
580
|
|
|
* @param string $response |
581
|
|
|
* @param int $httpCode |
582
|
|
|
* |
583
|
|
|
* @return array |
584
|
|
|
* |
585
|
|
|
* @throws Exception |
586
|
|
|
*/ |
587
|
|
|
private function parseResult($response, $httpCode) |
588
|
|
|
{ |
589
|
|
|
$result = json_decode($response, true); |
590
|
|
|
|
591
|
|
|
if (is_array($result)) { |
592
|
|
|
return $result; |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
throw new Exception('"' . $this->getLastJsonError() . '" in ' . $response, $httpCode); |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
/** |
599
|
|
|
* Error message based on the most recent JSON error. |
600
|
|
|
* |
601
|
|
|
* @see http://nl1.php.net/manual/en/function.json-last-error.php |
602
|
|
|
* |
603
|
|
|
* @return string |
604
|
|
|
*/ |
605
|
|
|
private function getLastJsonError() |
606
|
|
|
{ |
607
|
|
|
$jsonLastError = json_last_error(); |
608
|
|
|
$messages = [ |
609
|
|
|
JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', |
610
|
|
|
JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch', |
611
|
|
|
JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', |
612
|
|
|
JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', |
613
|
|
|
JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded', |
614
|
|
|
]; |
615
|
|
|
|
616
|
|
|
return (isset($messages[$jsonLastError]) ? $messages[$jsonLastError] : 'Empty response received'); |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
/** |
620
|
|
|
* @param string $id |
621
|
|
|
* |
622
|
|
|
* @return bool |
623
|
|
|
*/ |
624
|
|
|
public static function isIdValid($id) |
625
|
|
|
{ |
626
|
|
|
if (empty($id)) { |
627
|
|
|
return false; |
628
|
|
|
} |
629
|
|
|
|
630
|
|
|
if (preg_match('#[0-9a-fA-F]{24}#', $id) === 0) { |
631
|
|
|
return false; |
632
|
|
|
} |
633
|
|
|
|
634
|
|
|
return true; |
635
|
|
|
} |
636
|
|
|
|
637
|
|
|
/** |
638
|
|
|
* Generate a Communibase compatible ID, that consists of: |
639
|
|
|
* |
640
|
|
|
* a 4-byte timestamp, |
641
|
|
|
* a 3-byte machine identifier, |
642
|
|
|
* a 2-byte process id, and |
643
|
|
|
* a 3-byte counter, starting with a random value. |
644
|
|
|
* |
645
|
|
|
* @return string |
646
|
|
|
*/ |
647
|
|
|
public static function generateId() |
648
|
|
|
{ |
649
|
|
|
static $inc = 0; |
650
|
|
|
|
651
|
|
|
$ts = pack('N', time()); |
652
|
|
|
$m = substr(md5(gethostname()), 0, 3); |
653
|
|
|
$pid = pack('n', 1); //posix_getpid() |
654
|
|
|
$trail = substr(pack('N', $inc++), 1, 3); |
655
|
|
|
|
656
|
|
|
$bin = sprintf("%s%s%s%s", $ts, $m, $pid, $trail); |
657
|
|
|
$id = ''; |
658
|
|
|
for ($i = 0; $i < 12; $i++) { |
659
|
|
|
$id .= sprintf("%02X", ord($bin[$i])); |
660
|
|
|
} |
661
|
|
|
|
662
|
|
|
return strtolower($id); |
663
|
|
|
} |
664
|
|
|
|
665
|
|
|
/** |
666
|
|
|
* Add extra headers to be added to each request |
667
|
|
|
* |
668
|
|
|
* @see http://php.net/manual/en/function.header.php |
669
|
|
|
* |
670
|
|
|
* @param array $extraHeaders |
671
|
|
|
*/ |
672
|
|
|
public function addExtraHeaders(array $extraHeaders) |
673
|
|
|
{ |
674
|
|
|
$this->extraHeaders = array_change_key_case($extraHeaders, CASE_LOWER); |
675
|
|
|
} |
676
|
|
|
|
677
|
|
|
/** |
678
|
|
|
* @param QueryLogger $logger |
679
|
|
|
*/ |
680
|
|
|
public function setQueryLogger(QueryLogger $logger) |
681
|
|
|
{ |
682
|
|
|
$this->logger = $logger; |
683
|
|
|
} |
684
|
|
|
|
685
|
|
|
/** |
686
|
|
|
* @return QueryLogger |
687
|
|
|
*/ |
688
|
|
|
public function getQueryLogger() |
689
|
|
|
{ |
690
|
|
|
return $this->logger; |
691
|
|
|
} |
692
|
|
|
|
693
|
|
|
/** |
694
|
|
|
* @return \GuzzleHttp\Client |
695
|
|
|
* @throws Exception |
696
|
|
|
*/ |
697
|
|
|
protected function getClient() |
698
|
|
|
{ |
699
|
|
|
if ($this->client instanceof ClientInterface) { |
700
|
|
|
return $this->client; |
701
|
|
|
} |
702
|
|
|
|
703
|
|
|
if (empty($this->apiKey)) { |
704
|
|
|
throw new Exception('Use of connector not possible without API key', Exception::INVALID_API_KEY); |
705
|
|
|
} |
706
|
|
|
|
707
|
|
|
$this->client = new \GuzzleHttp\Client([ |
708
|
|
|
'base_uri' => $this->serviceUrl, |
709
|
|
|
'headers' => array_merge($this->extraHeaders, [ |
710
|
|
|
'User-Agent' => 'Connector-PHP/2', |
711
|
|
|
'X-Api-Key' => $this->apiKey, |
712
|
|
|
]) |
713
|
|
|
]); |
714
|
|
|
|
715
|
|
|
return $this->client; |
716
|
|
|
} |
717
|
|
|
|
718
|
|
|
/** |
719
|
|
|
* @param string $method |
720
|
|
|
* @param array $arguments |
721
|
|
|
* |
722
|
|
|
* @return Promise |
723
|
|
|
* |
724
|
|
|
* @throws Exception |
725
|
|
|
*/ |
726
|
|
|
private function call($method, array $arguments) |
727
|
|
|
{ |
728
|
|
|
if (isset($this->extraHeaders['host'])) { |
729
|
|
|
$arguments[1]['headers']['Host'] = $this->extraHeaders['host']; |
730
|
|
|
} |
731
|
|
|
|
732
|
|
|
$idx = null; // the query index |
733
|
|
|
if ($this->getQueryLogger()) { |
734
|
|
|
$idx = $this->getQueryLogger()->startQuery($method . ' ' . reset($arguments), $arguments); |
735
|
|
|
} |
736
|
|
|
|
737
|
|
|
$promise = call_user_func_array([$this->getClient(), $method . 'Async'], $arguments); |
738
|
|
|
/* @var Promise $promise */ |
739
|
|
|
return $promise->then(function ($response) use ($idx) { |
740
|
|
|
|
741
|
|
|
if ($this->getQueryLogger()) { |
742
|
|
|
$this->getQueryLogger()->stopQuery($idx); |
743
|
|
|
} |
744
|
|
|
|
745
|
|
|
return $response; |
746
|
|
|
}); |
747
|
|
|
} |
748
|
|
|
|
749
|
|
|
} |
750
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.