Completed
Push — async-first ( 0001b9...fc551c )
by
unknown
03:44
created

Connector::getHistory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 1
Metric Value
c 4
b 0
f 1
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 2
1
<?php
2
namespace Communibase;
3
4
use Communibase\Logging\QueryLogger;
5
use GuzzleHttp\Client;
6
use GuzzleHttp\ClientInterface;
7
use GuzzleHttp\Exception\ClientException;
8
use GuzzleHttp\Promise\Promise;
9
use Psr\Http\Message\ResponseInterface;
10
use Psr\Http\Message\StreamInterface;
11
12
/**
13
 * Communibase (https://communibase.nl) data Connector for PHP
14
 *
15
 * For more information see https://communibase.nl
16
 *
17
 * Following are IDE hints for sync method versions:
18
 *
19
 * @method string getTemplateSync(string $entityType) Returns all the fields according to the definition.
20
 * @method array getByIdSync(string $entityType, string $id) Get an entity by id
21
 * @method array getByIdsSync(string $entityType, array $ids, array $params = []) Get an array of entities by their ids
22
 * @method array getAllSync(string $entityType, array $params) Get all entities of a certain type
23
 * @method array getIdSync(string $entityType, array $selector) Get the id of an entity based on a search
24
 * @method array getHistorySync(string $entityType, string $id) Returns an array of the history for the entity
25
 * @method array destroySync(string $entityType, string $id) Delete something from Communibase
26
 *
27
 * @package Communibase
28
 * @author Kingsquare ([email protected])
29
 * @copyright Copyright (c) Kingsquare BV (http://www.kingsquare.nl)
30
 * @license http://opensource.org/licenses/MIT The MIT License (MIT)
31
 */
