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

Client::getReport()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 2

Importance

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