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
|
|||
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. ![]() |
|||
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 |
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 theid
property of an instance of theAccount
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.