Completed
Push — master ( 87a749...a0de4f )
by Zhmayev
01:20
created

WSClient   C

Complexity

Total Complexity 78

Size/Duplication

Total Lines 611
Duplicated Lines 6.87 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 78
lcom 1
cbo 4
dl 42
loc 611
rs 5.4103
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 3
A checkForError() 0 14 3
A checkLogin() 0 7 2
B sendHttpRequest() 0 29 6
A passChallenge() 0 18 3
B login() 0 32 4
B loginPassword() 0 24 6
A getLastError() 0 4 1
A getVersion() 0 5 1
A getTypes() 0 19 2
A getType() 0 13 1
A getTypedID() 0 15 4
B invokeOperation() 5 22 6
A query() 0 18 2
A entityRetrieveByID() 16 16 1
A entityRetrieve() 0 8 3
B entityRetrieveID() 0 13 5
A entityCreate() 0 19 2
B entityUpdate() 0 32 5
A entityDelete() 16 16 1
C entitiesRetrieve() 5 22 7
A entitiesSync() 0 21 3
A buildQuery() 0 15 4
A isAssocArray() 0 9 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WSClient often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WSClient, and based on these observations, apply Extract Interface, too.

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

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
104
            $jsonResult['error']['message'],
105
            $jsonResult['error']['code']
106
        );
107
108
        return true;
109
    }
110
111
    /**
112
     * Checks and performs a login operation if requried and repeats login if needed
113
     * @access private
114
     */
115
    private function checkLogin()
116
    {
117
        if (time() <= $this->serviceExpireTime) {
118
            return;
119
        }
120
        $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...
121
    }
122
123
    /**
124
     * Sends HTTP request to VTiger web service API endpoint
125
     * @access private
126
     * @param  array $requestData HTTP request data
127
     * @param  string $method HTTP request method (GET, POST etc)
128
     * @return array Returns request result object (null in case of failure)
129
     */
130
    private function sendHttpRequest(array $requestData, $method = 'POST')
131
    {
132
        try {
133
            switch ($method) {
134
                case 'GET':
135
                    $response = $this->httpClient->get($this->serviceBaseURL, ['query' => $requestData]);
136
                    break;
137
                case 'POST':
138
                    $response = $this->httpClient->post($this->serviceBaseURL, ['form_params' => $requestData]);
139
                    break;
140
                default:
141
                    $this->lastErrorMessage = new WSClientError("Unknown request type {$method}");
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Salaros\Vtiger\VTWS...equest type {$method}") of type object<Salaros\Vtiger\VTWSCLib\WSClientError> is incompatible with the declared type boolean of property $lastErrorMessage.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
142
                    return null;
143
            }
144
        } catch (RequestException $ex) {
145
            $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...
146
                $ex->getMessage(),
147
                $ex->getCode()
148
            );
149
            return null;
150
        }
151
152
        $jsonRaw = $response->getBody();
153
        $jsonObj = json_decode($jsonRaw, true);
154
155
        return (!is_array($jsonObj) || $this->checkForError($jsonObj))
156
            ? null
157
            : $jsonObj['result'];
158
    }
159
160
    /**
161
     * Gets a challenge token from the server and stores for future requests
162
     * @access private
163
     * @param  string $username VTiger user name
164
     * @return bool Returns false in case of failure
165
     */
166
    private function passChallenge($username)
167
    {
168
        $getdata = [
169
            'operation' => 'getchallenge',
170
            'username'  => $username
171
        ];
172
        $result = $this->sendHttpRequest($getdata, 'GET');
173
        
174
        if (!is_array($result) || !isset($result['token'])) {
175
            return false;
176
        }
177
178
        $this->serviceServerTime = $result['serverTime'];
179
        $this->serviceExpireTime = $result['expireTime'];
180
        $this->serviceToken = $result['token'];
181
182
        return true;
183
    }
