Completed
Push — async-first ( bfee5e )
by
unknown
02:05
created

Connector::search()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 1
Metric Value
c 6
b 0
f 1
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 3
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
     * The official service URI; can be overridden via the constructor
35
     *
36
     * @var string
37
     */
38
    const SERVICE_PRODUCTION_URL = 'https://api.communibase.nl/0.1/';
39
40
    /**
41
     * The API key which is to be used for the api.
42
     * Is required to be set via the constructor.
43
     *
44
     * @var string
45
     */
46
    private $apiKey;
47
48
    /**
49
     * The url which is to be used for this connector. Defaults to the production url.
50
     * Can be set via the constructor.
51
     *
52
     * @var string
53
     */
54
    private $serviceUrl;
55
56
    /**
57
     * @var array of extra headers to send with each request
58
     */
59
    private $extraHeaders = [];
60
61
    /**
62
     * @var QueryLogger
63
     */
64
    private $logger;
65
66
    /**
67
     * @var ClientInterface
68
     */
69
    private $client;
70
71
    /**
72
     * Create a new Communibase Connector instance based on the given api-key and possible serviceUrl
73
     *
74
     * @param string $apiKey The API key for Communibase
75
     * @param string $serviceUrl The Communibase API endpoint; defaults to self::SERVICE_PRODUCTION_URL
76
     * @param ClientInterface $client An optional GuzzleHttp Client (or Interface for mocking)
77
     */
78
    public function __construct(
79
            $apiKey,
80
            $serviceUrl = self::SERVICE_PRODUCTION_URL,
81
            ClientInterface $client = null
82
    ) {
83
        $this->apiKey = $apiKey;
84
        $this->serviceUrl = $serviceUrl;
85
        $this->client = $client;
86
    }
87
88
    /**
89
     * Returns an array that has all the fields according to the definition in Communibase.
90
     *
91
     * @param string $entityType
92
     *
93
     * @return Promise of result
94
     *
95
     * @throws Exception
96
     */
97
    public function getTemplate($entityType)
98
    {
99
        $params = [
100
            'fields' => 'attributes.title',
101
            'limit' => 1,
102
        ];
103
104
        return $this->search('EntityType', ['title' => $entityType], $params)->then(function ($definition) {
105
            return array_fill_keys(array_merge(['_id'], array_column($definition[0]['attributes'], 'title')), null);
106
        });
107
    }
108
109
    /**
110
     * Get a single Entity by its id
111
     *
112
     * @param string $entityType
113
     * @param string $id
114
     * @param array $params (optional)
115
     *
116
     * @return Promise of result
117
     *
118
     * @throws Exception
119
     */
120
    public function getById($entityType, $id, array $params = [])
121
    {
122
        if (empty($id)) {
123
            throw new Exception('Id is empty');
124
        }
125
        if (!$this->isIdValid($id)) {
126
            throw new Exception('Id is invalid, please use a correctly formatted id');
127
        }
128
129
        return $this->doGet($entityType . '.json/crud/' . $id, $params);
130
    }
131
132
    /**
133
     * NOTE not yet async
134
     *
135
     * Get a single Entity by a ref-string
136
     *
137
     * @param string $ref
138
     * @param array $parentEntity (optional)
139
     *
140
     * @return array the referred Entity data
141
     *
142
     * @throws Exception
143
     */
144
    public function getByRef($ref, array $parentEntity = [])
145
    {
146
        $refParts = explode('.', $ref);
147
        if ($refParts[0] !== 'parent') {
148
            $entityParts = explode('|', $refParts[0]);
149
            $parentEntity = $this->getById($entityParts[0], $entityParts[1]);
150
        }
151
        if (empty($refParts[1])) {
152
            return $parentEntity;
0 ignored issues
show
Bug Compatibility introduced by
The expression return $parentEntity; of type GuzzleHttp\Promise\Promise|array is incompatible with the return type declared by the interface Communibase\ConnectorInterface::getByRef of type GuzzleHttp\Promise\Promise as it can also be of type array which is not included in this return type.
Loading history...
153
        }
154
        $propertyParts = explode('|', $refParts[1]);
155
        foreach ($parentEntity[$propertyParts[0]] as $subEntity) {
156
            if ($subEntity['_id'] === $propertyParts[1]) {
157
                return $subEntity;
158
            }
159
        }
160
        throw new Exception('Could not find the referred Entity');
161
    }
