Passed
Pull Request — master (#1)
by
unknown
03:11
created

Api::decodeModifiedBase64()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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
    /**
61
     * Api constructor.
62
     *
63
     * @param Client $client
64
     */
65 36
    public function __construct(Client $client)
66
    {
67 36
        $this->client = $client;
68 36
    }
69
70
    /**
71
     * API key from your Google console
72
     *
73
     * @param string $apiKey
74
     * @return $this
75
     */
76 1
    public function setApiKey(string $apiKey): self
77
    {
78 1
        $this->apiKey = $apiKey;
79
80 1
        return $this;
81
    }
82
83
    /**
84
     * Digital signature used to verify that any site generating requests.
85
     *
86
     * @param string $signature
87
     * @return $this
88
     */
89 2
    public function setSignature(string $signature): self
90
    {
91 2
        $this->signature = $signature;
92
93 2
        return $this;
94
    }
95
96
    /**
97
     * Used in conjunction with an API key, a URL signing secret can tag API requests with a higher degree of security.
98
     * Providing a signing secret will automatically generate digital signatures for subsequent requests.
99
     *
100
     * @param string $secret Base64 encoded signing secret
101
     * @return $this
102
     */
103 3
    public function setSigningSecret(string $secret): self
104
    {
105 3
        $this->signingSecret = $this->decodeModifiedBase64($secret);
106
107 3
        return $this;
108
    }
109
110
    /**
111
     * Determines the horizontal field of view of the image.
112
     * The field of view is expressed in degrees, with a maximum allowed value of 120.
113
     * When dealing with a fixed-size viewport, as with a Street View image of a set size,
114
     * field of view in essence represents zoom, with smaller numbers indicating a higher level of zoom.
115
     *
116
     * @param int $cameraFov
117
     * @return $this
118
     * @throws UnexpectedValueException
119
     */
120 2
    public function setCameraFov(int $cameraFov): self
121
    {
122 2
        if ($cameraFov > 120) {
123 1
            throw new UnexpectedValueException(
124 1
                'Camera FOV value cannot exceed 120 degrees.'
125
            );
126
        }
127
128 1
        $this->cameraFov = $cameraFov;
129
130 1
        return $this;
131
    }
132
133
    /**
134
     * Specifies the up or down angle of the camera relative to the Street View vehicle.
135
     * This is often, but not always, flat horizontal.
136
     * Positive values angle the camera up (with 90 degrees indicating straight up);
137
     * negative values angle the camera down (with -90 indicating straight down).
138
     *
139
     * @param int $cameraPitch
140
     * @return $this
141
     * @throws UnexpectedValueException
142
     */
143 3
    public function setCameraPitch(int $cameraPitch):self
144
    {
145 3
        if ($cameraPitch > 90) {
146 1
            throw new UnexpectedValueException(
147 1
                'Camera pitch value for Google Street View cannot exceed 90 degrees.'
148
            );
149
        }
150 2
        if ($cameraPitch < -90) {
151 1
            throw new UnexpectedValueException(
152 1
                'Camera pitch value for Google Street View cannot be inferior of -90 degrees.'
153
            );
154
        }
155
156 1
        $this->cameraPitch = $cameraPitch;
157
158 1
        return $this;
159
    }
160
161
    /**
162
     * Sets a radius, specified in meters,
163
     * in which to search for a panorama, centered on the given latitude and longitude.
164
     * Valid values are non-negative integers.
165
     *
166
     * @param int $radius
167
     * @return $this
168
     * @throws UnexpectedValueException
169
     */
170 2
    public function setRadius(int $radius): self
171
    {
172 2
        if ($radius < 0) {
173 1
            throw new UnexpectedValueException(
174 1
                'Radius value cannot be negative.'
175
            );
176
        }
177
178 1
        $this->radius = $radius;
179
180 1
        return $this;
181
    }
182
183
    /**
184
     * Indicates the compass heading of the camera.
185
     * Accepted values are from 0 to 360 (both values indicating North, with 90 indicating East, and 180 South).
186
     *
187
     * @param int $heading
188
     * @return $this
189
     * @throws UnexpectedValueException
190
     */
191 4
    public function setHeading(int $heading): self
192
    {
193 4
        if ($heading < 0) {
194 1
            throw new UnexpectedValueException(
195 1
                'Heading value cannot be inferior to zero degree.'
196
            );
197
        }
198 3
        if ($heading > 360) {
199 1
            throw new UnexpectedValueException(
200 1
                'Heading value cannot exceed 360 degrees.'
201
            );
202
        }
203
204 2
        $this->heading = $heading;
205
206 2
        return $this;
207
    }
208
209
    /**
210
     * Limits Street View searches to selected sources. Valid values are:
211
     *  - default uses the default sources for Street View; searches are not limited to specific sources.
212
     *  - outdoor limits searches to outdoor collections.
213
     *
214
     * @param string $source
215
     * @return $this
216
     * @throws UnexpectedValueException
217
     */
218 2
    public function setSource(string $source): self
219
    {
220 2
        if (!in_array($source, [self::SOURCE_DEFAULT, self::SOURCE_OUTDOOR], true)) {
221 1
            throw new UnexpectedValueException(sprintf(
222 1
                'Source value "%s" is unknown, only "%s" or "%s" values expected.',
223 1
                $source, self::SOURCE_DEFAULT, self::SOURCE_OUTDOOR
224
            ));
225
        }
226
227 1
        $this->source = $source;
228
229 1
        return $this;
230
    }
231
232
    /**
233
     * @param int $height
234
     * @return $this
235
     * @throws UnexpectedValueException
236
     */
237 2
    public function setImageHeight(int $height): self
238
    {
239 2
        if ($height < 1) {
240 1
            throw new UnexpectedValueException(
241 1
                'Image height value cannot be negative or equal to zero.'
242
            );
243
        }
244
245 1
        $this->imageHeight = $height;
246
247 1
        return $this;
248
    }
249
250
    /**
251
     * @param int $width
252
     * @return $this
253
     * @throws UnexpectedValueException
254
     */
255 2
    public function setImageWidth(int $width): self
256
    {
257 2
        if ($width < 1) {
258 1
            throw new UnexpectedValueException(
259 1
                'Image height value cannot be negative or equal to zero.'
260
            );
261
        }
262
263 1
        $this->imageWidth = $width;
264
265 1
        return $this;
266
    }
267
268
    /**
269
     * Returns URL to a static (non-interactive) Street View panorama or thumbnail.
270
     *
271
     * @param string $location
272
     * @return string
273
     * @throws BadStatusCodeException
274
     */
275 2
    public function getImageUrlByLocation(string $location): string
276
    {
277 2
        $parameters = $this->getRequestParameters([
278 2
            'location' => $location
279
        ]);
280
281 2
        return $this->getImageUrl($parameters);
282
    }
283
284
    /**
285
     * Returns URL to a static (non-interactive) Street View panorama or thumbnail
286
     *
287
     * @param float $latitude
288
     * @param float $longitude
289
     * @return string
290
     * @throws BadStatusCodeException
291
     */
292 1
    public function getImageUrlByLatitudeAndLongitude(float $latitude, float $longitude): string
293
    {
294 1
        $parameters = $this->getRequestParameters([
295 1
            'location' => $latitude . ',' . $longitude
296
        ]);
297
298 1
        return $this->getImageUrl($parameters);
299
    }
300
301
    /**
302
     * Returns URL to a static (non-interactive) Street View panorama or thumbnail
303
     *
304
     * @param string $panoramaId
305
     * @return string
306
     * @throws BadStatusCodeException
307
     */
308 1
    public function getImageUrlByPanoramaId(string $panoramaId): string
309
    {
310 1
        $parameters = $this->getRequestParameters([
311 1
            'pano' => $panoramaId
312
        ]);
313
314 1
        return $this->getImageUrl($parameters);
315
    }
316
317
    /**
318
     * Returns URL to a static (non-interactive) Street View panorama or thumbnail
319
     * The viewport is defined with URL parameters sent through a standard HTTP request, and is returned as a static image.
320
     *
321
     * @param array $parameters
322
     * @return string
323
     * @throws BadStatusCodeException
324
     */
325 4
    private function getImageUrl(array $parameters): string
326
    {
327 4
        if (empty($parameters['signature']) && $this->signingSecret) {
328
            $parameters['signature'] = $this->generateSignature($this->endpointImage, $parameters);
329
        }
330
331 4
        $uri = $this->endpointImage . '?' . http_build_query($parameters);
332
333 4
        $response = $this->client->get($uri);
334
335 4
        if ($response->getStatusCode() !== 200) {
336 1
            throw new BadStatusCodeException(
337 1
                'Could not connect to ' . $this->endpointImage,
338 1
                $response->getStatusCode()
339
            );
340
        }
341
342 3
        return $uri;
343
    }
344
345
    /**
346
     * Requests provide data about Street View panoramas.
347
     * Using the metadata, you can find out if a Street View image is available at a given location,
348
     * as well as getting programmatic access to the latitude and longitude,
349
     * the panorama ID, the date the photo was taken, and the copyright information for the image.
350
     * Accessing this metadata allows you to customize error behavior in your application.
351
     * Street View API metadata requests are free to use. No quota is consumed when you request metadata.
352
     *
353
     * @param string $location
354
     * @return array
355
     * @throws UnexpectedValueException
356
     * @throws RequestException
357
     * @throws BadStatusCodeException
358
     * @throws UnexpectedStatusException
359
     */
360 11
    public function getMetadata(string $location): array
361
    {
362 11
        $location = trim($location);
363
364 11
        if (empty($location)) {
365 1
            throw new UnexpectedValueException(
366 1
                'Location argument cannot be empty to request Google Street view API Metadata.'
367
            );
368
        }
369
370 10
        $parameters = $this->getRequestParameters(compact('location'));
371
372 10
        if (empty($parameters['signature']) && $this->signingSecret) {
373
            $parameters['signature'] = $this->generateSignature($this->endpointMetadata, $parameters);
374
        }
375
376 10
        $payload = ['query' => http_build_query($parameters)];
377
378
        try {
379 10
            $response = $this->client->request('GET', $this->endpointMetadata, $payload);
380 1
        } catch (GuzzleException $e) {
381 1
            throw new RequestException(
382 1
                'Guzzle http client request failed.', $e
383
            );
384
        }
385
386 9
        if ($response->getStatusCode() !== 200) {
387 1
            throw new BadStatusCodeException(
388 1
                'Could not connect to ' . $this->endpointMetadata,
389 1
                $response->getStatusCode()
390
            );
391
        }
392
393 8
        $response = json_decode($response->getBody());
394
395
        // Indicates that no panorama could be found near the provided location.
396
        // Indicates that no errors occurred; a panorama is found and metadata is returned.
397 8
        if ($response->status === 'OK') {
398 1
            return $this->formatMetadataResponse($response);
399
        }
400
401 7
        $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...
402
    }
403
404 7
    private function handleResponseStatus(string $status)
405
    {
406
        // This may occur if a non-existent or invalid panorama ID is given.
407 7
        if ($status === 'ZERO_RESULTS') {
408 1
            throw new UnexpectedStatusException('Google Street view return zero results.');
409
        }
410
        // Indicates that the address string provided in the location parameter could not be found.
411
        // This may occur if a non-existent address is given.
412 6
        if ($status === 'NOT_FOUND') {
413 1
            throw new UnexpectedStatusException('No Google Street view result found.');
414
        }
415
        // Indicates that you have exceeded your daily quota or per-second quota for this API.
416 5
        if ($status === 'OVER_QUERY_LIMIT') {
417 1
            throw new UnexpectedStatusException('Google Street view API quota exceed.');
418
        }
419
        // Indicates that your request was denied.
420
        // This may occur if you did not use an API key or client ID, or
421
        // if the Street View API is not activated in the Google Cloud Platform Console project containing your API key.
422 4
        if ($status === 'REQUEST_DENIED') {
423 1
            throw new UnexpectedStatusException('Google Street view denied the request.');
424
        }
425
        // Generally indicates that the query parameters (address or latlng or components) are missing.
426 3
        if ($status === 'INVALID_REQUEST') {
427 1
            throw new UnexpectedStatusException('Google Street view request is invalid.');
428
        }
429
        // Indicates that the request could not be processed due to a server error.
430
        // This is often a temporary status. The request may succeed if you try again.
431 2
        if ($status === 'UNKNOWN_ERROR') {
432 1
            throw new UnexpectedStatusException('Google Street view unknown error occurred. Please try again.');
433
        }
434
435 1
        throw new UnexpectedStatusException(
436 1
            'Google Street view respond an unknown status response : "' . $status . '".'
437
        );
438
    }
439
440 1
    protected function formatMetadataResponse($response): array
441
    {
442
        return [
443 1
            'lat'           => $response->location->lat,
444 1
            'lng'           => $response->location->lng,
445 1
            'date'          => $response->date,
446 1
            'copyright'     => $response->copyright,
447 1
            'panoramaId'    => $response->pano_id
448
        ];
449
    }
450
451 14
    private function getRequestParameters(array $parameters): array
452
    {
453
        $defaultParameters = [
454 14
            'key'       => $this->apiKey,
455 14
            'size'      => $this->imageWidth . 'x' . $this->imageHeight,
456 14
            'fov'       => $this->cameraFov,
457 14
            'pitch'     => $this->cameraPitch,
458 14
            'radius'    => $this->radius,
459 14
            'source'    => $this->source
460
        ];
461
462
        // optional parameters which have not default value
463 14
        if ($this->heading) {
464 1
            $defaultParameters['heading'] = $this->heading;
465
        }
466 14
        if ($this->signature) {
467 1
            $defaultParameters['signature'] = $this->signature;
468
        }
469
470 14
        return array_merge($defaultParameters, $parameters);
471
    }
472
473
    /**
474
     * Encode a string to URL-safe base64
475
     *
476
     * @param string $value
477
     * @return string
478
     */
479 2
    private function encodeModifiedBase64(string $value): string
480
    {
481 2
        return str_replace(['+', '/'], ['-', '_'], base64_encode($value));
482
    }
483
484
    /**
485
     * Decode a string from URL-safe base64
486
     *
487
     * @param string $value
488
     * @return string
489
     */
490 3
    private function decodeModifiedBase64(string $value): string
491
    {
492 3
        return base64_decode(str_replace(['-', '_'], ['+', '/'], $value));
493
    }
494
495
    /**
496
     * Sign a URL with the current signing secret
497
     *
498
     * @param string $url A valid URL that is properly URL-encoded
499
     * @param array $parameters Parameters to include in the URL
500
     * @return string
501
     */
502 2
    public function generateSignature(string $url, ?array $parameters = null): string
503
    {
504 2
        $url = parse_url($url);
505 2
        if (isset($parameters)) {
506 1
            $url['query'] = http_build_query($parameters);
507
        }
508
509 2
        $signable = $url['path'] . '?' . $url['query'];
510
511
        // Generate binary signature
512 2
        $signature = hash_hmac('sha1', $signable, $this->signingSecret, true);
513
514
        // Encode signature into base64
515 2
        return $this->encodeModifiedBase64($signature);
516
    }
517
}
518