Issues (71)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/AbstractZohoDao.php (19 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Wabel\Zoho\CRM;
4
5
use Wabel\Zoho\CRM\Exception\ZohoCRMException;
6
use Wabel\Zoho\CRM\Exception\ZohoCRMResponseException;
7
use Wabel\Zoho\CRM\Exception\ZohoCRMUpdateException;
8
use Wabel\Zoho\CRM\Request\Response;
9
10
/**
11
 * Base class that provides access to Zoho through Zoho beans.
12
 */
13
abstract class AbstractZohoDao
14
{
15
    const ON_DUPLICATE_THROW = 1;
16
    const ON_DUPLICATE_MERGE = 2;
17
    const MAX_GET_RECORDS = 200;
18
    const MAX_GET_RECORDS_BY_ID = 100;
19
    const MAX_SIMULTANEOUS_SAVE = 100;
20
21
    /**
22
     * The class implementing API methods not directly related to a specific module.
23
     *
24
     * @var ZohoClient
25
     */
26
    protected $zohoClient;
27
28
    public function __construct(ZohoClient $zohoClient)
29
    {
30
        $this->zohoClient = $zohoClient;
31
    }
32
33
    abstract protected function getModule();
34
    abstract protected function getSingularModuleName();
35
    abstract protected function getPluralModuleName();
36
    abstract protected function getBeanClassName();
37
    abstract protected function getFields();
38
39
    protected $flatFields;
40
41
    /**
42
     * Returns a flat list of all fields.
43
     *
44
     * @return array The array of field names for a module
45
     */
46
    protected function getFlatFields()
47
    {
48
        if ($this->flatFields === null) {
49
            $this->flatFields = array();
50
            foreach ($this->getFields() as $cat) {
51
                $this->flatFields = array_merge($this->flatFields, $cat);
52
            }
53
        }
54
55
        return $this->flatFields;
56
    }
57
58
    /**
59
     * Parse a Zoho Response in order to retrieve one or several ZohoBeans from it.
60
     *
61
     * @param Response $zohoResponse The response returned by the ZohoClient->call() method
62
     *
63
     * @return ZohoBeanInterface[] The array of Zoho Beans parsed from the response
64
     */
65
    protected function getBeansFromResponse(Response $zohoResponse)
66
    {
67
        $beanClass = $this->getBeanClassName();
68
        $fields = $this->getFlatFields();
69
70
        $beanArray = array();
71
72
        foreach ($zohoResponse->getRecords() as $record) {
73
74
            /** @var ZohoBeanInterface $bean */
75
            $bean = new $beanClass();
76
77
            // First, let's fill the ID.
78
            // The ID is CONTACTID or ACCOUNTID or Id depending on the Zoho type.
79
            $idName = strtoupper(rtrim($this->getModule(), 's'));
80
            if (isset($record[$idName.'ID'])) {
81
                $id = $record[$idName.'ID'];
82
            } elseif (isset($record[$idName.'_ID'])) {
83
                $id = $record[$idName.'_ID'];
84
            } else {
85
                $id = $record['Id'];
86
            }
87
            $bean->setZohoId($id);
88
            $bean->setCreatedTime(\DateTime::createFromFormat('Y-m-d H:i:s', $record['Created Time']));
0 ignored issues
show
It seems like \DateTime::createFromFor...record['Created Time']) targeting DateTime::createFromFormat() can also be of type false; however, Wabel\Zoho\CRM\ZohoBeanInterface::setCreatedTime() does only seem to accept object<DateTime>, did you maybe forget to handle an error condition?
Loading history...
89
            $bean->setModifiedTime(\DateTime::createFromFormat('Y-m-d H:i:s', $record['Modified Time']));
0 ignored issues
show
It seems like \DateTime::createFromFor...ecord['Modified Time']) targeting DateTime::createFromFormat() can also be of type false; however, Wabel\Zoho\CRM\ZohoBeanI...face::setModifiedTime() does only seem to accept object<DateTime>, did you maybe forget to handle an error condition?
Loading history...
90
91
            foreach ($record as $key => $value) {
92
                if (isset($fields[$key])) {
93
                    $setter = $fields[$key]['setter'];
94
95
                    switch ($fields[$key]['type']) {
96
                        case 'Date':
97
                            if ($dateObj = \DateTime::createFromFormat('M/d/Y', $value)) {
98
                                $value = $dateObj;
99
                            } elseif ($dateObj = \DateTime::createFromFormat('Y-m-d', $value)) {
100
                                $value = $dateObj;
101
                            } else {
102
                                throw new ZohoCRMException('Unable to convert the Date field "'.$key."\" into a DateTime PHP object from the the record $id of the module ".$this->getModule().'.');
103
                            }
104
                            break;
105
                        case 'DateTime':
106
                            $value = \DateTime::createFromFormat('Y-m-d H:i:s', $value);
107
                            break;
108
                        case 'Boolean':
109
                            $value = ($value == 'true');
110
                            break;
111
                        default:
112
                            break;
113
                    }
114
                    $bean->$setter($value);
115
                }
116
            }
117
118
            $beanArray[] = $bean;
119
        }
120
121
        return $beanArray;
122
    }