32
class Connector implements ConnectorInterface
33
{
34
35
    /**
36
     * The official service URI; can be overridden via the constructor
37
     *
38
     * @var string
39
     */
40
    const SERVICE_PRODUCTION_URL = 'https://api.communibase.nl/0.1/';
41
42
    /**
43
     * The API key which is to be used for the api.
44
     * Is required to be set via the constructor.
45
     *
46
     * @var string
47
     */
48
    private $apiKey;
49
50
    /**
51
     * The url which is to be used for this connector. Defaults to the production url.
52
     * Can be set via the constructor.
53
     *
54
     * @var string
55
     */
56
    private $serviceUrl;
57
58
    /**
59
     * @var array of extra headers to send with each request
60
     */
61
    private $extraHeaders = [];
62
63
    /**
64
     * @var QueryLogger
65
     */
66
    private $logger;
67
68
    /**
69
     * @var ClientInterface
70
     */
71
    private $client;
72
73
    /**
74
     * Create a new Communibase Connector instance based on the given api-key and possible serviceUrl
75
     *
76
     * @param string $apiKey The API key for Communibase
77
     * @param string $serviceUrl The Communibase API endpoint; defaults to self::SERVICE_PRODUCTION_URL
78
     * @param ClientInterface $client An optional GuzzleHttp Client (or Interface for mocking)
79
     */
80
    public function __construct(
81
        $apiKey,
82
        $serviceUrl = self::SERVICE_PRODUCTION_URL,
83
        ClientInterface $client = null
84
    ) {
85
        $this->apiKey = $apiKey;
86
        $this->serviceUrl = $serviceUrl;
87
        $this->client = $client;
88
    }
89
90
    /**
91
     * Returns an array that has all the fields according to the definition in Communibase.
92
     *
93
     * @param string $entityType
94
     *
95
     * @return Promise of result
96
     *
97
     * @throws Exception
98
     */
99
    public function getTemplate($entityType)
100
    {
101
        $params = [
102
            'fields' => 'attributes.title',
103
            'limit' => 1,
104
        ];
105
106
        return $this->search('EntityType', ['title' => $entityType], $params)->then(function ($definition) {
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->search('En..., 'title')), null); }); (GuzzleHttp\Promise\PromiseInterface) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::getTemplate of type array.

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...
107
            return array_fill_keys(array_merge(['_id'], array_column($definition[0]['attributes'], 'title')), null);
108
        });
109
    }
110
111
    /**
112
     * Get a single Entity by its id
113
     *
114
     * @param string $entityType
115
     * @param string $id
116
     * @param array $params (optional)
117
     * @param string|null $version
118
     *
119
     * @return Promise of result
120
     *
121
     * @return array entity
122
     *
123
     * @throws Exception
124
     */
125
    public function getById($entityType, $id, array $params = [], $version = null)
126
    {
127
        if (empty($id)) {
128
            throw new Exception('Id is empty');
129
        }
130
131
        if (!static::isIdValid($id)) {
132
            throw new Exception('Id is invalid, please use a correctly formatted id');
133
        }
134
135
        return ($version === null)
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $version === null...' . $version, $params); (GuzzleHttp\Promise\Promise) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::getById of type array.

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...
136
            ? $this->doGet($entityType . '.json/crud/' . $id, $params)
137
            : $this->doGet($entityType . '.json/history/' . $id . '/' . $version, $params);
138
    }
139
140
    /**
141
     * NOTE not yet async
142
     *
143
     * Get a single Entity by a ref-string
144
     *
145
     * @param array $ref
146
     * @param array $parentEntity (optional)
147
     *
148
     * @return array the referred Entity data
149
     *
150
     * @throws Exception
151
     */
152
    public function getByRef(array $ref, array $parentEntity = [])
153
    {
154
        if (strpos($ref['rootDocumentEntityType'], 'parent') !== false) {
155
            // something with parent
156
            throw new Exception('Not implemented (yet)');
157
        }
158
159
        $document = $parentEntity;
160
        if (empty($document['_id']) || $document['_id'] !== $ref['rootDocumentId']) {
161
            $document = $this->getById($ref['rootDocumentEntityType'], $ref['rootDocumentId']);
162
        }
163
164
        if (count($document) === 0) {
165
            throw new Exception('Invalid document reference (document cannot be found by Id)');
166
        }
167
168
        $container = $document;
169
        foreach ($ref['path'] as $pathInDocument) {
170
            if (!array_key_exists($pathInDocument['field'], $container)) {
171
                throw new Exception('Could not find the path in document');
172
            }
173
            $container = $container[$pathInDocument['field']];
174
            if (empty($pathInDocument['objectId'])) {
175
                continue;
176
            }
177
178
            if (!is_array($container)) {
179
                throw new Exception('Invalid value for path in document');
180
            }
181
            $result = array_filter($container, function ($item) use ($pathInDocument) {
182
                return $item['_id'] === $pathInDocument['objectId'];
183
            });
184
            if (count($result) === 0) {
185
                throw new Exception('Empty result of reference');
186
            }
187
            $container = reset($result);
188
        }
189
        return $container;
190
    }
191
192
    /**
193
     * Get an array of entities by their ids
194
     *
195
     * @param string $entityType
196
     * @param array $ids
197
     * @param array $params (optional)
198
     *
199
     * @return Promise of result
200
     *
201
     * @throws Exception
202
     */
203
    public function getByIds($entityType, array $ids, array $params = [])
204
    {
205
        $validIds = array_values(array_unique(array_filter($ids, [__CLASS__, 'isIdValid'])));
206
207
        if (count($validIds) === 0) {
208
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array(); (array) is incompatible with the return type documented by Communibase\Connector::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...
209
        }
210
211
        $doSortByIds = empty($params['sort']);
212
213
        return $this->search($entityType, ['_id' => ['$in' => $validIds]], $params)->then(function ($results) use ($doSortByIds, $validIds) {
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->search($en...esult) > 0; }); }); (GuzzleHttp\Promise\PromiseInterface) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::getByIds of type array.

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...
214
            if (!$doSortByIds) {
215
                return $results;
216
            }
217
218
            $flipped = array_flip($validIds);
219
            foreach ($results as $result) {
220
                $flipped[$result['_id']] = $result;
221
            }
222
            return array_filter(array_values($flipped), function ($result) {
223
                return is_array($result) && count($result) > 0;
224
            });
225
        });
226
    }
227
228
    /**
229
     * Get all entities of a certain type
230
     *
231
     * @param string $entityType
232
     * @param array $params (optional)
233
     *
234
     * @return Promise of result
235
     *
236
     * @throws Exception
237
     */
238
    public function getAll($entityType, array $params = [])
