Completed
Pull Request — master (#4)
by Laurens
03:17
created

Client   C

Complexity

Total Complexity 43

Size/Duplication

Total Lines 404
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 20

Importance

Changes 25
Bugs 2 Features 6
Metric Value
wmc 43
c 25
b 2
f 6
lcom 1
cbo 20
dl 0
loc 404
rs 5.2826

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 27 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\ReportRequest;
7
use BingAds\Reporting\ReportTimePeriod;
8
use BingAds\Reporting\SubmitGenerateReportRequest;
9
use Exception;
10
use SoapFault;
11
use SoapVar;
12
use Symfony\Component\Filesystem\Filesystem;
13
use Symfony\Component\Finder\Finder;
14
use Werkspot\BingAdsApiBundle\Api\Helper\Csv;
15
use Werkspot\BingAdsApiBundle\Api\Helper\File;
16
use Werkspot\BingAdsApiBundle\Api\Helper\Time;
17
use Werkspot\BingAdsApiBundle\Api\Report\ReportInterface;
18
use Werkspot\BingAdsApiBundle\Guzzle\OauthTokenService;
19
use Werkspot\BingAdsApiBundle\Model\AccessToken;
20
use Werkspot\BingAdsApiBundle\Model\ApiDetails;
21
22
class Client
23
{
24
    /**
25
     * @var array
26
     */
27
    private $config = [];
28
29
    /**
30
     * @var string
31
     */
32
    private $fileName;
33
34
    /**
35
     * @var ClientProxy
36
     */
37
    private $proxy;
38
39
    /**
40
     * @var array
41
     */
42
    public $report;
43
44
    /**
45
     * @var string
46
     */
47
    private $files;
48
49
    /**
50
     * @var OauthTokenService
51
     */
52
    private $oauthTokenService;
53
54
    /**
55
     * @var ApiDetails
56
     */
57
    private $apiDetails;
58
59
    /**
60
     * @var ClientProxy
61
     */
62
    private $clientProxy;
63
64
    /**
65
     * @var File
66
     */
67
    private $fileHelper;
68
69
    /**
70
     * @var Csv
71
     */
72
    private $csvHelper;
73
74
    /**
75
     * @var Time
76
     */
77
    private $timeHelper;
78
79
    /**
80
     * Client constructor.
81
     *
82
     * @param OauthTokenService $oauthTokenService
83
     * @param ApiDetails $apiDetails
84
     * @param ClientProxy $clientProxy
85
     * @param File $file
86
     * @param Csv $csv
87
     * @param Time $timeHelper
88
     */
89
    public function __construct(OauthTokenService $oauthTokenService, ApiDetails $apiDetails, ClientProxy $clientProxy, File $file, Csv $csv, Time $timeHelper)
90
    {
91
        $this->oauthTokenService = $oauthTokenService;
92
        $this->apiDetails = $apiDetails;
93
        $this->clientProxy = $clientProxy;
94
        $this->fileHelper = $file;
95
        $this->csvHelper = $csv;
96
        $this->timeHelper = $timeHelper;
97
98
        ini_set('soap.wsdl_cache_enabled', '0');
99
        ini_set('soap.wsdl_cache_ttl', '0');
100
101
        $this->fileName = 'report.zip';
102
103
        $this->report = [
104
            'GeoLocationPerformanceReport' => new Report\GeoLocationPerformanceReport(),
105
        ];
106
    }
107
108
    public function setApiDetails(ApiDetails $apiDetails)
109
    {
110
        $this->apiDetails = $apiDetails;
111
    }
112
113
    /**
114
     * Sets the configuration
115
     *
116
     * @param $config
117
     */
118
    public function setConfig($config)
119
    {
120
        $this->config = $config;
121
        $this->config['cache_dir'] = $this->config['cache_dir'] . '/' . 'BingAdsApiBundle'; //<-- important for the cache clear function
122
        $this->config['csv']['fixHeader']['removeColumnHeader'] = true; //-- fix till i know how to do this
123
    }
124
125
    public function getRefreshToken()
126
    {
127
        return $this->apiDetails->getRefreshToken();
128
    }
129
130
    /**
131
     * @param array $columns
132
     * @param string $name
133
     * @param $timePeriod
134
     * @param null|string $fileLocation
135
     *
136
     * @return array|string
137
     */
138
    public function get(array $columns, $name = 'GeoLocationPerformanceReport', $timePeriod = ReportTimePeriod::LastWeek, $fileLocation = null)
139
    {
140
        $tokens = $this->oauthTokenService->refreshToken(
141
            $this->apiDetails->getClientId(),
142
            $this->apiDetails->getSecret(),
143
            $this->apiDetails->getRedirectUri(),
144
            new AccessToken(null, $this->apiDetails->getRefreshToken())
145
        );
146
147
        $accessToken = $tokens->getAccessToken();
148
        $this->apiDetails->setRefreshToken($tokens->getRefreshToken());
149
150
        $report = $this->report[$name];
151
        $report->setTimePeriod($timePeriod);
152
        $report->setColumns($columns);
153
        $reportRequest = $report->getRequest();
154
        $this->setProxy($report::WSDL, $accessToken);
155
        $files = $this->getFilesFromReportRequest($reportRequest, $name, "{$this->getCacheDir()}/{$this->fileName}", $report);
156
157
        if ($fileLocation) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fileLocation of type null|string is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
158
            $this->moveFirstFile($fileLocation);
159
160
            return $fileLocation;
161
        } else {
162
            return $files;
163
        }
164
    }
165
166
    /**
167
     * @param string $wsdl
168
     * @param string $accessToken
169
     */
170
    private function setProxy($wsdl, $accessToken)
171
    {
172
        $this->proxy = $this->clientProxy->ConstructWithCredentials($wsdl, null, null, $this->apiDetails->getDevToken(), $accessToken);
173
    }
174
175
    /**
176
     * @return string
177
     */
178
    private function getCacheDir()
179
    {
180
        $fs = new Filesystem();
181
        if (!$fs->exists($this->config['cache_dir'])) {
182
            $fs->mkdir($this->config['cache_dir'], 0700);
183
        }
184
185
        return $this->config['cache_dir'];
186
    }
187
188
    /**
189
     * @param ReportRequest $reportRequest
190
     * @param string $name
191
     * @param string $downloadFile
192
     * @param ReportInterface $report
193
     *
194
     * @throws Exception
195
     *
196
     * @return array
197
     */
198
    private function getFilesFromReportRequest(ReportRequest $reportRequest, $name, $downloadFile, ReportInterface $report)
199
    {
200
        $reportRequestId = $this->submitGenerateReport($reportRequest, $name);
201
        $reportRequestStatus = $this->waitForStatus($reportRequestId);
202
        $reportDownloadUrl = $reportRequestStatus->ReportDownloadUrl;
203
        $zipFile = $this->fileHelper->getFile($reportDownloadUrl, $downloadFile);
204
        $this->files = $this->fileHelper->unZip($zipFile);
1 ignored issue
show
Bug introduced by
It seems like $zipFile defined by $this->fileHelper->getFi...loadUrl, $downloadFile) on line 203 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...
205
        $this->fixFile($report);
206
207
        return $this->files;
208
    }