123
124
    /**
125
     * Convert an array of ZohoBeans into a SimpleXMLElement.
126
     *
127
     * @param $zohoBeans ZohoBeanInterface[]
128
     *
129
     * @return \SimpleXMLElement The SimpleXMLElement containing the XML for a request
130
     */
131
    public function toXml($zohoBeans)
132
    {
133
        $module = $this->getModule();
134
135
        $no = 1;
136
        $module = new \SimpleXMLElement("<$module/>");
137
138
        foreach ($zohoBeans as $zohoBean) {
139
            if (!$zohoBean instanceof ZohoBeanInterface) {
140
                throw new ZohoCRMException('Zoho beans sent to save must implement the ZohoBeanInterface.');
141
            }
142
143
            $properties = $this->getFlatFields();
144
            $row = $module->addChild('row');
145
            $row->addAttribute('no', $no);
146
147
            $fl = $row->addChild('FL', $zohoBean->getZohoId());
148
            $fl->addAttribute('val', 'Id');
149
150
            foreach ($properties as $name => $params) {
151
                $camelCaseName = $params['name'];
152
                $isDirty = $zohoBean->isDirty($camelCaseName);
153
                if (!$isDirty) {
154
                    continue;
155
                }
156
157
                $getter = $params['getter'];
158
                $value = $zohoBean->$getter();
159
160
                if (!empty($value) || is_bool($value)) {
161
162
                    // We convert the value back to a proper format if the Zoho Type is Date, DateTime or Boolean
163
                    switch ($params['type']) {
164
                        case 'Date':
165
                            /** @var \DateTime $value */
166
                            $value = $value->format('m/d/Y');
167
                            break;
168
                        case 'DateTime':
169
                            /** @var \DateTime $value */
170
                            $value = $value->format('Y-m-d H:i:s');
171
                            break;
172
                        case 'Boolean':
173
                            /** @var bool $value */
174
                            $value = $value ? 'true' : 'false';
175
                            break;
176
                        default:
177
                            break;
178
                    }
179
                }
180
181
                $fl = $row->addChild('FL', htmlspecialchars($value));
182
                $fl->addAttribute('val', $name);
183
            }
184
            ++$no;
185
        }
186
187
        return $module;
188
    }
189
190
    /**
191
     * Implements deleteRecords API method.
192
     *
193
     * @param string $id Zoho Id of the record to delete
194
     *
195
     * @throws ZohoCRMResponseException
196
     */
197
    public function delete($id)
198
    {
199
        $this->zohoClient->deleteRecords($this->getModule(), $id);
200
    }
201
202
    /**
203
     * Implements getRecordById API method.
204
     *
205
     * @param string|array $id Zoho Id of the record to retrieve OR an array of IDs
206
     *
207
     * @return ZohoBeanInterface[] The array of Zoho Beans parsed from the response
208
     *
209
     * @throws ZohoCRMResponseException
210
     */
211
    public function getById($id)
