Test Failed
Push — master ( 6d6b42...6e7e3d )
by Joël
02:00 queued 12s
created

Api::decodeModifiedBase64()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Defro\Google\StreetView;
4
5
use Defro\Google\StreetView\Exception\BadStatusCodeException;
6
use Defro\Google\StreetView\Exception\RequestException;
7
use Defro\Google\StreetView\Exception\UnexpectedStatusException;
8
use Defro\Google\StreetView\Exception\UnexpectedValueException;
9
use GuzzleHttp\Client;
10
use GuzzleHttp\Exception\GuzzleException;
11
12
class Api
13
{
14
    /** @var string default source */
15
    const SOURCE_DEFAULT = 'default';
16
17
    /** @var string outdoor source */
18
    const SOURCE_OUTDOOR = 'outdoor';
19
20
    /** @var string */
21
    private $endpointImage = 'https://maps.googleapis.com/maps/api/streetview';
22
23
    /** @var string */
24
    private $endpointMetadata = 'https://maps.googleapis.com/maps/api/streetview/metadata';
25
26
    /** @var \GuzzleHttp\Client */
27
    private $client;
28
29
    /** @var string */
30
    private $apiKey;
31
32
    /** @var string */
33
    private $signature;
34
35
    /** @var string */
36
    private $signingSecret;
37
38
    /** @var int */
39
    private $imageWidth = 600;
40
41
    /** @var int */
42
    private $imageHeight = 600;
43
44
    /** @var int */
45
    private $heading;
46
47
    /** @var int */
48
    private $cameraFov = 90;
49
50
    /** @var int */
51
    private $cameraPitch = 0;
52
53
    /** @var int */
54
    private $radius = 50;
55
56
    /** @var string */
57
    private $source = 'default';
58
59
    /**
60
     * Api constructor.
61 33
     *
62
     * @param Client $client
63 33
     */
64 33
    public function __construct(Client $client)
65
    {
66
        $this->client = $client;
67
    }
68
69
    /**
70
     * API key from your Google console.
71
     *
72
     * @param string $apiKey
73 1
     *
74
     * @return $this
75 1
     */
76
    public function setApiKey(string $apiKey): self
77 1
    {
78
        $this->apiKey = $apiKey;
79
80
        return $this;
81
    }
82
83
    /**
84
     * Digital signature used to verify that any site generating requests.
85
     *
86
     * @param string $signature
87 2
     *
88
     * @return $this
89 2
     */
90
    public function setSignature(string $signature): self
91 2
    {
92
        $this->signature = $signature;
93
94
        return $this;
95
    }
96
97
    /**
98
     * Used in conjunction with an API key, a URL signing secret can tag API requests with a higher degree of security.
99
     * Providing a signing secret will automatically generate digital signatures for subsequent requests.
100
     *
101
     * @param string $secret Base64 encoded signing secret
102
     * @return $this
103
     */
104
    public function setSigningSecret(string $secret): self
105
    {
106 2
        $this->signingSecret = $this->decodeModifiedBase64($secret);
107
108 2
        return $this;
109 1
    }
110 1
111
    /**
112
     * Determines the horizontal field of view of the image.
113
     * The field of view is expressed in degrees, with a maximum allowed value of 120.
114 1
     * When dealing with a fixed-size viewport, as with a Street View image of a set size,
115
     * field of view in essence represents zoom, with smaller numbers indicating a higher level of zoom.
116 1
     *
117
     * @param int $cameraFov
118
     *
119
     * @throws UnexpectedValueException
120
     *
121
     * @return $this
122
     */
123
    public function setCameraFov(int $cameraFov): self
124
    {
125
        if ($cameraFov > 120) {
126
            throw new UnexpectedValueException(
127
                'Camera FOV value cannot exceed 120 degrees.'
128
            );
129
        }
130
131 3
        $this->cameraFov = $cameraFov;
132
133 3
        return $this;
134 1
    }
135 1
136
    /**
137
     * Specifies the up or down angle of the camera relative to the Street View vehicle.
138 2
     * This is often, but not always, flat horizontal.
139 1
     * Positive values angle the camera up (with 90 degrees indicating straight up);
140 1
     * negative values angle the camera down (with -90 indicating straight down).
141
     *
142
     * @param int $cameraPitch
143
     *
144 1
     * @throws UnexpectedValueException
145
     *
146 1
     * @return $this
147
     */
148
    public function setCameraPitch(int $cameraPitch): self
149
    {
150
        if ($cameraPitch > 90) {
151
            throw new UnexpectedValueException(
152
                'Camera pitch value for Google Street View cannot exceed 90 degrees.'
153
            );
154
        }
155
        if ($cameraPitch < -90) {
156
            throw new UnexpectedValueException(
157
                'Camera pitch value for Google Street View cannot be inferior of -90 degrees.'
158
            );
159
        }
160 2
161
        $this->cameraPitch = $cameraPitch;
162 2
163 1
        return $this;
164 1
    }
165
166
    /**
167
     * Sets a radius, specified in meters,
168 1
     * in which to search for a panorama, centered on the given latitude and longitude.
169
     * Valid values are non-negative integers.
170 1
     *
171
     * @param int $radius
172
     *
173
     * @throws UnexpectedValueException
174
     *
175
     * @return $this
176
     */
177
    public function setRadius(int $radius): self
178
    {
179
        if ($radius < 0) {
180
            throw new UnexpectedValueException(
181
                'Radius value cannot be negative.'
182
            );
183 4
        }
184
185 4
        $this->radius = $radius;
186 1
187 1
        return $this;
188
    }
189
190 3
    /**
191 1
     * Indicates the compass heading of the camera.
192 1
     * Accepted values are from 0 to 360 (both values indicating North, with 90 indicating East, and 180 South).
193
     *
194
     * @param int $heading
195
     *
196 2
     * @throws UnexpectedValueException
197
     *
198 2
     * @return $this
199
     */
200
    public function setHeading(int $heading): self
201
    {
202
        if ($heading < 0) {
203
            throw new UnexpectedValueException(
204
                'Heading value cannot be inferior to zero degree.'
205
            );
206
        }
207
        if ($heading > 360) {
208
            throw new UnexpectedValueException(
209
                'Heading value cannot exceed 360 degrees.'
210
            );
211
        }
212 2
213
        $this->heading = $heading;
214 2
215 1
        return $this;
216 1
    }
217 1
218 1
    /**
219 1
     * Limits Street View searches to selected sources. Valid values are:
220
     *  - default uses the default sources for Street View; searches are not limited to specific sources.
221
     *  - outdoor limits searches to outdoor collections.
222
     *
223 1
     * @param string $source
224
     *
225 1
     * @throws UnexpectedValueException
226
     *
227
     * @return $this
228
     */
229
    public function setSource(string $source): self
230
    {
231
        if (!in_array($source, [self::SOURCE_DEFAULT, self::SOURCE_OUTDOOR], true)) {
232
            throw new UnexpectedValueException(sprintf(
233
                'Source value "%s" is unknown, only "%s" or "%s" values expected.',
234
                $source,
235 2
                self::SOURCE_DEFAULT,
236
                self::SOURCE_OUTDOOR
237 2
            ));
238 1
        }
239 1
240
        $this->source = $source;
241
242
        return $this;
243 1
    }
244
245 1
    /**
246
     * @param int $height
247
     *
248
     * @throws UnexpectedValueException
249
     *
250
     * @return $this
251
     */
252
    public function setImageHeight(int $height): self
253
    {
254
        if ($height < 1) {
255 2
            throw new UnexpectedValueException(
256
                'Image height value cannot be negative or equal to zero.'
257 2
            );
258 1
        }
259 1
260
        $this->imageHeight = $height;
261
262
        return $this;
263 1
    }
264
265 1
    /**
266
     * @param int $width
267
     *
268
     * @throws UnexpectedValueException
269
     *
270
     * @return $this
271
     */
272
    public function setImageWidth(int $width): self
273
    {
274
        if ($width < 1) {
275
            throw new UnexpectedValueException(
276
                'Image height value cannot be negative or equal to zero.'
277 2
            );
278
        }
279 2
280 2
        $this->imageWidth = $width;
281
282
        return $this;
283 2
    }
284
285
    /**
286
     * Returns URL to a static (non-interactive) Street View panorama or thumbnail.
287
     *
288
     * @param string $location
289
     *
290
     * @throws BadStatusCodeException
291
     *
292
     * @return string
293
     */
294
    public function getImageUrlByLocation(string $location): string
295
    {
296 1
        $parameters = $this->getRequestParameters([
297
            'location' => $location,
298 1
        ]);
299 1
300
        return $this->getImageUrl($parameters);
301
    }
302 1
303
    /**
304
     * Returns URL to a static (non-interactive) Street View panorama or thumbnail.
305
     *
306
     * @param float $latitude
307
     * @param float $longitude
308
     *
309
     * @throws BadStatusCodeException
310
     *
311
     * @return string
312
     */
313
    public function getImageUrlByLatitudeAndLongitude(float $latitude, float $longitude): string
314 1
    {
315
        $parameters = $this->getRequestParameters([
316 1
            'location' => $latitude.','.$longitude,
317 1
        ]);
318
319
        return $this->getImageUrl($parameters);
320 1
    }
321
322
    /**
323
     * Returns URL to a static (non-interactive) Street View panorama or thumbnail.
324
     *
325
     * @param string $panoramaId
326
     *
327
     * @throws BadStatusCodeException
328
     *
329
     * @return string
330
     */
331
    public function getImageUrlByPanoramaId(string $panoramaId): string
332
    {
333 4
        $parameters = $this->getRequestParameters([
334
            'pano' => $panoramaId,
335 4
        ]);
336
337 4
        return $this->getImageUrl($parameters);
338
    }
339 4
340 1
    /**
341 1
     * Returns URL to a static (non-interactive) Street View panorama or thumbnail
342 1
     * The viewport is defined with URL parameters sent through a standard HTTP request, and is returned as a static image.
343
     *
344
     * @param array $parameters
345
     *
346 3
     * @throws BadStatusCodeException
347
     *
348
     * @return string
349
     */
350
    private function getImageUrl(array $parameters): string
351
    {
352
        if (empty($parameters['signature']) && $this->signingSecret) {
353
            $parameters['signature'] = $this->generateSignature($this->endpointImage, $parameters);
354
        }
355
356
        $uri = $this->endpointImage.'?'.http_build_query($parameters);
357
358
        $response = $this->client->get($uri);
359
360
        if ($response->getStatusCode() !== 200) {
361
            throw new BadStatusCodeException(
362
                'Could not connect to '.$this->endpointImage,
363
                $response->getStatusCode()
364
            );
365
        }
366 11
367
        return $uri;
368 11
    }
369
370 11
    /**
371 1
     * Requests provide data about Street View panoramas.
372 1
     * Using the metadata, you can find out if a Street View image is available at a given location,
373
     * as well as getting programmatic access to the latitude and longitude,
374
     * the panorama ID, the date the photo was taken, and the copyright information for the image.
375
     * Accessing this metadata allows you to customize error behavior in your application.
376 10
     * Street View API metadata requests are free to use. No quota is consumed when you request metadata.
377
     *
378
     * @param string $location
379 10
     *
380 1
     * @throws UnexpectedValueException
381 1
     * @throws RequestException
382 1
     * @throws BadStatusCodeException
383 1
     * @throws UnexpectedStatusException
384
     *
385
     * @return array
386
     */
387 9
    public function getMetadata(string $location): array
388 1
    {
389 1
        $location = trim($location);
390 1
391
        if (empty($location)) {
392
            throw new UnexpectedValueException(
393
                'Location argument cannot be empty to request Google Street view API Metadata.'
394 8
            );
395
        }
396
397
        $parameters = $this->getRequestParameters(compact('location'));
398 8
399 1
        if (empty($parameters['signature']) && $this->signingSecret) {
400
            $parameters['signature'] = $this->generateSignature($this->endpointMetadata, $parameters);
401
        }
402 7
403
        $payload = ['query' => http_build_query($parameters)];
404
405 7
        try {
406
            $response = $this->client->request('GET', $this->endpointMetadata, $payload);
407
        } catch (GuzzleException $e) {
408 7
            throw new RequestException(
409 1
                'Guzzle http client request failed.',
410
                $e
411
            );
412
        }
413 6
414 1
        if ($response->getStatusCode() !== 200) {
415
            throw new BadStatusCodeException(
416
                'Could not connect to '.$this->endpointMetadata,
417 5
                $response->getStatusCode()
418 1
            );
419
        }
420
421
        $response = json_decode($response->getBody());
422
423 4
        // Indicates that no panorama could be found near the provided location.
424 1
        // Indicates that no errors occurred; a panorama is found and metadata is returned.
425
        if ($response->status === 'OK') {
426
            return $this->formatMetadataResponse($response);
427 3
        }
428 1
429
        $this->handleResponseStatus($response->status);
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return array. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
430
    }
431
432 2
    private function handleResponseStatus(string $status)
433 1
    {
434
        // This may occur if a non-existent or invalid panorama ID is given.
435
        if ($status === 'ZERO_RESULTS') {
436 1
            throw new UnexpectedStatusException('Google Street view return zero results.');
437 1
        }
438
        // Indicates that the address string provided in the location parameter could not be found.
439
        // This may occur if a non-existent address is given.
440
        if ($status === 'NOT_FOUND') {
441 1
            throw new UnexpectedStatusException('No Google Street view result found.');
442
        }
443
        // Indicates that you have exceeded your daily quota or per-second quota for this API.
444 1
        if ($status === 'OVER_QUERY_LIMIT') {
445 1
            throw new UnexpectedStatusException('Google Street view API quota exceed.');
446 1
        }
447 1
        // Indicates that your request was denied.
448 1
        // This may occur if you did not use an API key or client ID, or
449
        // if the Street View API is not activated in the Google Cloud Platform Console project containing your API key.
450
        if ($status === 'REQUEST_DENIED') {
451
            throw new UnexpectedStatusException('Google Street view denied the request.');
452 10
        }
453
        // Generally indicates that the query parameters (address or latlng or components) are missing.
454 10
        if ($status === 'INVALID_REQUEST') {
455
            throw new UnexpectedStatusException('Google Street view request is invalid.');
456
        }
457 14
        // Indicates that the request could not be processed due to a server error.
458
        // This is often a temporary status. The request may succeed if you try again.
459
        if ($status === 'UNKNOWN_ERROR') {
460 14
            throw new UnexpectedStatusException('Google Street view unknown error occurred. Please try again.');
461 14
        }
462 14
463 14
        throw new UnexpectedStatusException(
464 14
            'Google Street view respond an unknown status response : "'.$status.'".'
465 14
        );
466
    }
467
468
    protected function formatMetadataResponse($response): array
469 14
    {
470 1
        return [
471
            'lat'           => $response->location->lat,
472 14
            'lng'           => $response->location->lng,
473 1
            'date'          => $response->date,
474
            'copyright'     => $response->copyright,
475
            'panoramaId'    => $response->pano_id,
476 14
        ];
477
    }
478
479
    private function getRequestParameters(array $parameters): array
480
    {
481
        $defaultParameters = [
482
            'key'       => $this->apiKey,
483
            'size'      => $this->imageWidth.'x'.$this->imageHeight,
484
            'fov'       => $this->cameraFov,
485
            'pitch'     => $this->cameraPitch,
486
            'radius'    => $this->radius,
487
            'source'    => $this->source,
488
        ];
489
490
        // optional parameters which have not default value
491
        if ($this->heading) {
492
            $defaultParameters['heading'] = $this->heading;
493
        }
494
        if ($this->signature) {
495
            $defaultParameters['signature'] = $this->signature;
496
        }
497
498
        return array_merge($defaultParameters, $parameters);
499
    }
500
501
    /**
502
     * Encode a string to URL-safe base64
503
     *
504
     * @param string $value
505
     * @return string
506
     */
507
    private function encodeModifiedBase64(string $value): string
508
    {
509
        return str_replace(['+', '/'], ['-', '_'], base64_encode($value));
510
    }
511
512
    /**
513
     * Decode a string from URL-safe base64
514
     *
515
     * @param string $value
516
     * @return string
517
     */
518
    private function decodeModifiedBase64(string $value): string
519
    {
520
        return base64_decode(str_replace(['-', '_'], ['+', '/'], $value));
521
    }
522
523
    /**
524
     * Sign a URL with the current signing secret
525
     *
526
     * @param string $url A valid URL that is properly URL-encoded
527
     * @param array $parameters Parameters to include in the URL
528
     * @return string
529
     */
530
    public function generateSignature(string $url, ?array $parameters = null): string
531
    {
532
        $url = parse_url($url);
533
534
        if (!is_null($parameters)) {
535
            $url['query'] = http_build_query($parameters);
536
        }
537
538
        $signable = $url['path'] . '?' . $url['query'];
539
540
        // Generate binary signature
541
        $signature = hash_hmac('sha1', $signable, $this->signingSecret, true);
542
543
        // Encode signature into base64
544
        return $this->encodeModifiedBase64($signature);
545
    }
546
547
}
548