Completed
Pull Request — master (#22)
by
unknown
04:12 queued 02:09
created

Connector::searchAsync()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
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 non-async method versions:
17
 *
18
 * @method string getTemplate(string $entityType) Returns all the fields according to the definition.
19
 * @method array getById(string $entityType, string $id) Get an entity by id
20
 * @method array getByIds(string $entityType, array $ids, array $params = []) Get an array of entities by their ids
21
 * @method array getAll(string $entityType, array $params) Get all entities of a certain type
22
 * @method array getId(string $entityType, array $selector) Get the id of an entity based on a search
23
 * @method array getHistory(string $entityType, string $id) Returns an array of the history for the entity
24
 * @method array destroy(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 getTemplateAsync($entityType)
98
    {
99
        $params = [
100
            'fields' => 'attributes.title',
101
            'limit' => 1,
102
        ];
103
104
        return $this->searchAsync('EntityType', ['title' => $entityType], $params)->then(function ($definition) {
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->searchAsyn..., 'title')), null); }); (GuzzleHttp\Promise\PromiseInterface) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::getTemplateAsync 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...
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 getByIdAsync($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);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->doGet($ent...crud/' . $id, $params); (GuzzleHttp\Promise\Promise) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::getByIdAsync 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...
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;
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 getByIdsAsync($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 documented by Communibase\Connector::getByIdsAsync 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->searchAsync($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->searchAsyn...y($result); }); }); (GuzzleHttp\Promise\PromiseInterface) is incompatible with the return type declared by the interface Communibase\ConnectorInterface::getByIdsAsync 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...
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 getAllAsync($entityType, array $params = [])
206
    {
207
        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::getAllAsync 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...
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 getIdsAsync($entityType, array $selector = [], array $params = [])
220
    {
221
        $params['fields'] = '_id';
222
223
        return $this->search($entityType, $selector, $params)->then(function ($results) {
0 ignored issues
show
Bug introduced by
The method search() does not exist on Communibase\Connector. Did you maybe mean searchAsync()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

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