239
    {
240
        return $this->doGet($entityType . '.json/crud/', $params);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->doGet($ent....json/crud/', $params); (GuzzleHttp\Promise\Promise) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::getAll of type array|null.

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...
241
    }
242
243
    /**
244
     * Get result entityIds of a certain search
245
     *
246
     * @param string $entityType
247
     * @param array $selector (optional)
248
     * @param array $params (optional)
249
     *
250
     * @return Promise of result
251
     *
252
     * @throws Exception
253
     */
254
    public function getIds($entityType, array $selector = [], array $params = [])
255
    {
256
        $params['fields'] = '_id';
257
258
        return $this->search($entityType, $selector, $params)->then(function ($results) {
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->search($en...n($results, '_id'); }); (GuzzleHttp\Promise\PromiseInterface) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::getIds of type array.

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...
259
            return array_column($results, '_id');
260
        });
261
    }
262
263
    /**
264
     * Get the id of an entity based on a search
265
     *
266
     * @param string $entityType i.e. Person
267
     * @param array $selector (optional) i.e. ['firstName' => 'Henk']
268
     *
269
     * @return Promise of result
270
     */
271
    public function getId($entityType, array $selector = [])
272
    {
273
        $params = ['limit' => 1];
274
        $ids = (array)$this->getIds($entityType, $selector, $params);
275
276
        return array_shift($ids);
277
    }
278
279
    /**
280
     * Call the aggregate endpoint with a given set of pipeline definitions:
281
     * E.g. [
282
     * { "$match": { "_id": {"$ObjectId": "52f8fb85fae15e6d0806e7c7"} } },
283
     * { "$unwind": "$participants" },
284
     * { "$group": { "_id": "$_id", "participantCount": { "$sum": 1 } } }
285
     * ]
286
     *
287
     * @see http://docs.mongodb.org/manual/core/aggregation-pipeline/
288
     *
289
     * @param $entityType
290
     * @param array $pipeline
291
     *
292
     * @return Promise of a result
293
     *
294
     * @throws Exception
295
     */
296
    public function aggregate($entityType, array $pipeline)
