Failed Conditions
Pull Request — master (#8)
by Laurens
02:18
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
     * @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
    public function getRefreshToken()
133
    {
134
        return $this->apiDetails->getRefreshToken();
135
    }
136
137
    /**
138 24
     * @param string $reportName
139
     * @param array $columns
140 24
     * @param $timePeriod
141 24
     * @param null|string $fileLocation
142 24
     */
143 24
    public function getReport($reportName, array $columns, $timePeriod = ReportTimePeriod::LastWeek, $fileLocation)
144 24
    {
145 24
        $this->ensureValidReportName($reportName);
146
        $oauthToken = $this->getOauthToken();
147 24
        $this->apiDetails->setRefreshToken($oauthToken->getRefreshToken());
148 24
149
        $report = $this->report[$reportName];
150 24
        $report->setTimePeriod($timePeriod);
151 24
        $report->setColumns($columns);
152 24
        $reportRequest = $report->getRequest();
153 24
        $this->setProxy($report::WSDL, $oauthToken->getAccessToken());
154 24
        $files = $this->getFilesFromReportRequest($reportRequest, $reportName, "{$this->getCacheDir()}/{$this->fileName}", $report);
155 24
156
        $this->moveFirstFile($files, $fileLocation);
157 3
        $this->files = $files;
0 ignored issues
show
Documentation Bug introduced by
It seems like $files can also be of type boolean or string. However, the property $files is declared as type array<integer,string>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

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