diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2f4002..b262d6ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. This project adheres to Semantic Versioning (http://semver.org/). +### 7.2.0 +* Add support for search requests ### 7.1.1 * Restore the DeleteProduct operation (from 6.2.2) diff --git a/src/.env b/src/.env index 9171c3fa..5a2d8eab 100644 --- a/src/.env +++ b/src/.env @@ -1,6 +1,7 @@ NOSTO_SERVER_URL=connect.nosto.com NOSTO_EMAIL_WIDGET_BASE_URL=https://connect.nosto.com NOSTO_API_BASE_URL=https://api.nosto.com +NOSTO_SEARCH_BASE_URL=https://search.nosto.com NOSTO_OAUTH_BASE_URL=https://my.nosto.com/oauth NOSTO_WEB_HOOK_BASE_URL=https://my.nosto.com NOSTO_GRAPHQL_BASE_URL=https://api.nosto.com diff --git a/src/Nosto.php b/src/Nosto.php index 81a26589..e8fdd791 100644 --- a/src/Nosto.php +++ b/src/Nosto.php @@ -62,6 +62,7 @@ class Nosto const DEFAULT_NOSTO_OAUTH_BASE_URL = 'https://my.nosto.com/oauth'; const DEFAULT_NOSTO_API_BASE_URL = 'https://api.nosto.com'; const DEFAULT_NOSTO_GRAPHQL_BASE_URL = 'https://api.nosto.com'; + const DEFAULT_NOSTO_SEARCH_BASE_URL = 'https://search.nosto.com'; const URL_PARAM_MESSAGE_TYPE = 'message_type'; const URL_PARAM_MESSAGE_CODE = 'message_code'; @@ -109,6 +110,11 @@ public static function getGraphqlBaseUrl() return self::getEnvVariable('NOSTO_GRAPHQL_BASE_URL', self::DEFAULT_NOSTO_GRAPHQL_BASE_URL); } + public static function getSearchBaseUrl() + { + return self::getEnvVariable('NOSTO_SEARCH_BASE_URL', self::DEFAULT_NOSTO_SEARCH_BASE_URL); + } + /** * Throws a new HttpException exception with info about both the * request and response. diff --git a/src/Operation/AbstractOperation.php b/src/Operation/AbstractOperation.php index 0eecbd84..4f8887b4 100644 --- a/src/Operation/AbstractOperation.php +++ b/src/Operation/AbstractOperation.php @@ -41,6 +41,7 @@ use Nosto\NostoException; use Nosto\Request\Http\HttpRequest; use Nosto\Request\Graphql\GraphqlRequest; +use Nosto\Request\Graphql\SearchRequest; use Nosto\Result\ResultHandler; /** @@ -69,7 +70,7 @@ abstract class AbstractOperation * @param string|null $nostoAccount * @param string|null $domain * @param bool $isTokenNeeded - * @return ApiRequest|GraphqlRequest|HttpRequest + * @return ApiRequest|GraphqlRequest|SearchRequest|HttpRequest * @throws NostoException */ protected function initRequest( @@ -103,7 +104,7 @@ protected function initRequest( /** * Return type of request object * - * @return HttpRequest|ApiRequest|GraphqlRequest + * @return HttpRequest|ApiRequest|GraphqlRequest|SearchRequest */ abstract protected function getRequestType(); diff --git a/src/Operation/AbstractSearchOperation.php b/src/Operation/AbstractSearchOperation.php new file mode 100644 index 00000000..234723ac --- /dev/null +++ b/src/Operation/AbstractSearchOperation.php @@ -0,0 +1,144 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Operation; + +use Nosto\NostoException; +use Nosto\Request\Api\Token; +use Nosto\Request\Http\Exception\AbstractHttpException; +use Nosto\Request\Http\Exception\HttpResponseException; +use Nosto\Request\Graphql\SearchRequest; +use Nosto\Types\Signup\AccountInterface; + +abstract class AbstractSearchOperation extends AbstractOperation +{ + /** + * @var AccountInterface Nosto configuration + */ + protected $account; + + /** + * @var string active domain + */ + protected $activeDomain; + + /** + * Constructor + * + * @param AccountInterface $account the account object. + * @param string $activeDomain + */ + public function __construct(AccountInterface $account, $activeDomain = '') + { + $this->account = $account; + $this->activeDomain = $activeDomain; + } + + /** + * Returns the result + * + * @return mixed|null + * @throws AbstractHttpException + * @throws HttpResponseException + * @throws NostoException + */ + public function execute() + { + $request = $this->initRequest( + $this->account->getApiToken(Token::API_SEARCH), + $this->account->getName(), + ); + $payload = ['query' => $this->getQuery(), 'variables' => $this->getVariables()]; + $response = $request->postRaw( + json_encode($payload) + ); + + return $request->getResultHandler()->parse($response); + } + + /** + * Builds the recommendation API request + * + * @return string + */ + abstract public function getQuery(); + + /** + * @return array + */ + abstract public function getVariables(); + + /** + * @inheritDoc + */ + protected function initRequest( + Token $token = null, + $nostoAccount = null, + $domain = null, + $isTokenNeeded = true + ) { + $request = parent::initRequest($token, $nostoAccount, $domain, false); + + $request->setAuthBearer($token->getValue()); + + return $request; + + } + + /** + * @inheritdoc + */ + protected function getRequestType() + { + return new SearchRequest(); + } + + /** + * @inheritdoc + */ + protected function getContentType() + { + return self::CONTENT_TYPE_APPLICATION_JSON; + } + + /** + * @inheritdoc + */ + protected function getPath() + { + return SearchRequest::PATH_SEARCH; + } +} diff --git a/src/Operation/Search/SearchOperation.php b/src/Operation/Search/SearchOperation.php new file mode 100644 index 00000000..9c08949e --- /dev/null +++ b/src/Operation/Search/SearchOperation.php @@ -0,0 +1,300 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Operation\Search; + +use Nosto\Operation\AbstractSearchOperation; +use Nosto\Result\Graphql\Search\SearchResultHandler; + +class SearchOperation extends AbstractSearchOperation +{ + private string $accountId; + + private string $query = ''; + + private ?string $categoryId = null; + + private ?string $categoryPath = null; + + private ?string $variationId = null; + + private int $size = 20; + + private int $from = 0; + + private array $sort = []; + + private array $filters = []; + + private ?array $sessionParams = null; + + private bool $explain = false; + + private ?string $redirect = null; + + private ?float $time = null; + + private array $rules = []; + + private array $customRules = []; + + private array $segments = []; + + private ?array $keywords = null; + + public function setAccountId(string $accountId): void + { + $this->accountId = $accountId; + } + + public function setQuery(string $query): void + { + $this->query = $query; + } + + public function setCategoryId(string $categoryId): void + { + $this->categoryId = $categoryId; + } + + public function setCategoryPath(string $categoryPath): void + { + $this->categoryPath = $categoryPath; + } + + public function setVariationId(string $variationId): void + { + $this->variationId = $variationId; + } + + public function setSort(string $field, string $order): void + { + $this->sort = [ + "field" => $field, + "order" => strtolower($order), + ]; + } + + public function setFrom(int $from): void + { + $this->from = $from; + } + + public function setSize(int $size): void + { + $this->size = $size; + } + + public function addValueFilter(string $filterField, string $value): void + { + if (array_key_exists($filterField, $this->filters)) { + $this->filters[$filterField]['value'][] = $value; + } else { + $this->filters[$filterField] = [ + 'field' => $filterField, + 'value' => [$value], + ]; + } + } + + public function addRangeFilter(string $filterField, ?string $min = null, ?string $max = null): void + { + $range = []; + + if (!is_null($min)) { + $range['gt'] = $min; + } + if (!is_null($max)) { + $range['lt'] = $max; + } + + if (array_key_exists($filterField, $this->filters)) { + $this->filters[$filterField]['range'] = array_merge( + $this->filters[$filterField]['range'], + $range, + ); + } else { + $this->filters[$filterField] = [ + 'field' => $filterField, + 'range' => $range, + ]; + } + } + + public function setSessionParams(array $sessionParams): void + { + $this->sessionParams = $sessionParams; + } + + public function setExplain(bool $explain): void + { + $this->explain = $explain; + } + + public function setRedirect(string $redirect): void + { + $this->redirect = $redirect; + } + + public function setTime(float $time): void + { + $this->time = $time; + } + + public function setRules(array $rules): void + { + $this->rules = $rules; + } + + public function setCustomRules(array $customRules): void + { + $this->customRules = $customRules; + } + + public function setSegments(array $segments): void + { + $this->segments = $segments; + } + + public function setKeywords(array $keywords): void + { + $this->keywords = $keywords; + } + + public function getQuery() + { + return << $this->accountId, + 'query' => $this->query, + 'categoryId' => $this->categoryId, + 'categoryPath' => $this->categoryPath, + 'variationId' => $this->variationId, + 'sort' => $this->sort, + 'size' => $this->size, + 'from' => $this->from, + 'filter' => array_values($this->filters), + 'sessionParams' => $this->sessionParams, + 'explain' => $this->explain, + 'redirect' => $this->redirect, + 'time' => $this->time, + 'rules' => $this->rules, + 'customRules' => $this->customRules, + 'segments' => $this->segments, + 'keywords' => $this->keywords, + ]; + } +} \ No newline at end of file diff --git a/src/Request/Api/Token.php b/src/Request/Api/Token.php index 24e1e32d..508d3131 100644 --- a/src/Request/Api/Token.php +++ b/src/Request/Api/Token.php @@ -53,6 +53,7 @@ class Token extends AbstractObject implements ValidatableInterface const API_EMAIL = 'email'; const API_CREATE = 'create'; // Special token related to the platform const API_GRAPHQL = 'apps'; // Special token related to the platform + const API_SEARCH = 'search'; /** * @var array list of valid api tokens to request from Nosto. @@ -63,7 +64,8 @@ class Token extends AbstractObject implements ValidatableInterface self::API_EXCHANGE_RATES, self::API_SETTINGS, self::API_EMAIL, - self::API_GRAPHQL + self::API_GRAPHQL, + self::API_SEARCH ]; /** * @var string the token name, must be one of the defined tokens from self::$tokenNames. diff --git a/src/Request/Graphql/SearchRequest.php b/src/Request/Graphql/SearchRequest.php new file mode 100644 index 00000000..75043003 --- /dev/null +++ b/src/Request/Graphql/SearchRequest.php @@ -0,0 +1,56 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Request\Graphql; + +use Nosto\Nosto; +use Nosto\Request\Api\ApiRequest; + +/** + * API request class for making API requests to Nosto. + */ +class SearchRequest extends ApiRequest +{ + const PATH_SEARCH = '/v1/graphql'; + + /** + * @inheritdoc + */ + public function setPath($path) + { + $this->setUrl(Nosto::getSearchBaseUrl() . $path); + } +} diff --git a/src/Result/Graphql/Search/SearchResult.php b/src/Result/Graphql/Search/SearchResult.php new file mode 100644 index 00000000..a0a76b2f --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult.php @@ -0,0 +1,63 @@ +redirect = GraphQLUtils::getProperty($data, 'redirect'); + $this->query = GraphQLUtils::getProperty($data, 'query'); + $this->explain = GraphQLUtils::getClassProperty($data, 'explain', Explain::class); + $this->products = GraphQLUtils::getClassProperty($data, 'products', Products::class); + } + + /** + * @return ?string + */ + public function getRedirect() + { + return $this->redirect; + } + + /** + * @return ?string + */ + public function getQuery() + { + return $this->query; + } + + /** + * @return ?Explain + */ + public function getExplain() + { + return $this->explain; + } + + /** + * @return ?Products + */ + public function getProducts() + { + return $this->products; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Explain.php b/src/Result/Graphql/Search/SearchResult/Explain.php new file mode 100644 index 00000000..bed49a17 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Explain.php @@ -0,0 +1,26 @@ +matchedRules = GraphQLUtils::getArrayProperty($data, 'matchedRules', MatchedRule::class); + } + + /** + * @return ?MatchedRule[] + */ + public function getMatchedRules() + { + return $this->matchedRules; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Explain/MatchedRule.php b/src/Result/Graphql/Search/SearchResult/Explain/MatchedRule.php new file mode 100644 index 00000000..9ef9a692 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Explain/MatchedRule.php @@ -0,0 +1,49 @@ +id = GraphQLUtils::getProperty($data, 'id'); + $this->name = GraphQLUtils::getProperty($data, 'name'); + $this->set = GraphQLUtils::getProperty($data, 'set'); + } + + /** + * @return ?string + */ + public function getId() + { + return $this->id; + } + + /** + * @return ?string + */ + public function getName() + { + return $this->name; + } + + /** + * @return ?stdClass + */ + public function getSet() + { + return $this->set; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products.php b/src/Result/Graphql/Search/SearchResult/Products.php new file mode 100644 index 00000000..63babf8e --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products.php @@ -0,0 +1,130 @@ +total = GraphQLUtils::getProperty($data, 'total'); + $this->size = GraphQLUtils::getProperty($data, 'size'); + $this->from = GraphQLUtils::getProperty($data, 'from'); + $this->collapse = GraphQLUtils::getProperty($data, 'collapse'); + $this->fuzzy = GraphQLUtils::getProperty($data, 'fuzzy'); + $this->categoryId = GraphQLUtils::getProperty($data, 'categoryId'); + $this->categoryPath = GraphQLUtils::getProperty($data, 'categoryPath'); + $this->hits = GraphQLUtils::getArrayProperty($data, 'hits', Hit::class); + $this->facets = property_exists($data, 'facets') && $data->facets + ? array_map( + function (stdClass $facet) { + return Facet::getInstance($facet); + }, + $data->facets + ) + : null; + } + + /** + * @return ?int + */ + public function getTotal() + { + return $this->total; + } + + /** + * @return ?int + */ + public function getSize() + { + return $this->size; + } + + /** + * @return ?int + */ + public function getFrom() + { + return $this->from; + } + + /** + * @return ?string + */ + public function getCollapse() + { + return $this->collapse; + } + + /** + * @return ?bool + */ + public function getFuzzy() + { + return $this->fuzzy; + } + + /** + * @return ?string + */ + public function getCategoryId() + { + return $this->categoryId; + } + + /** + * @return ?string + */ + public function getCategoryPath() + { + return $this->categoryPath; + } + + /** + * @return ?Hit[] + */ + public function getHits() + { + return $this->hits; + } + + /** + * @return ?Facet[] + */ + public function getFacets() + { + return $this->facets; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/BasicFacet.php b/src/Result/Graphql/Search/SearchResult/Products/BasicFacet.php new file mode 100644 index 00000000..f91dd815 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/BasicFacet.php @@ -0,0 +1,7 @@ +id = GraphQLUtils::getProperty($data, 'id'); + $this->name = GraphQLUtils::getProperty($data, 'name'); + $this->field = GraphQLUtils::getProperty($data, 'field'); + $this->type = GraphQLUtils::getProperty($data, 'type'); + } + + /** + * @return Facet + * @throws Exception + */ + public static function getInstance(stdClass $facet) + { + switch ($facet->type) { + case self::STATS_TYPE: + return new StatsFacet($facet); + case self::TERMS_TYPE: + return new TermsFacet($facet); + default: + return new BasicFacet($facet); + } + } + + /** + * @return ?string + */ + public function getId() + { + return $this->id; + } + + /** + * @return ?string + */ + public function getName() + { + return $this->name; + } + + /** + * @return ?string + */ + public function getField() + { + return $this->field; + } + + /** + * @return ?string + */ + public function getType() + { + return $this->type; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/Facet/TermsFacetValue.php b/src/Result/Graphql/Search/SearchResult/Products/Facet/TermsFacetValue.php new file mode 100644 index 00000000..07c988c9 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/Facet/TermsFacetValue.php @@ -0,0 +1,49 @@ +value = GraphQLUtils::getProperty($data, 'value'); + $this->count = GraphQLUtils::getProperty($data, 'count'); + $this->selected = GraphQLUtils::getProperty($data, 'selected'); + } + + /** + * @return ?string + */ + public function getValue() + { + return $this->value; + } + + /** + * @return ?int + */ + public function getCount() + { + return $this->count; + } + + /** + * @return ?bool + */ + public function getSelected() + { + return $this->selected; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/Hit.php b/src/Result/Graphql/Search/SearchResult/Products/Hit.php new file mode 100644 index 00000000..4501a1d0 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/Hit.php @@ -0,0 +1,597 @@ +productId = GraphQLUtils::getProperty($data, 'productId'); + $this->url = GraphQLUtils::getProperty($data, 'url'); + $this->name = GraphQLUtils::getProperty($data, 'name'); + $this->imageUrl = GraphQLUtils::getProperty($data, 'imageUrl'); + $this->thumbUrl = GraphQLUtils::getProperty($data, 'thumbUrl'); + $this->description = GraphQLUtils::getProperty($data, 'description'); + $this->brand = GraphQLUtils::getProperty($data, 'brand'); + $this->variantId = GraphQLUtils::getProperty($data, 'variantId'); + $this->availability = GraphQLUtils::getProperty($data, 'availability'); + $this->price = GraphQLUtils::getProperty($data, 'price'); + $this->priceText = GraphQLUtils::getProperty($data, 'priceText'); + $this->categoryIds = GraphQLUtils::getProperty($data, 'categoryIds'); + $this->categories = GraphQLUtils::getProperty($data, 'categories'); + $this->tags1 = GraphQLUtils::getProperty($data, 'tags1'); + $this->tags2 = GraphQLUtils::getProperty($data, 'tags2'); + $this->tags3 = GraphQLUtils::getProperty($data, 'tags3'); + $this->customFields = GraphQLUtils::getArrayProperty($data, 'customFields', CustomField::class); + $this->priceCurrencyCode = GraphQLUtils::getProperty($data, 'priceCurrencyCode'); + $this->datePublished = GraphQLUtils::getProperty($data, 'datePublished'); + $this->listPrice = GraphQLUtils::getProperty($data, 'listPrice'); + $this->unitPricingBaseMeasure = GraphQLUtils::getProperty($data, 'unitPricingBaseMeasure'); + $this->unitPricingUnit = GraphQLUtils::getProperty($data, 'unitPricingUnit'); + $this->unitPricingMeasure = GraphQLUtils::getProperty($data, 'unitPricingMeasure'); + $this->googleCategory = GraphQLUtils::getProperty($data, 'googleCategory'); + $this->gtin = GraphQLUtils::getProperty($data, 'gtin'); + $this->ageGroup = GraphQLUtils::getProperty($data, 'ageGroup'); + $this->gender = GraphQLUtils::getProperty($data, 'gender'); + $this->condition = GraphQLUtils::getProperty($data, 'condition'); + $this->alternateImageUrls = GraphQLUtils::getProperty($data, 'alternateImageUrls'); + $this->ratingValue = GraphQLUtils::getProperty($data, 'ratingValue'); + $this->reviewCount = GraphQLUtils::getProperty($data, 'reviewCount'); + $this->inventoryLevel = GraphQLUtils::getProperty($data, 'inventoryLevel'); + $this->supplierCost = GraphQLUtils::getProperty($data, 'supplierCost'); + $this->skus = GraphQLUtils::getArrayProperty($data, 'skus', Sku::class); + $this->variations = GraphQLUtils::getArrayProperty($data, 'variations', Variation::class); + $this->pid = GraphQLUtils::getProperty($data, 'pid'); + $this->stats = GraphQLUtils::getClassProperty($data, 'stats', Stats::class); + $this->isExcluded = GraphQLUtils::getProperty($data, 'isExcluded'); + $this->onDiscount = GraphQLUtils::getProperty($data, 'onDiscount'); + $this->extras = GraphQLUtils::getArrayProperty($data, 'extra', Extra::class); + $this->explain = GraphQLUtils::getClassProperty($data, '_explain', Explain::class); + $this->score = GraphQLUtils::getProperty($data, '_score'); + $this->pinned = GraphQLUtils::getProperty($data, '_pinned'); + $this->saleable = GraphQLUtils::getProperty($data, 'saleable'); + $this->available = GraphQLUtils::getProperty($data, 'available'); + $this->realVariantIds = GraphQLUtils::getProperty($data, 'realVariantIds'); + $this->ai = GraphQLUtils::getClassProperty($data, 'ai', Ai::class); + $this->affinities = GraphQLUtils::getClassProperty($data, 'affinities', Affinities::class); + } + + /** + * @return ?string + */ + public function getProductId() + { + return $this->productId; + } + + /** + * @return ?string + */ + public function getUrl() + { + return $this->url; + } + + /** + * @return ?string + */ + public function getName() + { + return $this->name; + } + + /** + * @return ?string + */ + public function getImageUrl() + { + return $this->imageUrl; + } + + /** + * @return ?string + */ + public function getThumbUrl() + { + return $this->thumbUrl; + } + + /** + * @return ?string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @return ?string + */ + public function getBrand() + { + return $this->brand; + } + + /** + * @return ?string + */ + public function getVariantId() + { + return $this->variantId; + } + + /** + * @return ?string + */ + public function getAvailability() + { + return $this->availability; + } + + /** + * @return ?float + */ + public function getPrice() + { + return $this->price; + } + + /** + * @return ?string + */ + public function getPriceText() + { + return $this->priceText; + } + + /** + * @return ?string[] + */ + public function getCategoryIds() + { + return $this->categoryIds; + } + + /** + * @return ?string[] + */ + public function getCategories() + { + return $this->categories; + } + + /** + * @return ?string[] + */ + public function getTags1() + { + return $this->tags1; + } + + /** + * @return ?string[] + */ + public function getTags2() + { + return $this->tags2; + } + + /** + * @return ?string[] + */ + public function getTags3() + { + return $this->tags3; + } + + /** + * @return ?CustomField[] + */ + public function getCustomFields() + { + return $this->customFields; + } + + /** + * @return ?string + */ + public function getPriceCurrencyCode() + { + return $this->priceCurrencyCode; + } + + /** + * @return ?int + */ + public function getDatePublished() + { + return $this->datePublished; + } + + /** + * @return ?float + */ + public function getListPrice() + { + return $this->listPrice; + } + + /** + * @return ?float + */ + public function getUnitPricingBaseMeasure() + { + return $this->unitPricingBaseMeasure; + } + + /** + * @return ?string + */ + public function getUnitPricingUnit() + { + return $this->unitPricingUnit; + } + + /** + * @return ?float + */ + public function getUnitPricingMeasure() + { + return $this->unitPricingMeasure; + } + + /** + * @return ?string + */ + public function getGoogleCategory() + { + return $this->googleCategory; + } + + /** + * @return ?string + */ + public function getGtin() + { + return $this->gtin; + } + + /** + * @return ?string + */ + public function getAgeGroup() + { + return $this->ageGroup; + } + + /** + * @return ?string + */ + public function getGender() + { + return $this->gender; + } + + /** + * @return ?string + */ + public function getCondition() + { + return $this->condition; + } + + /** + * @return ?string[] + */ + public function getAlternateImageUrls() + { + return $this->alternateImageUrls; + } + + /** + * @return ?float + */ + public function getRatingValue() + { + return $this->ratingValue; + } + + /** + * @return ?int + */ + public function getReviewCount() + { + return $this->reviewCount; + } + + /** + * @return ?int + */ + public function getInventoryLevel() + { + return $this->inventoryLevel; + } + + /** + * @return ?float + */ + public function getSupplierCost() + { + return $this->supplierCost; + } + + /** + * @return ?Sku[] + */ + public function getSkus() + { + return $this->skus; + } + + /** + * @return ?Variation[] + */ + public function getVariations() + { + return $this->variations; + } + + /** + * @return ?string + */ + public function getPid() + { + return $this->pid; + } + + /** + * @return ?Stats + */ + public function getStats() + { + return $this->stats; + } + + /** + * @return ?bool + */ + public function getIsExcluded() + { + return $this->isExcluded; + } + + /** + * @return ?bool + */ + public function getOnDiscount() + { + return $this->onDiscount; + } + + /** + * @return ?Extra[] + */ + public function getExtras() + { + return $this->extras; + } + + /** + * @return ?Explain + */ + public function getExplain() + { + return $this->explain; + } + + /** + * @return ?float + */ + public function getScore() + { + return $this->score; + } + + /** + * @return ?bool + */ + public function getPinned() + { + return $this->pinned; + } + + /** + * @return ?bool + */ + public function getSaleable() + { + return $this->saleable; + } + + /** + * @return ?bool + */ + public function getAvailable() + { + return $this->available; + } + + /** + * @return ?string[] + */ + public function getRealVariantIds() + { + return $this->realVariantIds; + } + + /** + * @return ?Ai + */ + public function getAi() + { + return $this->ai; + } + + /** + * @return ?Affinities + */ + public function getAffinities() + { + return $this->affinities; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/Hit/Affinities.php b/src/Result/Graphql/Search/SearchResult/Products/Hit/Affinities.php new file mode 100644 index 00000000..e3cfe7d3 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/Hit/Affinities.php @@ -0,0 +1,61 @@ +brand = GraphQLUtils::getProperty($data, 'brand'); + $this->categories = GraphQLUtils::getProperty($data, 'categories'); + $this->color = GraphQLUtils::getProperty($data, 'color'); + $this->size = GraphQLUtils::getProperty($data, 'size'); + } + + /** + * @return ?string + */ + public function getBrand() + { + return $this->brand; + } + + /** + * @return ?string[] + */ + public function getCategories() + { + return $this->categories; + } + + /** + * @return ?string[] + */ + public function getColor() + { + return $this->color; + } + + /** + * @return ?string[] + */ + public function getSize() + { + return $this->size; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/Hit/Ai.php b/src/Result/Graphql/Search/SearchResult/Products/Hit/Ai.php new file mode 100644 index 00000000..4b02c029 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/Hit/Ai.php @@ -0,0 +1,49 @@ +primaryColor = GraphQLUtils::getProperty($data, 'primaryColor'); + $this->overridingColor = GraphQLUtils::getProperty($data, 'overridingColor'); + $this->dominantColors = GraphQLUtils::getProperty($data, 'dominantColors'); + } + + /** + * @return ?string + */ + public function getPrimaryColor() + { + return $this->primaryColor; + } + + /** + * @return ?string + */ + public function getOverridingColor() + { + return $this->overridingColor; + } + + /** + * @return ?string[] + */ + public function getDominantColors() + { + return $this->dominantColors; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/Hit/CustomField.php b/src/Result/Graphql/Search/SearchResult/Products/Hit/CustomField.php new file mode 100644 index 00000000..12449765 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/Hit/CustomField.php @@ -0,0 +1,37 @@ +key = GraphQLUtils::getProperty($data, 'key'); + $this->value = GraphQLUtils::getProperty($data, 'value'); + } + + /** + * @return ?string + */ + public function getKey() + { + return $this->key; + } + + /** + * @return ?string + */ + public function getValue() + { + return $this->value; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/Hit/Explain.php b/src/Result/Graphql/Search/SearchResult/Products/Hit/Explain.php new file mode 100644 index 00000000..6f0c4cda --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/Hit/Explain.php @@ -0,0 +1,61 @@ +match = GraphQLUtils::getProperty($data, 'match'); + $this->value = GraphQLUtils::getProperty($data, 'value'); + $this->description = GraphQLUtils::getProperty($data, 'description'); + $this->details = GraphQLUtils::getArrayProperty($data, 'details', Explain::class, []); + } + + /** + * @return ?bool + */ + public function getMatch() + { + return $this->match; + } + + /** + * @return float|null + */ + public function getValue() + { + return $this->value; + } + + /** + * @return string|null + */ + public function getDescription() + { + return $this->description; + } + + /** + * @return Explain[] + */ + public function getDetails() + { + return $this->details; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/Hit/Extra.php b/src/Result/Graphql/Search/SearchResult/Products/Hit/Extra.php new file mode 100644 index 00000000..77b9b716 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/Hit/Extra.php @@ -0,0 +1,37 @@ +key = GraphQLUtils::getProperty($data, 'key'); + $this->value = GraphQLUtils::getProperty($data, 'value'); + } + + /** + * @return ?string + */ + public function getKey() + { + return $this->key; + } + + /** + * @return ?string[] + */ + public function getValue() + { + return $this->value; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/Hit/Sku.php b/src/Result/Graphql/Search/SearchResult/Products/Hit/Sku.php new file mode 100644 index 00000000..b72cf0a3 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/Hit/Sku.php @@ -0,0 +1,145 @@ +id = GraphQLUtils::getProperty($data, 'id'); + $this->url = GraphQLUtils::getProperty($data, 'url'); + $this->name = GraphQLUtils::getProperty($data, 'name'); + $this->imageUrl = GraphQLUtils::getProperty($data, 'imageUrl'); + $this->availability = GraphQLUtils::getProperty($data, 'availability'); + $this->price = GraphQLUtils::getProperty($data, 'price'); + $this->priceText = GraphQLUtils::getProperty($data, 'priceText'); + $this->customFields = GraphQLUtils::getArrayProperty($data, 'customFields', CustomField::class); + $this->listPrice = GraphQLUtils::getProperty($data, 'listPrice'); + $this->inventoryLevel = GraphQLUtils::getProperty($data, 'inventoryLevel'); + $this->ai = GraphQLUtils::getClassProperty($data, 'ai', Ai::class); + } + + /** + * @return ?string + */ + public function getId() + { + return $this->id; + } + + /** + * @return ?string + */ + public function getUrl() + { + return $this->url; + } + + /** + * @return ?string + */ + public function getName() + { + return $this->name; + } + + /** + * @return ?string + */ + public function getImageUrl() + { + return $this->imageUrl; + } + + /** + * @return ?string + */ + public function getAvailability() + { + return $this->availability; + } + + /** + * @return ?float + */ + public function getPrice() + { + return $this->price; + } + + /** + * @return ?string + */ + public function getPriceText() + { + return $this->priceText; + } + + /** + * @return ?CustomField[] + */ + public function getCustomFields() + { + return $this->customFields; + } + + /** + * @return ?float + */ + public function getListPrice() + { + return $this->listPrice; + } + + /** + * @return ?int + */ + public function getInventoryLevel() + { + return $this->inventoryLevel; + } + + /** + * @return ?Ai + */ + public function getAi() + { + return $this->ai; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/Hit/Stats.php b/src/Result/Graphql/Search/SearchResult/Products/Hit/Stats.php new file mode 100644 index 00000000..6eb5747f --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/Hit/Stats.php @@ -0,0 +1,302 @@ +price = GraphQLUtils::getProperty($data, 'price'); + $this->listPrice = GraphQLUtils::getProperty($data, 'listPrice'); + $this->discount = GraphQLUtils::getProperty($data, 'discount'); + $this->ratingValue = GraphQLUtils::getProperty($data, 'ratingValue'); + $this->reviewCount = GraphQLUtils::getProperty($data, 'reviewCount'); + $this->margin = GraphQLUtils::getProperty($data, 'margin'); + $this->marginPercentage = GraphQLUtils::getProperty($data, 'marginPercentage'); + $this->inventoryLevel = GraphQLUtils::getProperty($data, 'inventoryLevel'); + $this->age = GraphQLUtils::getProperty($data, 'age'); + $this->published = GraphQLUtils::getProperty($data, 'published'); + $this->impressions = GraphQLUtils::getProperty($data, 'impressions'); + $this->views = GraphQLUtils::getProperty($data, 'views'); + $this->clicks = GraphQLUtils::getProperty($data, 'clicks'); + $this->buys = GraphQLUtils::getProperty($data, 'buys'); + $this->orders = GraphQLUtils::getProperty($data, 'orders'); + $this->conversion = GraphQLUtils::getProperty($data, 'conversion'); + $this->cartRatio = GraphQLUtils::getProperty($data, 'cartRatio'); + $this->revenue = GraphQLUtils::getProperty($data, 'revenue'); + $this->revenuePerImpression = GraphQLUtils::getProperty($data, 'revenuePerImpression'); + $this->revenuePerView = GraphQLUtils::getProperty($data, 'revenuePerView'); + $this->profitPerImpression = GraphQLUtils::getProperty($data, 'profitPerImpression'); + $this->profitPerView = GraphQLUtils::getProperty($data, 'profitPerView'); + $this->inventoryTurnover = GraphQLUtils::getProperty($data, 'inventoryTurnover'); + $this->availabilityRatio = GraphQLUtils::getProperty($data, 'availabilityRatio'); + } + + /** + * @return ?float + */ + public function getPrice() + { + return $this->price; + } + + /** + * @return ?float + */ + public function getListPrice() + { + return $this->listPrice; + } + + /** + * @return ?float + */ + public function getDiscount() + { + return $this->discount; + } + + /** + * @return ?float + */ + public function getRatingValue() + { + return $this->ratingValue; + } + + /** + * @return ?float + */ + public function getReviewCount() + { + return $this->reviewCount; + } + + /** + * @return ?float + */ + public function getMargin() + { + return $this->margin; + } + + /** + * @return ?float + */ + public function getMarginPercentage() + { + return $this->marginPercentage; + } + + /** + * @return ?float + */ + public function getInventoryLevel() + { + return $this->inventoryLevel; + } + + /** + * @return ?float + */ + public function getAge() + { + return $this->age; + } + + /** + * @return ?float + */ + public function getPublished() + { + return $this->published; + } + + /** + * @return ?float + */ + public function getImpressions() + { + return $this->impressions; + } + + /** + * @return ?float + */ + public function getViews() + { + return $this->views; + } + + /** + * @return ?float + */ + public function getClicks() + { + return $this->clicks; + } + + /** + * @return ?float + */ + public function getBuys() + { + return $this->buys; + } + + /** + * @return ?float + */ + public function getOrders() + { + return $this->orders; + } + + /** + * @return ?float + */ + public function getConversion() + { + return $this->conversion; + } + + /** + * @return ?float + */ + public function getCartRatio() + { + return $this->cartRatio; + } + + /** + * @return ?float + */ + public function getRevenue() + { + return $this->revenue; + } + + /** + * @return ?float + */ + public function getRevenuePerImpression() + { + return $this->revenuePerImpression; + } + + /** + * @return ?float + */ + public function getRevenuePerView() + { + return $this->revenuePerView; + } + + /** + * @return ?float + */ + public function getProfitPerImpression() + { + return $this->profitPerImpression; + } + + /** + * @return ?float + */ + public function getProfitPerView() + { + return $this->profitPerView; + } + + /** + * @return ?float + */ + public function getInventoryTurnover() + { + return $this->inventoryTurnover; + } + + /** + * @return ?float + */ + public function getAvailabilityRatio() + { + return $this->availabilityRatio; + } + +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/Hit/Variation.php b/src/Result/Graphql/Search/SearchResult/Products/Hit/Variation.php new file mode 100644 index 00000000..1a366960 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/Hit/Variation.php @@ -0,0 +1,38 @@ +key = GraphQLUtils::getProperty($data, 'key'); + $this->value = GraphQLUtils::getClassProperty($data, 'value', VariationValue::class); + } + + /** + * @return ?string + */ + public function getKey() + { + return $this->key; + } + + /** + * @return ?VariationValue + */ + public function getValue() + { + return $this->value; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/Hit/Variation/VariationValue.php b/src/Result/Graphql/Search/SearchResult/Products/Hit/Variation/VariationValue.php new file mode 100644 index 00000000..9618829d --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/Hit/Variation/VariationValue.php @@ -0,0 +1,61 @@ +availability = GraphQLUtils::getProperty($data, 'availability'); + $this->price = GraphQLUtils::getProperty($data, 'price'); + $this->listPrice = GraphQLUtils::getProperty($data, 'listPrice'); + $this->priceCurrencyCode = GraphQLUtils::getProperty($data, 'priceCurrencyCode'); + } + + /** + * @return ?string + */ + public function getAvailability() + { + return $this->availability; + } + + /** + * @return ?float + */ + public function getPrice() + { + return $this->price; + } + + /** + * @return ?float + */ + public function getListPrice() + { + return $this->listPrice; + } + + /** + * @return ?string + */ + public function getPriceCurrencyCode() + { + return $this->priceCurrencyCode; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/StatsFacet.php b/src/Result/Graphql/Search/SearchResult/Products/StatsFacet.php new file mode 100644 index 00000000..6a472ab9 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/StatsFacet.php @@ -0,0 +1,39 @@ +min = GraphQLUtils::getProperty($data, 'min'); + $this->max = GraphQLUtils::getProperty($data, 'max'); + } + + /** + * @return ?float + */ + public function getMin() + { + return $this->min; + } + + /** + * @return ?float + */ + public function getMax() + { + return $this->max; + } +} diff --git a/src/Result/Graphql/Search/SearchResult/Products/TermsFacet.php b/src/Result/Graphql/Search/SearchResult/Products/TermsFacet.php new file mode 100644 index 00000000..27081042 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResult/Products/TermsFacet.php @@ -0,0 +1,28 @@ +data = GraphQLUtils::getArrayProperty($data, 'data', TermsFacetValue::class); + } + + /** + * @return ?TermsFacetValue[] + */ + public function getData() + { + return $this->data; + } +} diff --git a/src/Result/Graphql/Search/SearchResultHandler.php b/src/Result/Graphql/Search/SearchResultHandler.php new file mode 100644 index 00000000..8c2a82c5 --- /dev/null +++ b/src/Result/Graphql/Search/SearchResultHandler.php @@ -0,0 +1,14 @@ +search); + } +} diff --git a/src/Util/GraphQLUtils.php b/src/Util/GraphQLUtils.php new file mode 100644 index 00000000..23975960 --- /dev/null +++ b/src/Util/GraphQLUtils.php @@ -0,0 +1,34 @@ +$propertyName) + ? $data->$propertyName + : $default; + } + + public static function getClassProperty(stdClass $data, $propertyName, $className, $default = null) + { + return property_exists($data, $propertyName) && !is_null($data->$propertyName) + ? new $className($data->$propertyName) + : $default; + } + + public static function getArrayProperty(stdClass $data, $propertyName, $className, $default = null) + { + return property_exists($data, $propertyName) && !is_null($data->$propertyName) + ? array_map( + function ($value) use ($className) { + return new $className($value); + }, + $data->$propertyName + ) + : $default; + } +} diff --git a/tests/unit/Result/SearchTest.php b/tests/unit/Result/SearchTest.php new file mode 100644 index 00000000..41b70fd5 --- /dev/null +++ b/tests/unit/Result/SearchTest.php @@ -0,0 +1,104 @@ + + * @copyright 2023 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Test\Unit\Result; + +use Codeception\TestCase\Test; +use Exception; +use Nosto\Request\Http\HttpRequest; +use Nosto\Request\Http\HttpResponse; +use Nosto\Result\Graphql\Category\CategoryUpdateResultHandler; +use Codeception\Specify; +use Nosto\Result\Graphql\Search\SearchResult; +use Nosto\Result\Graphql\Search\SearchResultHandler; + +class SearchTest extends Test +{ + use Specify; + + public function testSuccessfulSearch() + { + $id = uniqid(); + $responseBody = sprintf('{ + "data": { + "search": { + "products": { + "hits": [ + { + "productId": "%s" + } + ] + } + } + } + }', $id); + + $response = new HttpResponse(['HTTP/1.1 200 OK'], $responseBody); + + $request = new HttpRequest(); + $request->setResultHandler(new SearchResultHandler()); + $result = $request->getResultHandler()->parse($response); + + $this->assertInstanceOf(SearchResult::class, $result); + $this->assertEquals($id, $result->getProducts()->getHits()[0]->getProductId()); + } + + public function testResultWithErrors() + { + $message = "Test error"; + $expectedMessage = "Test error | "; + $resultBody = sprintf('{ + "errors": [ + { + "message": "%s", + "path": null, + "extensions": null, + "errorType": "ValidationError", + "locations": null + } + ] + }', $message); + $response = new HttpResponse(['HTTP/1.1 200 OK'], $resultBody); + $request = new HttpRequest(); + $request->setResultHandler(new SearchResultHandler()); + + $this->expectException('Nosto\NostoException'); + $this->expectExceptionMessage($expectedMessage); + + $request->getResultHandler()->parse($response); + } +} diff --git a/tests/unit/Util/GraphQLUtilsTest.php b/tests/unit/Util/GraphQLUtilsTest.php new file mode 100644 index 00000000..1faac815 --- /dev/null +++ b/tests/unit/Util/GraphQLUtilsTest.php @@ -0,0 +1,111 @@ + 'someRandomKey', true], + ['string value' => 'someRandomKey', 'testValue'], + ['int value' => 'someRandomKey', 187], + ['float value' => 'someRandomKey', 1.87], + ['array value' => 'someRandomKey', ['testValue']], + ['null' => 'someRandomKey', null], + ['empty value' => 'someRandomKey', ''], + ['non existent key' => 'sabdsajkdas', null], + ]; + } + + /** + * @dataProvider propertyValueProvider + */ + public function testNormalPropertyIsReturned($key, $expectedValue) { + $data = new stdClass(); + $data->someRandomKey = $expectedValue; + + $this->assertEquals($expectedValue, GraphQLUtils::getProperty($data, $key)); + } + + public function testDefaultValueIsReturnedForProperties() { + $data = new stdClass(); + $expectedValue = 'theDefault'; + $data->someRandomKey = null; + + $this->assertEquals($expectedValue, GraphQLUtils::getProperty($data, 'someRandomKey', $expectedValue)); + $this->assertEquals($expectedValue, GraphQLUtils::getProperty($data, 'anotherRandomKey', $expectedValue)); + } + + public function testClassPropertyIsReturned() { + $data = new stdClass(); + $expectedValue1 = 'test message'; + $expectedValue2 = ''; + $data->someRandomKey = $expectedValue1; + $data->anotherRandomKey = $expectedValue2; + + $exception = GraphQLUtils::getClassProperty($data, 'someRandomKey', Exception::class); + $this->assertInstanceOf(Exception::class, $exception); + $this->assertEquals($expectedValue1, $exception->getMessage()); + + $exception = GraphQLUtils::getClassProperty($data, 'anotherRandomKey', Exception::class); + $this->assertInstanceOf(Exception::class, $exception); + $this->assertEquals($expectedValue2, $exception->getMessage()); + } + + public function testDefaultValueIsReturnedForClassProperties() { + $data = new stdClass(); + $expectedValue = new Exception('test message'); + $data->someRandomKey = null; + + $this->assertEquals( + $expectedValue, + GraphQLUtils::getClassProperty($data, 'someRandomKey', Exception::class, $expectedValue) + ); + $this->assertEquals( + $expectedValue, + GraphQLUtils::getClassProperty($data, 'anotherRandomKey', Exception::class, $expectedValue) + ); + } + + public function testArrayPropertyIsReturned() { + $data = new stdClass(); + $data->someRandomKey = ['message1', 'message2', 'message3']; + $data->anotherRandomKey = []; + + $exceptions = GraphQLUtils::getArrayProperty($data, 'someRandomKey', Exception::class); + $this->assertCount(3, $exceptions); + + foreach ($exceptions as $key => $exception) { + $expectedValue = 'message' . ($key + 1); + + $this->assertInstanceOf(Exception::class, $exception); + $this->assertEquals($expectedValue, $exception->getMessage()); + } + + $this->assertEquals([], GraphQLUtils::getArrayProperty($data, 'anotherRandomKey', Exception::class)); + } + + public function testDefaultValueIsReturnedForArrayProperties() { + $data = new stdClass(); + $expectedValue = [ + new Exception('message1'), + new Exception('message2'), + ]; + $data->someRandomKey = null; + + $this->assertEquals( + $expectedValue, + GraphQLUtils::getArrayProperty($data, 'someRandomKey', Exception::class, $expectedValue) + ); + $this->assertEquals( + $expectedValue, + GraphQLUtils::getArrayProperty($data, 'anotherRandomKey', Exception::class, $expectedValue) + ); + } +}