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

Client::ensureValidReportName()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

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