184
185
    /**
186
     * Login to the server using username and VTiger access key token
187
     * @access public
188
     * @param  string $username VTiger user name
189
     * @param  string $accessKey VTiger access key token (visible on user profile/settings page)
190
     * @return boolean Returns true if login operation has been successful
191
     */
192
    public function login($username, $accessKey)
193
    {
194
        // Do the challenge before loggin in
195
        if ($this->passChallenge($username) === false) {
196
            return false;
197
        }
198
199
        $postdata = [
200
            'operation' => 'login',
201
            'username'  => $username,
202
            'accessKey' => md5($this->serviceToken.$accessKey)
203
        ];
204
205
        $result = $this->sendHttpRequest($postdata);
206
        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...
207
            return false;
208
        }
209
210
        // Backuping logged in user credentials
211
        $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...
212
        $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...
213
214
        // Session data
215
        $this->sessionName = $result['sessionName'];
216
        $this->userID = $result['userId'];
217
218
        // Vtiger CRM and WebServices API version
219
        $this->apiVersion = $result['version'];
220
        $this->vtigerVersion = $result['vtigerVersion'];
221
222
        return true;
223
    }
224
225
    /**
226
     * Allows you to login using username and password instead of access key (works on some VTige forks)
227
     * @access public
228
     * @param  string $username VTiger user name
229
     * @param  string $password VTiger password (used to access CRM using the standard login page)
230
     * @param  string $accessKey This parameter will be filled with user's VTiger access key
231
     * @return boolean  Returns true if login operation has been successful
232
     */
233
    public function loginPassword($username, $password, &$accessKey = null)
234
    {
235
        // Do the challenge before loggin in
236
        if ($this->passChallenge($username) === false) {
237
            return false;
238
        }
239
240
        $postdata = [
241
            'operation' => 'login_pwd',
242
            'username' => $username,
243
            'password' => $password
244
        ];
245
246
        $result = $this->sendHttpRequest($postdata);
247
        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...
248
            return false;
249
        }
250
251
        $accessKey = array_key_exists('accesskey', $result)
252
            ? $result['accesskey']
253
            : $result[0];
254
255
        return $this->login($username, $accessKey);
256
    }
257
258
    /**
259
     * Gets last operation error, if any
260
     * @access public
261
     * @return WSClientError The error object
262
     */
263
    public function getLastError()
264
    {
265
        return $this->lastErrorMessage;
266
    }
267
268
    /**
269
     * Returns the client library version.
270
     * @access public
271
     * @return string Client library version
272
     */
273
    public function getVersion()
274
    {
275
        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...
276
        return $wsclient_version;
277
    }
278
279
    /**
280
     * Lists all the Vtiger entity types available through the API
281
     * @access public
282
     * @return array List of entity types
283
     */
284
    public function getTypes()
285
    {
286
        // Perform re-login if required.
287
        $this->checkLogin();
288
289
        $getdata = [
290
            'operation' => 'listtypes',
291
            'sessionName'  => $this->sessionName
292
        ];
293
294
        $result = $this->sendHttpRequest($getdata, 'GET');
295
        $modules = $result['types'];
296
297
        $result = array();
298
        foreach ($modules as $moduleName) {
299
            $result[$moduleName] = ['name' => $moduleName];
300
        }
301
        return $result;
302
    }
303
304
    /**
305
     * Get the type information about a given VTiger entity type.
306
     * @access public
307
     * @param  string $moduleName Name of the module / entity type
308
     * @return array  Result object
309
     */
310
    public function getType($moduleName)
311
    {
312
        // Perform re-login if required.
313
        $this->checkLogin();
314
315
        $getdata = [
316
            'operation' => 'describe',
317
            'sessionName'  => $this->sessionName,
318
            'elementType' => $moduleName
319
        ];
320
321
        return $this->sendHttpRequest($getdata, 'GET');
322
    }
323
324
    /**
325
     * Gets the entity ID prepended with module / entity type ID
326
     * @access private
327
     * @param  string       $moduleName   Name of the module / entity type
328
     * @param  string       $entityID     Numeric entity ID
329
     * @return boolean|string Returns false if it is not possible to retrieve module / entity type ID
330
     */
