Completed
Push — master ( fac198...605f43 )
by Zhmayev
01:28
created

WSClient::entityRetrieveTypeID()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 8.8571
c 0
b 0
f 0
cc 6
eloc 9
nc 5
nop 2
1
<?php
2
3
/**
4
 * Vtiger Web Services PHP Client Library
5
 *
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2015, Zhmayev Yaroslav <[email protected]>
9
 *
10
 * Permission is hereby granted, free of charge, to any person obtaining a copy
11
 * of this software and associated documentation files (the "Software"), to deal
12
 * in the Software without restriction, including without limitation the rights
13
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
 * copies of the Software, and to permit persons to whom the Software is
15
 * furnished to do so, subject to the following conditions:
16
 *
17
 * The above copyright notice and this permission notice shall be included in
18
 * all copies or substantial portions of the Software.
19
 *
20
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26
 * THE SOFTWARE.
27
 *
28
 * @author    Zhmayev Yaroslav <[email protected]>
29
 * @copyright 2015-2016 Zhmayev Yaroslav
30
 * @license   The MIT License (MIT)
31
 */
32
33
namespace Salaros\Vtiger\VTWSCLib;
34
35
use GuzzleHttp\Client;
36
use GuzzleHttp\Exception\RequestException;
37
38
/**
39
 * Vtiger Web Services PHP Client
40
 *
41
 * Class WSClient
42
 * @package Salaros\Vtiger\VTWSCLib
43
 */
