diff --git a/.gitignore b/.gitignore index ac7011ce..c96f0893 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,10 @@ # PHPUnit /phpunit.xml +/phpunit.xml.dist +/phpunit.ci.xml /build + +#PHPStorm folder +/.idea + diff --git a/composer.json b/composer.json index 7071c2e7..d0ee2341 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "php-http/cache-plugin": "^1.4", "php-http/guzzle6-adapter": "^1.0", "phpunit/phpunit": "^5.4", - "phpunit/phpunit-selenium": "dev-master", + "phpunit/phpunit-selenium": "^3.0", "symfony/cache": "^3.2", "symfony/phpunit-bridge": "^2.7|^3.0" }, diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index fc2291d3..3ef41e8e 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -2,12 +2,13 @@ FROM php:latest # APT packages RUN apt-get update && apt-get install -y \ - zlib1g-dev \ + zlib1g-dev libzip-dev zip\ git \ && rm -rf /var/lib/apt/lists/* # PHP extensions -RUN docker-php-ext-install zip +RUN docker-php-ext-configure zip --with-libzip \ + && docker-php-ext-install zip # XDebug extensions RUN pecl install xdebug \ diff --git a/src/Service/Place/Base/Place.php b/src/Service/Place/Base/Place.php index 60167ac2..a6c7f03a 100644 --- a/src/Service/Place/Base/Place.php +++ b/src/Service/Place/Base/Place.php @@ -119,6 +119,11 @@ class Place */ private $reviews = []; + /** + * @var int|null + */ + private $userRatingsTotal; + /** * @var string[] */ @@ -393,6 +398,30 @@ public function setRating($rating) $this->rating = $rating; } + /** + * @return bool + */ + public function hasUserRatingsTotal() + { + return $this->userRatingsTotal !== null; + } + + /** + * @return int|null + */ + public function getUserRatingsTotal() + { + return $this->userRatingsTotal; + } + + /** + * @param int|null $userRatingsTotal + */ + public function setUserRatingsTotal($userRatingsTotal) + { + $this->userRatingsTotal = $userRatingsTotal; + } + /** * @return bool */ diff --git a/src/Service/Place/Base/Review.php b/src/Service/Place/Base/Review.php index 3e2b0bb2..a233e3ee 100644 --- a/src/Service/Place/Base/Review.php +++ b/src/Service/Place/Base/Review.php @@ -26,6 +26,11 @@ class Review */ private $authorUrl; + /** + * @var string|null + */ + private $profilePhotoUrl; + /** * @var string|null */ @@ -41,6 +46,11 @@ class Review */ private $time; + /** + * @var string|null + */ + private $relativeTimeDescription; + /** * @var string|null */ @@ -99,6 +109,30 @@ public function setAuthorUrl($authorUrl) $this->authorUrl = $authorUrl; } + /** + * @return bool + */ + public function hasProfilePhotoUrl() + { + return $this->profilePhotoUrl !== null; + } + + /** + * @return string|null + */ + public function getProfilePhotoUrl() + { + return $this->profilePhotoUrl; + } + + /** + * @param string|null $profilePhotoUrl + */ + public function setProfilePhotoUrl($profilePhotoUrl) + { + $this->profilePhotoUrl = $profilePhotoUrl; + } + /** * @return bool */ @@ -171,6 +205,30 @@ public function setTime(\DateTime $time = null) $this->time = $time; } + /** + * @return bool + */ + public function hasRelativeTimeDescription() + { + return $this->relativeTimeDescription !== null; + } + + /** + * @return string|null + */ + public function getRelativeTimeDescription() + { + return $this->relativeTimeDescription; + } + + /** + * @param string|null $relativeTimeDescription + */ + public function setRelativeTimeDescription($relativeTimeDescription) + { + $this->relativeTimeDescription = $relativeTimeDescription; + } + /** * @return bool */ diff --git a/src/Service/Place/Detail/Request/PlaceDetailRequest.php b/src/Service/Place/Detail/Request/PlaceDetailRequest.php index 04adcafe..eb675776 100644 --- a/src/Service/Place/Detail/Request/PlaceDetailRequest.php +++ b/src/Service/Place/Detail/Request/PlaceDetailRequest.php @@ -17,15 +17,63 @@ class PlaceDetailRequest implements PlaceDetailRequestInterface { /** + * A textual identifier that uniquely identifies a place, returned from a Place Search. + * * @var string */ private $placeId; /** + * The language code, indicating in which language the results should be returned, if possible. + * Note that some fields may not be available in the requested language. + * * @var string|null */ private $language; + /** + * The region code, specified as a ccTLD (country code top-level domain) two-character value. + * Most ccTLD codes are identical to ISO 3166-1 codes, with some exceptions. + * This parameter will only influence, not fully restrict, results. + * If more relevant results exist outside of the specified region, they may be included. + * When this parameter is used, the country name is omitted from the resulting formatted_address for results in the specified region. + * + * @var string|null + */ + private $region; + + /** + * A random string which identifies an autocomplete session for billing purposes. + * Use this for Place Details requests that are called following an autocomplete request in the same user session. + * + * @var string|null + */ + private $sessionToken; + + /** + * The types of place data to return. + * If you do not specify at least one field with a request, or if you omit the fields parameter from a request, + * ALL possible fields will be returned, and you will be billed accordingly. + * + * @var string[]|null + */ + private $fields; + + const FIELDS_BASIC = [ + 'icon', 'name', 'url', 'photo', 'permanently_closed', 'utc_offset', + 'address_component', 'adr_address', 'formatted_address', 'geometry', + 'place_id', 'plus_code', 'type', 'vicinity' + ]; + + const FIELDS_CONTACT = [ + 'formatted_phone_number', 'international_phone_number', + 'opening_hours', 'website' + ]; + + const FIELDS_ATMOSPHERE = [ + 'price_level', 'rating', 'review', 'user_ratings_total' + ]; + /** * @param string $placeId */ @@ -44,10 +92,13 @@ public function getPlaceId() /** * @param string $placeId + * @return static */ public function setPlaceId($placeId) { $this->placeId = $placeId; + + return $this; } /** @@ -68,10 +119,216 @@ public function getLanguage() /** * @param string|null $language + * @return static */ public function setLanguage($language) { $this->language = $language; + + return $this; + } + + /** + * @return bool + */ + public function hasRegion() + { + return $this->region !== null; + } + + /** + * @return string|null + */ + public function getRegion() + { + return $this->region; + } + + /** + * @param string|null $region + * @return static + */ + public function setRegion($region) + { + $this->region = $region; + return $this; + } + + /** + * @return bool + */ + public function hasSessionToken() + { + return $this->sessionToken !== null; + } + + /** + * @return string|null + */ + public function getSessionToken() + { + return $this->sessionToken; + } + + /** + * @param string|null $sessionToken + * @return static + */ + public function setSessionToken($sessionToken) + { + $this->sessionToken = $sessionToken; + + return $this; + } + + /** + * @return bool + */ + public function hasSpecificFields() + { + return !empty($this->fields); + } + + /** + * @return string[]|null + */ + public function getFields() + { + return $this->fields; + } + + /** + * @param string[] $fields + * @param bool $intersect + * @return string[]|null + */ + private function prepareFields($fields, $intersect = true) + { + if (empty($fields) || !is_array($fields)) { + return []; + } + + $res = array_unique(array_filter(array_map('trim', array_filter($fields, 'is_string')))); + + return $intersect ? array_intersect($res, static::getAllAvailableFields()) : $res; + } + + /** + * @return string[]|null + */ + public static function getAllAvailableFields() + { + return array_merge(static::FIELDS_BASIC, static::FIELDS_CONTACT, static::FIELDS_ATMOSPHERE); + } + + /** + * @param string|string[] $fields + * @return static + */ + public function withFields($fields) + { + if (is_string($fields)) { + $fields = explode(',', $fields); + } + + if (is_array($fields) && !empty($fields = $this->prepareFields($fields))) { + $this->fields = empty($this->fields) ? $fields : array_unique(array_merge($this->fields, $fields)); + } + + return $this; + } + + /** + * @param string|string[] $fields + * @return static + */ + public function withOnlyFields($fields) + { + if (is_string($fields)) { + $fields = explode(',', $fields); + } + + if (is_array($fields) && !empty($fields = $this->prepareFields($fields))) { + $this->fields = $fields; + } + + return $this; + } + + /** + * @param string|string[] $fields + * @return static + */ + public function withoutFields($fields) + { + if (is_string($fields)) { + $fields = explode(',', $fields); + } + + if (is_array($fields) && !empty($fields = $this->prepareFields($fields, false))) { + $fields = array_diff(empty($this->fields) ? static::getAllAvailableFields() : $this->fields, $fields); + $this->fields = empty($fields) ? null : $fields; + } + + return $this; + } + + /** + * @return static + */ + public function withoutBasicFields() + { + return $this->withoutFields(static::FIELDS_BASIC); + } + + /** + * @return static + */ + public function withoutContactFields() + { + return $this->withoutFields(static::FIELDS_CONTACT); + } + + /** + * @return static + */ + public function withoutAtmosphereFields() + { + return $this->withoutFields(static::FIELDS_ATMOSPHERE); + } + + /** + * @return static + */ + public function withBasicFields() + { + return $this->withFields(static::FIELDS_BASIC); + } + + /** + * @return static + */ + public function withContactFields() + { + return $this->withFields(static::FIELDS_CONTACT); + } + + /** + * @return static + */ + public function withAtmosphereFields() + { + return $this->withFields(static::FIELDS_ATMOSPHERE); + } + + /** + * @return static + */ + public function withAllFields() + { + $this->fields = null; + + return $this; } /** @@ -85,6 +342,18 @@ public function buildQuery() $query['language'] = $this->language; } + if ($this->hasRegion()) { + $query['region'] = $this->region; + } + + if ($this->hasSessionToken()) { + $query['sessiontoken'] = $this->sessionToken; + } + + if ($this->hasSpecificFields()) { + $query['fields'] = join(',', $this->fields); + } + return $query; } } diff --git a/src/Service/Serializer/Place/Base/Place.xml b/src/Service/Serializer/Place/Base/Place.xml index 099363b3..5219cb65 100644 --- a/src/Service/Serializer/Place/Base/Place.xml +++ b/src/Service/Serializer/Place/Base/Place.xml @@ -143,6 +143,12 @@ xml-key-as-attribute="false" /> + + + + + + [], 'alt_ids' => [], 'reviews' => [], + 'user_ratings_total' => null, 'types' => [], 'permanently_close' => null, ], $options); @@ -79,6 +80,7 @@ protected function assertPlace($place, array $options = []) $this->assertSame($options['types'], $place->getTypes()); $this->assertSame($options['permanently_close'], $place->isPermanentlyClose()); $this->assertSame(isset($options['rating']) ? (float) $options['rating'] : null, $place->getRating()); + $this->assertSame(isset($options['user_ratings_total']) ? (int) $options['user_ratings_total'] : null, $place->getUserRatingsTotal()); $this->assertGeometry($place->getGeometry(), $options['geometry']); $this->assertOpeningHours($place->getOpeningHours(), $options['opening_hours']); @@ -200,22 +202,26 @@ protected function assertReview($review, array $options = []) } $options = array_merge([ - 'author_name' => null, - 'author_url' => null, - 'text' => null, - 'rating' => null, - 'time' => null, - 'language' => null, - 'aspects' => [], + 'author_name' => null, + 'author_url' => null, + 'profile_photo_url' => null, + 'text' => null, + 'rating' => null, + 'time' => null, + 'relative_time_description' => null, + 'language' => null, + 'aspects' => [], ], $options); $this->assertInstanceOf(Review::class, $review); $this->assertSame($options['author_name'], $review->getAuthorName()); $this->assertSame($options['author_url'], $review->getAuthorUrl()); + $this->assertSame($options['profile_photo_url'], $review->getProfilePhotoUrl()); $this->assertSame($options['text'], $review->getText()); $this->assertSame((float) $options['rating'], $review->getRating()); $this->assertSame((new \DateTime('@'.$options['time']))->getTimestamp(), $review->getTime()->getTimestamp()); + $this->assertSame($options['relative_time_description'], $review->getRelativeTimeDescription()); $this->assertSame($options['language'], $review->getLanguage()); $this->assertCount(count($options['aspects']), $aspects = $review->getAspects()); diff --git a/tests/Service/Place/Base/PlaceTest.php b/tests/Service/Place/Base/PlaceTest.php index f6e15649..3304e877 100644 --- a/tests/Service/Place/Base/PlaceTest.php +++ b/tests/Service/Place/Base/PlaceTest.php @@ -64,6 +64,8 @@ public function testDefaultState() $this->assertNull($this->result->getPriceLevel()); $this->assertFalse($this->result->hasRating()); $this->assertNull($this->result->getRating()); + $this->assertFalse($this->result->hasUserRatingsTotal()); + $this->assertNull($this->result->getUserRatingsTotal()); $this->assertFalse($this->result->hasUtcOffset()); $this->assertNull($this->result->getUtcOffset()); $this->assertFalse($this->result->hasVicinity()); @@ -176,6 +178,14 @@ public function testRating() $this->assertSame($rating, $this->result->getRating()); } + public function testUserRatingsTotal() + { + $this->result->setUserRatingsTotal($userRatingsTotal = 48); + + $this->assertTrue($this->result->hasUserRatingsTotal()); + $this->assertSame($userRatingsTotal, $this->result->getUserRatingsTotal()); + } + public function testUtcOffset() { $this->result->setUtcOffset($utcOffset = 100); diff --git a/tests/Service/Place/Base/ReviewTest.php b/tests/Service/Place/Base/ReviewTest.php index 7f6cd7a9..fdb9f7d9 100644 --- a/tests/Service/Place/Base/ReviewTest.php +++ b/tests/Service/Place/Base/ReviewTest.php @@ -38,12 +38,16 @@ public function testDefaultState() $this->assertNull($this->review->getAuthorName()); $this->assertFalse($this->review->hasAuthorUrl()); $this->assertNull($this->review->getAuthorUrl()); + $this->assertFalse($this->review->hasProfilePhotoUrl()); + $this->assertNull($this->review->getProfilePhotoUrl()); $this->assertFalse($this->review->hasText()); $this->assertNull($this->review->getText()); $this->assertFalse($this->review->hasRating()); $this->assertNull($this->review->getRating()); $this->assertFalse($this->review->hasTime()); $this->assertNull($this->review->getTime()); + $this->assertFalse($this->review->hasRelativeTimeDescription()); + $this->assertNull($this->review->getRelativeTimeDescription()); $this->assertFalse($this->review->hasLanguage()); $this->assertNull($this->review->getLanguage()); $this->assertFalse($this->review->hasAspects()); @@ -66,6 +70,14 @@ public function testAuthorUrl() $this->assertSame($authorUrl, $this->review->getAuthorUrl()); } + public function testProfilePhotoUrl() + { + $this->review->setProfilePhotoUrl($profilePhotoUrl = 'photo_url'); + + $this->assertTrue($this->review->hasProfilePhotoUrl()); + $this->assertSame($profilePhotoUrl, $this->review->getProfilePhotoUrl()); + } + public function testText() { $this->review->setText($text = 'foo'); @@ -90,6 +102,14 @@ public function testTime() $this->assertSame($time, $this->review->getTime()); } + public function testRelativeTimeDescription() + { + $this->review->setRelativeTimeDescription($time = "a month ago"); + + $this->assertTrue($this->review->hasRelativeTimeDescription()); + $this->assertSame($time, $this->review->getRelativeTimeDescription()); + } + public function testLanguage() { $this->review->setLanguage($language = 'fr'); diff --git a/tests/Service/Place/Detail/PlaceDetailServiceTest.php b/tests/Service/Place/Detail/PlaceDetailServiceTest.php index 4306a0ab..e00bd21f 100644 --- a/tests/Service/Place/Detail/PlaceDetailServiceTest.php +++ b/tests/Service/Place/Detail/PlaceDetailServiceTest.php @@ -74,6 +74,39 @@ public function testProcessWithLanguage($format) $this->assertPlaceDetailResponse($response, $request); } + /** + * @param string $format + * + * @dataProvider formatProvider + */ + public function testProcessWithRegion($format) + { + $request = $this->createRequest(); + $request->setRegion('uk'); + + $this->service->setFormat($format); + $response = $this->service->process($request); + + $this->assertPlaceDetailResponse($response, $request); + } + + /** + * @param string $format + * + * @dataProvider formatProvider + */ + public function testProcessWithBasicFields($format) + { + $request = $this->createRequest()->withBasicFields(); + + $this->service->setFormat($format); + $response = $this->service->process($request); + + $this->assertPlaceDetailResponse($response, $request); + $this->assertFalse($response->getResult()->hasReviews()); + $this->assertTrue($response->getResult()->hasFormattedAddress()); + } + /** * @param string $format * diff --git a/tests/Service/Place/Detail/Request/PlaceDetailRequestTest.php b/tests/Service/Place/Detail/Request/PlaceDetailRequestTest.php index 46a17b5b..8c82044f 100644 --- a/tests/Service/Place/Detail/Request/PlaceDetailRequestTest.php +++ b/tests/Service/Place/Detail/Request/PlaceDetailRequestTest.php @@ -49,6 +49,12 @@ public function testDefaultState() $this->assertSame($this->placeId, $this->request->getPlaceId()); $this->assertFalse($this->request->hasLanguage()); $this->assertNull($this->request->getLanguage()); + $this->assertFalse($this->request->hasRegion()); + $this->assertNull($this->request->getRegion()); + $this->assertFalse($this->request->hasSessionToken()); + $this->assertNull($this->request->getSessionToken()); + $this->assertFalse($this->request->hasSpecificFields()); + $this->assertNull($this->request->getFields()); } public function testPlaceId() @@ -66,6 +72,63 @@ public function testLanguage() $this->assertSame($language, $this->request->getLanguage()); } + public function testRegion() + { + $this->request->setRegion($region = 'uk'); + + $this->assertTrue($this->request->hasRegion()); + $this->assertSame($region, $this->request->getRegion()); + } + + public function testSessionToken() + { + $this->request->setSessionToken($token = 'session_token'); + + $this->assertTrue($this->request->hasSessionToken()); + $this->assertSame($token, $this->request->getSessionToken()); + } + + public function testFields() + { + $this->request->withoutAtmosphereFields(); + + $expected = array_merge(PlaceDetailRequest::FIELDS_BASIC, PlaceDetailRequest::FIELDS_CONTACT); + $this->assertTrue($this->request->hasSpecificFields()); + $this->assertEquals($expected, $this->request->getFields(), 'Testing withoutAtmosphereFields Function', 0.0, 10, true); + + $this->request->withFields('photo '); + $this->assertTrue($this->request->hasSpecificFields()); + $this->assertEquals($expected, $this->request->getFields(), 'Testing adding a duplicate field', 0.0, 10, true); + + $this->request->withoutFields(' photo'); + $this->request->withFields('invalid_field'); + $expected_1 = array_diff($expected, ['photo']); + $this->assertTrue($this->request->hasSpecificFields()); + $this->assertEquals($expected_1, $this->request->getFields(), 'Testing removing a field and adding an invalid field', 0.0, 10, true); + + $this->request->withFields(' photo, review '); + $expected_1 = array_merge($expected, ['review']); + $this->assertTrue($this->request->hasSpecificFields()); + $this->assertEquals($expected_1, $this->request->getFields(), 'Testing adding 2 non duplicate fields from 2 different field sets', 0.0, 10, true); + + $this->request->withAllFields(); + $this->assertFalse($this->request->hasSpecificFields()); + $this->assertNull($this->request->getFields()); + + $this->request->withFields('review'); + $this->request->withOnlyFields(array_merge(['photo '], $expected = ['name', 'photo', 'website', 'opening_hours'])); + $this->assertTrue($this->request->hasSpecificFields()); + $this->assertEquals($expected, $this->request->getFields(), 'Testing withOnlyFields', 0.0, 10, true); + + $this->request->withoutContactFields(); + $this->assertTrue($this->request->hasSpecificFields()); + $this->assertEquals(array_diff($expected, PlaceDetailRequest::FIELDS_CONTACT), $this->request->getFields(), 'Testing withoutContactFields', 0.0, 10, true); + + $this->request->withoutBasicFields(); + $this->assertFalse($this->request->hasSpecificFields()); + $this->assertNull($this->request->getFields()); + } + public function testBuildQuery() { $this->assertSame(['placeid' => $this->placeId], $this->request->buildQuery()); @@ -76,8 +139,48 @@ public function testBuildQueryWithLanguage() $this->request->setLanguage($language = 'fr'); $this->assertSame([ - 'placeid' => $this->placeId, + 'placeid' => $this->placeId, 'language' => $language, ], $this->request->buildQuery()); } + + public function testBuildQueryWithRegion() + { + $this->request->setRegion($region = 'uk'); + + $this->assertSame([ + 'placeid' => $this->placeId, + 'region' => $region, + ], $this->request->buildQuery()); + } + + public function testBuildQueryWithSessionToken() + { + $this->request->setSessionToken($token = 'token'); + + $this->assertSame([ + 'placeid' => $this->placeId, + 'sessiontoken' => $token, + ], $this->request->buildQuery()); + } + + public function testBuildQueryWithBasicFields() + { + $this->request->withFields($fields = PlaceDetailRequest::FIELDS_BASIC); + + $this->assertSame([ + 'placeid' => $this->placeId, + 'fields' => join(',', $fields), + ], $this->request->buildQuery()); + } + + public function testBuildQueryWithSpecificFields() + { + $this->request->withFields($fields ='icon,name,url,photo,invalid_field,website, rating , review,user_ratings_total '); + + $this->assertSame([ + 'placeid' => $this->placeId, + 'fields' => 'icon,name,url,photo,website,rating,review,user_ratings_total', + ], $this->request->buildQuery()); + } }