Completed
Pull Request — master (#4)
by Laurens
05:13
created

Client   C

Complexity

Total Complexity 43

Size/Duplication

Total Lines 396
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 19

Importance

Changes 23
Bugs 2 Features 6
Metric Value
wmc 43
c 23
b 2
f 6
lcom 1
cbo 19
dl 0
loc 396
rs 5.6245

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 1
A setApiDetails() 0 4 1
A setConfig() 0 6 1
A getRefreshToken() 0 4 1
B get() 0 25 2
A setProxy() 0 4 1
A getCacheDir() 0 9 2
A getFilesFromReportRequest() 0 11 1
A submitGenerateReport() 0 11 2
A getReportRequest() 0 6 1
D waitForStatus() 0 35 9
A pollGenerateReport() 0 10 2
A fixFile() 0 15 3
A moveFirstFile() 0 7 1
A clearCache() 0 17 3
C parseSoapFault() 0 30 12

How to fix   Complexity   

Complex Class

Complex classes like Client 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 Client, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Werkspot\BingAdsApiBundle\Api;
3
4
use BingAds\Proxy\ClientProxy;
5
use BingAds\Reporting\PollGenerateReportRequest;
6
use BingAds\Reporting\ReportTimePeriod;
7
use BingAds\Reporting\SubmitGenerateReportRequest;
8
use SoapFault;
9
use SoapVar;
10
use Symfony\Component\Filesystem\Filesystem;
11
use Symfony\Component\Finder\Finder;
12
use Werkspot\BingAdsApiBundle\Api\Helper\Csv;
13
use Werkspot\BingAdsApiBundle\Api\Helper\File;
14
use Werkspot\BingAdsApiBundle\Api\Helper\Time;
15
use Werkspot\BingAdsApiBundle\Api\Report\ReportInterface;
16
use Werkspot\BingAdsApiBundle\Guzzle\OauthTokenService;
17
use Werkspot\BingAdsApiBundle\Model\AccessToken;
18
use Werkspot\BingAdsApiBundle\Model\ApiDetails;
19
20
class Client
21
{
22
    /**
23
     * @var array
24
     */
25
    private $config = [];
26
27
    /**
28
     * @var string
29
     */
30
    private $fileName;
31
32
    /**
33
     * @var ClientProxy
34
     */
35
    private $proxy;
36
37
    /**
38
     * @var array
39
     */
40
    public $report;
41
42
    /**
43
     * @var string
44
     */
45
    private $files;
46
47
    /**
48
     * @var OauthTokenService
49
     */
50
    private $oauthTokenService;
51
52
    /**
53
     * @var ApiDetails
54
     */
55
    private $apiDetails;
56
57
    /**
58
     * @var ClientProxy
59
     */
60
    private $clientProxy;
61
62
    /**
63
     * @var Time
64
     */
65
    private $timeHelper;
66
67
    /**
68
     * @var File
69
     */
70
    private $fileHelper;
71
72
    /**
73
     * Client constructor.
74
     *
75
     * @param OauthTokenService $oauthTokenService
76
     * @param ApiDetails $apiDetails
77
     * @param ClientProxy $clientProxy
78
     * @param File $file
79
     * @param Csv $csv
80
     * @param Time $timeHelper
81
     */
82
    public function __construct(OauthTokenService $oauthTokenService, ApiDetails $apiDetails, ClientProxy $clientProxy, File $file, Csv $csv, Time $timeHelper)
83
    {
84
        $this->oauthTokenService = $oauthTokenService;
85
        $this->apiDetails = $apiDetails;
86
        $this->clientProxy = $clientProxy;
87
        $this->fileHelper = $file;
88
        $this->csvHelper = $csv;
0 ignored issues
show
Bug introduced by
The property csvHelper does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
89
        $this->timeHelper = $timeHelper;
90
91
        ini_set('soap.wsdl_cache_enabled', '0');
92
        ini_set('soap.wsdl_cache_ttl', '0');
93
94
        $this->fileName = 'report.zip';
95
96
        $this->report = [
97
            'GeoLocationPerformanceReport' => new Report\GeoLocationPerformanceReport(),
98
        ];
99
    }
100
101
    public function setApiDetails(ApiDetails $apiDetails)
102
    {
103
        $this->apiDetails = $apiDetails;
104
    }
105
106
    /**
107
     * Sets the configuration
108
     *
109
     * @param $config
110
     */
111
    public function setConfig($config)
112
    {
113
        $this->config = $config;
114
        $this->config['cache_dir'] = $this->config['cache_dir'] . '/' . 'BingAdsApiBundle'; //<-- important for the cache clear function
115
        $this->config['csv']['fixHeader']['removeColumnHeader'] = true; //-- fix till i know how to do this
116
    }
117
118
    public function getRefreshToken()
119
    {
120
        return $this->apiDetails->getRefreshToken();
121
    }
122
123
    /**
124
     * @param array $columns
125
     * @param string $name
126
     * @param $timePeriod
127
     * @param null $fileLocation
128
     *
129
     * @return array|string
130
     */
131
    public function get(array $columns, $name = 'GeoLocationPerformanceReport', $timePeriod = ReportTimePeriod::LastWeek, $fileLocation = null)
132
    {
133
        $tokens = $this->oauthTokenService->refreshToken(
134
            $this->apiDetails->getClientId(),
135
            $this->apiDetails->getSecret(),
136
            $this->apiDetails->getRedirectUri(),
137
            new AccessToken(null, $this->apiDetails->getRefreshToken())
138
        );
139
140
        $accessToken = $tokens->getAccessToken();
141
        $this->apiDetails->setRefreshToken($tokens->getRefreshToken());
142
143
        $report = $this->report[$name];
144
        $reportRequest = $report->getRequest($columns, $timePeriod);
145
        $this->setProxy($report::WSDL, $accessToken);
146
        $files = $this->getFilesFromReportRequest($reportRequest, $name, "{$this->getCacheDir()}/{$this->fileName}", $report);
147
148
        if ($fileLocation) {
149
            $this->moveFirstFile($fileLocation);
150
151
            return $fileLocation;
152
        } else {
153
            return $files;
154
        }
155
    }
156
157
    /**
158
     * @param string $wsdl
159
     * @param string $accessToken
160
     */
161
    private function setProxy($wsdl, $accessToken)
162
    {
163
        $this->proxy = $this->clientProxy->ConstructWithCredentials($wsdl, null, null, $this->apiDetails->getDevToken(), $accessToken);
164
    }
165
166
    /**
167
     * @return string
168
     */
169
    private function getCacheDir()
170
    {
171
        $fs = new Filesystem();
172
        if (!$fs->exists($this->config['cache_dir'])) {
173
            $fs->mkdir($this->config['cache_dir'], 0700);
174
        }
175
176
        return $this->config['cache_dir'];
177
    }
178
179
    /**
180
     * @param $reportRequest
181
     * @param $name
182
     * @param $downloadFile
183
     *
184
     * @throws \Exception
185
     *
186
     * @return string
187
     */
188
    private function getFilesFromReportRequest($reportRequest, $name, $downloadFile, ReportInterface $report)
189
    {
190
        $reportRequestId = $this->submitGenerateReport($reportRequest, $name);
191
        $reportRequestStatus = $this->waitForStatus($reportRequestId);
192
        $reportDownloadUrl = $reportRequestStatus->ReportDownloadUrl;
193
        $zipFile = $this->fileHelper->getFile($reportDownloadUrl, $downloadFile);
194
        $this->files = $this->fileHelper->unZip($zipFile);
2 ignored issues
show
Bug introduced by
It seems like $zipFile defined by $this->fileHelper->getFi...loadUrl, $downloadFile) on line 193 can also be of type boolean; however, Werkspot\BingAdsApiBundle\Api\Helper\File::unZip() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Documentation Bug introduced by
It seems like $this->fileHelper->unZip($zipFile) of type array is incompatible with the declared type string of property $files.

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...
195
        $this->fixFile($report);
196
197
        return $this->files;
198
    }
199
200
    /**
201
     * SubmitGenerateReport helper method calls the corresponding Bing Ads service operation
202
     * to request the report identifier. The identifier is used to check report generation status
203
     * before downloading the report.
204
     *
205
     * @param mixed  $report
206
     * @param string $name
207
     *
208
     * @return string ReportRequestId
209
     */
210
    private function submitGenerateReport($report, $name)
211
    {
212
        $request = new SubmitGenerateReportRequest();
213
        try {
214
            $request->ReportRequest = $this->getReportRequest($report, $name);
1 ignored issue
show
Documentation Bug introduced by
It seems like $this->getReportRequest($report, $name) of type object<SoapVar> is incompatible with the declared type object<BingAds\Reporting\ReportRequest> of property $ReportRequest.

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...
215
216
            return $this->proxy->GetService()->SubmitGenerateReport($request)->ReportRequestId;
217
        } catch (SoapFault $e) {
218
            $this->parseSoapFault($e);
219
        }
220
    }
221
222
    /**
223
     * @param mixed  $report
224
     * @param string $name
225
     *
226
     * @return SoapVar
227
     */
228
    private function getReportRequest($report, $name)
229
    {
230
        $name = "{$name}Request";
231
232
        return new SoapVar($report, SOAP_ENC_OBJECT, $name, $this->proxy->GetNamespace());
233
    }
234
235
    /**
236
     * Check if the report is ready for download
237
     * if not wait 10 sec and retry. (up to 6,5 hour)
238
     * After 30 tries check every 1 minute
239
     * After 34 tries check every 5 minutes
240
     * After 39 tries check every 15 minutes
241
     * After 43 tries check every 30 minutes
242
     *
243
     * @param string  $reportRequestId
244
     * @param int     $count
245
     * @param int     $maxCount
246
     * @param int     $sleep
247
     * @param bool $incrementTime
248
     *
249
     * @throws Exceptions\ReportRequestErrorException
250
     * @throws Exceptions\RequestTimeoutException
251
     *
252
     * @return string
253
     */
254
    private function waitForStatus($reportRequestId, $count = 1, $maxCount = 48, $sleep = 10, $incrementTime = true)
255
    {
256
        if ($count > $maxCount) {
257
            throw new Exceptions\RequestTimeoutException("The request is taking longer than expected.\nSave the report ID ({$reportRequestId}) and try again later.");
258
        }
259
260
        $reportRequestStatus = $this->pollGenerateReport($reportRequestId);
261
        if ($reportRequestStatus->Status == 'Pending') {
262
            ++$count;
263
            $this->timeHelper->sleep($sleep);
264
            if ($incrementTime) {
265
                switch ($count) {
266
                    case 31: // after 5 minutes
267
                        $sleep = (1 * 60);
268
                        break;
269
                    case 35: // after 10 minutes
270
                        $sleep = (5 * 60);
271
                        break;
272
                    case 40: // after 30 minutes
273
                        $sleep = (15 * 60);
274
                        break;
275
                    case 44: // after 1,5 hours
1 ignored issue
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
276
                        $sleep = (30 * 60);
277
                        break;
278
                }
279
            }
280
            $reportRequestStatus = $this->waitForStatus($reportRequestId, $count, $maxCount, $sleep, $incrementTime);
281
        }
282
283
        if ($reportRequestStatus->Status == 'Error') {
284
            throw new Exceptions\ReportRequestErrorException("The request failed. Try requesting the report later.\nIf the request continues to fail, contact support.", $reportRequestStatus->Status, $reportRequestId);
285
        }
286
287
        return $reportRequestStatus;
288
    }
289
290
    /**
291
     * Check the status of the report request. The guidance of how often to poll
292
     * for status is from every five to 15 minutes depending on the amount
293
     * of data being requested. For smaller reports, you can poll every couple
294
     * of minutes. You should stop polling and try again later if the request
295
     * is taking longer than an hour.
296
     *
297
     * @param $reportRequestId
298
     *
299
     * @return string ReportRequestStatus
300
     */
301
    private function pollGenerateReport($reportRequestId)
302
    {
303
        $request = new PollGenerateReportRequest();
304
        $request->ReportRequestId = $reportRequestId;
305
        try {
306
            return $this->proxy->GetService()->PollGenerateReport($request)->ReportRequestStatus;
307
        } catch (SoapFault $e) {
308
            $this->parseSoapFault($e);
309
        }
310
    }
311
312
    /**
313
     * @param array|null $files
314
     *
315
     * @return self
316
     */
317
    private function fixFile(ReportInterface $report, array $files = null)
318
    {
319
        $files = (!$files) ? $this->files : $files;
320
        foreach ($files as $file) {
0 ignored issues
show
Bug introduced by
The expression $files of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
321
            $lines = file($file);
322
            $lines = $this->csvHelper->removeHeaders($lines, $this->config['csv']['fixHeader']['removeColumnHeader'], $report::FILE_HEADERS, $report::COLUMN_HEADERS);
323
            $lines = $this->csvHelper->removeLastLines($lines);
324
            $lines = $this->csvHelper->convertDateMDYtoYMD($lines);
325
            $fp = fopen($file, 'w');
326
            fwrite($fp, implode('', $lines));
327
            fclose($fp);
328
        }
329
330
        return $this;
331
    }
332
333
    /**
334
     * Move first file form array $this->files to the target location
335
     *
336
     * @param string $target
337
     *
338
     * @return self
339
     */
340
    private function moveFirstFile($target)
341
    {
342
        $fs = new Filesystem();
343
        $fs->rename($this->files[0], $target);
344
345
        return $this;
346
    }
347
348
    /**
349
     * Clear Bundle Cache directory
350
     *
351
     * @param bool $allFiles delete all files in bundles cache, if false deletes only extracted files ($this->files)
352
     *
353
     * @return self
354
     *
355
     * @codeCoverageIgnore
356
     */
357
    public function clearCache($allFiles = false)
358
    {
359
        $fileSystem = new Filesystem();
360
361
        if ($allFiles) {
362
            $finder = new Finder();
363
            $files = $finder->files()->in($this->config['cache_dir']);
364
        } else {
365
            $files = $this->files;
366
        }
367
368
        foreach ($files as $file) {
369
            $fileSystem->remove($file);
370
        }
371
372
        return $this;
373
    }
374
375
    /**
376
     * @param SoapFault $e
377
     *
378
     * @throws Exceptions\SoapInternalErrorException
379
     * @throws Exceptions\SoapInvalidCredentialsException
380
     * @throws Exceptions\SoapNoCompleteDataAvailableException
381
     * @throws Exceptions\SoapReportingServiceInvalidReportIdException
382
     * @throws Exceptions\SoapUnknownErrorException
383
     * @throws Exceptions\SoapUserIsNotAuthorizedException
384
     */
385
    private function parseSoapFault(SoapFault $e)
386
    {
387
        if (isset($e->detail->AdApiFaultDetail)) {
388
            $error = $e->detail->AdApiFaultDetail->Errors->AdApiError;
1 ignored issue
show
Bug introduced by
The property detail does not seem to exist in SoapFault.

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...
389
        } elseif (isset($e->detail->ApiFaultDetail)) {
390
            if (!empty($e->detail->ApiFaultDetail->BatchErrors)) {
391
                $error = $error = $e->detail->ApiFaultDetail->Errors->AdApiError;
392
            } elseif (!empty($e->detail->ApiFaultDetail->OperationErrors)) {
393
                $error = $e->detail->ApiFaultDetail->OperationErrors->OperationError;
394
            }
395
        }
396
        $errors = is_array($error) ? $error : ['error' => $error];
0 ignored issues
show
Bug introduced by
The variable $error does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
397
        foreach ($errors as $error) {
398
            switch ($error->Code) {
399
                case 0:
400
                    throw new Exceptions\SoapInternalErrorException($error->Message, $error->Code);
401
                case 105:
402
                    throw new Exceptions\SoapInvalidCredentialsException($error->Message, $error->Code);
403
                case 106:
404
                    throw new Exceptions\SoapUserIsNotAuthorizedException($error->Message, $error->Code);
405
                case 2004:
406
                    throw new Exceptions\SoapNoCompleteDataAvailableException($error->Message, $error->Code);
407
                case 2100:
408
                    throw new Exceptions\SoapReportingServiceInvalidReportIdException($error->Message, $error->Code);
409
                default:
410
                    $errorMessage = "[{$error->Code}]\n{$error->Message}";
411
                    throw new Exceptions\SoapUnknownErrorException($errorMessage, $error->Code);
412
            }
413
        }
414
    }
415
}
416