212
    {
213
        try {
214
            $module = $this->getModule();
215
            $beans = [];
216
217
            // If there's several IDs to process, we divide them by pools of 100 and implode them before requesting
218
            if (is_array($id)) {
219
                foreach (array_chunk($id, self::MAX_GET_RECORDS_BY_ID) as $pool) {
220
                    $idlist = implode(';', $pool);
221
                    $response = $this->zohoClient->getRecordById($module, $idlist);
222
                    $beans = array_merge($beans, $this->getBeansFromResponse($response));
223
                }
224
            }
225
            // if not, we simply request our record
226
            else {
227
                $response = $this->zohoClient->getRecordById($module, $id);
228
                $beans = $this->getBeansFromResponse($response);
229
                $beans = array_shift($beans);
230
            }
231
232
            return $beans;
233
        } catch (ZohoCRMResponseException $e) {
234
            // No records found? Let's return an empty array!
235
            if ($e->getCode() == 4422) {
236
                return array();
237
            } else {
238
                throw $e;
239
            }
240
        }
241
    }
242
243
    /**
244
     * Implements getRecords API method.
245
     *
246
     * @param $sortColumnString
247
     * @param $sortOrderString
248
     * @param \DateTime $lastModifiedTime
249
     * @param $selectColumns
250
     * @param $limit
251
     *
252
     * @return ZohoBeanInterface[] The array of Zoho Beans parsed from the response
253
     *
254
     * @throws ZohoCRMResponseException
255
     */
256
    public function getRecords($sortColumnString = null, $sortOrderString = null, \DateTime $lastModifiedTime = null, $selectColumns = null, $limit = null)
257
    {
258
        $globalResponse = array();
259
260
        do {
261
            try {
262
                $fromIndex = count($globalResponse) + 1;
263
                $toIndex = $fromIndex + self::MAX_GET_RECORDS - 1;
264
265
                if ($limit) {
266
                    $toIndex = min($limit, $toIndex);
267
                }
268
269
                $response = $this->zohoClient->getRecords($this->getModule(), $sortColumnString, $sortOrderString, $lastModifiedTime, $selectColumns, $fromIndex, $toIndex);
0 ignored issues
show
It seems like $lastModifiedTime defined by parameter $lastModifiedTime on line 256 can also be of type object<DateTime>; however, Wabel\Zoho\CRM\ZohoClient::getRecords() does only seem to accept null|object<DateTimeInterface>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
270
                $beans = $this->getBeansFromResponse($response);
271
            } catch (ZohoCRMResponseException $e) {
272
                // No records found? Let's return an empty array!
273
                if ($e->getCode() == 4422) {
274
                    $beans = array();
275
                } else {
276
                    throw $e;
277
                }
278
            }
279
280
            $globalResponse = array_merge($globalResponse, $beans);
281
        } while (count($beans) == self::MAX_GET_RECORDS);
282
283
        return $globalResponse;
284
    }
285
286
    /**
287
     * Returns the list of deleted records.
288
     *
289
     * @param \DateTimeInterface|null $lastModifiedTime
290
     * @param int                     $limit
291
     *
292
     * @return array
293
     *
294
     * @throws ZohoCRMResponseException
295
     * @throws \Exception
296
     */
297 View Code Duplication
    public function getDeletedRecordIds(\DateTimeInterface $lastModifiedTime = null, $limit = null)
0 ignored issues
show
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...
298
    {
299
        $globalDeletedIDs = array();
300
301
        do {
302
            try {
303
                $fromIndex = count($globalDeletedIDs) + 1;
304
                $toIndex = $fromIndex + self::MAX_GET_RECORDS - 1;
305
306
                if ($limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
307
                    $toIndex = min($limit - 1, $toIndex);
308
                }
309
310
                $response = $this->zohoClient->getDeletedRecordIds($this->getModule(), $lastModifiedTime, $fromIndex, $toIndex);
311
                $deletedIDs = $response->getDeletedIds();
312
            } catch (ZohoCRMResponseException $e) {
313
                // No records found? Let's return an empty array!
314
                if ($e->getZohoCode() == 4422) {
315
                    $deletedIDs = array();
316
                } else {
317
                    throw $e;
318
                }
319
            }
320
321
            $globalDeletedIDs = array_merge($globalDeletedIDs, $deletedIDs);
322
        } while (count($deletedIDs) == self::MAX_GET_RECORDS);
323
324
        return $globalDeletedIDs;
325
    }
