Failed Conditions
Pull Request — master (#8)
by Laurens
02:49
created

Api/Client.php (5 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
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\Exceptions\InvalidReportNameException;
15
use Werkspot\BingAdsApiBundle\Api\Helper\Csv;
16
use Werkspot\BingAdsApiBundle\Api\Helper\File;
17
use Werkspot\BingAdsApiBundle\Api\Helper\Time;
18
use Werkspot\BingAdsApiBundle\Api\Report\ReportInterface;
19
use Werkspot\BingAdsApiBundle\Guzzle\OauthTokenService;
20
use Werkspot\BingAdsApiBundle\Model\AccessToken;
21
use Werkspot\BingAdsApiBundle\Model\ApiDetails;
22
23
class Client
24
{
25
    const CACHE_SUBDIRECTORY = 'BingAdsApiBundle';
26
    /**
27
     * @var array
28
     */
29
    private $config = [];
30
    /**
31
     * @var string
32
     */
33
    private $fileName;
34
35
    /**
36
     * @var ClientProxy
37
     */
38
    private $proxy;
39
40
    /**
41
     * @var array
42
     */
43
    public $report;
44
45
    /**
46
     * @var string
47
     */
48
    private $files;
49
50
    /**
51
     * @var OauthTokenService
52
     */
53
    private $oauthTokenService;
54
55
    /**
56
     * @var ApiDetails
57
     */
58
    private $apiDetails;
59
60
    /**
61
     * @var ClientProxy
62
     */
63
    private $clientProxy;
64
65
    /**
66
     * @var File
67
     */
68
    private $fileHelper;
69
70
    /**
71
     * @var Csv
72
     */
73
    private $csvHelper;
74
75
    /**
76
     * @var Time
77
     */
78
    private $timeHelper;
79
80
    /**
81
     * @var Filesystem
82
     */
83
    private $filesystem;
84
85
    /**
86
     * @param OauthTokenService $oauthTokenService
87
     * @param ApiDetails $apiDetails
88
     * @param ClientProxy $clientProxy
89 25
     * @param File $file
90 1
     * @param Csv $csv
91 25
     * @param Time $timeHelper
92 25
     * @param Filesystem $fileSystem
93 25
     */
94 25
    public function __construct(OauthTokenService $oauthTokenService, ApiDetails $apiDetails, ClientProxy $clientProxy, File $file, Csv $csv, Time $timeHelper, Filesystem $fileSystem)
95 25
    {
96 25
        $this->filesystem = $fileSystem;
97
        $this->oauthTokenService = $oauthTokenService;
98 25
        $this->apiDetails = $apiDetails;
99 25
        $this->clientProxy = $clientProxy;
100
        $this->fileHelper = $file;
101 25
        $this->csvHelper = $csv;
102
        $this->timeHelper = $timeHelper;
103 25
104 25
        ini_set('soap.wsdl_cache_enabled', '0');
105
        ini_set('soap.wsdl_cache_ttl', '0');
106 25
107
        $this->fileName = 'report.zip';
108 1
109
        $this->report = [
110 1
            'GeoLocationPerformanceReport' => new Report\GeoLocationPerformanceReport(),
111 1
        ];
112
    }
113
114
    public function setApiDetails(ApiDetails $apiDetails)
115
    {
116
        $this->apiDetails = $apiDetails;
117
    }
118 24
119
    /**
120 24
     * Sets the configuration
121 24
     *
122 24
     * @param $config
123 24
     */
124
    public function setConfig($config)
125 1
    {
126
        //TODO: make this specific setter
127 1
        $this->config = $config;
128
        $this->config['cache_dir'] = $this->config['cache_dir'] . '/' . self::CACHE_SUBDIRECTORY; //<-- important for the cache clear function
129
        $this->config['csv']['fixHeader']['removeColumnHeader'] = true; //-- fix till i know how to do this
130
    }
131
132
    /**
133
     * @param $cacheDir
134
     */
135
    public function setCacheDir($cacheDir)
136
    {
137
        $this->cacheDir = $cacheDir;
0 ignored issues
show
The property cacheDir 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...
138 24
    }
139
140 24
    public function getRefreshToken()
141 24
    {
142 24
        return $this->apiDetails->getRefreshToken();
143 24
    }
144 24
145 24
    /**
146
     * @param string $reportName
147 24
     * @param array $columns
148 24
     * @param $timePeriod
149
     * @param null|string $fileLocation
150 24
     */
151 24
    public function getReport($reportName, array $columns, $timePeriod = ReportTimePeriod::LastWeek, $fileLocation)
152 24
    {
153 24
        $this->ensureValidReportName($reportName);
154 24
        $oauthToken = $this->getOauthToken();
155 24
        $this->apiDetails->setRefreshToken($oauthToken->getRefreshToken());
156
157 3
        $report = $this->report[$reportName];
158 1
        $report->setTimePeriod($timePeriod);
159
        $report->setColumns($columns);
160 1
        $reportRequest = $report->getRequest();
161
        $this->setProxy($report::WSDL, $oauthToken->getAccessToken());
162 2
        $files = $this->getFilesFromReportRequest($reportRequest, $reportName, "{$this->getCacheDir()}/{$this->fileName}", $report);
163
164
        $this->moveFirstFile($files, $fileLocation);
165
        $this->files = $files;
0 ignored issues
show
Documentation Bug introduced by
It seems like $files 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...
166
    }
167
168
    /**
169
     * @return AccessToken
170 24
     */
171
    protected function getOauthToken()
172 24
    {
173 24
        return  $this->oauthTokenService->refreshToken(
174
            $this->apiDetails->getClientId(),
175
            $this->apiDetails->getSecret(),
176
            $this->apiDetails->getRedirectUri(),
177
            new AccessToken(null, $this->apiDetails->getRefreshToken())
178 24
        );
179
    }
180 24
181 24
    /**
182 1
     * @param string $wsdl
183 1
     * @param string $accessToken
184
     */
185 24
    private function setProxy($wsdl, $accessToken)
186
    {
187
        $this->proxy = $this->clientProxy->ConstructWithCredentials($wsdl, null, null, $this->apiDetails->getDevToken(), $accessToken);
188
    }
189
190
    /**
191
     * @return string
192
     */
193
    private function getCacheDir()
194
    {
195
        $this->createCacheDirIfNotExists();
196
197
        return $this->config['cache_dir'];
198 24
    }
199
200 24
    /**
201 6
     * @param ReportRequest $reportRequest
202 3
     * @param string $name
203 3
     * @param string $downloadFile
204 3
     * @param ReportInterface $report
205 3
     *
206 3
     * @throws Exception
207 3
     *
208
     * @return array|string
209 3
     */
210
    private function getFilesFromReportRequest(ReportRequest $reportRequest, $name, $downloadFile, ReportInterface $report)
211
    {
212
        $reportRequestId = $this->submitGenerateReport($reportRequest, $name);
213
        $reportRequestStatus = $this->waitForStatus($reportRequestId);
214
        $reportDownloadUrl = $reportRequestStatus->ReportDownloadUrl;
215
        $zipFile = $this->fileHelper->copyFile($reportDownloadUrl, $downloadFile);
216
        if ($zipFile !== false) {
217
            $files = $this->fixFile($report, $this->fileHelper->unZip($zipFile));
0 ignored issues
show
It seems like $zipFile defined by $this->fileHelper->copyF...loadUrl, $downloadFile) on line 215 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...
218
        }
219
220
        return $files;
0 ignored issues
show
The variable $files 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...
221
    }
222 24
223
    /**
224 24
     * SubmitGenerateReport helper method calls the corresponding Bing Ads service operation
225
     * to request the report identifier. The identifier is used to check report generation status
226 24
     * before downloading the report.
227
     *
228 24
     * @param mixed  $report
229 18
     * @param string $name
230 18
     *
231
     * @return string ReportRequestId
232
     */
233
    private function submitGenerateReport($report, $name)
234
    {
235
        $request = new SubmitGenerateReportRequest();
236
        try {
237
            $request->ReportRequest = $this->getReportRequest($report, $name);
238
239
            return $this->proxy->GetService()->SubmitGenerateReport($request)->ReportRequestId;
240 24
        } catch (SoapFault $e) {
241
            $this->parseSoapFault($e);
242 24
        }
243
    }
244 24
245
    /**
246
     * @param mixed  $report
247
     * @param string $name
248
     *
249
     * @return SoapVar
250
     */
251
    private function getReportRequest($report, $name)
252
    {
253
        $name = "{$name}Request";
254
255
        return new SoapVar($report, SOAP_ENC_OBJECT, $name, $this->proxy->GetNamespace());
256
    }
257
258
    /**
259
     * Check if the report is ready for download
260
     * if not wait 10 sec and retry. (up to 6,5 hour)
261
     * After 30 tries check every 1 minute
262
     * After 34 tries check every 5 minutes
263
     * After 39 tries check every 15 minutes
264
     * After 43 tries check every 30 minutes
265
     *
266 6
     * @param string  $reportRequestId
267
     * @param int     $count
268 6
     * @param int     $maxCount
269 1
     * @param int     $sleep
270
     * @param bool $incrementTime
271
     *
272 6
     * @throws Exceptions\ReportRequestErrorException
273 5
     * @throws Exceptions\RequestTimeoutException
274 1
     *
275 1
     * @return string
276 1
     */
277
    private function waitForStatus($reportRequestId, $count = 1, $maxCount = 48, $sleep = 10, $incrementTime = true)
278 1
    {
279 1
        if ($count > $maxCount) {
280 1
            throw new Exceptions\RequestTimeoutException("The request is taking longer than expected.\nSave the report ID ({$reportRequestId}) and try again later.");
281 1
        }
282 1
283 1
        $reportRequestStatus = $this->pollGenerateReport($reportRequestId);
284 1
        if ($reportRequestStatus->Status == 'Pending') {
285 1
            ++$count;
286 1
            $this->timeHelper->sleep($sleep);
287 1
            if ($incrementTime) {
288 1
                switch ($count) {
289 1
                    case 31: // after 5 minutes
290
                        $sleep = (1 * 60);
291 1
                        break;
292 1
                    case 35: // after 10 minutes
293
                        $sleep = (5 * 60);
294
                        break;
295 4
                    case 40: // after 30 minutes
296 1
                        $sleep = (15 * 60);
297
                        break;
298
                    case 44: // after 1,5 hours
299 3
                        $sleep = (30 * 60);
300
                        break;
301
                }
302
            }
303
            $reportRequestStatus = $this->waitForStatus($reportRequestId, $count, $maxCount, $sleep, $incrementTime);
304
        }
305
306
        if ($reportRequestStatus->Status == 'Error') {
307
            throw new Exceptions\ReportRequestErrorException("The request failed. Try requesting the report later.\nIf the request continues to fail, contact support.", $reportRequestStatus->Status, $reportRequestId);
308
        }
309
310
        return $reportRequestStatus;
311
    }
312
313 6
    /**
314
     * Check the status of the report request. The guidance of how often to poll
315 6
     * for status is from every five to 15 minutes depending on the amount
316 6
     * of data being requested. For smaller reports, you can poll every couple
317
     * of minutes. You should stop polling and try again later if the request
318 6
     * is taking longer than an hour.
319 1
     *
320 1
     * @param string $reportRequestId
321
     *
322
     * @return string ReportRequestStatus
323
     */
324
    private function pollGenerateReport($reportRequestId)
325
    {
326
        $request = new PollGenerateReportRequest();
327
        $request->ReportRequestId = $reportRequestId;
328
        try {
329 3
            return $this->proxy->GetService()->PollGenerateReport($request)->ReportRequestStatus;
330
        } catch (SoapFault $e) {
331 3
            $this->parseSoapFault($e);
332 3
        }
333 3
    }
334 3
335 3
    /**
336 3
     * @param array|null $files
337 3
     *
338 3
     * @return self
339 3
     */
340 3
    private function fixFile(ReportInterface $report, array $files)
341
    {
342 3
        foreach ($files as $file) {
343
            $lines = $this->fileHelper->readFileLinesIntoArray($file);
344
345
            $lines = $this->csvHelper->removeHeaders($lines, $this->config['csv']['fixHeader']['removeColumnHeader'], $report::FILE_HEADERS, $report::COLUMN_HEADERS);
346
            $lines = $this->csvHelper->removeLastLines($lines);
347
            $lines = $this->csvHelper->convertDateMDYtoYMD($lines);
348
349
            $this->fileHelper->writeLinesToFile($lines, $file);
350
        }
351
352 1
        return $files;
353
    }
354 1
355 1
    /**
356
     * @param array $files
357 1
     * @param string $target
358
     */
359
    private function moveFirstFile(array $files, $target)
360
    {
361
        $fs = $this->getFileSystem();
362
        $fs->rename($files[0], $target);
363
    }
364
365
    /**
366
     * Clear Bundle Cache directory
367
     *
368
     * @param bool $allFiles delete all files in bundles cache, if false deletes only extracted files ($this->files)
369
     *
370
     * @return self
371
     *
372
     * @codeCoverageIgnore
373
     */
374
    public function clearCache($allFiles = false)
375
    {
376
        $fileSystem = $this->getFileSystem();
377
378
        if ($allFiles) {
379
            $finder = new Finder();
380
            $files = $finder->files()->in($this->config['cache_dir']);
381
        } else {
382
            $files = $this->files;
383
        }
384
385
        foreach ($files as $file) {
0 ignored issues
show
The expression $files of type object<Symfony\Component\Finder\Finder>|string 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...
386
            $fileSystem->remove($file);
387
        }
388
389
        return $this;
390
    }
391
392
    /**
393
     * @param SoapFault $e
394
     *
395
     * @throws Exceptions\SoapInternalErrorException
396
     * @throws Exceptions\SoapInvalidCredentialsException
397 19
     * @throws Exceptions\SoapNoCompleteDataAvailableException
398
     * @throws Exceptions\SoapReportingServiceInvalidReportIdException
399 19
     * @throws Exceptions\SoapUnknownErrorException
400 19
     * @throws Exceptions\SoapUserIsNotAuthorizedException
401 7
     */
402 19
    private function parseSoapFault(SoapFault $e)
403 12
    {
404 6
        $error = null;
405 12
        if (isset($e->detail->AdApiFaultDetail)) {
406 6
            $error = $e->detail->AdApiFaultDetail->Errors->AdApiError;
407 6
        } elseif (isset($e->detail->ApiFaultDetail)) {
408 12
            if (!empty($e->detail->ApiFaultDetail->BatchErrors)) {
409 19
                $error = $error = $e->detail->ApiFaultDetail->BatchErrors->BatchError;
410 19
            } elseif (!empty($e->detail->ApiFaultDetail->OperationErrors)) {
411 19
                $error = $e->detail->ApiFaultDetail->OperationErrors->OperationError;
412 19
            }
413 4
        }
414 15
        $errors = is_array($error) ? $error : ['error' => $error];
415 3
        foreach ($errors as $error) {
416 12
            switch ($error->Code) {
417 3
                case 0:
418 9
                    throw new Exceptions\SoapInternalErrorException($error->Message, $error->Code);
419 3
                case 105:
420 6
                    throw new Exceptions\SoapInvalidCredentialsException($error->Message, $error->Code);
421 3
                case 106:
422 3
                    throw new Exceptions\SoapUserIsNotAuthorizedException($error->Message, $error->Code);
423 3
                case 2004:
424 3
                    throw new Exceptions\SoapNoCompleteDataAvailableException($error->Message, $error->Code);
425 3
                case 2100:
426
                    throw new Exceptions\SoapReportingServiceInvalidReportIdException($error->Message, $error->Code);
427
                default:
428
                    $errorMessage = "[{$error->Code}]\n{$error->Message}";
429
                    throw new Exceptions\SoapUnknownErrorException($errorMessage, $error->Code);
430
            }
431
        }
432
    }
433
434
    /**
435
     * @param $reportName
436
     *
437
     * @throws InvalidReportNameException
438
     */
439
    private function ensureValidReportName($reportName)
440
    {
441
       if ($reportName === '') {
442
           throw new InvalidReportNameException();
443
       }
444
    }
445
446
    /**
447
     * @return Filesystem
448
     */
449
    private function getFileSystem()
450
    {
451
        return $this->filesystem;
452
    }
453
454
    private function createCacheDirIfNotExists()
455
    {
456
        $fs = $this->getFileSystem();
457
458
        if (!$fs->exists($this->config['cache_dir'])) {
459
            $fs->mkdir($this->config['cache_dir'], 0700);
460
        }
461
    }
462
}
463