44
class WSClient
45
{
46
    // HTTP Client instance
47
    protected $httpClient = null;
48
49
    // Service URL to which client connects to
50
    protected $serviceBaseURL = 'webservice.php';
51
52
    // Webservice login validity
53
    private $serviceServerTime = false;
54
    private $serviceExpireTime = false;
55
    private $serviceToken = false;
56
57
    // Webservice user credentials
58
    private $userName = false;
59
    private $accessKey = false;
60
61
    // Webservice login credentials
62
    private $userID = false;
63
    private $sessionName = false;
64
65
    // Vtiger CRM and WebServices API version
66
    private $apiVersion = false;
67
    private $vtigerVersion = false;
68
69
    // Last operation error information
70
    protected $lastErrorMessage = null;
71
72
    /**
73
     * Class constructor
74
     * @param string $url The URL of the remote WebServices server
75
     */
76
    public function __construct($url)
77
    {
78
        if (!preg_match('/^https?:\/\//i', $url)) {
79
            $url = sprintf('http://%s', $url);
80
        }
81
        if (strripos($url, '/') != (strlen($url)-1)) {
82
            $url .= '/';
83
        }
84
85
        // Gets target URL for WebServices API requests
86
        $this->httpClient = new Client([
87
            'base_uri' => $url
88
        ]);
89
    }
90
91
    /**
92
     * Check if server response contains an error, therefore the requested operation has failed
93
     * @access private
94
     * @param  array $jsonResult Server response object to check for errors
95
     * @return boolean  True if response object contains an error
96
     */
97
    private function checkForError(array $jsonResult)
98
    {
99
        if (isset($jsonResult['success']) && (bool)$jsonResult['success'] === true) {
100
            $this->lastErrorMessage = null;
101
            return false;
102
        }
103
104
        $this->lastErrorMessage = new WSClientError(
105
            $jsonResult['error']['message'],
106
            $jsonResult['error']['code']
107
        );
108
109
        return true;
110
    }
111
112
    /**
113
     * Checks and performs a login operation if requried and repeats login if needed
114
     * @access private
115
     */
116
    private function checkLogin()
117
    {
118
        if (time() <= $this->serviceExpireTime) {
119
            return;
120
        }
121
        $this->login($this->userName, $this->accessKey);
0 ignored issues
show
Documentation introduced by
$this->userName is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
$this->accessKey is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
122
    }
123
124
    /**
125
     * Sends HTTP request to VTiger web service API endpoint
126
     * @access private
127
     * @param  array $requestData HTTP request data
128
     * @param  string $method HTTP request method (GET, POST etc)
129
     * @return array Returns request result object (null in case of failure)
130
     */
131
    private function sendHttpRequest(array $requestData, $method = 'POST')
132
    {
133
        try {
134
            switch ($method) {
135
                case 'GET':
136
                    $response = $this->httpClient->get($this->serviceBaseURL, ['query' => $requestData]);
137
                    break;
138
                case 'POST':
139
                    $response = $this->httpClient->post($this->serviceBaseURL, ['form_params' => $requestData]);
140
                    break;
141
                default:
142
                    $this->lastErrorMessage = new WSClientError("Unknown request type {$method}");
143
                    return null;
144
            }
145
        } catch (RequestException $ex) {
146
            $this->lastError = new WSClientError(
0 ignored issues
show
Bug introduced by
The property lastError does not seem to exist. Did you mean lastErrorMessage?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
147
                $ex->getMessage(),
148
                $ex->getCode()
149
            );
150
            return null;
151
        }
152
153
        $jsonRaw = $response->getBody();
154
        $jsonObj = json_decode($jsonRaw, true);
155
156
        return (!is_array($jsonObj) || $this->checkForError($jsonObj))
157
            ? null
158
            : $jsonObj['result'];
159
    }
160
161
    /**
162
     * Gets a challenge token from the server and stores for future requests
163
     * @access private
164
     * @param  string $username VTiger user name
165
     * @return booleanReturns false in case of failure
166
     */
167
    private function passChallenge($username)
168
    {
169
        $getdata = [
170
            'operation' => 'getchallenge',
171
            'username'  => $username
172
        ];
173
        $result = $this->sendHttpRequest($getdata, 'GET');
174
        
175
        if (!is_array($result) || !isset($result['token'])) {
176
            return false;
177
        }
178
179
        $this->serviceServerTime = $result['serverTime'];
180
        $this->serviceExpireTime = $result['expireTime'];
181
        $this->serviceToken = $result['token'];
182
183
        return true;
184
    }
185
186
    /**
187
     * Login to the server using username and VTiger access key token
188
     * @access public
189
     * @param  string $username VTiger user name
190
     * @param  string $accessKey VTiger access key token (visible on user profile/settings page)
191
     * @return boolean Returns true if login operation has been successful
192
     */
193
    public function login($username, $accessKey)
194
    {
195
        // Do the challenge before loggin in
196
        if ($this->passChallenge($username) === false) {
197
            return false;
198
        }
199
200
        $postdata = [
201
            'operation' => 'login',
202
            'username'  => $username,
203
            'accessKey' => md5($this->serviceToken.$accessKey)
204
        ];
205
206
        $result = $this->sendHttpRequest($postdata);
207
        if (!$result || !is_array($result)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
208
            return false;
209
        }
210
211
        // Backuping logged in user credentials
212
        $this->userName = $username;
0 ignored issues
show
Documentation Bug introduced by
The property $userName was declared of type boolean, but $username is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
213
        $this->accessKey = $accessKey;
0 ignored issues
show
Documentation Bug introduced by
The property $accessKey was declared of type boolean, but $accessKey is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
214
215
        // Session data
216
        $this->sessionName = $result['sessionName'];
217
        $this->userID = $result['userId'];
218
219
        // Vtiger CRM and WebServices API version
220
        $this->apiVersion = $result['version'];
221
        $this->vtigerVersion = $result['vtigerVersion'];
222
223
        return true;
224
    }
225
226
    /**
227
     * Allows you to login using username and password instead of access key (works on some VTige forks)
228
     * @access public
229
     * @param  string $username VTiger user name
230
     * @param  string $password VTiger password (used to access CRM using the standard login page)
231
     * @param  string $accessKey This parameter will be filled with user's VTiger access key
232
     * @return boolean  Returns true if login operation has been successful
233
     */
234
    public function loginPassword($username, $password, &$accessKey = null)
235
    {
236
        // Do the challenge before loggin in
237
        if ($this->passChallenge($username) === false) {
238
            return false;
239
        }
240
241
        $postdata = [
242
            'operation' => 'login_pwd',
243
            'username' => $username,
244
            'password' => $password
245
        ];
246
247
        $result = $this->sendHttpRequest($postdata);
248
        if (!$result || !is_array($result) || count($result) !== 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
249
            return false;
250
        }
251
252
        $accessKey = array_key_exists('accesskey', $result)
253
            ? $result['accesskey']
254
            : $result[0];
255
256
        return $this->login($username, $accessKey);
257
    }
258
259
    /**
260
     * Gets last operation error, if any
261
     * @access public
262
     * @return WSClientError The error object
263
     */
264
    public function getLastError()
265
    {
266
        return $this->lastErrorMessage;
267
    }
268
269
    /**
270
     * Returns the client library version.
271
     * @access public
272
     * @return string Client library version
273
     */
274
    public function getVersion()
275
    {
276
        global $wsclient_version;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
277
        return $wsclient_version;
278
    }
279
280
    /**
281
     * Lists all the Vtiger entity types available through the API
282
     * @access public
283
     * @return array List of entity types
284
     */
285
    public function getTypes()
286
    {
287
        // Perform re-login if required.
288
        $this->checkLogin();
289
290
        $getdata = [
291
            'operation' => 'listtypes',
292
            'sessionName'  => $this->sessionName
293
        ];
294
295
        $result = $this->sendHttpRequest($getdata, 'GET');
296
        $modules = $result['types'];
297
298
        $result = array();
299
        foreach ($modules as $moduleName) {
300
            $result[$moduleName] = ['name' => $moduleName];
301
        }
302
        return $result;
303
    }
304
305
    /**
306
     * Get the type information about a given VTiger entity type.
307
     * @access public
308
     * @param  string $moduleName Name of the module / entity type
309
     * @return array  Result object
310
     */
311
    public function getType($moduleName)
312
    {
313
        // Perform re-login if required.
314
        $this->checkLogin();
315
316
        $getdata = [
317
            'operation' => 'describe',
318
            'sessionName'  => $this->sessionName,
319
            'elementType' => $moduleName
320
        ];
321
322
        return $this->sendHttpRequest($getdata, 'GET');
323
    }
324
325
    /**
326
     * Gets the entity ID prepended with module / entity type ID
327
     * @access private
328
     * @param  string       $moduleName   Name of the module / entity type
329
     * @param  string       $entityID     Numeric entity ID
330
     * @return boolean|string Returns false if it is not possible to retrieve module / entity type ID
331
     */
332
    private function getTypedID($moduleName, $entityID)
333
    {
334
        if (stripos((string)$entityID, 'x') !== false) {
335
            return $entityID;
336
        }
337
338
        if (empty($entityID) || intval($entityID) < 1) {
339
            throw new \Exception('Entity ID must be a valid number');
340
        }
341
342
        $type = $this->getType($moduleName);
343
        if (!$type || !array_key_exists('idPrefix', $type)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
344
            $errorMessage = sprintf("The following module is not installed: %s", $moduleName);
345
            $this->lastErrorMessage = new WSClientError($errorMessage);
346
            return false;
347
        }
348
349
        return "{$type['idPrefix']}x{$entityID}";
350
    }
351
352
    /**
353
     * Invokes custom operation (defined in vtiger_ws_operation table)
354
     * @access public
355
     * @param  string  $operation  Name of the webservice to invoke
356
     * @param  array   [$params = null] Parameter values to operation
357
     * @param  string  [$method   = 'POST'] HTTP request method (GET, POST etc)
358
     * @return array Result object
359
     */
360
    public function invokeOperation($operation, array $params = null, $method = 'POST')
361
    {
362 View Code Duplication
        if (!empty($params) || !is_array($params) || !$this->isAssocArray($params)) {
363
            $this->lastErrorMessage = new WSClientError(
364
                "You have to specified a list of operation parameters, 
365
                but apparently it's not an associative array ('prop' => value), you must fix it!"
366
            );
367
            return false;
368
        }
369
370
        // Perform re-login if required
371
        $this->checkLogin();
372
373
        $requestData = [
374
            'operation' => $operation,
375
            'sessionName' => $this->sessionName
376
        ];
377
378
        if (!empty($params) && is_array($params)) {
379
            $requestData = array_merge($requestData, $params);
380
        }
381
382
        return $this->sendHttpRequest($requestData, $method);
383
    }
384
385
    /**
386
     * VTiger provides a simple query language for fetching data.
387
     * This language is quite similar to select queries in SQL.
388
     * There are limitations, the queries work on a single Module,
389
     * embedded queries are not supported, and does not support joins.
390
     * But this is still a powerful way of getting data from Vtiger.
391
     * Query always limits its output to 100 records,
392
     * Client application can use limit operator to get different records.
393
     * @access public
394
     * @param  string $query SQL-like expression
395
     * @return array  Query results
396
     */
397
    public function query($query)
398
    {
399
        // Perform re-login if required.
400
        $this->checkLogin();
401
402
        // Make sure the query ends with ;
403
        $query = (strripos($query, ';') != strlen($query)-1)
404
            ? trim($query .= ';')
405
            : trim($query);
406
407
        $getdata = [
408
            'operation' => 'query',
409
            'sessionName' => $this->sessionName,
410
            'query' => $query
411
        ];
412
413
        return $this->sendHttpRequest($getdata, 'GET');
414
    }
415
416
    /**
417
     * Retrieves an entity by ID
418
     * @param  string $moduleName The name of the module / entity type
419
     * @param  string $entityID The ID of the entity to retrieve
420
     * @return boolean  Entity data
421
     */
422 View Code Duplication
    public function entityRetrieveByID($moduleName, $entityID)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
423
    {
424
        // Perform re-login if required.
425
        $this->checkLogin();
426
427
        // Preprend so-called moduleid if needed
428
        $entityID = $this->getTypedID($moduleName, $entityID);
429
430
        $getdata = [
431
            'operation' => 'retrieve',
432
            'sessionName' => $this->sessionName,
433
            'id' => $entityID
434
        ];
435
436
        return $this->sendHttpRequest($getdata, 'GET');
437
    }
438
439
    /**
440
     * Uses VTiger queries to retrieve the entity matching a list of constraints
441
     * @param  string  $moduleName   The name of the module / entity type
442
     * @param  array   $params  Data used to find a matching entry
443
     * @return array   $select  The list of fields to select (defaults to SQL-like '*' - all the fields)
444
     * @return int  The matching record
445
     */
446
    public function entityRetrieve($moduleName, array $params, array $select = [])
447
    {
448
        $records = $this->entitiesRetrieve($moduleName, $params, $select, 1);
449
        if (false === $records || !isset($records[0])) {
450
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Salaros\Vtiger\VTWSCLib\WSClient::entityRetrieve 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...
451
        }
452
        return $records[0];
453
    }
454
455
    /**
456
     * Retrieves the ID of the entity matching a list of constraints + prepends '<module_id>x' string to it
457
     * @param  string $moduleName   The name of the module / entity type
458
     * @param  array   $params  Data used to find a matching entry
459
     * @return int  Type ID (a numeric ID + '<module_id>x')
460
     */
461
    public function entityRetrieveTypeID($moduleName, array $params)
462
    {
463
        $records = $this->entitiesRetrieve($moduleName, $params, ['id'], 1);
464
        if (false === $records || !isset($records[0]['id']) || empty($records[0]['id'])) {
465
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Salaros\Vtiger\VTWSCLib\...t::entityRetrieveTypeID of type integer.

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...
466
        }
467
468
        $entityID = $records[0]['id'];
469
        $entityIDParts = explode('x', $entityID, 2);
470
        return (is_array($entityIDParts) && count($entityIDParts) === 2)
471
            ? $entityIDParts[1]
472
            : -1;
473
    }
474
475
    /**
476
     * Retrieve a numeric ID of the entity matching a list of constraints
477
     * @param  string $moduleName   The name of the module / entity type
478
     * @param  array   $params  Data used to find a matching entry
479
     * @return int  Numeric ID
480
     */
481
    public function entityRetrieveID($moduleName, array $params)
482
    {
483
        $entityID = $this->entityRetrieveTypeID($moduleName, $params);
484
        $entityIDParts = explode('x', $entityID, 2);
485
        return (is_array($entityIDParts) && count($entityIDParts) === 2)
486
            ? $entityIDParts[1]
487
            : -1;
488
    }
489
490
    /**
491
     * Creates an entity for the giving module
492
     * @param  string $moduleName   Name of the module / entity type for which the entry has to be created
493
     * @param  array $params Entity data
494
     * @return array  Entity creation results
495
     */
496
    public function entityCreate($moduleName, array $params)
497
    {
498
        if (!$this->checkParams($params, 'be able to create an entity')) {
499
            return false;
500
        }
501
502
        // Perform re-login if required.
503
        $this->checkLogin();
504
505
        // Assign record to logged in user if not specified
506
        if (!isset($params['assigned_user_id'])) {
507
            $params['assigned_user_id'] = $this->userID;
508
        }
509
510
        $postdata = [
511
            'operation'   => 'create',
512
            'sessionName' => $this->sessionName,
513
            'elementType' => $moduleName,
514
            'element'     => json_encode($params)
515
        ];
516
517
        return $this->sendHttpRequest($postdata);
518
    }
519
520
    /**
521
     * Updates an entity
522
     * @param  string $moduleName   The name of the module / entity type
523
     * @param  array $params Entity data
524
     * @return array  Entity update result
525
     */
526
    public function entityUpdate($moduleName, array $params)
527
    {
528
        if (!$this->checkParams($params, 'be able to update the entity(ies)')) {
529
            return false;
530
        }
531
532
        // Fail if no ID was supplied
533
        if (!array_key_exists('id', $params) || empty($params['id'])) {
534
            $this->lastErrorMessage = new WSClientError(
535
                "The list of contraints must contain a valid ID"
536
            );
537
            return false;
538
        }
539
540
        // Perform re-login if required.
541
        $this->checkLogin();
542
543
        // Preprend so-called moduleid if needed
544
        $params['id'] = $this->getTypedID($moduleName, $params['id']);
545
546
        // Check if the entity exists + retrieve its data so it can be used below
547
        $entityData = $this->entityRetrieve($moduleName, [ 'id' => $params['id'] ]);
548
        if ($entityData === false && !is_array($entityData)) {
549
            $this->lastErrorMessage = new WSClientError("Such entity doesn't exist, so it cannot be updated");
550
            return false;
551
        }
552
553
        // The new data overrides the existing one needed to provide
554
        // mandatory field values to WS 'update' operation
555
        $params = array_merge(
556
            $entityData,
557
            $params
558
        );
559
560
        $postdata = [
561
            'operation'   => 'update',
562
            'sessionName' => $this->sessionName,
563
            'elementType' => $moduleName,
564
            'element'     => json_encode($params)
565
        ];
566
567
        return $this->sendHttpRequest($postdata);
568
    }
569
570
    /**
571
     * Provides entity removal functionality
572
     * @param  string $moduleName   The name of the module / entity type
573
     * @param  string $entityID The ID of the entity to delete
574
     * @return array  Removal status object
575
     */
576 View Code Duplication
    public function entityDelete($moduleName, $entityID)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
577
    {
578
        // Perform re-login if required.
579
        $this->checkLogin();
580
581
        // Preprend so-called moduleid if needed
582
        $entityID = $this->getTypedID($moduleName, $entityID);
583
584
        $postdata = [
585
            'operation' => 'delete',
586
            'sessionName' => $this->sessionName,
587
            'id' => $entityID
588
        ];
589
590
        return $this->sendHttpRequest($postdata);
591
    }
592
593
    /**
594
     * Retrieves multiple records using module name and a set of constraints
595
     * @param  string   $moduleName  The name of the module / entity type
596
     * @param  array    $params  Data used to find matching entries
597
     * @return array    $select  The list of fields to select (defaults to SQL-like '*' - all the fields)
598
     * @return int      $limit  limit the list of entries to N records (acts like LIMIT in SQL)
599
     * @return bool|array  The array containing matching entries or false if nothing was found
600
     */
601
    public function entitiesRetrieve($moduleName, array $params, array $select = [], $limit = 0)
602
    {
603
        if (!$this->checkParams($params, 'be able to retrieve entity(ies)')) {
604
            return false;
605
        }
606
607
        // Perform re-login if required.
608
        $this->checkLogin();
609
610
        // Builds the query
611
        $query = $this->buildQuery($moduleName, $params, $select, $limit);
612
613
        // Run the query
614
        $records = $this->query($query);
615
        if (false === $records || !is_array($records) || (count($records) <= 0)) {
616
            return false;
617
        }
618
619
        return $records;
620
    }
621
622
    /**
623
     * Sync will return a sync result object containing details of changes after modifiedTime
624
     * @param  int [$modifiedTime = null]    The date of the first change
625
     * @param  string [$moduleName = null]   The name of the module / entity type
626
     * @return array  Sync result object
627
     */
628
    public function entitiesSync($modifiedTime = null, $moduleName = null)
629
    {
630
        // Perform re-login if required.
631
        $this->checkLogin();
632
633
        $modifiedTime = (empty($modifiedTime))
634
            ? strtotime('today midnight')
635
            : intval($modifiedTime);
636
637
        $requestData = [
638
            'operation' => 'sync',
639
            'sessionName' => $this->sessionName,
640
            'modifiedTime' => $modifiedTime
641
        ];
642
643
        if (!empty($moduleName)) {
644
            $requestData['elementType'] = $moduleName;
645
        }
646
647
        return $this->sendHttpRequest($requestData, true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
648
    }
649
650
    /**
651
     * Builds the query using the supplied parameters
652
     * @param  string   $moduleName  The name of the module / entity type
653
     * @param  array    $params  Data used to find matching entries
654
     * @return array    $select  The list of fields to select (defaults to SQL-like '*' - all the fields)
655
     * @return int      $limit  limit the list of entries to N records (acts like LIMIT in SQL)
656
     * @return string   The query build out of the supplied parameters
657
     */
658
    private function buildQuery($moduleName, array $params, array $select = [], $limit = 0)
659
    {
660
        $criteria = array();
661
        $select=(empty($select)) ? '*' : implode(',', $select);
662
        $query=sprintf("SELECT %s FROM $moduleName WHERE ", $select);
663
        foreach ($params as $param => $value) {
664
            $criteria[] = "{$param} LIKE '{$value}'";
665
        }
666
667
        $query.=implode(" AND ", $criteria);
668
        if (intval($limit) > 0) {
669
            $query.=sprintf(" LIMIT %s", intval($limit));
670
        }
671
        return $query;
672
    }
673
674
    /**
675
     * Checks if if params holds valid entity data/search constraints, otherwise returns false
676
     * @param  array    $params  Array holding entity data/search constraints
677
     * @return boolean  Returns true if params holds valid entity data/search constraints, otherwise returns false
678
     */
679
    private function checkParams(array $params, $paramsPurpose)
680
    {
681 View Code Duplication
        if (empty($params) || !is_array($params) || !$this->isAssocArray($params)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
682
            $this->lastErrorMessage = new WSClientError(sprintf(
683
                "You have to specify at least one search parameter (prop => value) in order to %s",
684
                $paramsPurpose
685
            ));
686
            return false;
687
        }
688
        return true;
689
    }
690
691
    /**
692
     * A helper method, used to check in an array is associative or not
693
     * @param  string  Array to test
694
     * @return boolean Returns true in a given array is associative and false if it's not
695
     */
696
    private function isAssocArray(array $array)
697
    {
698
        foreach (array_keys($array) as $key) {
699
            if (!is_int($key)) {
700
                return true;
701
            }
702
        }
703
        return false;
704
    }
705
}
706