Completed
Pull Request — master (#4)
by Laurens
02:47
created

Client::submitGenerateReport()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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