209
210
    /**
211
     * SubmitGenerateReport helper method calls the corresponding Bing Ads service operation
212
     * to request the report identifier. The identifier is used to check report generation status
213
     * before downloading the report.
214
     *
215
     * @param mixed  $report
216
     * @param string $name
217
     *
218
     * @return string ReportRequestId
219
     */
220
    private function submitGenerateReport($report, $name)
221
    {
222
        $request = new SubmitGenerateReportRequest();
223
        try {
224
            $request->ReportRequest = $this->getReportRequest($report, $name);
225
226
            return $this->proxy->GetService()->SubmitGenerateReport($request)->ReportRequestId;
227
        } catch (SoapFault $e) {
228
            $this->parseSoapFault($e);
229
        }
230
    }
231
232
    /**
233
     * @param mixed  $report
234
     * @param string $name
235
     *
236
     * @return SoapVar
237
     */
238
    private function getReportRequest($report, $name)
239
    {
240
        $name = "{$name}Request";
241
242
        return new SoapVar($report, SOAP_ENC_OBJECT, $name, $this->proxy->GetNamespace());
243
    }
244
245
    /**
246
     * Check if the report is ready for download
247
     * if not wait 10 sec and retry. (up to 6,5 hour)
248
     * After 30 tries check every 1 minute
249
     * After 34 tries check every 5 minutes
250
     * After 39 tries check every 15 minutes
251
     * After 43 tries check every 30 minutes
252
     *
253
     * @param string  $reportRequestId
254
     * @param int     $count
255
     * @param int     $maxCount
256
     * @param int     $sleep
257
     * @param bool $incrementTime
258
     *
259
     * @throws Exceptions\ReportRequestErrorException
260
     * @throws Exceptions\RequestTimeoutException
261
     *
262
     * @return string
263
     */
264
    private function waitForStatus($reportRequestId, $count = 1, $maxCount = 48, $sleep = 10, $incrementTime = true)
265
    {
266
        if ($count > $maxCount) {
267
            throw new Exceptions\RequestTimeoutException("The request is taking longer than expected.\nSave the report ID ({$reportRequestId}) and try again later.");
268
        }
269
270
        $reportRequestStatus = $this->pollGenerateReport($reportRequestId);
271
        if ($reportRequestStatus->Status == 'Pending') {
272
            ++$count;
273
            $this->timeHelper->sleep($sleep);
274
            if ($incrementTime) {
275
                switch ($count) {
276
                    case 31: // after 5 minutes
277
                        $sleep = (1 * 60);
278
                        break;
279
                    case 35: // after 10 minutes
280
                        $sleep = (5 * 60);
281
                        break;
282
                    case 40: // after 30 minutes
283
                        $sleep = (15 * 60);
284
                        break;
285
                    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...
286
                        $sleep = (30 * 60);
287
                        break;
288
                }
289
            }
290
            $reportRequestStatus = $this->waitForStatus($reportRequestId, $count, $maxCount, $sleep, $incrementTime);
291
        }
292
293
        if ($reportRequestStatus->Status == 'Error') {
294
            throw new Exceptions\ReportRequestErrorException("The request failed. Try requesting the report later.\nIf the request continues to fail, contact support.", $reportRequestStatus->Status, $reportRequestId);
295
        }
296
297
        return $reportRequestStatus;
298
    }