326
327
    /**
328
     * Implements getRecords API method.
329
     *
330
     * @param string $id           Zoho Id of the record to delete
331
     * @param string $parentModule The parent module of the records
332
     * @param int    $limit        The max number of records to fetch
333
     *
334
     * @return ZohoBeanInterface[] The array of Zoho Beans parsed from the response
335
     *
336
     * @throws ZohoCRMResponseException
337
     */
338 View Code Duplication
    public function getRelatedRecords($id, $parentModule, $limit = null)
0 ignored issues
show
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...
339
    {
340
        $globalResponse = array();
341
342
        do {
343
            try {
344
                $fromIndex = count($globalResponse) + 1;
345
                $toIndex = $fromIndex + self::MAX_GET_RECORDS - 1;
346
347
                if ($limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
348
                    $toIndex = min($limit - 1, $toIndex);
349
                }
350
351
                $response = $this->zohoClient->getRelatedRecords($this->getModule(), $id, $parentModule, $fromIndex, $toIndex);
352
                $beans = $this->getBeansFromResponse($response);
353
            } catch (ZohoCRMResponseException $e) {
354
                // No records found? Let's return an empty array!
355
                if ($e->getCode() == 4422) {
356
                    $beans = array();
357
                } else {
358
                    throw $e;
359
                }
360
            }
361
362
            $globalResponse = array_merge($globalResponse, $beans);
363
        } while (count($beans) == self::MAX_GET_RECORDS);
364
365
        return $globalResponse;
366
    }
367
368
    /**
369
     * Implements searchRecords API method.
370
     *
371
     * @param string    $searchCondition  The search criteria formatted like
372
     * @param int       $limit            The maximum number of beans returned from Zoho
373
     * @param \DateTime $lastModifiedTime
374
     * @param string    $selectColumns    The list
375
     *
376
     * @return ZohoBeanInterface[] The array of Zoho Beans parsed from the response
377
     *
378
     * @throws ZohoCRMResponseException
379
     */
380
    public function searchRecords($searchCondition = null, $limit = null, \DateTime $lastModifiedTime = null, $selectColumns = null)
381
    {
382
        $globalResponse = array();
383
384
        do {
385
            try {
386
                $fromIndex = count($globalResponse) + 1;
387
                $toIndex = $fromIndex + self::MAX_GET_RECORDS - 1;
388
389
                if ($limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
390
                    $toIndex = min($limit - 1, $toIndex);
391
                }
392
393
                $response = $this->zohoClient->searchRecords($this->getModule(), $searchCondition, $fromIndex, $toIndex, $lastModifiedTime, $selectColumns);
0 ignored issues
show
It seems like $selectColumns defined by parameter $selectColumns on line 380 can also be of type string; however, Wabel\Zoho\CRM\ZohoClient::searchRecords() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
394
                $beans = $this->getBeansFromResponse($response);
395
            } catch (ZohoCRMResponseException $e) {
396
                // No records found? Let's return an empty array!
397
                if ($e->getCode() == 4422) {
398
                    $beans = array();
399
                } else {
400
                    throw $e;
401
                }
402
            }
403
404
            $globalResponse = array_merge($globalResponse, $beans);
405
        } while (count($beans) == self::MAX_GET_RECORDS);
406
407
        return $globalResponse;
408
    }
409
410
    /**
411
     * Implements insertRecords API method.
412
     *
413
     * WARNING : When setting wfTrigger to true, this method will use an API call per bean
414
     * passed in argument. This is caused by Zoho limitation which forbids triggering any
415
     * workflow when inserting several beans simultaneously.
416
     *
417
     * @param ZohoBeanInterface[] $beans          The Zoho Beans to insert in the CRM
418
     * @param bool                $wfTrigger      Whether or not the call should trigger the workflows related to a "created" event
419
     * @param int                 $duplicateCheck 1 : Throwing error when a duplicate is found; 2 : Merging with existing duplicate
420
     * @param bool                $isApproval     Whether or not to push the record into an approval sandbox first
421
     *
422
     * @throws ZohoCRMResponseException
423
     */
