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

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