299
300
    /**
301
     * Check the status of the report request. The guidance of how often to poll
302
     * for status is from every five to 15 minutes depending on the amount
303
     * of data being requested. For smaller reports, you can poll every couple
304
     * of minutes. You should stop polling and try again later if the request
305
     * is taking longer than an hour.
306
     *
307
     * @param string $reportRequestId
308
     *
309
     * @return string ReportRequestStatus
310
     */
311
    private function pollGenerateReport($reportRequestId)
312
    {
313
        $request = new PollGenerateReportRequest();
314
        $request->ReportRequestId = $reportRequestId;
315
        try {
316
            return $this->proxy->GetService()->PollGenerateReport($request)->ReportRequestStatus;
317
        } catch (SoapFault $e) {
318
            $this->parseSoapFault($e);
319
        }
320
    }
321
322
    /**
323
     * @param array|null $files
324
     *
325
     * @return self
326
     */
327
    private function fixFile(ReportInterface $report, array $files = null)
328
    {
329
        $files = (!$files) ? $this->files : $files;
330
        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...
331
            $lines = file($file);
332
            $lines = $this->csvHelper->removeHeaders($lines, $this->config['csv']['fixHeader']['removeColumnHeader'], $report::FILE_HEADERS, $report::COLUMN_HEADERS);
333
            $lines = $this->csvHelper->removeLastLines($lines);
334
            $lines = $this->csvHelper->convertDateMDYtoYMD($lines);
335
            $fp = fopen($file, 'w');
336
            fwrite($fp, implode('', $lines));
337
            fclose($fp);
338
        }
339
340
        return $this;
341
    }
342
343
    /**
344
     * Move first file form array $this->files to the target location
345
     *
346
     * @param string $target
347
     *
348
     * @return self
349
     */
350
    private function moveFirstFile($target)
351
    {
352
        $fs = new Filesystem();
353
        $fs->rename($this->files[0], $target);
354
355
        return $this;
356
    }
357
358
    /**
359
     * Clear Bundle Cache directory
360
     *
361
     * @param bool $allFiles delete all files in bundles cache, if false deletes only extracted files ($this->files)
362
     *
363
     * @return self
364
     *
365
     * @codeCoverageIgnore
366
     */
367
    public function clearCache($allFiles = false)
368
    {
369
        $fileSystem = new Filesystem();
370
371
        if ($allFiles) {
372
            $finder = new Finder();
373
            $files = $finder->files()->in($this->config['cache_dir']);
374
        } else {
375
            $files = $this->files;
376
        }
377
378
        foreach ($files as $file) {
379
            $fileSystem->remove($file);
380
        }
381
382
        return $this;
383
    }
384
385
    /**
386
     * @param SoapFault $e
387
     *
388
     * @throws Exceptions\SoapInternalErrorException
389
     * @throws Exceptions\SoapInvalidCredentialsException
390
     * @throws Exceptions\SoapNoCompleteDataAvailableException
391
     * @throws Exceptions\SoapReportingServiceInvalidReportIdException
392
     * @throws Exceptions\SoapUnknownErrorException
393
     * @throws Exceptions\SoapUserIsNotAuthorizedException
394
     */
395
    private function parseSoapFault(SoapFault $e)
396
    {
397
        if (isset($e->detail->AdApiFaultDetail)) {
398
            $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...
399
        } elseif (isset($e->detail->ApiFaultDetail)) {
400
            if (!empty($e->detail->ApiFaultDetail->BatchErrors)) {
401
                $error = $error = $e->detail->ApiFaultDetail->Errors->AdApiError;
402
            } elseif (!empty($e->detail->ApiFaultDetail->OperationErrors)) {
403
                $error = $e->detail->ApiFaultDetail->OperationErrors->OperationError;
404
            }
405
        }
406
        $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...
407
        foreach ($errors as $error) {
408
            switch ($error->Code) {
409
                case 0:
410
                    throw new Exceptions\SoapInternalErrorException($error->Message, $error->Code);
411
                case 105:
412
                    throw new Exceptions\SoapInvalidCredentialsException($error->Message, $error->Code);
413
                case 106:
414
                    throw new Exceptions\SoapUserIsNotAuthorizedException($error->Message, $error->Code);
415
                case 2004:
416
                    throw new Exceptions\SoapNoCompleteDataAvailableException($error->Message, $error->Code);
417
                case 2100:
418
                    throw new Exceptions\SoapReportingServiceInvalidReportIdException($error->Message, $error->Code);
419
                default:
420
                    $errorMessage = "[{$error->Code}]\n{$error->Message}";
421
                    throw new Exceptions\SoapUnknownErrorException($errorMessage, $error->Code);
422
            }
423
        }
424
    }
425
}
426