331
    private function getTypedID($moduleName, $entityID)
332
    {
333
        if (stripos((string)$entityID, 'x') !== false) {
334
            return $entityID;
335
        }
336
337
        $type = $this->getType($moduleName);
338
        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...
339
            $errorMessage = sprintf("The following module is not installed: %s", $moduleName);
340
            $this->lastErrorMessage = new WSClientError($errorMessage);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Salaros\Vtiger\VTWS...entError($errorMessage) of type object<Salaros\Vtiger\VTWSCLib\WSClientError> is incompatible with the declared type boolean of property $lastErrorMessage.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
341
            return false;
342
        }
343
344
        return "{$type['idPrefix']}x{$entityID}";
345
    }
346
347
    /**
348
     * Invokes custom operation (defined in vtiger_ws_operation table)
349
     * @access public
350
     * @param  string  $operation  Name of the webservice to invoke
351
     * @param  array   [$params = null] Parameter values to operation
352
     * @param  string  [$method   = 'POST'] HTTP request method (GET, POST etc)
353
     * @return array Result object
354
     */
355
    public function invokeOperation($operation, array $params = null, $method = 'POST')
356
    {
357 View Code Duplication
        if (!empty($params) || !is_array($params) || !$this->isAssocArray($params)) {
1 ignored issue
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...
358
            $errorMessage = "You have to specified a list of operation parameters, but apparently it's not an associative array ('prop' => value), you must fix it!";
359
            $this->lastErrorMessage = new WSClientError($errorMessage);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Salaros\Vtiger\VTWS...entError($errorMessage) of type object<Salaros\Vtiger\VTWSCLib\WSClientError> is incompatible with the declared type boolean of property $lastErrorMessage.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
360
            return false;
361
        }
362
363
        // Perform re-login if required
364
        $this->checkLogin();
365
366
        $requestData = [
367
            'operation' => $operation,
368
            'sessionName' => $this->sessionName
369
        ];
370
371
        if (!empty($params) && is_array($params)) {
372
            $requestData = array_merge($requestData, $params);
373
        }
374
375
        return $this->sendHttpRequest($requestData, $method);
376
    }
377
378
    /**
379
     * VTiger provides a simple query language for fetching data.
380
     * This language is quite similar to select queries in SQL.
381
     * There are limitations, the queries work on a single Module,
382
     * embedded queries are not supported, and does not support joins.
383
     * But this is still a powerful way of getting data from Vtiger.
384
     * Query always limits its output to 100 records,
385
     * Client application can use limit operator to get different records.
386
     * @access public
387
     * @param  string $query SQL-like expression
388
     * @return array  Query results
389
     */
390
    public function query($query)
391
    {
392
        // Perform re-login if required.
393
        $this->checkLogin();
394
395
        // Make sure the query ends with ;
396
        $query = (strripos($query, ';') != strlen($query)-1)
397
            ? trim($query .= ';')
398
            : trim($query);
399
400
        $getdata = [
401
            'operation' => 'query',
402
            'sessionName' => $this->sessionName,
403
            'query' => $query
404
        ];
405
406
        return $this->sendHttpRequest($getdata, 'GET');
407
    }
408
409
    /**
410
     * Retrieves an entity by ID
411
     * @param  string $moduleName The name of the module / entity type
412
     * @param  string $entityID The ID of the entity to retrieve
413
     * @return boolean  Entity data
414
     */
415 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...
416
    {
417
        // Perform re-login if required.
418
        $this->checkLogin();
419
420
        // Preprend so-called moduleid if needed
421
        $entityID = $this->getTypedID($moduleName, $entityID);
422
423
        $getdata = [
424
            'operation' => 'retrieve',
425
            'sessionName' => $this->sessionName,
426
            'id' => $entityID
427
        ];
428
429
        return $this->sendHttpRequest($getdata, 'GET');
430
    }