424
    public function insertRecords($beans, $wfTrigger = null, $duplicateCheck = 2, $isApproval = null)
425
    {
426
        $records = [];
427
428
        if ($wfTrigger) {
429
            // If we trigger workflows, we trigger the insert of beans one by one.
430
            foreach ($beans as $bean) {
431
                $xmlData = $this->toXml([$bean]);
432
                $response = $this->zohoClient->insertRecords($this->getModule(), $xmlData, $wfTrigger, $duplicateCheck, $isApproval);
433
                $records = array_merge($records, $response->getRecords());
434
            }
435 View Code Duplication
        } else {
0 ignored issues
show
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...
436
            // We can't pass more than 100 records to Zoho, so we split the request into pieces of 100
437
            foreach (array_chunk($beans, 100) as $beanPool) {
438
                $xmlData = $this->toXml($beanPool);
439
                $response = $this->zohoClient->insertRecords($this->getModule(), $xmlData, $wfTrigger, $duplicateCheck, $isApproval);
440
                $records = array_merge($records, $response->getRecords());
441
            }
442
        }
443 View Code Duplication
        if (count($records) != count($beans)) {
0 ignored issues
show
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...
444
            throw new ZohoCRMException('Error while inserting beans in Zoho. '.count($beans).' passed in parameter, but '.count($records).' returned.');
445
        }
446
447
        foreach ($beans as $key => $bean) {
448
            $record = $records[$key];
449
450
            if ($wfTrigger && (!isset($record['Id']) || empty($record['Id']))) {
451
                // This field is probably in error!
452
                throw new ZohoCRMException('An error occurred while inserting records and triggering workflow: '.$record['message'], $record['code']);
453
            } elseif (!$wfTrigger && substr($record['code'], 0, 1) != '2') {
0 ignored issues
show
Bug Best Practice introduced by
The expression $wfTrigger of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
454
                // This field is probably in error!
455
                throw new ZohoCRMException('An error occurred while inserting records: '.$record['message'], $record['code']);
456
            }
457
458
            $bean->setZohoId($record['Id']);
459
            $bean->setCreatedTime(\DateTime::createFromFormat('Y-m-d H:i:s', $record['Created Time']));
0 ignored issues
show
It seems like \DateTime::createFromFor...record['Created Time']) targeting DateTime::createFromFormat() can also be of type false; however, Wabel\Zoho\CRM\ZohoBeanInterface::setCreatedTime() does only seem to accept object<DateTime>, did you maybe forget to handle an error condition?
Loading history...
460
            if ($record['Modified Time']) {
461
                $bean->setModifiedTime(\DateTime::createFromFormat('Y-m-d H:i:s', $record['Modified Time']));
0 ignored issues
show
It seems like \DateTime::createFromFor...ecord['Modified Time']) targeting DateTime::createFromFormat() can also be of type false; however, Wabel\Zoho\CRM\ZohoBeanI...face::setModifiedTime() does only seem to accept object<DateTime>, did you maybe forget to handle an error condition?
Loading history...
462
            }
463
        }
464
    }
465
466
    /**
467
     * Implements updateRecords API method.
468
     *
469
     * @param array $beans     The list of beans to update.
470
     * @param bool  $wfTrigger Set value as true to trigger the workflow rule in Zoho
471
     *
472
     * @return Response The Response object
473
     *
474
     * @throws ZohoCRMException
475
     */
476
    public function updateRecords(array $beans, $wfTrigger = null)