162
163
    /**
164
     * Get an array of entities by their ids
165
     *
166
     * @param string $entityType
167
     * @param array $ids
168
     * @param array $params (optional)
169
     *
170
     * @return Promise of result
171
     */
172
    public function getByIds($entityType, array $ids, array $params = [])
173
    {
174
        $validIds = array_values(array_unique(array_filter($ids, [$this, 'isIdValid'])));
175
176
        if (empty($validIds)) {
177
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array(); (array) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::getByIds of type GuzzleHttp\Promise\Promise.

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:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
178
        }
179
180
        $doSortByIds = empty($params['sort']);
181
182
        return $this->search($entityType, ['_id' => ['$in' => $validIds]], $params)->then(function ($results) use ($doSortByIds, $validIds) {
183
            if (!$doSortByIds) {
184
                return $results;
185
            }
186
187
            $flipped = array_flip($validIds);
188
            foreach ($results as $result) {
189
                $flipped[$result['_id']] = $result;
190
            }
191
            return array_filter(array_values($flipped), function ($result) {
192
                return is_array($result) && !empty($result);
193
            });
194
        });
195
    }
196
197
    /**
198
     * Get all entities of a certain type
199
     *
200
     * @param string $entityType
201
     * @param array $params (optional)
202
     *
203
     * @return Promise of result
204
     */
205
    public function getAll($entityType, array $params = [])
206
    {
207
        return $this->doGet($entityType . '.json/crud/', $params);
208
    }
209
210
    /**
211
     * Get result entityIds of a certain search
212
     *
213
     * @param string $entityType
214
     * @param array $selector (optional)
215
     * @param array $params (optional)
216
     *
217
     * @return Promise of result
218
     */
219
    public function getIds($entityType, array $selector = [], array $params = [])
220
    {
221
        $params['fields'] = '_id';
222
223
        return $this->search($entityType, $selector, $params)->then(function ($results) {
224
            return array_column($results, '_id');
225
        });
226
    }
227
228
    /**
229
     * Get the id of an entity based on a search
230
     *
231
     * @param string $entityType i.e. Person
232
     * @param array $selector (optional) i.e. ['firstName' => 'Henk']
233
     *
234
     * @return Promise of result
235
     */
236
    public function getId($entityType, array $selector = [])
237
    {
238
        $params = ['limit' => 1];
239
        $ids = (array)$this->getIds($entityType, $selector, $params);
240
241
        return array_shift($ids);
242
    }
243
244
    /**
245
     * Returns an array of the history for the entity with the following format:
246
     *
247
     * <code>
248
     *  [
249
     *        [
250
     *            'updatedBy' => '', // name of the user
251
     *            'updatedAt' => '', // a string according to the DateTime::ISO8601 format
252
     *            '_id' => '', // the ID of the entity which can ge fetched seperately
253
     *        ],
254
     *        ...
255
     * ]
256
     * </code>
257
     *
258
     * @param string $entityType
259
     * @param string $id
260
     *
261
     * @return Promise of result
262
     *
263
     * @throws Exception
264
     */
265
    public function getHistory($entityType, $id)
266
    {
267
        return $this->doGet($entityType . '.json/history/' . $id);
268
    }
269
270
    /**
271
     * Search for the given entity by optional passed selector/params
272
     *
273
     * @param string $entityType
274
     * @param array $querySelector
275
     * @param array $params (optional)
276
     *
277
     * @return Promise of result
278
     *
279
     * @throws Exception
280
     */
281
    public function search($entityType, array $querySelector, array $params = [])
282
    {
283
        return $this->doPost($entityType . '.json/search', $params, $querySelector);
284
    }
285
286
    /**
287
     * This will save an entity in Communibase. When a _id-field is found, this entity will be updated
288
     *
289
     * NOTE: When updating, depending on the Entity, you may need to include all fields.
290
     *
291
     * @param string $entityType
292
     * @param array $properties - the to-be-saved entity data
293
     *
294
     * @returns Promise of result
295
     *
296
     * @throws Exception
297
     */