431
432
    /**
433
     * Uses VTiger queries to retrieve the entity matching a list of constraints
434
     * @param  string  $moduleName   The name of the module / entity type
435
     * @param  array   $params  Data used to find a matching entry
436
     * @return array   $select  The list of fields to select (defaults to SQL-like '*' - all the fields)
437
     * @return int  The matching record
438
     */
439
    public function entityRetrieve($moduleName, array $params, array $select = [])
440
    {
441
        $records = $this->entitiesRetrieve($moduleName, $params, $select, 1);
442
        if (false === $records || !isset($records[0])) {
443
            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...
444
        }
445
        return $records[0];
446
    }
447
448
    /**
449
     * Uses VTiger queries to retrieve the ID of the entity matching a list of constraints
450
     * @param  string $moduleName   The name of the module / entity type
451
     * @param  array   $params  Data used to find a matching entry
452
     * @return int  Numeric ID
453
     */
454
    public function entityRetrieveID($moduleName, array $params) // TODO check if params is an assoc array
455
    {
456
        $record = $this->entityRetrieve($moduleName, $params, ['id']);
457
        if (false === $record || !isset($record['id'])) {
458
            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::entityRetrieveID 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...
459
        }
460
461
        $entityID = $record['id'];
462
        $entityIDParts = explode('x', $entityID, 2);
463
        return (is_array($entityIDParts) && count($entityIDParts) === 2)
464
            ? $entityIDParts[1]
465
            : -1;
466
    }
467
468
    /**
469
     * Creates an entity for the giving module
470
     * @param  string $moduleName   Name of the module / entity type for which the entry has to be created
471
     * @param  array $params Entity data
472
     * @return array  Entity creation results
473
     */
474
    public function entityCreate($moduleName, array $params) // TODO check if params is an assoc array
475
    {
476
        // Perform re-login if required.
477
        $this->checkLogin();
478
479
        // Assign record to logged in user if not specified
480
        if (!isset($params['assigned_user_id'])) {
481
            $params['assigned_user_id'] = $this->userID;
482
        }
483
484
        $postdata = [
485
            'operation'   => 'create',
486
            'sessionName' => $this->sessionName,
487
            'elementType' => $moduleName,
488
            'element'     => json_encode($params)
489
        ];
490
491
        return $this->sendHttpRequest($postdata);
492
    }
493
494
    /**
495
     * Updates an entity
496
     * @param  string $moduleName   The name of the module / entity type
497
     * @param  array $params Entity data
498
     * @return array  Entity update result
499
     */
500
    public function entityUpdate($moduleName, array $params) // TODO check if params is an assoc array
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
        // TODO implement the case when no ID is given
511
        if (array_key_exists('id', $params)) {
512
            $data = $this->entityRetrieveByID($moduleName, $params['id']);
513
            if ($data !== false && is_array($data)) {
514
                $entityID = $data['id'];
515
                $params = array_merge(
516
                    $data,      // needed to provide mandatory field values
517
                    $params,    // updated data override
518
                    ['id'=>$entityID] // fixing id, might be useful when non <moduleid>x<id> one was specified
519
                );
520
            }
521
        }
522
523
        $postdata = [
524
                'operation'   => 'update',
525
                'sessionName' => $this->sessionName,
526
                'elementType' => $moduleName,
527
                'element'     => json_encode($params)
528
        ];
529
530
        return $this->sendHttpRequest($postdata);
531
    }
532
533
    /**
534
     * Provides entity removal functionality
535
     * @param  string $moduleName   The name of the module / entity type
536
     * @param  string $entityID The ID of the entity to delete
537
     * @return array  Removal status object
538
     */
539 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...
540
    {
541
        // Perform re-login if required.
542
        $this->checkLogin();
543
544
        // Preprend so-called moduleid if needed
545
        $entityID = $this->getTypedID($moduleName, $entityID);
546
547
        $postdata = [
548
            'operation' => 'delete',
549
            'sessionName' => $this->sessionName,
550
            'id' => $entityID
551
        ];
552
553
        return $this->sendHttpRequest($postdata);
554
    }
