Completed
Pull Request — master (#8)
by Laurens
02:18
created

Client::getOauthToken()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
ccs 2
cts 2
cp 1
rs 9.6666
cc 1
eloc 6
nc 1
nop 0
crap 1
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 25
    public function __construct(OauthTokenService $oauthTokenService, ApiDetails $apiDetails, ClientProxy $clientProxy, File $file, Csv $csv, Time $timeHelper)
90 1
    {
91 25
        $this->oauthTokenService = $oauthTokenService;
92 25
        $this->apiDetails = $apiDetails;
93 25
        $this->clientProxy = $clientProxy;
94 25
        $this->fileHelper = $file;
95 25
        $this->csvHelper = $csv;
96 25
        $this->timeHelper = $timeHelper;
97
98 25
        ini_set('soap.wsdl_cache_enabled', '0');
99 25
        ini_set('soap.wsdl_cache_ttl', '0');
100
101 25
        $this->fileName = 'report.zip';
102
103 25
        $this->report = [
104 25
            'GeoLocationPerformanceReport' => new Report\GeoLocationPerformanceReport(),
105
        ];
106 25
    }
107
108 1
    public function setApiDetails(ApiDetails $apiDetails)
109
    {
110 1
        $this->apiDetails = $apiDetails;
111 1
    }
112
113
    /**
114
     * Sets the configuration
115
     *
116
     * @param $config
117
     */
118 24
    public function setConfig($config)
119
    {
120 24
        $this->config = $config;
121 24
        $this->config['cache_dir'] = $this->config['cache_dir'] . '/' . 'BingAdsApiBundle'; //<-- important for the cache clear function
122 24
        $this->config['csv']['fixHeader']['removeColumnHeader'] = true; //-- fix till i know how to do this
123 24
    }
124
125 1
    public function getRefreshToken()
126
    {
127 1
        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 24
    public function getReport(array $columns, $name = 'GeoLocationPerformanceReport', $timePeriod = ReportTimePeriod::LastWeek, $fileLocation = null)
139
    {
140 24
        $oauthToken = $this->getOauthToken();
141 24
        $this->apiDetails->setRefreshToken($oauthToken->getRefreshToken());
142 24
143 24
        $report = $this->report[$name];
144 24
        $report->setTimePeriod($timePeriod);
145 24
        $report->setColumns($columns);
146
        $reportRequest = $report->getRequest();
147 24
        $this->setProxy($report::WSDL, $oauthToken->getAccessToken());
148 24
        $files = $this->getFilesFromReportRequest($reportRequest, $name, "{$this->getCacheDir()}/{$this->fileName}", $report);
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $this instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
149
150 24
        if ($fileLocation !== null) {
151 24
            $this->moveFirstFile($fileLocation);
152 24
153 24
            return $fileLocation;
154 24
        } else {
155 24
            return $files;
156
        }
157 3
    }
158 1
159
    /**
160 1
     * @return AccessToken
161
     */
162 2
    protected function getOauthToken()
163
    {
164
        return  $this->oauthTokenService->refreshToken(
165
            $this->apiDetails->getClientId(),
166
            $this->apiDetails->getSecret(),
167
            $this->apiDetails->getRedirectUri(),
168
            new AccessToken(null, $this->apiDetails->getRefreshToken())
169
        );
170 24
    }
171
172 24
    /**
173 24
     * @param string $wsdl
174
     * @param string $accessToken
175
     */
176
    private function setProxy($wsdl, $accessToken)
177
    {
178 24
        $this->proxy = $this->clientProxy->ConstructWithCredentials($wsdl, null, null, $this->apiDetails->getDevToken(), $accessToken);
179
    }
180 24
181 24
    /**
182 1
     * @return string
183 1
     */
184
    private function getCacheDir()
185 24
    {
186
        $fs = new Filesystem();
187
        if (!$fs->exists($this->config['cache_dir'])) {
188
            $fs->mkdir($this->config['cache_dir'], 0700);
189
        }
190
191
        return $this->config['cache_dir'];
192
    }
193
194
    /**
195
     * @param ReportRequest $reportRequest
196
     * @param string $name
197
     * @param string $downloadFile
198 24
     * @param ReportInterface $report
199
     *
200 24
     * @throws Exception
201 6
     *
202 3
     * @return array|string
203 3
     */
204 3
    private function getFilesFromReportRequest(ReportRequest $reportRequest, $name, $downloadFile, ReportInterface $report)
205 3
    {
206 3
        $reportRequestId = $this->submitGenerateReport($reportRequest, $name);
207 3
        $reportRequestStatus = $this->waitForStatus($reportRequestId);
208
        $reportDownloadUrl = $reportRequestStatus->ReportDownloadUrl;
209 3
        $zipFile = $this->fileHelper->getFile($reportDownloadUrl, $downloadFile);
210
        if ($zipFile !== false) {
211
            $this->files = $this->fileHelper->unZip($zipFile);
212
            $this->fixFile($report);
213
        }
214
215
        return $this->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.");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $reportRequestId instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
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 self
334 3
     */
335 3
    private function fixFile(ReportInterface $report, array $files = null)
336 3
    {
337 3
        $files = (!$files) ? $this->files : $files;
338 3
        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...
339 3
            $lines = file($file);
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
            $fp = fopen($file, 'w');
344
            fwrite($fp, implode('', $lines));
345
            fclose($fp);
346
        }
347
348
        return $this;
349
    }
350
351
    /**
352 1
     * Move first file form array $this->files to the target location
353
     *
354 1
     * @param string $target
355 1
     *
356
     * @return self
357 1
     */
358
    private function moveFirstFile($target)
359
    {
360
        $fs = new Filesystem();
361
        $fs->rename($this->files[0], $target);
362
363
        return $this;
364
    }
365
366
    /**
367
     * Clear Bundle Cache directory
368
     *
369
     * @param bool $allFiles delete all files in bundles cache, if false deletes only extracted files ($this->files)
370
     *
371
     * @return self
372
     *
373
     * @codeCoverageIgnore
374
     */
375
    public function clearCache($allFiles = false)
376
    {
377
        $fileSystem = new Filesystem();
378
379
        if ($allFiles) {
380
            $finder = new Finder();
381
            $files = $finder->files()->in($this->config['cache_dir']);
382
        } else {
383
            $files = $this->files;
384
        }
385
386
        foreach ($files as $file) {
387
            $fileSystem->remove($file);
388
        }
389
390
        return $this;
391
    }
392
393
    /**
394
     * @param SoapFault $e
395
     *
396
     * @throws Exceptions\SoapInternalErrorException
397 19
     * @throws Exceptions\SoapInvalidCredentialsException
398
     * @throws Exceptions\SoapNoCompleteDataAvailableException
399 19
     * @throws Exceptions\SoapReportingServiceInvalidReportIdException
400 19
     * @throws Exceptions\SoapUnknownErrorException
401 7
     * @throws Exceptions\SoapUserIsNotAuthorizedException
402 19
     */
403 12
    private function parseSoapFault(SoapFault $e)
404 6
    {
405 12
        $error = null;
406 6
        if (isset($e->detail->AdApiFaultDetail)) {
407 6
            $error = $e->detail->AdApiFaultDetail->Errors->AdApiError;
408 12
        } elseif (isset($e->detail->ApiFaultDetail)) {
409 19
            if (!empty($e->detail->ApiFaultDetail->BatchErrors)) {
410 19
                $error = $error = $e->detail->ApiFaultDetail->BatchErrors->BatchError;
411 19
            } elseif (!empty($e->detail->ApiFaultDetail->OperationErrors)) {
412 19
                $error = $e->detail->ApiFaultDetail->OperationErrors->OperationError;
413 4
            }
414 15
        }
415 3
        $errors = is_array($error) ? $error : ['error' => $error];
416 12
        foreach ($errors as $error) {
417 3
            switch ($error->Code) {
418 9
                case 0:
419 3
                    throw new Exceptions\SoapInternalErrorException($error->Message, $error->Code);
420 6
                case 105:
421 3
                    throw new Exceptions\SoapInvalidCredentialsException($error->Message, $error->Code);
422 3
                case 106:
423 3
                    throw new Exceptions\SoapUserIsNotAuthorizedException($error->Message, $error->Code);
424 3
                case 2004:
425 3
                    throw new Exceptions\SoapNoCompleteDataAvailableException($error->Message, $error->Code);
426
                case 2100:
427
                    throw new Exceptions\SoapReportingServiceInvalidReportIdException($error->Message, $error->Code);
428
                default:
429
                    $errorMessage = "[{$error->Code}]\n{$error->Message}";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $error instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
430
                    throw new Exceptions\SoapUnknownErrorException($errorMessage, $error->Code);
431
            }
432
        }
433
    }
434
}
435