298
    public function update($entityType, array $properties)
299
    {
300
        $isNew = empty($properties['_id']);
301
302
        return $this->{$isNew ? 'doPost' : 'doPut'}(
303
            $entityType . '.json/crud/' . ($isNew ? '' : $properties['_id']),
304
            [],
305
            $properties
306
        );
307
    }
308
309
    /**
310
     * Finalize an invoice by adding an invoiceNumber to it.
311
     * Besides, invoice items will receive a "generalLedgerAccountNumber".
312
     * This number will be unique and sequential within the "daybook" of the invoice.
313
     *
314
     * NOTE: this is Invoice specific
315
     *
316
     * @param string $entityType
317
     * @param string $id
318
     *
319
     * @return Promise of result
320
     *
321
     * @throws Exception
322
     */
323
    public function finalize($entityType, $id)
324
    {
325
        if ($entityType !== 'Invoice') {
326
            throw new Exception('Cannot call finalize on ' . $entityType);
327
        }
328
329
        return $this->doPost($entityType . '.json/finalize/' . $id);
330
    }
331
332
    /**
333
     * Delete something from Communibase
334
     *
335
     * @param string $entityType
336
     * @param string $id
337
     *
338
     * @return Promise of result
339
     */
340
    public function destroy($entityType, $id)
341
    {
342
        return $this->doDelete($entityType . '.json/crud/' . $id);
343
    }
344
345
    /**
346
     * Get the binary contents of a file by its ID
347
     *
348
     * NOTE: for meta-data like filesize and mimetype, one can use the getById()-method.
349
     *
350
     * @param string $id id string for the file-entity
351
     *
352
     * @return StreamInterface Binary contents of the file. Since the stream can be made a string this works like a charm!
353
     *
354
     * @throws Exception
355
     */
356
    public function getBinary($id)
357
    {
358
        if (!$this->isIdValid($id)) {
359
            throw new Exception('Invalid $id passed. Please provide one.');
360
        }
361
362
        return $this->call('get', ['File.json/binary/' . $id])->then(function (ResponseInterface $response) {
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->call('get'...esponse->getBody(); }); (GuzzleHttp\Promise\PromiseInterface) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::getBinary of type Psr\Http\Message\StreamInterface.

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:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
363
            return $response->getBody();
364
        });
365
    }
366
367
    /**
368
     * Uploads the contents of the resource (this could be a file handle) to Communibase
369
     *
370
     * @param StreamInterface $resource
371
     * @param string $name
372
     * @param string $destinationPath
373
     * @param string $id
374
     *
375
     * @return array|mixed
376
     * @throws Exception
377
     */
378
    public function updateBinary(StreamInterface $resource, $name, $destinationPath, $id = '')
379
    {
380
        $metaData = ['path' => $destinationPath];
381
        if (empty($id)) {
382
            $options = [
383
                'multipart' => [
384
                    [
385
                        'name' => 'File',
386
                        'filename' => $name,
387
                        'contents' => $resource
388
                    ],
389
                    [
390
                        'name' => 'metadata',
391
                        'contents' => json_encode($metaData),
392
                    ]
393
                ]
394
            ];
395
396
            return $this->call('post', ['File.json/binary', $options])->then(function (ResponseInterface $response) {
397
                return $this->parseResult($response->getBody(), $response->getStatusCode());
398
            });
399
400
        }
401
402
        return $this->doPut('File.json/crud/' . $id, [], [
403
                'filename' => $name,
404
                'length' => $resource->getSize(),
405
                'uploadDate' => date('c'),
406
                'metadata' => $metaData,
407
                'content' => base64_encode($resource->getContents()),
408
        ]);
409
    }
410
411
    /**
412
     * MAGIC for making async sync
413
     *
414
     * @param string $name
415
     * @param array $arguments
416
     *
417
     * @return mixed
418
     */
419
    public function __call($name, $arguments)
420
    {
421
        if (preg_match('#(.*)Sync$#', $name, $matches)) {
422
            if (is_callable([$this, $matches[1]])) {
423
                $promise = call_user_func_array([$this, $matches[1]], $arguments);
424
425
                /* @var Promise $promise */
426
                return $promise->wait();
427
            }
428
        }
429
        return null;
430
    }
