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

Api/Client.php (3 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);
0 ignored issues
show
It seems like $files defined by $this->getFilesFromRepor...s->fileName}", $report) on line 154 can also be of type false; however, Werkspot\BingAdsApiBundl...Client::moveFirstFile() does only seem to accept array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

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