555
556
    /**
557
     * Retrieves multiple records using module name and a set of constraints
558
     * @param  string   $moduleName  The name of the module / entity type
559
     * @param  array    $params  Data used to find the matching entries
560
     * @return array    $select  The list of fields to select (defaults to SQL-like '*' - all the fields)
561
     * @return int      $limit  limit the list of entries to N records (acts like LIMIT in SQL)
562
     * @return bool|array  The array containing the matching entries or false if nothing was found
563
     */
564
    public function entitiesRetrieve($moduleName, array $params, array $select = [], $limit = 0)
565
    {
566 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...
567
            $errorMessage = "You have to specify at least one search parameter (prop => value) in order to filter entities";
568
            $this->lastErrorMessage = new WSClientError($errorMessage);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Salaros\Vtiger\VTWS...entError($errorMessage) of type object<Salaros\Vtiger\VTWSCLib\WSClientError> is incompatible with the declared type boolean of property $lastErrorMessage.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
569
            return false;
570
        }
571
572
        // Perform re-login if required.
573
        $this->checkLogin();
574
575
        // Builds the query
576
        $query = $this->buildQuery($moduleName, $params, $select, $limit);
577
578
        // Run the query
579
        $records = $this->query($query);
580
        if (false === $records || !is_array($records) || (count($records) <= 0)) {
581
            return false;
582
        }
583
584
        return $records;
585
    }
586
587
    /**
588
     * Sync will return a sync result object containing details of changes after modifiedTime
589
     * @param  int [$modifiedTime = null]    The date of the first change
590
     * @param  string [$moduleName = null]   The name of the module / entity type
591
     * @return array  Sync result object
592
     */
593
    public function entitiesSync($modifiedTime = null, $moduleName = null)
594
    {
595
        // Perform re-login if required.
596
        $this->checkLogin();
597
598
        $modifiedTime = (empty($modifiedTime))
599
            ? strtotime('today midnight')
600
            : intval($modifiedTime);
601
602
        $requestData = [
603
            'operation' => 'sync',
604
            'sessionName' => $this->sessionName,
605
            'modifiedTime' => $modifiedTime
606
        ];
607
608
        if (!empty($moduleName)) {
609
            $requestData['elementType'] = $moduleName;
610
        }
611
612
        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...
613
    }
614
615
    /**
616
     * Builds the query using the supplied parameters
617
     * @param  string   $moduleName  The name of the module / entity type
618
     * @param  array    $params  Data used to find the matching entries
619
     * @return array    $select  The list of fields to select (defaults to SQL-like '*' - all the fields)
620
     * @return int      $limit  limit the list of entries to N records (acts like LIMIT in SQL)
621
     * @return string   The query build out of the supplied parameters
622
     */
623
    private function buildQuery($moduleName, array $params, array $select = [], $limit = 0)
624
    {
625
        $criteria = array();
626
        $select=(empty($select)) ? '*' : implode(',', $select);
627
        $query=sprintf("SELECT %s FROM $moduleName WHERE ", $select);
628
        foreach ($params as $param => $value) {
629
            $criteria[] = "{$param} LIKE '{$value}'";
630
        }
631
632
        $query.=implode(" AND ", $criteria);
633
        if (intval($limit) > 0) {
634
            $query.=sprintf(" LIMIT %s", intval($limit));
635
        }
636
        return $query;
637
    }
638
639
    /**
640
     * A helper method, used to check in an array is associative or not
641
     * @param  string  Array to test
642
     * @return bool  Returns true in a given array is associative and false if it's not
643
     */
644
    private function isAssocArray(array $array)
645
    {
646
        foreach (array_keys($array) as $key) {
647
            if (!is_int($key)) {
648
                return true;
649
            }
650
        }
651
        return false;
652
    }
653
}
654