431
432
    /**
433
     * @param string $path
434
     * @param array $params
435
     * @param array $data
436
     *
437
     * @return Promise
438
     *
439
     * @throws Exception
440
     */
441
    protected function doGet($path, array $params = null, array $data = null)
442
    {
443
        return $this->getResult('GET', $path, $params, $data);
444
    }
445
446
    /**
447
     * @param string $path
448
     * @param array $params
449
     * @param array $data
450
     *
451
     * @return Promise
452
     *
453
     * @throws Exception
454
     */
455
    protected function doPost($path, array $params = null, array $data = null)
456
    {
457
        return $this->getResult('POST', $path, $params, $data);
458
    }
459
460
    /**
461
     * @param string $path
462
     * @param array $params
463
     * @param array $data
464
     *
465
     * @return Promise
466
     *
467
     * @throws Exception
468
     */
469
    protected function doPut($path, array $params = null, array $data = null)
470
    {
471
        return $this->getResult('PUT', $path, $params, $data);
472
    }
473
474
    /**
475
     * @param string $path
476
     * @param array $params
477
     * @param array $data
478
     *
479
     * @return Promise
480
     *
481
     * @throws Exception
482
     */
483
    protected function doDelete($path, array $params = null, array $data = null)
484
    {
485
        return $this->getResult('DELETE', $path, $params, $data);
486
    }
487
488
    /**
489
     * Process the request
490
     *
491
     * @param string $method
492
     * @param string $path
493
     * @param array $params
494
     * @param array $data
495
     *
496
     * @return Promise array i.e. [success => true|false, [errors => ['message' => 'this is broken', ..]]]
497
     *
498
     * @throws Exception
499
     */
500
    protected function getResult($method, $path, array $params = null, array $data = null)
501
    {
502
        if ($params === null) {
503
            $params = [];
504
        }
505
        $options = [
506
            'query' => $this->preParseParams($params),
507
        ];
508
        if (!empty($data)) {
509
            $options['json'] = $data;
510
        }
511
512
        return $this->call($method, [$path, $options])->then(function (ResponseInterface $response) {
513
514
            return $this->parseResult($response->getBody(), $response->getStatusCode());
515
516
        })->otherwise(function (\Exception $ex) {
517
518
            // GuzzleHttp\Exception\ClientException
519
            // Communibase\Exception
520
521
            if ($ex instanceof ClientException) {
522
523
                if ($ex->getResponse()->getStatusCode() !== 200) {
524
                    throw new Exception(
525
                        $ex->getMessage(),
526
                        $ex->getResponse()->getStatusCode(),
527
                        null,
528
                        []
529
                    );
530
                }
531
532
            }
533
534
            throw $ex;
535
536
        });
537
538
    }
539
540
    /**
541
     * @param array $params
542
     *
543
     * @return mixed
544
     */
545
    private function preParseParams(array $params)
546
    {
547
        if (!array_key_exists('fields', $params) || !is_array($params['fields'])) {
548
            return $params;
549
        }
550
551
        $fields = [];
552
        foreach ($params['fields'] as $index => $field) {
553
            if (!is_numeric($index)) {
554
                $fields[$index] = $field;
555
                continue;
556
            }
557
558
            $modifier = 1;
559
            $firstChar = substr($field, 0, 1);
560
            if ($firstChar == '+' || $firstChar == '-') {
561
                $modifier = $firstChar == '+' ? 1 : 0;
562
                $field = substr($field, 1);
563
            }
564
            $fields[$field] = $modifier;
565
        }
566
        $params['fields'] = $fields;
567
568
        return $params;
569
    }
570
571
    /**
572
     * Parse the Communibase result and if necessary throw an exception
573
     *
574
     * @param string $response
575
     * @param int $httpCode
576
     *
577
     * @return array
578
     *
579
     * @throws Exception
580
     */
581
    private function parseResult($response, $httpCode)
582
    {
583
        $result = json_decode($response, true);
584
585
        if (is_array($result)) {
586
            return $result;
587
        }
588
589
        throw new Exception('"' . $this->getLastJsonError() . '" in ' . $response, $httpCode);
590
    }