297
    {
298
        return $this->doPost($entityType . '.json/aggregate', [], $pipeline);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->doPost($en...', array(), $pipeline); (GuzzleHttp\Promise\Promise) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::aggregate of type array.

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...
299
    }
300
301
    /**
302
     * Returns an array of the history for the entity with the following format:
303
     *
304
     * <code>
305
     *  [
306
     *        [
307
     *            'updatedBy' => '', // name of the user
308
     *            'updatedAt' => '', // a string according to the DateTime::ISO8601 format
309
     *            '_id' => '', // the ID of the entity which can ge fetched seperately
310
     *        ],
311
     *        ...
312
     * ]
313
     * </code>
314
     *
315
     * @param string $entityType
316
     * @param string $id
317
     *
318
     * @return Promise of result
319
     *
320
     * @throws Exception
321
     */
322
    public function getHistory($entityType, $id)
323
    {
324
        return $this->doGet($entityType . '.json/history/' . $id);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->doGet($ent....json/history/' . $id); (GuzzleHttp\Promise\Promise) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::getHistory of type array.

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...
325
    }
326
327
    /**
328
     * Search for the given entity by optional passed selector/params
329
     *
330
     * @param string $entityType
331
     * @param array $querySelector
332
     * @param array $params (optional)
333
     *
334
     * @return Promise of result
335
     *
336
     * @throws Exception
337
     */
338
    public function search($entityType, array $querySelector, array $params = [])
339
    {
340
        return $this->doPost($entityType . '.json/search', $params, $querySelector);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->doPost($en...arams, $querySelector); (GuzzleHttp\Promise\Promise) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::search of type array.

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...
341
    }
342
343
    /**
344
     * This will save an entity in Communibase. When a _id-field is found, this entity will be updated
345
     *
346
     * NOTE: When updating, depending on the Entity, you may need to include all fields.
347
     *
348
     * @param string $entityType
349
     * @param array $properties - the to-be-saved entity data
350
     *
351
     * @returns Promise of result
352
     *
353
     * @throws Exception
354
     */
355
    public function update($entityType, array $properties)
356
    {
357
        $isNew = empty($properties['_id']);
358
359
        return $this->{$isNew ? 'doPost' : 'doPut'}(
360
            $entityType . '.json/crud/' . ($isNew ? '' : $properties['_id']),
361
            [],
362
            $properties
363
        );
364
    }
365
366
    /**
367
     * Finalize an invoice by adding an invoiceNumber to it.
368
     * Besides, invoice items will receive a 'generalLedgerAccountNumber'.
369
     * This number will be unique and sequential within the 'daybook' of the invoice.
370
     *
371
     * NOTE: this is Invoice specific
372
     *
373
     * @param string $entityType
374
     * @param string $id
375
     *
376
     * @return Promise of result
377
     *
378
     * @throws Exception
379
     */
380
    public function finalize($entityType, $id)
381
    {
382
        if ($entityType !== 'Invoice') {
383
            throw new Exception('Cannot call finalize on ' . $entityType);
384
        }
385
386
        return $this->doPost($entityType . '.json/finalize/' . $id);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->doPost($en...json/finalize/' . $id); (GuzzleHttp\Promise\Promise) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::finalize of type array.

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...
387
    }
388
389
    /**
390
     * Delete something from Communibase
391
     *
392
     * @param string $entityType
393
     * @param string $id
394
     *
395
     * @return Promise of result
396
     *
397
     * @throws Exception
398
     */
399
    public function destroy($entityType, $id)
400
    {
401
        return $this->doDelete($entityType . '.json/crud/' . $id);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->doDelete($.... '.json/crud/' . $id); (GuzzleHttp\Promise\Promise) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::destroy of type array.

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...
402
    }
403
404
    /**
405
     * Get the binary contents of a file by its ID
406
     *
407
     * NOTE: for meta-data like filesize and mimetype, one can use the getById()-method.
408
     *
409
     * @param string $id id string for the file-entity
410
     *
411
     * @return StreamInterface Binary contents of the file. Since the stream can be made a string this works like a charm!
412
     *
413
     * @throws Exception
414
     */
415
    public function getBinary($id)
416
    {
417
        if (!static::isIdValid($id)) {
418
            throw new Exception('Invalid $id passed. Please provide one.');
419
        }
420
421
        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...
422
            return $response->getBody();
423
        });
424
    }
425
426
    /**
427
     * Uploads the contents of the resource (this could be a file handle) to Communibase
428
     *
429
     * @param StreamInterface $resource
430
     * @param string $name
431
     * @param string $destinationPath
432
     * @param string $id
433
     *
434
     * @return array|mixed
435
     *
436
     * @throws \RuntimeException | Exception
437
     */
438
    public function updateBinary(StreamInterface $resource, $name, $destinationPath, $id = '')
439
    {
440
        $metaData = ['path' => $destinationPath];
441
        if (!empty($id)) {
442
            if (!static::isIdValid($id)) {
443
                throw new Exception('Id is invalid, please use a correctly formatted id');
444
            }
445
446
            return $this->doPut('File.json/crud/' . $id, [], [
447
                'filename' => $name,
448
                'length' => $resource->getSize(),
449
                'uploadDate' => date('c'),
450
                'metadata' => $metaData,
451
                'content' => base64_encode($resource->getContents()),
452
            ]);
453
        }
454
455
        $options = [
456
            'multipart' => [
457
                [
458
                    'name' => 'File',
459
                    'filename' => $name,
460
                    'contents' => $resource
461
                ],
462
                [
463
                    'name' => 'metadata',
464
                    'contents' => json_encode($metaData),
465
                ]
466
            ]
467
        ];
468
469
        return $this->call('post', ['File.json/binary', $options])->then(function (ResponseInterface $response) {
470
            return $this->parseResult($response->getBody(), $response->getStatusCode());
471
        });
472
    }
473
474
    /**
475
     * MAGIC for making sync requests
476
     *
477
     * @param string $name
478
     * @param array $arguments
479
     *
480
     * @return mixed
481
     */
482
    public function __call($name, $arguments)
483
    {
484
        if (preg_match('#(.*)Sync$#', $name, $matches) && is_callable([$this, $matches[1]])) {
485
            $promise = call_user_func_array([$this, $matches[1]], $arguments);
486
487
            /* @var Promise $promise */
488
            return $promise->wait(); // wait for response
489
        }
490
491
        // fallback to known methods
492
        return null;
493
    }
494
495
    /**
496
     * Perform the actual GET
497
     *
498
     * @param string $path
499
     * @param array $params
500
     * @param array $data
501
     *
502
     * @return Promise
503
     *
504
     * @throws Exception
505
     */
506
    protected function doGet($path, array $params = null, array $data = null)
507
    {
508
        return $this->getResult('GET', $path, $params, $data);
509
    }
510
511
    /**
512
     * Perform the actual POST
513
     *
514
     * @param string $path
515
     * @param array $params
516
     * @param array $data
517
     *
518
     * @return Promise
519
     *
520
     * @throws Exception
521
     */
522
    protected function doPost($path, array $params = null, array $data = null)
523
    {
524
        return $this->getResult('POST', $path, $params, $data);
525
    }
526
527
    /**
528
     * Perform the actual PUT
529
     *
530
     * @param string $path
531
     * @param array $params
532
     * @param array $data
533
     *
534
     * @return Promise
535
     *
536
     * @throws Exception
537
     */
538
    protected function doPut($path, array $params = null, array $data = null)
539
    {
540
        return $this->getResult('PUT', $path, $params, $data);
541
    }
542
543
    /**
544
     * Perform the actual DELETE
545
     *
546
     * @param string $path
547
     * @param array $params
548
     * @param array $data
549
     *
550
     * @return Promise
551
     *
552
     * @throws Exception
553
     */
554
    protected function doDelete($path, array $params = null, array $data = null)
555
    {
556
        return $this->getResult('DELETE', $path, $params, $data);
557
    }
558
559
    /**
560
     * Process the request
561
     *
562
     * @param string $method
563
     * @param string $path
564
     * @param array $params
565
     * @param array $data
566
     *
567
     * @return Promise array i.e. [success => true|false, [errors => ['message' => 'this is broken', ..]]]
568
     *
569
     * @throws Exception
570
     */
571
    protected function getResult($method, $path, array $params = null, array $data = null)
572
    {
573
        if ($params === null) {
574
            $params = [];
575
        }
576
        $options = [
577
            'query' => $this->preParseParams($params),
578
        ];
579
        if (!empty($data)) {
580
            $options['json'] = $data;
581
        }
582
583
        return $this->call($method, [$path, $options])->then(function (ResponseInterface $response) {
584
585
            return $this->parseResult($response->getBody(), $response->getStatusCode());
586
587
        })->otherwise(function (\Exception $ex) {
588
589
            // GuzzleHttp\Exception\ClientException
590
            // Communibase\Exception
591
592
            if ($ex instanceof ClientException) {
593
594
                if ($ex->getResponse()->getStatusCode() !== 200) {
595
                    throw new Exception(
596
                        $ex->getMessage(),
597
                        $ex->getResponse()->getStatusCode(),
598
                        null,
599
                        []
600
                    );
601
                }
602
603
            }
604
605
            throw $ex;
606
607
        });
608
609
    }
610
611
    /**
612
     * @param array $params
613
     *
614
     * @return mixed
615
     */
616
    private function preParseParams(array $params)
617
    {
618
        if (!array_key_exists('fields', $params) || !is_array($params['fields'])) {
619
            return $params;
620
        }
621
622
        $fields = [];
623
        foreach ($params['fields'] as $index => $field) {
624
            if (!is_numeric($index)) {
625
                $fields[$index] = $field;
626
                continue;
627
            }
628
629
            $modifier = 1;
630
            $firstChar = substr($field, 0, 1);
631
            if ($firstChar === '+' || $firstChar === '-') {
632
                $modifier = $firstChar === '+' ? 1 : 0;
633
                $field = substr($field, 1);
634
            }
635
            $fields[$field] = $modifier;
636
        }
637
        $params['fields'] = $fields;
638
639
        return $params;
640
    }
641
642
    /**
643
     * Parse the Communibase result and if necessary throw an exception
644
     *
645
     * @param string $response
646
     * @param int $httpCode
647
     *
648
     * @return array
649
     *
650
     * @throws Exception
651
     */
652
    private function parseResult($response, $httpCode)
653
    {
654
        $result = json_decode($response, true);
655
656
        if (is_array($result)) {
657
            return $result;
658
        }
659
660
        throw new Exception('"' . $this->getLastJsonError() . '" in ' . $response, $httpCode);
661
    }
662
663
    /**
664
     * Error message based on the most recent JSON error.
665
     *
666
     * @see http://nl1.php.net/manual/en/function.json-last-error.php
667
     *
668
     * @return string
669
     */
670
    private function getLastJsonError()
671
    {
672
        static $messages = [
673
            JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
674
            JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch',
675
            JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
676
            JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
677
            JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded',
678
        ];
679
        $jsonLastError = json_last_error();
680
681
        return array_key_exists($jsonLastError, $messages) ? $messages[$jsonLastError] : 'Empty response received';
682
    }
683
684
    /**
685
     * Verify the given $id is a valid Communibase string according to format
686
     *
687
     * @param string $id
688
     *
689
     * @return bool
690
     */
691
    public static function isIdValid($id)
692
    {
693
        if (empty($id)) {
694
            return false;
695
        }
696
697
        if (preg_match('#[0-9a-fA-F]{24}#', $id) === 0) {
698
            return false;
699
        }
700
701
        return true;
702
    }
703
704
    /**
705
     * Generate a Communibase compatible ID, that consists of:
706
     *
707
     * a 4-byte timestamp,
708
     * a 3-byte machine identifier,
709
     * a 2-byte process id, and
710
     * a 3-byte counter, starting with a random value.
711
     *
712
     * @return string
713
     */
714
    public static function generateId()
715
    {
716
        static $inc = 0;
717
718
        $ts = pack('N', time());
719
        $m = substr(md5(gethostname()), 0, 3);
720
        $pid = pack('n', 1);
721
        $trail = substr(pack('N', $inc++), 1, 3);
722
723
        $bin = sprintf('%s%s%s%s', $ts, $m, $pid, $trail);
724
        $id = '';
725
        for ($i = 0; $i < 12; $i++) {
726
            $id .= sprintf('%02X', ord($bin[$i]));
727
        }
728
729
        return strtolower($id);
730
    }
731
732
    /**
733
     * Add extra headers to be added to each request
734
     *
735
     * @see http://php.net/manual/en/function.header.php
736
     *
737
     * @param array $extraHeaders
738
     */
739
    public function addExtraHeaders(array $extraHeaders)
740
    {
741
        $this->extraHeaders = array_change_key_case($extraHeaders, CASE_LOWER);
742
    }
743
744
    /**
745
     * @param QueryLogger $logger
746
     */
747
    public function setQueryLogger(QueryLogger $logger)
748
    {
749
        $this->logger = $logger;
750
    }
751
752
    /**
753
     * @return QueryLogger
754
     */
755
    public function getQueryLogger()
756
    {
757
        return $this->logger;
758
    }
759
760
    /**
761
     * @return ClientInterface
762
     *
763
     * @throws Exception
764
     */
765
    protected function getClient()
766
    {
767
        if ($this->client instanceof ClientInterface) {
768
            return $this->client;
769
        }
770
771
        if (empty($this->apiKey)) {
772
            throw new Exception('Use of connector not possible without API key', Exception::INVALID_API_KEY);
773
        }
774
775
        $this->client = new Client([
776
            'base_uri' => $this->serviceUrl,
777
            'headers' => array_merge($this->extraHeaders, [
778
                'User-Agent' => 'Connector-PHP/2',
779
                'X-Api-Key' => $this->apiKey,
780
            ])
781
        ]);
782
783
        return $this->client;
784
    }
785
786
    /**
787
     * Perform the actual call to Communibase
788
     *
789
     * @param string $method
790
     * @param array $arguments
791
     *
792
     * @return Promise
793
     *
794
     * @throws Exception
795
     */
796
    private function call($method, array $arguments)
797
    {
798
        try {
799
800
            /**
801
             * Due to GuzzleHttp not passing a default host header given to the client to _every_ request made by the client
802
             * we manually check to see if we need to add a hostheader to requests.
803
             * When the issue is resolved the foreach can be removed (as the function might even?)
804
             *
805
             * @see https://github.com/guzzle/guzzle/issues/1297
806
             */
807
            if (isset($this->extraHeaders['host'])) {
808
                $arguments[1]['headers']['Host'] = $this->extraHeaders['host'];
809
            }
810
811
            $idx = null; // the query index
812
            if ($this->getQueryLogger()) {
813
                $idx = $this->getQueryLogger()->startQuery($method . ' ' . reset($arguments), $arguments);
814
            }
815
816
            $promise = call_user_func_array([$this->getClient(), $method . 'Async'], $arguments);
817
            /* @var Promise $promise */
818
            return $promise->then(function ($response) use ($idx) {
819
820
                if ($this->getQueryLogger()) {
821
                    $this->getQueryLogger()->stopQuery($idx);
822
                }
823
824
                return $response;
825
            });
826
827
            // try to catch the Guzzle client exception (404's, validation errors etc) and wrap them into a CB exception
828
        } catch (ClientException $e) {
829
830
            $response = json_decode($e->getResponse()->getBody(), true);
831
832
            throw new Exception(
833
                $response['message'],
834
                $response['code'],
835
                $e,
836
                (($_ =& $response['errors']) ?: [])
837
            );
838
839
        }
840
    }
841
842
}
843