477
    {
478
        $records = [];
479
480
        if ($wfTrigger) {
481
            // If we trigger workflows, we trigger the insert of beans one by one.
482
            foreach ($beans as $bean) {
483
                $xmlData = $this->toXml([$bean]);
484
                $response = $this->zohoClient->updateRecords($this->getModule(), $xmlData, $bean->getZohoId(), $wfTrigger);
485
                $records = array_merge($records, $response->getRecords());
486
            }
487 View Code Duplication
        } else {
0 ignored issues
show
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...
488
            // We can't pass more than 100 records to Zoho, so we split the request into pieces of 100
489
            foreach (array_chunk($beans, 100) as $beanPool) {
490
                $xmlData = $this->toXml($beanPool);
491
                $response = $this->zohoClient->updateRecords($this->getModule(), $xmlData, null, $wfTrigger);
492
                $records = array_merge($records, $response->getRecords());
493
            }
494
        }
495 View Code Duplication
        if (count($records) != count($beans)) {
0 ignored issues
show
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...
496
            throw new ZohoCRMException('Error while inserting beans in Zoho. '.count($beans).' passed in parameter, but '.count($records).' returned.');
497
        }
498
499
        $exceptions = new \SplObjectStorage();
500
501
        foreach ($beans as $key => $bean) {
502
            $record = $records[$key];
503
504
            if ($wfTrigger && (!isset($record['Id']) || empty($record['Id']))) {
505
                // This field is probably in error!
506
                throw new ZohoCRMException('An error occurred while updating records and triggering workflow: '.$record['message'], $record['code']);
507
            } elseif (!$wfTrigger && substr($record['code'], 0, 1) != '2') {
0 ignored issues
show
Bug Best Practice introduced by
The expression $wfTrigger of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
508
                // This field is probably in error!
509
                $exceptions->attach($bean, new ZohoCRMException('An error occurred while updating records. '.(isset($record['message']) ? $record['message'] : ''), $record['code']));
510
                continue;
511
            }
512
513
            if ($record['Id'] != $bean->getZohoId()) {
514
                // This field is probably in error!
515
                $exceptions->attach($bean, new ZohoCRMException('An error occurred while updating records. The Zoho ID to update was '.$bean->getZohoId().', returned '.$record['Id']));
516
                continue;
517
            }
518
519
            if ($record['Modified Time']) {
520
                $bean->setModifiedTime(\DateTime::createFromFormat('Y-m-d H:i:s', $record['Modified Time']));
521
            }
522
        }
523
        if ($exceptions->count() != 0) {
524
            throw new ZohoCRMUpdateException($exceptions);
525
        }
526
    }
527
528
    /**
529
     * Implements uploadFile API method.
530
     *
531
     * @param string $id      Zoho Id of the record to retrieve
532
     * @param string $content The string containing the file
533
     *
534
     * @return Response The Response object
535
     *
536
     * @throws ZohoCRMResponseException
537
     */
538
    public function uploadFile($id, $content)
539
    {
540
        return $this->zohoClient->uploadFile($this->getModule(), $id, $content);
541
    }
542
543
    /**
544
     * Implements downloadFile API method.
545
     *
546
     * @param string $id unique ID of the attachment
547
     *
548
     * @return Response The Response object
549
     */
550
    public function downloadFile($id)
551
    {
552
        return $this->zohoClient->downloadFile($this->getModule(), $id);
553
    }
554
555
    /**
556
     * Saves the bean or array of beans passed in Zoho.
557
     * It will perform an insert if the bean has no ZohoID or an update if the bean has a ZohoID.
558
     *
559
     * @param array|object $beans A bean or an array of beans.
560
     *
561
     * TODO: isApproval is not used by each module.
562
     * TODO: wfTrigger only usable for a single record update/insert.
563
     */
564
    public function save($beans, $wfTrigger = false, $duplicateCheck = self::ON_DUPLICATE_MERGE, $isApproval = false)
565
    {
566
        if (!is_array($beans)) {
567
            $beans = [$beans];
568
        }
569
570
        $toInsert = [];
571
        $toUpdate = [];
572
573
        foreach ($beans as $bean) {
574
            if ($bean->getZohoId()) {
575
                $toUpdate[] = $bean;
576
            } else {
577
                $toInsert[] = $bean;
578
            }
579
        }
580
581
        if ($toInsert) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $toInsert 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...
582
            $this->insertRecords($toInsert, $wfTrigger, $duplicateCheck, $isApproval);
583
        }
584
        if ($toUpdate) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $toUpdate 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...
585
            $this->updateRecords($toUpdate, $wfTrigger);
586
        }
587
    }
588
}
589