591
592
    /**
593
     * Error message based on the most recent JSON error.
594
     *
595
     * @see http://nl1.php.net/manual/en/function.json-last-error.php
596
     *
597
     * @return string
598
     */
599
    private function getLastJsonError()
600
    {
601
        $jsonLastError = json_last_error();
602
        $messages = [
603
                JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
604
                JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch',
605
                JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
606
                JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
607
                JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded',
608
        ];
609
610
        return (isset($messages[$jsonLastError]) ? $messages[$jsonLastError] : 'Empty response received');
611
    }
612
613
    /**
614
     * @param string $id
615
     *
616
     * @return bool
617
     */
618
    public static function isIdValid($id)
619
    {
620
        if (empty($id)) {
621
            return false;
622
        }
623
624
        if (preg_match('#[0-9a-fA-F]{24}#', $id) === 0) {
625
            return false;
626
        }
627
628
        return true;
629
    }
630
631
    /**
632
     * Generate a Communibase compatible ID, that consists of:
633
     *
634
     * a 4-byte timestamp,
635
     * a 3-byte machine identifier,
636
     * a 2-byte process id, and
637
     * a 3-byte counter, starting with a random value.
638
     *
639
     * @return string
640
     */
641
    public static function generateId()
642
    {
643
        static $inc = 0;
644
645
        $ts = pack('N', time());
646
        $m = substr(md5(gethostname()), 0, 3);
647
        $pid = pack('n', 1); //posix_getpid()
648
        $trail = substr(pack('N', $inc++), 1, 3);
649
650
        $bin = sprintf("%s%s%s%s", $ts, $m, $pid, $trail);
651
        $id = '';
652
        for ($i = 0; $i < 12; $i++) {
653
            $id .= sprintf("%02X", ord($bin[$i]));
654
        }
655
656
        return strtolower($id);
657
    }
658
659
    /**
660
     * Add extra headers to be added to each request
661
     *
662
     * @see http://php.net/manual/en/function.header.php
663
     *
664
     * @param array $extraHeaders
665
     */
666
    public function addExtraHeaders(array $extraHeaders)
667
    {
668
        $this->extraHeaders = array_change_key_case($extraHeaders, CASE_LOWER);
669
    }
670
671
    /**
672
     * @param QueryLogger $logger
673
     */
674
    public function setQueryLogger(QueryLogger $logger)
675
    {
676
        $this->logger = $logger;
677
    }
678
679
    /**
680
     * @return QueryLogger
681
     */
682
    public function getQueryLogger()
683
    {
684
        return $this->logger;
685
    }
686
687
    /**
688
     * @return \GuzzleHttp\Client
689
     * @throws Exception
690
     */
691
    protected function getClient()
692
    {
693
        if ($this->client instanceof ClientInterface) {
694
            return $this->client;
695
        }
696
697
        if (empty($this->apiKey)) {
698
            throw new Exception('Use of connector not possible without API key', Exception::INVALID_API_KEY);
699
        }
700
701
        $this->client = new \GuzzleHttp\Client([
702
            'base_uri' => $this->serviceUrl,
703
            'headers' => array_merge($this->extraHeaders, [
704
                'User-Agent' => 'Connector-PHP/2',
705
                'X-Api-Key' => $this->apiKey,
706
            ])
707
        ]);
708
709
        return $this->client;
710
    }
711
712
    /**
713
     * @param string $method
714
     * @param array $arguments
715
     *
716
     * @return Promise
717
     *
718
     * @throws Exception
719
     */
720
    private function call($method, array $arguments)
721
    {
722
        if (isset($this->extraHeaders['host'])) {
723
            $arguments[1]['headers']['Host'] = $this->extraHeaders['host'];
724
        }
725
726
        $idx = null; // the query index
727
        if ($this->getQueryLogger()) {
728
            $idx = $this->getQueryLogger()->startQuery($method . ' ' . reset($arguments), $arguments);
729
        }
730
731
        $promise = call_user_func_array([$this->getClient(), $method . 'Async'], $arguments);
732
        /* @var Promise $promise */
733
        return $promise->then(function ($response) use ($idx) {
734
735
            if ($this->getQueryLogger()) {
736
                $this->getQueryLogger()->stopQuery($idx);
737
            }
738
739
            return $response;
740
        });
741
    }
742
743
}
744