From e1601739fa2feca1ba05893a6fe5cf941c2d7784 Mon Sep 17 00:00:00 2001 From: Thai Phan Date: Tue, 8 Mar 2016 10:32:25 +1100 Subject: [PATCH 01/30] Excluded attributes array now separate function This change allows us to better customise which attributes we want to exclude. --- code/Helper/Entity/Producthelper.php | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/code/Helper/Entity/Producthelper.php b/code/Helper/Entity/Producthelper.php index fc0ec213..23599241 100644 --- a/code/Helper/Entity/Producthelper.php +++ b/code/Helper/Entity/Producthelper.php @@ -33,13 +33,7 @@ public function getAllAttributes($add_empty_row = false) $productAttributes = array_merge(array('name', 'path', 'categories', 'categories_without_path', 'description', 'ordered_qty', 'total_ordered', 'stock_qty', 'rating_summary', 'media_gallery'), $allAttributes); - $excludedAttributes = array( - 'all_children', 'available_sort_by', 'children', 'children_count', 'custom_apply_to_products', - 'custom_design', 'custom_design_from', 'custom_design_to', 'custom_layout_update', 'custom_use_parent_settings', - 'default_sort_by', 'display_mode', 'filter_price_range', 'global_position', 'image', 'include_in_menu', 'is_active', - 'is_always_include_in_menu', 'is_anchor', 'landing_page', 'level', 'lower_cms_block', - 'page_layout', 'path_in_store', 'position', 'small_image', 'thumbnail', 'url_key', 'url_path', - 'visible_in_menu'); + $excludedAttributes = $this->getExcludedAttributes(); $productAttributes = array_diff($productAttributes, $excludedAttributes); @@ -59,6 +53,18 @@ public function getAllAttributes($add_empty_row = false) return $attributes; } + protected function getExcludedAttributes() + { + return array( + 'all_children', 'available_sort_by', 'children', 'children_count', 'custom_apply_to_products', + 'custom_design', 'custom_design_from', 'custom_design_to', 'custom_layout_update', 'custom_use_parent_settings', + 'default_sort_by', 'display_mode', 'filter_price_range', 'global_position', 'image', 'include_in_menu', 'is_active', + 'is_always_include_in_menu', 'is_anchor', 'landing_page', 'level', 'lower_cms_block', + 'page_layout', 'path_in_store', 'position', 'small_image', 'thumbnail', 'url_key', 'url_path', + 'visible_in_menu' + ); + } + public function isAttributeEnabled($additionalAttributes, $attr_name) { foreach ($additionalAttributes as $attr) From 10fbdc5b59b6423a0fddd569fe5fec7efe22bcfd Mon Sep 17 00:00:00 2001 From: Richard Brown Date: Thu, 17 Mar 2016 14:17:30 +0000 Subject: [PATCH 02/30] TYPO: ressource->resource --- CHANGELOG.md | 2 +- code/Helper/Entity/Categoryhelper.php | 6 +++--- code/Helper/Entity/Producthelper.php | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a80c5ab4..2f37fe17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +58,7 @@ ### 1.4.8 - NEW: allow to have custom product types -- NEW: make image generation size parameter customizable to be able to save ressources when already in cache +- NEW: make image generation size parameter customizable to be able to save resources when already in cache - UPDATED: remove root category when fetching product categories - UPDATED: rewrite image class to be able to log the error when not being able to generate it - UPDATED: Handle display price with AND without tax diff --git a/code/Helper/Entity/Categoryhelper.php b/code/Helper/Entity/Categoryhelper.php index 41b2ab2c..3a18609c 100644 --- a/code/Helper/Entity/Categoryhelper.php +++ b/code/Helper/Entity/Categoryhelper.php @@ -158,11 +158,11 @@ public function getObject(Mage_Catalog_Model_Category $category) { $value = $category->getData($attribute['attribute']); - $attribute_ressource = $category->getResource()->getAttribute($attribute['attribute']); + $attribute_resource = $category->getResource()->getAttribute($attribute['attribute']); - if ($attribute_ressource) + if ($attribute_resource) { - $value = $attribute_ressource->getFrontend()->getValue($category); + $value = $attribute_resource->getFrontend()->getValue($category); } if (isset($data[$attribute['attribute']])) diff --git a/code/Helper/Entity/Producthelper.php b/code/Helper/Entity/Producthelper.php index 686c6cf8..52360ba1 100644 --- a/code/Helper/Entity/Producthelper.php +++ b/code/Helper/Entity/Producthelper.php @@ -695,11 +695,11 @@ public function getObject(Mage_Catalog_Model_Product $product) $value = $product->getData($attribute['attribute']); - $attribute_ressource = $product->getResource()->getAttribute($attribute['attribute']); + $attribute_resource = $product->getResource()->getAttribute($attribute['attribute']); - if ($attribute_ressource) + if ($attribute_resource) { - $attribute_ressource = $attribute_ressource->setStoreId($product->getStoreId()); + $attribute_resource = $attribute_resource->setStoreId($product->getStoreId()); if ($value === null) { @@ -728,7 +728,7 @@ public function getObject(Mage_Catalog_Model_Product $product) if ($value_text) $values[] = $value_text; else - $values[] = $attribute_ressource->getFrontend()->getValue($sub_product); + $values[] = $attribute_resource->getFrontend()->getValue($sub_product); } } @@ -752,8 +752,8 @@ public function getObject(Mage_Catalog_Model_Product $product) $value = $value_text; else { - $attribute_ressource = $attribute_ressource->setStoreId($product->getStoreId()); - $value = $attribute_ressource->getFrontend()->getValue($product); + $attribute_resource = $attribute_resource->setStoreId($product->getStoreId()); + $value = $attribute_resource->getFrontend()->getValue($product); } if ($value) From c0ef911d572c647e8db6184cbd9a0f131d1658f0 Mon Sep 17 00:00:00 2001 From: Richard Brown Date: Thu, 17 Mar 2016 14:27:17 +0000 Subject: [PATCH 03/30] ProductHelper->getObject: use var for attribute name --- code/Helper/Entity/Producthelper.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/code/Helper/Entity/Producthelper.php b/code/Helper/Entity/Producthelper.php index 52360ba1..28adfe71 100644 --- a/code/Helper/Entity/Producthelper.php +++ b/code/Helper/Entity/Producthelper.php @@ -690,12 +690,13 @@ public function getObject(Mage_Catalog_Model_Product $product) foreach ($additionalAttributes as $attribute) { - if (isset($customData[$attribute['attribute']])) + $attribute_name = $attribute['attribute']; + if (isset($customData[$attribute_name])) continue; - $value = $product->getData($attribute['attribute']); + $value = $product->getData($attribute_name); - $attribute_resource = $product->getResource()->getAttribute($attribute['attribute']); + $attribute_resource = $product->getResource()->getAttribute($attribute_name); if ($attribute_resource) { @@ -719,11 +720,11 @@ public function getObject(Mage_Catalog_Model_Product $product) $all_sub_products_out_of_stock = false; - $value = $sub_product->getData($attribute['attribute']); + $value = $sub_product->getData($attribute_name); if ($value) { - $value_text = $sub_product->getAttributeText($attribute['attribute']); + $value_text = $sub_product->getAttributeText($attribute_name); if ($value_text) $values[] = $value_text; @@ -734,7 +735,7 @@ public function getObject(Mage_Catalog_Model_Product $product) if (is_array($values) && count($values) > 0) { - $customData[$attribute['attribute']] = array_values(array_unique($values)); + $customData[$attribute_name] = array_values(array_unique($values)); } if ($customData['in_stock'] && $all_sub_products_out_of_stock) { @@ -746,7 +747,7 @@ public function getObject(Mage_Catalog_Model_Product $product) } else { - $value_text = $product->getAttributeText($attribute['attribute']); + $value_text = $product->getAttributeText($attribute_name); if ($value_text) $value = $value_text; @@ -758,7 +759,7 @@ public function getObject(Mage_Catalog_Model_Product $product) if ($value) { - $customData[$attribute['attribute']] = $value; + $customData[$attribute_name] = $value; } } } From b8b8389fae6aeba263276ed3a02219a442b7ec3d Mon Sep 17 00:00:00 2001 From: Richard Brown Date: Thu, 17 Mar 2016 15:02:12 +0000 Subject: [PATCH 04/30] Refactor part of ProductHelper->getObject This part of the function is duplicated, and I want to use it again in my next patch --- code/Helper/Entity/Producthelper.php | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/code/Helper/Entity/Producthelper.php b/code/Helper/Entity/Producthelper.php index 28adfe71..decf5f7a 100644 --- a/code/Helper/Entity/Producthelper.php +++ b/code/Helper/Entity/Producthelper.php @@ -474,6 +474,15 @@ protected function handlePrice(&$product, $sub_products, &$customData) } } + protected function getValueOrValueText(Mage_Catalog_Model_Product $product, $name, $resource) + { + $value_text = $product->getAttributeText($name); + if (!$value_text) { + $value_text = $resource->getFrontend()->getValue($product); + } + return $value_text; + } + public function getObject(Mage_Catalog_Model_Product $product) { $type = $this->config->getMappedProductType($product->getTypeId()); @@ -700,7 +709,7 @@ public function getObject(Mage_Catalog_Model_Product $product) if ($attribute_resource) { - $attribute_resource = $attribute_resource->setStoreId($product->getStoreId()); + $attribute_resource->setStoreId($product->getStoreId()); if ($value === null) { @@ -724,12 +733,7 @@ public function getObject(Mage_Catalog_Model_Product $product) if ($value) { - $value_text = $sub_product->getAttributeText($attribute_name); - - if ($value_text) - $values[] = $value_text; - else - $values[] = $attribute_resource->getFrontend()->getValue($sub_product); + $values[] = $this->getValueOrValueText($sub_product, $attribute_name, $attribute_resource); } } @@ -747,15 +751,7 @@ public function getObject(Mage_Catalog_Model_Product $product) } else { - $value_text = $product->getAttributeText($attribute_name); - - if ($value_text) - $value = $value_text; - else - { - $attribute_resource = $attribute_resource->setStoreId($product->getStoreId()); - $value = $attribute_resource->getFrontend()->getValue($product); - } + $value = $this->getValueOrValueText($product, $attribute_name, $attribute_resource); if ($value) { From 6bc70857488e78305541d28e22d71136ba9f0cf5 Mon Sep 17 00:00:00 2001 From: Richard Brown Date: Thu, 17 Mar 2016 15:11:37 +0000 Subject: [PATCH 05/30] Make sub_product skus searchable Currently Producthelper->getObject only returns attribute values from child products if the parent product attribute value is null. We would like to be able to search for the sku of both parent and child products. This patch adds an exception to getObject to include all the skus in the returned object. --- code/Helper/Entity/Producthelper.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/code/Helper/Entity/Producthelper.php b/code/Helper/Entity/Producthelper.php index decf5f7a..145fbe84 100644 --- a/code/Helper/Entity/Producthelper.php +++ b/code/Helper/Entity/Producthelper.php @@ -711,12 +711,16 @@ public function getObject(Mage_Catalog_Model_Product $product) { $attribute_resource->setStoreId($product->getStoreId()); - if ($value === null) + if ($value === null || 'sku' == $attribute_name) { /** Get values as array in children */ if ($type == 'configurable' || $type == 'grouped' || $type == 'bundle') { - $values = array(); + if ($value === null) { + $values = array(); + } else { + $values = array($this->getValueOrValueText($product, $attribute_name, $attribute_resource)); + } $all_sub_products_out_of_stock = true; From d69425a28c7571950f8798ebf88559248cddffc5 Mon Sep 17 00:00:00 2001 From: Maxime Locqueville Date: Wed, 23 Mar 2016 10:55:05 +0100 Subject: [PATCH 06/30] Fallback to default search in case there is a error in algolia backend search. Fixes #341 --- code/Helper/Logger.php | 4 ++-- code/Model/Resource/Fulltext/Collection.php | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/code/Helper/Logger.php b/code/Helper/Logger.php index 510d8fd4..2e6160fb 100644 --- a/code/Helper/Logger.php +++ b/code/Helper/Logger.php @@ -51,9 +51,9 @@ public function stop($action) $this->log('<<<<< END ' .$action. ' (' . $this->formatTime($this->timers[$action], microtime(true)) . ')'); } - public function log($message) + public function log($message, $forceLog = false) { - if ($this->config->isLoggingEnabled()) { + if ($this->config->isLoggingEnabled() || $forceLog) { Mage::log($message, null, 'algolia.log'); } } diff --git a/code/Model/Resource/Fulltext/Collection.php b/code/Model/Resource/Fulltext/Collection.php index d07ba573..f96ab1da 100644 --- a/code/Model/Resource/Fulltext/Collection.php +++ b/code/Model/Resource/Fulltext/Collection.php @@ -20,7 +20,19 @@ public function addSearchFilter($query) if ($config->isInstantEnabled($storeId) === false || $config->makeSeoRequest($storeId)) { $algolia_query = $query !== '__empty__' ? $query : ''; - $data = Mage::helper('algoliasearch')->getSearchResult($algolia_query, $storeId); + + try + { + $data = Mage::helper('algoliasearch')->getSearchResult($algolia_query, $storeId); + } + catch (\Exception $e) + { + $logger = Mage::helper('algoliasearch/logger'); + $logger->log($e->getMessage(), true); + $logger->log($e->getTraceAsString(), true); + + return parent::addSearchFilter($query); + } } From 7a85c4a6e4239f37d174ef30af526c4c71612f72 Mon Sep 17 00:00:00 2001 From: Maxime Locqueville Date: Wed, 30 Mar 2016 14:22:08 +0200 Subject: [PATCH 07/30] Fix bug with last additional attribute. Fixes #345 --- code/Helper/Entity/Producthelper.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/Helper/Entity/Producthelper.php b/code/Helper/Entity/Producthelper.php index 145fbe84..3afa2a48 100644 --- a/code/Helper/Entity/Producthelper.php +++ b/code/Helper/Entity/Producthelper.php @@ -95,8 +95,10 @@ public function getProductCollectionQuery($storeId, $productIds = null, $only_vi $additionalAttr = $this->config->getProductAdditionalAttributes($storeId); - foreach ($additionalAttr as &$attr) - $attr = $attr['attribute']; + /** Map instead of foreach because otherwise it adds quotes to the last attribute **/ + $additionalAttr = array_map(function($attr) { + return $attr['attribute']; + }, $additionalAttr); $products = $products->addAttributeToSelect(array_values(array_merge(static::$_predefinedProductAttributes, $additionalAttr))); From eec9279eb821865efaec7c6a4bd7cb8c62caf0d0 Mon Sep 17 00:00:00 2001 From: Maxime Locqueville Date: Thu, 31 Mar 2016 11:51:50 +0200 Subject: [PATCH 08/30] replace isPriceAttribute code from strpos to equality --- code/Helper/Entity/Producthelper.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/Helper/Entity/Producthelper.php b/code/Helper/Entity/Producthelper.php index 3afa2a48..8c84baff 100644 --- a/code/Helper/Entity/Producthelper.php +++ b/code/Helper/Entity/Producthelper.php @@ -232,7 +232,7 @@ public function setSettings($storeId) foreach ($sorting_indices as $values) { - if ($this->config->isCustomerGroupsEnabled($storeId) && strpos($values['attribute'], 'price') !== false) + if ($this->config->isCustomerGroupsEnabled($storeId) && $values['attribute'] === 'price') { foreach ($groups = Mage::getModel('customer/group')->getCollection() as $group) { @@ -240,7 +240,7 @@ public function setSettings($storeId) $suffix_index_name = 'group_' . $group_id; - $sort_attribute = strpos($values['attribute'], 'price') !== false ? $values['attribute'] . '.' . $currencies[0] . '.' . $suffix_index_name : $values['attribute']; + $sort_attribute = $values['attribute'] === 'price' ? $values['attribute'] . '.' . $currencies[0] . '.' . $suffix_index_name : $values['attribute']; $mergeSettings['ranking'] = array($values['sort'] . '(' . $sort_attribute . ')', 'typo', 'geo', 'words', 'proximity', 'attribute', 'exact', 'custom'); @@ -249,7 +249,7 @@ public function setSettings($storeId) } else { - $sort_attribute = strpos($values['attribute'], 'price') !== false ? $values['attribute'] . '.' . $currencies[0] . '.' . 'default' : $values['attribute']; + $sort_attribute = $values['attribute'] === 'price' ? $values['attribute'] . '.' . $currencies[0] . '.' . 'default' : $values['attribute']; $mergeSettings['ranking'] = array($values['sort'] . '(' . $sort_attribute . ')', 'typo', 'geo', 'words', 'proximity', 'attribute', 'exact', 'custom'); From 3d32f0b872496a7fd9aa79df2adb844357724a16 Mon Sep 17 00:00:00 2001 From: dannydnz Date: Mon, 4 Apr 2016 19:42:16 +1200 Subject: [PATCH 09/30] Added cache for popular queries suggestions. --- code/Helper/Entity/Suggestionhelper.php | 58 ++++++++++++++++++------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/code/Helper/Entity/Suggestionhelper.php b/code/Helper/Entity/Suggestionhelper.php index 7dfc33b2..1d16383f 100644 --- a/code/Helper/Entity/Suggestionhelper.php +++ b/code/Helper/Entity/Suggestionhelper.php @@ -2,6 +2,9 @@ class Algolia_Algoliasearch_Helper_Entity_Suggestionhelper extends Algolia_Algoliasearch_Helper_Entity_Helper { + protected $_popularQueries = null; + protected $_popularQueriesCacheId = "algoliasearch_popular_queries_cache_tag"; + protected function getIndexNameSuffix() { return '_suggestions'; @@ -32,27 +35,50 @@ public function getObject(Mage_CatalogSearch_Model_Query $suggestion) public function getPopularQueries($storeId) { - $collection = Mage::getResourceModel('catalogsearch/query_collection'); - $collection->getSelect()->where('num_results >= '.$this->config->getMinNumberOfResults().' AND popularity >= ' . $this->config->getMinPopularity() .' AND query_text != "__empty__"'); - $collection->getSelect()->limit(12); - $collection->setOrder('popularity', 'DESC'); - $collection->setOrder('num_results', 'DESC'); - $collection->setOrder('updated_at', 'ASC'); + if ($this->_popularQueries == null) { - if ($storeId) { - $collection->getSelect()->where('store_id = ?', (int) $storeId); - } + // load from cache if we can + $cachedPopularQueries = Mage::app()->loadCache($this->_popularQueriesCacheId); + if ($cachedPopularQueries) { + $this->_popularQueries = unserialize($cachedPopularQueries); + } else { + $collection = Mage::getResourceModel('catalogsearch/query_collection'); + $collection->getSelect()->where('num_results >= ' . $this->config->getMinNumberOfResults() . ' AND popularity >= ' . $this->config->getMinPopularity() . ' AND query_text != "__empty__"'); + $collection->getSelect()->limit(12); + $collection->setOrder('popularity', 'DESC'); + $collection->setOrder('num_results', 'DESC'); + $collection->setOrder('updated_at', 'ASC'); + + if ($storeId) { + $collection->getSelect()->where('store_id = ?', (int)$storeId); + } - $collection->load(); + $collection->load(); - $suggestions = array(); + $suggestions = array(); - /** @var $suggestion Mage_Catalog_Model_Category */ - foreach ($collection as $suggestion) - if (strlen($suggestion['query_text']) >= 3) - $suggestions[] = $suggestion['query_text']; + /** @var $suggestion Mage_Catalog_Model_Category */ + foreach ($collection as $suggestion) + if (strlen($suggestion['query_text']) >= 3) + $suggestions[] = $suggestion['query_text']; + + $this->_popularQueries = array_slice($suggestions, 0, 9); + try { //save to cache + $cacheContent = serialize($this->_popularQueries); + $tags = array( + Mage_CatalogSearch_Model_Query::CACHE_TAG + ); + $lifetime = Mage::getStoreConfig('core/cache/lifetime'); + Mage::app()->saveCache($cacheContent, $this->_popularQueriesCacheId, $tags, $lifetime); + + } catch (Exception $e) { + // Exception = no caching + Mage::logException($e); + } + } + } - return array_slice($suggestions, 0, 9); + return $this->_popularQueries; } public function getSuggestionCollectionQuery($storeId) From 4a2bd652e8641172c4bbfcbc4ef3193d758aa9f6 Mon Sep 17 00:00:00 2001 From: Maxime Locqueville Date: Wed, 6 Apr 2016 20:42:15 +0200 Subject: [PATCH 10/30] take into account maxValuesPerFacet option --- design/frontend/template/beforetopsearch.phtml | 3 +++ design/frontend/template/topsearch.phtml | 1 + 2 files changed, 4 insertions(+) diff --git a/design/frontend/template/beforetopsearch.phtml b/design/frontend/template/beforetopsearch.phtml index 2786a529..734d8bce 100644 --- a/design/frontend/template/beforetopsearch.phtml +++ b/design/frontend/template/beforetopsearch.phtml @@ -116,6 +116,7 @@ if ($config->isInstantEnabled() && $isSearchPage) { priceKey: '', currencySymbol: 'getLocale()->currency(Mage::app()->getStore()->getCurrentCurrencyCode())->getSymbol(); ?>', currency_code: '', + maxValuesPerFacet: getMaxValuesPerFacet(); ?>, autofocus: true, request: { query: html_entity_decode($query))); ?>.value, @@ -211,6 +212,7 @@ if ($config->isInstantEnabled() && $isSearchPage) { return algoliaBundle.instantsearch.widgets.refinementList({ container: facet.wrapper.appendChild(document.createElement('div')), attributeName: facet.attribute, + limit: algoliaConfig.maxValuesPerFacet, operator: 'and', templates: templates, cssClasses: { @@ -225,6 +227,7 @@ if ($config->isInstantEnabled() && $isSearchPage) { return algoliaBundle.instantsearch.widgets.refinementList({ container: facet.wrapper.appendChild(document.createElement('div')), attributeName: facet.attribute, + limit: algoliaConfig.maxValuesPerFacet, operator: 'or', templates: templates, cssClasses: { diff --git a/design/frontend/template/topsearch.phtml b/design/frontend/template/topsearch.phtml index 6d319da4..a019db4a 100644 --- a/design/frontend/template/topsearch.phtml +++ b/design/frontend/template/topsearch.phtml @@ -635,6 +635,7 @@ $placeholder = $this->__('Search for products, categories, ...'); attributes: hierarchical_levels, separator: ' /// ', alwaysGetRootLevel: true, + limit: algoliaConfig.maxValuesPerFacet, templates: templates, sortBy: ['name:asc'], cssClasses: { From 32d451a5ae7c882a73e862736cd70537f27e0539 Mon Sep 17 00:00:00 2001 From: dannydnz Date: Thu, 7 Apr 2016 08:44:12 +1200 Subject: [PATCH 11/30] Set cache lifetime to one week. --- code/Helper/Entity/Suggestionhelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/Helper/Entity/Suggestionhelper.php b/code/Helper/Entity/Suggestionhelper.php index 1d16383f..6b5a3c7e 100644 --- a/code/Helper/Entity/Suggestionhelper.php +++ b/code/Helper/Entity/Suggestionhelper.php @@ -68,8 +68,8 @@ public function getPopularQueries($storeId) $tags = array( Mage_CatalogSearch_Model_Query::CACHE_TAG ); - $lifetime = Mage::getStoreConfig('core/cache/lifetime'); - Mage::app()->saveCache($cacheContent, $this->_popularQueriesCacheId, $tags, $lifetime); + + Mage::app()->saveCache($cacheContent, $this->_popularQueriesCacheId, $tags, 604800); } catch (Exception $e) { // Exception = no caching From ab291cfe187b09458931cdb0fedf5fcb2df6f0c7 Mon Sep 17 00:00:00 2001 From: Pierre Noyaret Date: Thu, 7 Apr 2016 10:03:57 +0200 Subject: [PATCH 12/30] store_id value in algolia_additional_section_item_index_before event --- code/Helper/Entity/Additionalsectionshelper.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/code/Helper/Entity/Additionalsectionshelper.php b/code/Helper/Entity/Additionalsectionshelper.php index f26fd7b4..e8bfe9ea 100644 --- a/code/Helper/Entity/Additionalsectionshelper.php +++ b/code/Helper/Entity/Additionalsectionshelper.php @@ -45,7 +45,7 @@ public function getAttributeValues($storeId, $section) $values = array($values); } - $values = array_map(function ($value) use ($section) { + $values = array_map(function ($value) use ($section, $storeId) { $record = array( 'objectID' => $value, @@ -54,7 +54,8 @@ public function getAttributeValues($storeId, $section) $transport = new Varien_Object($record); - Mage::dispatchEvent('algolia_additional_section_item_index_before', array('section' => $section, 'record' => $transport)); + Mage::dispatchEvent('algolia_additional_section_item_index_before', + array('section' => $section, 'record' => $transport, 'store_id' => $storeId)); $record = $transport->getData(); From 61013ca9b05467b6f0777c320fefde84d020d2e1 Mon Sep 17 00:00:00 2001 From: Pierre Noyaret Date: Thu, 7 Apr 2016 10:24:13 +0200 Subject: [PATCH 13/30] multiselect array unique --- code/Helper/Entity/Additionalsectionshelper.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/Helper/Entity/Additionalsectionshelper.php b/code/Helper/Entity/Additionalsectionshelper.php index f26fd7b4..3d574dcf 100644 --- a/code/Helper/Entity/Additionalsectionshelper.php +++ b/code/Helper/Entity/Additionalsectionshelper.php @@ -25,7 +25,8 @@ public function getAttributeValues($storeId, $section) ->addAttributeToFilter($attributeCode, array('neq' => '')) ->addAttributeToSelect($attributeCode); - $usedAttributeValues = array_unique($products->getColumnValues($attributeCode)); + $usedAttributeValues = array_keys(array_flip( // array unique + explode(',', implode(',', $products->getColumnValues($attributeCode))))); $attributeModel = Mage::getSingleton('eav/config') ->getAttribute('catalog_product', $attributeCode) From b33dddc2f3698295f36d6300a322c1164c645f00 Mon Sep 17 00:00:00 2001 From: PhilipRowe Date: Thu, 21 Apr 2016 10:08:59 +0100 Subject: [PATCH 14/30] Set product slaves for all sorting indices when customer groups are enabled When customer groups are enabled slaves were only being set for price sorts not other sorts that were set such as price, rating etc. --- code/Helper/Entity/Producthelper.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/code/Helper/Entity/Producthelper.php b/code/Helper/Entity/Producthelper.php index 8c84baff..d885f780 100644 --- a/code/Helper/Entity/Producthelper.php +++ b/code/Helper/Entity/Producthelper.php @@ -205,18 +205,15 @@ public function setSettings($storeId) foreach ($sorting_indices as $values) { - if ($this->config->isCustomerGroupsEnabled($storeId)) + if ($this->config->isCustomerGroupsEnabled($storeId) && $values['attribute'] === 'price') { - if ($values['attribute'] === 'price') + foreach ($groups = Mage::getModel('customer/group')->getCollection() as $group) { - foreach ($groups = Mage::getModel('customer/group')->getCollection() as $group) - { - $group_id = (int)$group->getData('customer_group_id'); - - $suffix_index_name = 'group_' . $group_id; - - $slaves[] = $this->getIndexName($storeId) . '_' .$values['attribute'].'_' . $suffix_index_name . '_' . $values['sort']; - } + $group_id = (int) $group->getData('customer_group_id'); + + $suffix_index_name = 'group_'.$group_id; + + $slaves[] = $this->getIndexName($storeId).'_'.$values['attribute'].'_'.$suffix_index_name.'_'.$values['sort']; } } else From 648a4b8270ddc9aa5ded216e20b2841491b04438 Mon Sep 17 00:00:00 2001 From: PhilipRowe Date: Thu, 21 Apr 2016 16:14:12 +0100 Subject: [PATCH 15/30] Only get attributes for products that are enabled When adding additional sections like brands some attributes are shown that are only against products that are disabled so will never have any results in the search --- code/Helper/Entity/Additionalsectionshelper.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/Helper/Entity/Additionalsectionshelper.php b/code/Helper/Entity/Additionalsectionshelper.php index 6301da59..f3934364 100644 --- a/code/Helper/Entity/Additionalsectionshelper.php +++ b/code/Helper/Entity/Additionalsectionshelper.php @@ -21,6 +21,7 @@ public function getAttributeValues($storeId, $section) $products = Mage::getResourceModel('catalog/product_collection') ->addStoreFilter($storeId) ->addAttributeToFilter('visibility', array('in' => Mage::getSingleton('catalog/product_visibility')->getVisibleInSearchIds())) + ->addAttributeToFilter('status', array('eq' => Mage_Catalog_Model_Product_Status::STATUS_ENABLED)) ->addAttributeToFilter($attributeCode, array('notnull' => true)) ->addAttributeToFilter($attributeCode, array('neq' => '')) ->addAttributeToSelect($attributeCode); @@ -65,4 +66,4 @@ public function getAttributeValues($storeId, $section) return $values; } -} \ No newline at end of file +} From df9d221206982b994d926e413d1b8cb488479222 Mon Sep 17 00:00:00 2001 From: PhilipRowe Date: Thu, 21 Apr 2016 17:32:06 +0100 Subject: [PATCH 16/30] Strip magento block directives Remove magento specific syntax from indexed pages. When searching for "black..." in the autocomplete dropdown it was showing {{block type="core/template" template="page/html... below the page's name. --- code/Helper/Entity/Helper.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/Helper/Entity/Helper.php b/code/Helper/Entity/Helper.php index 2731b915..1832831c 100644 --- a/code/Helper/Entity/Helper.php +++ b/code/Helper/Entity/Helper.php @@ -70,6 +70,7 @@ protected function strip($s) $s = trim(preg_replace('/\s+/', ' ', $s)); $s = preg_replace('/ /', ' ', $s); $s = preg_replace('!\s+!', ' ', $s); + $s = preg_replace('/\{\{[^}]+\}\}/', ' ', $s); return trim(strip_tags($s)); } @@ -231,4 +232,4 @@ public static function getStores($store_id) return $store_ids; } -} \ No newline at end of file +} From a0b2d1d75ad53a68718bf84de920196f8894d98e Mon Sep 17 00:00:00 2001 From: Maxime Locqueville Date: Fri, 22 Apr 2016 16:57:14 +0200 Subject: [PATCH 17/30] Apply new magento2 update strategy. Fixes #337 --- code/Model/Indexer/Algolia.php | 171 +++++---------------------------- 1 file changed, 26 insertions(+), 145 deletions(-) diff --git a/code/Model/Indexer/Algolia.php b/code/Model/Indexer/Algolia.php index 8d25f35b..0a76d7aa 100644 --- a/code/Model/Indexer/Algolia.php +++ b/code/Model/Indexer/Algolia.php @@ -109,19 +109,16 @@ protected function _registerCatalogInventoryStockItemEvent(Mage_Index_Model_Even $product = Mage::getModel('catalog/product')->load($object->getProductId()); - if ($object->getData('is_in_stock') == false || (int) $product->getStockItem()->getQty() <= 0) + try // In case of wrong credentials or overquota or block account. To avoid checkout process to fail { - try // In case of wrong credentials or overquota or block account. To avoid checkout process to fail - { - $event->addNewData('catalogsearch_delete_product_id', $product->getId()); - $event->addNewData('catalogsearch_update_category_id', $product->getCategoryIds()); - } - catch(\Exception $e) - { - $this->logger->log('Error while trying to update stock'); - $this->logger->log($e->getMessage()); - $this->logger->log($e->getTraceAsString()); - } + $event->addNewData('catalogsearch_delete_product_id', $product->getId()); + $event->addNewData('catalogsearch_update_category_id', $product->getCategoryIds()); + } + catch(\Exception $e) + { + $this->logger->log('Error while trying to update stock'); + $this->logger->log($e->getMessage()); + $this->logger->log($e->getTraceAsString()); } } } @@ -132,84 +129,33 @@ protected function _registerCatalogProductEvent(Mage_Index_Model_Event $event) case Mage_Index_Model_Event::TYPE_SAVE: /** @var $product Mage_Catalog_Model_Product */ $product = $event->getDataObject(); - $delete = FALSE; - $visibleInSite = Mage::getSingleton('catalog/product_visibility')->getVisibleInSiteIds(); - - if ($product->getStatus() == Mage_Catalog_Model_Product_Status::STATUS_DISABLED) - { - $delete = TRUE; - } - elseif (! in_array($product->getData('visibility'), $visibleInSite)) - { - $delete = TRUE; - } + $event->addNewData('catalogsearch_update_product_id', $product->getId()); + $event->addNewData('catalogsearch_update_category_id', $product->getCategoryIds()); - if ($delete) - { - $event->addNewData('catalogsearch_delete_product_id', $product->getId()); - $event->addNewData('catalogsearch_update_category_id', $product->getCategoryIds()); - } - else + /* product_categories is filled in Observer::saveProduct */ + if (isset(static::$product_categories[$product->getId()])) { - $event->addNewData('catalogsearch_update_product_id', $product->getId()); + $oldCategories = static::$product_categories[$product->getId()]; + $newCategories = $product->getCategoryIds(); - /* product_categories is filled in Observer::saveProduct */ - if (isset(static::$product_categories[$product->getId()])) - { - $oldCategories = static::$product_categories[$product->getId()]; - $newCategories = $product->getCategoryIds(); + $diffCategories = array_merge(array_diff($oldCategories, $newCategories), array_diff($newCategories, $oldCategories)); - $diffCategories = array_merge(array_diff($oldCategories, $newCategories), array_diff($newCategories, $oldCategories)); - - $event->addNewData('catalogsearch_update_category_id', $diffCategories); - } + $event->addNewData('catalogsearch_update_category_id', $diffCategories); } - break; case Mage_Index_Model_Event::TYPE_DELETE: /** @var $product Mage_Catalog_Model_Product */ $product = $event->getDataObject(); - $event->addNewData('catalogsearch_delete_product_id', $product->getId()); + $event->addNewData('catalogsearch_update_product_id', $product->getId()); $event->addNewData('catalogsearch_update_category_id', $product->getCategoryIds()); - break; + case Mage_Index_Model_Event::TYPE_MASS_ACTION: /** @var $actionObject Varien_Object */ $actionObject = $event->getDataObject(); - $reindexData = array(); - - // Check if status changed - $attrData = $actionObject->getAttributesData(); - - if (isset($attrData['status'])) - { - $reindexData['catalogsearch_status'] = $attrData['status']; - } - - // Check changed websites - if ($actionObject->getWebsiteIds()) - { - $reindexData['catalogsearch_website_ids'] = $actionObject->getWebsiteIds(); - $reindexData['catalogsearch_action_type'] = $actionObject->getActionType(); - } - - $reindexData['catalogsearch_force_reindex'] = TRUE; - - if ($actionObject->getIsDeleted()) - { - $reindexData['catalogsearch_delete_product_id'] = $actionObject->getProductIds(); - } - else - { - $reindexData['catalogsearch_product_ids'] = $actionObject->getProductIds(); - } - - foreach ($reindexData as $k => $v) - { - $event->addNewData($k, $v); - } + $event->addNewData('catalogsearch_update_product_id', $actionObject->getProductIds()); break; } @@ -247,81 +193,18 @@ protected function _processEvent(Mage_Index_Model_Event $event) $process->changeStatus(Mage_Index_Model_Process::STATUS_REQUIRE_REINDEX); } - /* - * Clear index for the deleted product. - */ - else if ( ! empty($data['catalogsearch_delete_product_id'])) { - $productId = $data['catalogsearch_delete_product_id']; - - if ( ! $this->_isProductComposite($productId)) { - $parentIds = $this->_getResource()->getRelationsByChild($productId); - if ( ! empty($parentIds)) { - $this->engine - ->rebuildProductIndex(null, $parentIds); - } - } - - $this->engine - ->removeProducts(null, $productId); - } - - // Mass action - else if ( ! empty($data['catalogsearch_product_ids'])) { - $productIds = $data['catalogsearch_product_ids']; - - if (!empty($productIds)) - { - if ( ! empty($data['catalogsearch_website_ids'])) - { - $websiteIds = $data['catalogsearch_website_ids']; - $actionType = $data['catalogsearch_action_type']; - foreach ($websiteIds as $websiteId) - { - foreach (Mage::app()->getWebsite($websiteId)->getStoreIds() as $storeId) { - if ($actionType == 'remove') - { - $this->engine->removeProducts($storeId, $productIds); - } - else if ($actionType == 'add') - { - $this->engine->rebuildProductIndex($storeId, $productIds); - } - } - } - } - else if (isset($data['catalogsearch_status'])) - { - $status = $data['catalogsearch_status']; - if ($status == Mage_Catalog_Model_Product_Status::STATUS_ENABLED) { - $this->engine->rebuildProductIndex(null, $productIds); - } - else - { - $this->engine->removeProducts(null, $productIds); - } - } - else if (isset($data['catalogsearch_force_reindex'])) - { - $this->engine - ->rebuildProductIndex(null, $productIds); - } - } - } - if ( ! empty($data['catalogsearch_update_category_id'])) { $updateCategoryIds = $data['catalogsearch_update_category_id']; $updateCategoryIds = is_array($updateCategoryIds) ? $updateCategoryIds : array($updateCategoryIds); - foreach ($updateCategoryIds as $id) - { + foreach ($updateCategoryIds as $id) { $categories = Mage::getModel('catalog/category')->getCategories($id); foreach ($categories as $category) $updateCategoryIds[] = $category->getId(); } - $this->engine - ->rebuildCategoryIndex(null, $updateCategoryIds); + $this->engine->rebuildCategoryIndex(null, $updateCategoryIds); } /* @@ -333,20 +216,18 @@ protected function _processEvent(Mage_Index_Model_Event $event) $updateProductIds = is_array($updateProductIds) ? $updateProductIds : array($updateProductIds); $productIds = $updateProductIds; - foreach ($updateProductIds as $updateProductId) - { - if (! $this->_isProductComposite($updateProductId)) - { + foreach ($updateProductIds as $updateProductId) { + if (! $this->_isProductComposite($updateProductId)) { $parentIds = $this->_getResource()->getRelationsByChild($updateProductId); - if (! empty($parentIds)) - { + if (! empty($parentIds)) { $productIds = array_merge($productIds, $parentIds); } } } if (!empty($productIds)) { + $this->engine->removeProducts(null, $productIds); $this->engine->rebuildProductIndex(null, $productIds); } } From bbd6a73cc3feb00e3e770dbf7be59881777c1dd2 Mon Sep 17 00:00:00 2001 From: Maxime Locqueville Date: Fri, 22 Apr 2016 17:05:24 +0200 Subject: [PATCH 18/30] add trims on credentials. Fixes #288 --- code/Helper/Config.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/Helper/Config.php b/code/Helper/Config.php index 54e66546..9c086872 100644 --- a/code/Helper/Config.php +++ b/code/Helper/Config.php @@ -297,22 +297,22 @@ public function getSortingIndices($storeId = NULL) public function getApplicationID($storeId = NULL) { - return Mage::getStoreConfig(self::APPLICATION_ID, $storeId); + return trim(Mage::getStoreConfig(self::APPLICATION_ID, $storeId)); } public function getAPIKey($storeId = NULL) { - return Mage::getStoreConfig(self::API_KEY, $storeId); + return trim(Mage::getStoreConfig(self::API_KEY, $storeId)); } public function getSearchOnlyAPIKey($storeId = NULL) { - return Mage::getStoreConfig(self::SEARCH_ONLY_API_KEY, $storeId); + return trim(Mage::getStoreConfig(self::SEARCH_ONLY_API_KEY, $storeId)); } public function getIndexPrefix($storeId = NULL) { - return Mage::getStoreConfig(self::INDEX_PREFIX, $storeId); + return trim(Mage::getStoreConfig(self::INDEX_PREFIX, $storeId)); } public function getCategoryAdditionalAttributes($storeId = NULL) From 02e8a5c6af069a5d8599271be8d3ad2b5a638549 Mon Sep 17 00:00:00 2001 From: Maxime Locqueville Date: Fri, 22 Apr 2016 17:11:52 +0200 Subject: [PATCH 19/30] update php client --- lib/AlgoliaSearch/AlgoliaException.php | 4 +- lib/AlgoliaSearch/Client.php | 1014 +++++++++++++++----- lib/AlgoliaSearch/ClientContext.php | 187 +++- lib/AlgoliaSearch/Index.php | 1219 ++++++++++++++++-------- lib/AlgoliaSearch/IndexBrowser.php | 160 ++++ lib/AlgoliaSearch/PlacesIndex.php | 81 ++ lib/AlgoliaSearch/Version.php | 6 +- 7 files changed, 2019 insertions(+), 652 deletions(-) create mode 100644 lib/AlgoliaSearch/IndexBrowser.php create mode 100644 lib/AlgoliaSearch/PlacesIndex.php diff --git a/lib/AlgoliaSearch/AlgoliaException.php b/lib/AlgoliaSearch/AlgoliaException.php index 8a4c1ec1..2d2cb654 100644 --- a/lib/AlgoliaSearch/AlgoliaException.php +++ b/lib/AlgoliaSearch/AlgoliaException.php @@ -1,4 +1,5 @@ context = new ClientContext($applicationID, $apiKey, null); - } else { - $this->context = new ClientContext($applicationID, $apiKey, $hostsArray); - } - if(!function_exists('curl_init')){ + protected $caInfoPath; + + /** + * @var array + */ + protected $curlConstants; + + /** + * @var array + */ + protected $curlOptions = []; + + /** + * @var bool + */ + protected $placesEnabled = false; + + /** + * Algolia Search initialization. + * + * @param string $applicationID the application ID you have in your admin interface + * @param string $apiKey a valid API key for the service + * @param array|null $hostsArray the list of hosts that you have received for the service + * @param array $options + * + * @throws \Exception + */ + public function __construct($applicationID, $apiKey, $hostsArray = null, $options = []) + { + if (!function_exists('curl_init')) { throw new \Exception('AlgoliaSearch requires the CURL PHP extension.'); } - if(!function_exists('json_decode')){ + if (!function_exists('json_decode')) { throw new \Exception('AlgoliaSearch requires the JSON PHP extension.'); } - $this->cainfoPath = __DIR__ . '/resources/ca-bundle.crt'; + + $this->caInfoPath = __DIR__.'/../../resources/ca-bundle.crt'; foreach ($options as $option => $value) { - if ($option == "cainfo") { - $this->cainfoPath = $value; - } else { - throw new \Exception('Unknown option: ' . $option); + switch ($option) { + case self::CAINFO: + $this->caInfoPath = $value; + break; + case self::CURLOPT: + $this->curlOptions = $this->checkCurlOptions($value); + break; + case self::PLACES_ENABLED: + $this->placesEnabled = (bool) $value; + break; + default: + throw new \Exception('Unknown option: '.$option); } } + + $this->context = new ClientContext($applicationID, $apiKey, $hostsArray, $this->placesEnabled); } - /* - * Release curl handle + /** + * Release curl handle. */ - function __destruct() { + public function __destruct() + { } - /* - * Change the default connect timeout of 2s to a custom value (only useful if your server has a very slow connectivity to Algolia backend) - * @param connectTimeout the connection timeout - * @param timeout the read timeout for the query - * @param searchTimeout the read timeout used for search queries only + /** + * Change the default connect timeout of 2s to a custom value + * (only useful if your server has a very slow connectivity to Algolia backend). + * + * @param int $connectTimeout the connection timeout + * @param int $timeout the read timeout for the query + * @param int $searchTimeout the read timeout used for search queries only + * + * @throws AlgoliaException */ - public function setConnectTimeout($connectTimeout, $timeout = 30, $searchTimeout = 5) { + public function setConnectTimeout($connectTimeout, $timeout = 30, $searchTimeout = 5) + { $version = curl_version(); - if ((version_compare(phpversion(), '5.2.3', '<') || version_compare($version['version'], '7.16.2', '<')) && $this->context->connectTimeout < 1) { - throw new AlgoliaException("The timeout can't be a float with a PHP version less than 5.2.3 or a curl version less than 7.16.2"); + $isPhpOld = version_compare(phpversion(), '5.2.3', '<'); + $isCurlOld = version_compare($version['version'], '7.16.2', '<'); + + if (($isPhpOld || $isCurlOld) && $this->context->connectTimeout < 1) { + throw new AlgoliaException( + "The timeout can't be a float with a PHP version less than 5.2.3 or a curl version less than 7.16.2" + ); } $this->context->connectTimeout = $connectTimeout; $this->context->readTimeout = $timeout; $this->context->searchTimeout = $searchTimeout; } - /* + /** * Allow to use IP rate limit when you have a proxy between end-user and Algolia. - * This option will set the X-Forwarded-For HTTP header with the client IP and the X-Forwarded-API-Key with the API Key having rate limits. - * @param adminAPIKey the admin API Key you can find in your dashboard - * @param endUserIP the end user IP (you can use both IPV4 or IPV6 syntax) - * @param rateLimitAPIKey the API key on which you have a rate limit + * This option will set the X-Forwarded-For HTTP header with the client IP + * and the X-Forwarded-API-Key with the API Key having rate limits. + * + * @param string $adminAPIKey the admin API Key you can find in your dashboard + * @param string $endUserIP the end user IP (you can use both IPV4 or IPV6 syntax) + * @param string $rateLimitAPIKey the API key on which you have a rate limit */ - public function enableRateLimitForward($adminAPIKey, $endUserIP, $rateLimitAPIKey) { + public function enableRateLimitForward($adminAPIKey, $endUserIP, $rateLimitAPIKey) + { $this->context->setRateLimit($adminAPIKey, $endUserIP, $rateLimitAPIKey); } - /* - * Disable IP rate limit enabled with enableRateLimitForward() function + /** + * The aggregation of the queries to retrieve the latest query + * uses the IP or the user token to work efficiently. + * If the queries are made from your backend server, + * the IP will be the same for all of the queries. + * We're supporting the following HTTP header to forward the IP of your end-user + * to the engine, you just need to set it for each query. + * + * @see https://www.algolia.com/doc/faq/analytics/will-the-analytics-still-work-if-i-perform-the-search-through-my-backend + * + * @param string $ip */ - public function disableRateLimitForward() { + public function setForwardedFor($ip) + { + $this->context->setForwardedFor($ip); + } + + /** + * It's possible to use the following token to track users that have the same IP + * or to track users that use different devices. + * + * @see https://www.algolia.com/doc/faq/analytics/will-the-analytics-still-work-if-i-perform-the-search-through-my-backend + * + * @param string $token + */ + public function setAlgoliaUserToken($token) + { + $this->context->setAlgoliaUserToken($token); + } + + /** + * Disable IP rate limit enabled with enableRateLimitForward() function. + */ + public function disableRateLimitForward() + { $this->context->disableRateLimit(); } - /* - * Call isAlive + /** + * Call isAlive. */ - public function isAlive() { - $this->request($this->context, "GET", "/1/isalive", null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function isAlive() + { + $this->request( + $this->context, + 'GET', + '/1/isalive', + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Allow to set custom headers + /** + * Allow to set custom headers. + * + * @param string $key + * @param string $value */ - public function setExtraHeader($key, $value) { + public function setExtraHeader($key, $value) + { $this->context->setExtraHeader($key, $value); } - /* - * This method allows to query multiple indexes with one API call + /** + * This method allows to query multiple indexes with one API call. + * + * @param array $queries + * @param string $indexNameKey + * @param string $strategy * + * @return mixed + * + * @throws AlgoliaException + * @throws \Exception */ - public function multipleQueries($queries, $indexNameKey = "indexName", $strategy = "none") { + public function multipleQueries($queries, $indexNameKey = 'indexName', $strategy = 'none') + { if ($queries == null) { throw new \Exception('No query provided'); } - $requests = array(); + $requests = []; foreach ($queries as $query) { if (array_key_exists($indexNameKey, $query)) { $indexes = $query[$indexNameKey]; @@ -133,245 +240,493 @@ public function multipleQueries($queries, $indexNameKey = "indexName", $strategy } else { throw new \Exception('indexName is mandatory'); } - foreach ($query as $key => $value) { - if (gettype($value) == "array") { - $query[$key] = json_encode($value); - } - } - $req = array("indexName" => $indexes, "params" => http_build_query($query)); + $req = ['indexName' => $indexes, 'params' => $this->buildQuery($query)]; + array_push($requests, $req); } - return $this->request($this->context, "POST", "/1/indexes/*/queries?strategy=" . $strategy, array(), array("requests" => $requests), $this->context->readHostsArray, $this->context->connectTimeout, $this->context->searchTimeout); + + return $this->request( + $this->context, + 'POST', + '/1/indexes/*/queries?strategy='.$strategy, + [], + ['requests' => $requests], + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->searchTimeout + ); } - /* + /** * List all existing indexes * return an object in the form: - * array("items" => array( - * array("name" => "contacts", "createdAt" => "2013-01-18T15:33:13.556Z"), - * array("name" => "notes", "createdAt" => "2013-01-18T15:33:13.556Z") - * )) + * array( + * "items" => array( + * array("name" => "contacts", "createdAt" => "2013-01-18T15:33:13.556Z"), + * array("name" => "notes", "createdAt" => "2013-01-18T15:33:13.556Z") + * ) + * ). + * + * @return mixed + * + * @throws AlgoliaException */ - public function listIndexes() { - return $this->request($this->context, "GET", "/1/indexes/", null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function listIndexes() + { + return $this->request( + $this->context, + 'GET', + '/1/indexes/', + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Delete an index + /** + * Delete an index. * - * @param indexName the name of index to delete - * return an object containing a "deletedAt" attribute + * @param string $indexName the name of index to delete + * + * @return mixed an object containing a "deletedAt" attribute */ - public function deleteIndex($indexName) { - return $this->request($this->context, "DELETE", "/1/indexes/" . urlencode($indexName), null, null, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function deleteIndex($indexName) + { + return $this->request( + $this->context, + 'DELETE', + '/1/indexes/'.urlencode($indexName), + null, + null, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } /** * Move an existing index. - * @param srcIndexName the name of index to copy. - * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). + * + * @param string $srcIndexName the name of index to copy. + * @param string $dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overwritten + * if it already exist). + * + * @return mixed */ - public function moveIndex($srcIndexName, $dstIndexName) { - $request = array("operation" => "move", "destination" => $dstIndexName); - return $this->request($this->context, "POST", "/1/indexes/" . urlencode($srcIndexName) . "/operation", array(), $request, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function moveIndex($srcIndexName, $dstIndexName) + { + $request = ['operation' => 'move', 'destination' => $dstIndexName]; + + return $this->request( + $this->context, + 'POST', + '/1/indexes/'.urlencode($srcIndexName).'/operation', + [], + $request, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } /** * Copy an existing index. - * @param srcIndexName the name of index to copy. - * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). + * + * @param string $srcIndexName the name of index to copy. + * @param string $dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overwritten + * if it already exist). + * + * @return mixed */ - public function copyIndex($srcIndexName, $dstIndexName) { - $request = array("operation" => "copy", "destination" => $dstIndexName); - return $this->request($this->context, "POST", "/1/indexes/" . urlencode($srcIndexName) . "/operation", array(), $request, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function copyIndex($srcIndexName, $dstIndexName) + { + $request = ['operation' => 'copy', 'destination' => $dstIndexName]; + + return $this->request( + $this->context, + 'POST', + '/1/indexes/'.urlencode($srcIndexName).'/operation', + [], + $request, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } /** * Return last logs entries. - * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry). - * @param length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000. + * + * @param int $offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry). + * @param int $length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000. + * @param mixed $type + * + * @return mixed + * + * @throws AlgoliaException */ - public function getLogs($offset = 0, $length = 10, $type = "all") { - if (gettype($type) == "boolean") { //Old prototype onlyError + public function getLogs($offset = 0, $length = 10, $type = 'all') + { + if (gettype($type) == 'boolean') { //Old prototype onlyError if ($type) { - $type = "error"; + $type = 'error'; } else { - $type = "all"; + $type = 'all'; } } - return $this->request($this->context, "GET", "/1/logs?offset=" . $offset . "&length=" . $length . "&type=" . $type, null, null, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); - } - /* - * Get the index object initialized (no server call needed for initialization) + return $this->request( + $this->context, + 'GET', + '/1/logs?offset='.$offset.'&length='.$length.'&type='.$type, + null, + null, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); + } - * @param indexName the name of index + /** + * Get the index object initialized (no server call needed for initialization). + * + * @param string $indexName the name of index + * + * @return Index + * + * @throws AlgoliaException */ - public function initIndex($indexName) { + public function initIndex($indexName) + { if (empty($indexName)) { throw new AlgoliaException('Invalid index name: empty string'); } + return new Index($this->context, $this, $indexName); } - /* - * List all existing user keys with their associated ACLs + /** + * List all existing user keys with their associated ACLs. + * + * @return mixed * + * @throws AlgoliaException */ - public function listUserKeys() { - return $this->request($this->context, "GET", "/1/keys", null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function listUserKeys() + { + return $this->request( + $this->context, + 'GET', + '/1/keys', + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Get ACL of a user key + /** + * Get ACL of a user key. * + * @param string $key + * + * @return mixed */ - public function getUserKeyACL($key) { - return $this->request($this->context, "GET", "/1/keys/" . $key, null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function getUserKeyACL($key) + { + return $this->request( + $this->context, + 'GET', + '/1/keys/'.$key, + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Delete an existing user key + /** + * Delete an existing user key. + * + * @param string $key * + * @return mixed */ - public function deleteUserKey($key) { - return $this->request($this->context, "DELETE", "/1/keys/" . $key, null, null, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function deleteUserKey($key) + { + return $this->request( + $this->context, + 'DELETE', + '/1/keys/'.$key, + null, + null, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Create a new user key - * - * @param obj can be two different parameters: - * The list of parameters for this key. Defined by a NSDictionary that - * can contains the following values: - * - acl: array of string - * - indices: array of string - * - validity: int - * - referers: array of string - * - description: string - * - maxHitsPerQuery: integer - * - queryParameters: string - * - maxQueriesPerIPPerHour: integer - * Or the list of ACL for this key. Defined by an array of NSString that - * can contains the following values: - * - search: allow to search (https and http) - * - addObject: allows to add/update an object in the index (https only) - * - deleteObject : allows to delete an existing object (https only) - * - deleteIndex : allows to delete index content (https only) - * - settings : allows to get index settings (https only) - * - editSettings : allows to change index settings (https only) - * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) - * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. Defaults to 0 (no rate limit). - * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. Defaults to 0 (unlimited) - * @param indexes Specify the list of indices to target (null means all) - */ - public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0, $indexes = null) { + /** + * Create a new user key. + * + * @param $obj can be two different parameters: + * The list of parameters for this key. Defined by an array that + * can contain the following values: + * - acl: array of string + * - indices: array of string + * - validity: int + * - referrers: array of string + * - description: string + * - maxHitsPerQuery: integer + * - queryParameters: string + * - maxQueriesPerIPPerHour: integer + * Or the list of ACL for this key. Defined by an array of NSString that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param int $validity the number of seconds after which the key will be automatically removed (0 means + * no time limit for this key) + * @param int $maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. + * Defaults to 0 (no rate limit). + * @param int $maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. + * Defaults to 0 (unlimited) + * @param array|null $indexes Specify the list of indices to target (null means all) + * + * @return mixed + * + * @throws AlgoliaException + */ + public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0, $indexes = null) + { if ($obj !== array_values($obj)) { // is dict of value $params = $obj; - $params["validity"] = $validity; - $params["maxQueriesPerIPPerHour"] = $maxQueriesPerIPPerHour; - $params["maxHitsPerQuery"] = $maxHitsPerQuery; + $params['validity'] = $validity; + $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; + $params['maxHitsPerQuery'] = $maxHitsPerQuery; } else { - $params = array( - "acl" => $obj, - "validity" => $validity, - "maxQueriesPerIPPerHour" => $maxQueriesPerIPPerHour, - "maxHitsPerQuery" => $maxHitsPerQuery - ); + $params = [ + 'acl' => $obj, + 'validity' => $validity, + 'maxQueriesPerIPPerHour' => $maxQueriesPerIPPerHour, + 'maxHitsPerQuery' => $maxHitsPerQuery, + ]; } if ($indexes != null) { $params['indexes'] = $indexes; } - return $this->request($this->context, "POST", "/1/keys", array(), $params, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + + return $this->request( + $this->context, + 'POST', + '/1/keys', + [], + $params, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Update a user key - * - * @param obj can be two different parameters: - * The list of parameters for this key. Defined by a NSDictionary that - * can contains the following values: - * - acl: array of string - * - indices: array of string - * - validity: int - * - referers: array of string - * - description: string - * - maxHitsPerQuery: integer - * - queryParameters: string - * - maxQueriesPerIPPerHour: integer - * Or the list of ACL for this key. Defined by an array of NSString that - * can contains the following values: - * - search: allow to search (https and http) - * - addObject: allows to add/update an object in the index (https only) - * - deleteObject : allows to delete an existing object (https only) - * - deleteIndex : allows to delete index content (https only) - * - settings : allows to get index settings (https only) - * - editSettings : allows to change index settings (https only) - * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) - * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. Defaults to 0 (no rate limit). - * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. Defaults to 0 (unlimited) - * @param indexes Specify the list of indices to target (null means all) - */ - public function updateUserKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0, $indexes = null) { + /** + * Update a user key. + * + * @param string $key + * @param mixed $obj can be two different parameters: + * The list of parameters for this key. Defined by a array that + * can contains the following values: + * - acl: array of string + * - indices: array of string + * - validity: int + * - referrers: array of string + * - description: string + * - maxHitsPerQuery: integer + * - queryParameters: string + * - maxQueriesPerIPPerHour: integer + * Or the list of ACL for this key. Defined by an array of NSString that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param int $validity the number of seconds after which the key will be automatically removed (0 means + * no time limit for this key) + * @param int $maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. + * Defaults to 0 (no rate limit). + * @param int $maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. Defaults + * to 0 (unlimited) + * @param array|null $indexes Specify the list of indices to target (null means all) + * + * @return mixed + * + * @throws AlgoliaException + */ + public function updateUserKey( + $key, + $obj, + $validity = 0, + $maxQueriesPerIPPerHour = 0, + $maxHitsPerQuery = 0, + $indexes = null + ) { if ($obj !== array_values($obj)) { // is dict of value $params = $obj; - $params["validity"] = $validity; - $params["maxQueriesPerIPPerHour"] = $maxQueriesPerIPPerHour; - $params["maxHitsPerQuery"] = $maxHitsPerQuery; + $params['validity'] = $validity; + $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; + $params['maxHitsPerQuery'] = $maxHitsPerQuery; } else { - $params = array( - "acl" => $obj, - "validity" => $validity, - "maxQueriesPerIPPerHour" => $maxQueriesPerIPPerHour, - "maxHitsPerQuery" => $maxHitsPerQuery - ); + $params = [ + 'acl' => $obj, + 'validity' => $validity, + 'maxQueriesPerIPPerHour' => $maxQueriesPerIPPerHour, + 'maxHitsPerQuery' => $maxHitsPerQuery, + ]; } if ($indexes != null) { $params['indexes'] = $indexes; } - return $this->request($this->context, "PUT", "/1/keys/" . $key, array(), $params, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + + return $this->request( + $this->context, + 'PUT', + '/1/keys/'.$key, + [], + $params, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } /** - * Send a batch request targeting multiple indices - * @param $requests an associative array defining the batch request body + * Send a batch request targeting multiple indices. + * + * @param array $requests an associative array defining the batch request body + * + * @return mixed */ - public function batch($requests) { - return $this->request($this->context, "POST", "/1/indexes/*/batch", array(), array("requests" => $requests), - $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function batch($requests) + { + return $this->request( + $this->context, + 'POST', + '/1/indexes/*/batch', + [], + ['requests' => $requests], + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Generate a secured and public API Key from a list of tagFilters and an - * optional user token identifying the current user + /** + * Generate a secured and public API Key from a list of query parameters and an + * optional user token identifying the current user. * - * @param privateApiKey your private API Key - * @param tagFilters the list of tags applied to the query (used as security) - * @param userToken an optional token identifying the current user + * @param string $privateApiKey your private API Key + * @param mixed $query the list of query parameters applied to the query (used as security) + * @param string|null $userToken an optional token identifying the current user * + * @return string */ - public function generateSecuredApiKey($privateApiKey, $tagFilters, $userToken = null) { - if (is_array($tagFilters)) { - $tmp = array(); - foreach ($tagFilters as $tag) { - if (is_array($tag)) { - $tmp2 = array(); - foreach ($tag as $tag2) { - array_push($tmp2, $tag2); + public static function generateSecuredApiKey($privateApiKey, $query, $userToken = null) + { + $urlEncodedQuery = ''; + if (is_array($query)) { + $queryParameters = []; + if (array_keys($query) !== array_keys(array_keys($query))) { + // array of query parameters + $queryParameters = $query; + } else { + // array of tags + $tmp = []; + foreach ($query as $tag) { + if (is_array($tag)) { + array_push($tmp, '('.implode(',', $tag).')'); + } else { + array_push($tmp, $tag); } - array_push($tmp, '(' . join(',', $tmp2) . ')'); - } else { - array_push($tmp, $tag); } + $tagFilters = implode(',', $tmp); + $queryParameters['tagFilters'] = $tagFilters; + } + if ($userToken != null && strlen($userToken) > 0) { + $queryParameters['userToken'] = $userToken; + } + $urlEncodedQuery = static::buildQuery($queryParameters); + } else { + if (strpos($query, '=') === false) { + // String of tags + $queryParameters = ['tagFilters' => $query]; + + if ($userToken != null && strlen($userToken) > 0) { + $queryParameters['userToken'] = $userToken; + } + $urlEncodedQuery = static::buildQuery($queryParameters); + } else { + // url encoded query + $urlEncodedQuery = $query; + if ($userToken != null && strlen($userToken) > 0) { + $urlEncodedQuery = $urlEncodedQuery.'&userToken='.urlencode($userToken); + } + } + } + $content = hash_hmac('sha256', $urlEncodedQuery, $privateApiKey).$urlEncodedQuery; + + return base64_encode($content); + } + + /** + * @param array $args + * + * @return string + */ + public static function buildQuery($args) + { + foreach ($args as $key => $value) { + if (gettype($value) == 'array') { + $args[$key] = json_encode($value); } - $tagFilters = join(',', $tmp); } - return hash_hmac('sha256', $tagFilters . $userToken, $privateApiKey); + + return http_build_query($args); } - public function request($context, $method, $path, $params = array(), $data = array(), $hostsArray, $connectTimeout, $readTimeout) { - $exceptions = array(); + /** + * @param ClientContext $context + * @param string $method + * @param string $path + * @param array $params + * @param array $data + * @param array $hostsArray + * @param int $connectTimeout + * @param int $readTimeout + * + * @return mixed + * + * @throws AlgoliaException + */ + public function request( + $context, + $method, + $path, + $params, + $data, + $hostsArray, + $connectTimeout, + $readTimeout + ) { + $exceptions = []; $cnt = 0; foreach ($hostsArray as &$host) { $cnt += 1; @@ -381,25 +736,51 @@ public function request($context, $method, $path, $params = array(), $data = arr } try { $res = $this->doRequest($context, $method, $host, $path, $params, $data, $connectTimeout, $readTimeout); - if ($res !== null) + if ($res !== null) { return $res; + } } catch (AlgoliaException $e) { throw $e; } catch (\Exception $e) { $exceptions[$host] = $e->getMessage(); } } - throw new AlgoliaException('Hosts unreachable: ' . join(",", $exceptions)); + throw new AlgoliaException('Hosts unreachable: '.implode(',', $exceptions)); } - public function doRequest($context, $method, $host, $path, $params, $data, $connectTimeout, $readTimeout) { - if (strpos($host, "http") === 0) { - $url = $host . $path; + /** + * @param ClientContext $context + * @param string $method + * @param string $host + * @param string $path + * @param array $params + * @param array $data + * @param int $connectTimeout + * @param int $readTimeout + * + * @return mixed + * + * @throws AlgoliaException + * @throws \Exception + */ + public function doRequest( + $context, + $method, + $host, + $path, + $params, + $data, + $connectTimeout, + $readTimeout + ) { + if (strpos($host, 'http') === 0) { + $url = $host.$path; } else { - $url = "https://" . $host . $path; + $url = 'https://'.$host.$path; } + if ($params != null && count($params) > 0) { - $params2 = array(); + $params2 = []; foreach ($params as $key => $val) { if (is_array($val)) { $params2[$key] = json_encode($val); @@ -407,35 +788,47 @@ public function doRequest($context, $method, $host, $path, $params, $data, $conn $params2[$key] = $val; } } - $url .= "?" . http_build_query($params2); - + $url .= '?'.http_build_query($params2); } + // initialize curl library $curlHandle = curl_init(); + + // set curl options + try { + foreach ($this->curlOptions as $curlOption => $optionValue) { + curl_setopt($curlHandle, constant($curlOption), $optionValue); + } + } catch (\Exception $e) { + $this->invalidOptions($this->curlOptions, $e->getMessage()); + } + //curl_setopt($curlHandle, CURLOPT_VERBOSE, true); if ($context->adminAPIKey == null) { - curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array_merge(array( - 'X-Algolia-Application-Id: ' . $context->applicationID, - 'X-Algolia-API-Key: ' . $context->apiKey, - 'Content-type: application/json' - ), $context->headers)); + curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array_merge([ + 'X-Algolia-Application-Id: '.$context->applicationID, + 'X-Algolia-API-Key: '.$context->apiKey, + 'Content-type: application/json', + ], $context->headers)); } else { - curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array_merge(array( - 'X-Algolia-Application-Id: ' . $context->applicationID, - 'X-Algolia-API-Key: ' . $context->adminAPIKey, - 'X-Forwarded-For: ' . $context->endUserIP, - 'X-Forwarded-API-Key: ' . $context->rateLimitAPIKey, - 'Content-type: application/json' - ), $context->headers)); - } - curl_setopt($curlHandle, CURLOPT_USERAGENT, "Algolia for PHP " . Version::get()); + curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array_merge([ + 'X-Algolia-Application-Id: '.$context->applicationID, + 'X-Algolia-API-Key: '.$context->adminAPIKey, + 'X-Forwarded-For: '.$context->endUserIP, + 'X-Algolia-UserToken: '.$context->algoliaUserToken, + 'X-Forwarded-API-Key: '.$context->rateLimitAPIKey, + 'Content-type: application/json', + ], $context->headers)); + } + + curl_setopt($curlHandle, CURLOPT_USERAGENT, 'Algolia for PHP '.Version::get()); //Return the output instead of printing it curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); curl_setopt($curlHandle, CURLOPT_FAILONERROR, true); curl_setopt($curlHandle, CURLOPT_ENCODING, ''); curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($curlHandle, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($curlHandle, CURLOPT_CAINFO, $this->cainfoPath); + curl_setopt($curlHandle, CURLOPT_CAINFO, $this->caInfoPath); curl_setopt($curlHandle, CURLOPT_URL, $url); $version = curl_version(); @@ -447,26 +840,30 @@ public function doRequest($context, $method, $host, $path, $params, $data, $conn curl_setopt($curlHandle, CURLOPT_TIMEOUT, $readTimeout); } - curl_setopt($curlHandle, CURLOPT_NOSIGNAL, 1); # The problem is that on (Li|U)nix, when libcurl uses the standard name resolver, a SIGALRM is raised during name resolution which libcurl thinks is the timeout alarm. + // The problem is that on (Li|U)nix, when libcurl uses the standard name resolver, + // a SIGALRM is raised during name resolution which libcurl thinks is the timeout alarm. + curl_setopt($curlHandle, CURLOPT_NOSIGNAL, 1); curl_setopt($curlHandle, CURLOPT_FAILONERROR, false); if ($method === 'GET') { curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'GET'); curl_setopt($curlHandle, CURLOPT_HTTPGET, true); curl_setopt($curlHandle, CURLOPT_POST, false); - } else if ($method === 'POST') { - $body = ($data) ? json_encode($data) : ''; - curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'POST'); - curl_setopt($curlHandle, CURLOPT_POST, true); - curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body); - } elseif ($method === 'DELETE') { - curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'DELETE'); - curl_setopt($curlHandle, CURLOPT_POST, false); - } elseif ($method === 'PUT') { - $body = ($data) ? json_encode($data) : ''; - curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PUT'); - curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body); - curl_setopt($curlHandle, CURLOPT_POST, true); + } else { + if ($method === 'POST') { + $body = ($data) ? json_encode($data) : ''; + curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($curlHandle, CURLOPT_POST, true); + curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body); + } elseif ($method === 'DELETE') { + curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'DELETE'); + curl_setopt($curlHandle, CURLOPT_POST, false); + } elseif ($method === 'PUT') { + $body = ($data) ? json_encode($data) : ''; + curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PUT'); + curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body); + curl_setopt($curlHandle, CURLOPT_POST, true); + } } $mhandle = $context->getMHandle($curlHandle); @@ -475,6 +872,7 @@ public function doRequest($context, $method, $host, $path, $params, $data, $conn do { $mrc = curl_multi_exec($mhandle, $running); } while ($mrc == CURLM_CALL_MULTI_PERFORM); + while ($running && $mrc == CURLM_OK) { if (curl_multi_select($mhandle, 0.1) == -1) { usleep(100); @@ -483,17 +881,21 @@ public function doRequest($context, $method, $host, $path, $params, $data, $conn $mrc = curl_multi_exec($mhandle, $running); } while ($mrc == CURLM_CALL_MULTI_PERFORM); } - $http_status = (int)curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); + + $http_status = (int) curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); $response = curl_multi_getcontent($curlHandle); $error = curl_error($curlHandle); + if (!empty($error)) { throw new \Exception($error); } + if ($http_status === 0 || $http_status === 503) { // Could not reach host or service unavailable, try with another one if we have it $context->releaseMHandle($curlHandle); curl_close($curlHandle); - return null; + + return; } $answer = json_decode($response, true); @@ -501,10 +903,9 @@ public function doRequest($context, $method, $host, $path, $params, $data, $conn curl_close($curlHandle); if (intval($http_status / 100) == 4) { - throw new AlgoliaException(isset($answer['message']) ? $answer['message'] : $http_status + " error"); - } - elseif (intval($http_status / 100) != 2) { - throw new \Exception($http_status . ": " . $response); + throw new AlgoliaException(isset($answer['message']) ? $answer['message'] : $http_status + ' error'); + } elseif (intval($http_status / 100) != 2) { + throw new \Exception($http_status.': '.$response); } switch (json_last_error()) { @@ -520,7 +921,8 @@ public function doRequest($context, $method, $host, $path, $params, $data, $conn case JSON_ERROR_STATE_MISMATCH: $errorMsg = 'JSON parsing error: underflow or the modes mismatch'; break; - case (defined('JSON_ERROR_UTF8') ? JSON_ERROR_UTF8 : -1): // PHP 5.3 less than 1.2.2 (Ubuntu 10.04 LTS) + // PHP 5.3 less than 1.2.2 (Ubuntu 10.04 LTS) + case defined('JSON_ERROR_UTF8') ? JSON_ERROR_UTF8 : -1: $errorMsg = 'JSON parsing error: malformed UTF-8 characters, possibly incorrectly encoded'; break; case JSON_ERROR_NONE: @@ -528,10 +930,112 @@ public function doRequest($context, $method, $host, $path, $params, $data, $conn $errorMsg = null; break; } - if ($errorMsg !== null) + if ($errorMsg !== null) { throw new AlgoliaException($errorMsg); + } return $answer; } -} + /** + * Checks if curl option passed are valid curl options. + * + * @param array $curlOptions must be array but no type required while first test throw clear Exception + * + * @return array + */ + protected function checkCurlOptions($curlOptions) + { + if (!is_array($curlOptions)) { + throw new \InvalidArgumentException( + sprintf( + 'AlgoliaSearch requires %s option to be array of valid curl options.', + static::CURLOPT + ) + ); + } + + $checkedCurlOptions = array_intersect(array_keys($curlOptions), array_keys($this->getCurlConstants())); + + if (count($checkedCurlOptions) !== count($curlOptions)) { + $this->invalidOptions($curlOptions); + } + + return $curlOptions; + } + + /** + * Get all php curl available options. + * + * @return array + */ + protected function getCurlConstants() + { + if (!is_null($this->curlConstants)) { + return $this->curlConstants; + } + + $curlAllConstants = get_defined_constants(true); + + if (isset($curlAllConstants['curl'])) { + $curlAllConstants = $curlAllConstants['curl']; + } elseif (isset($curlAllConstants['Core'])) { // hhvm + $curlAllConstants = $curlAllConstants['Core']; + } else { + return $this->curlConstants; + } + + $curlConstants = []; + foreach ($curlAllConstants as $constantName => $constantValue) { + if (strpos($constantName, 'CURLOPT') === 0) { + $curlConstants[$constantName] = $constantValue; + } + } + + $this->curlConstants = $curlConstants; + + return $this->curlConstants; + } + + /** + * throw clear Exception when bad curl option is set. + * + * @param array $curlOptions + * @param string $errorMsg add specific message for disambiguation + */ + protected function invalidOptions(array $curlOptions = [], $errorMsg = '') + { + throw new \OutOfBoundsException( + sprintf( + 'AlgoliaSearch %s options keys are invalid. %s given. error message : %s', + static::CURLOPT, + json_encode($curlOptions), + $errorMsg + ) + ); + } + + /** + * @return PlacesIndex + */ + private function getPlacesIndex() + { + return new PlacesIndex($this->context, $this); + } + + /** + * @param string $appId + * @param string $apiKey + * @param array $hostsArray + * @param array $options + * + * @return PlacesIndex + */ + public static function initPlaces($appId, $apiKey, $hostsArray = null, $options = []) + { + $options['placesEnabled'] = true; + $client = new static($appId, $apiKey, $hostsArray, $options); + + return $client->getPlacesIndex(); + } +} \ No newline at end of file diff --git a/lib/AlgoliaSearch/ClientContext.php b/lib/AlgoliaSearch/ClientContext.php index 3bb642d8..57136647 100644 --- a/lib/AlgoliaSearch/ClientContext.php +++ b/lib/AlgoliaSearch/ClientContext.php @@ -1,4 +1,5 @@ connectTimeout = 2; // connect timeout of 2s by default - $this->readTimeout = 30; // global timeout of 30s by default - $this->searchTimeout = 5; // search timeout of 5s by default + /** + * ClientContext constructor. + * + * @param string $applicationID + * @param string $apiKey + * @param array $hostsArray + * @param bool $placesEnabled + * + * @throws Exception + */ + public function __construct($applicationID, $apiKey, $hostsArray, $placesEnabled = false) + { + // connect timeout of 2s by default + $this->connectTimeout = 2; + + // global timeout of 30s by default + $this->readTimeout = 30; + + // search timeout of 5s by default + $this->searchTimeout = 5; + $this->applicationID = $applicationID; $this->apiKey = $apiKey; $this->readHostsArray = $hostsArray; $this->writeHostsArray = $hostsArray; + if ($this->readHostsArray == null || count($this->readHostsArray) == 0) { - $this->readHostsArray = array($applicationID . "-dsn.algolia.net", $applicationID . "-1.algolianet.com", $applicationID . "-2.algolianet.com", $applicationID . "-3.algolianet.com"); - $this->writeHostsArray = array($applicationID . ".algolia.net", $applicationID . "-1.algolianet.com", $applicationID . "-2.algolianet.com", $applicationID . "-3.algolianet.com"); + $this->readHostsArray = $this->getDefaultReadHosts($placesEnabled); + $this->writeHostsArray = $this->getDefaultWriteHosts(); } + if ($this->applicationID == null || mb_strlen($this->applicationID) == 0) { throw new Exception('AlgoliaSearch requires an applicationID.'); } + if ($this->apiKey == null || mb_strlen($this->apiKey) == 0) { throw new Exception('AlgoliaSearch requires an apiKey.'); } - $this->curlMHandle = NULL; - $this->adminAPIKey = NULL; - $this->endUserIP = NULL; - $this->rateLimitAPIKey = NULL; - $this->headers = array(); + $this->curlMHandle = null; + $this->adminAPIKey = null; + $this->endUserIP = null; + $this->algoliaUserToken = null; + $this->rateLimitAPIKey = null; + $this->headers = []; + } + + /** + * @param bool $placesEnabled + * + * @return array + */ + private function getDefaultReadHosts($placesEnabled) + { + if ($placesEnabled) { + return [ + 'places-dsn.algolia.net', + 'places-1.algolianet.com', + 'places-2.algolianet.com', + 'places-3.algolianet.com', + ]; + } + + return [ + $this->applicationID.'-dsn.algolia.net', + $this->applicationID.'-1.algolianet.com', + $this->applicationID.'-2.algolianet.com', + $this->applicationID.'-3.algolianet.com', + ]; } - function __destruct() { + /** + * @return array + */ + private function getDefaultWriteHosts() + { + return [ + $this->applicationID.'.algolia.net', + $this->applicationID.'-1.algolianet.com', + $this->applicationID.'-2.algolianet.com', + $this->applicationID.'-3.algolianet.com', + ]; + } + + /** + * Closes eventually opened curl handles. + */ + public function __destruct() + { if ($this->curlMHandle != null) { curl_multi_close($this->curlMHandle); } } - public function getMHandle($curlHandle) { + /** + * @param $curlHandle + * + * @return resource + */ + public function getMHandle($curlHandle) + { if ($this->curlMHandle == null) { $this->curlMHandle = curl_multi_init(); } @@ -78,24 +185,58 @@ public function getMHandle($curlHandle) { return $this->curlMHandle; } - public function releaseMHandle($curlHandle) { + /** + * @param $curlHandle + */ + public function releaseMHandle($curlHandle) + { curl_multi_remove_handle($this->curlMHandle, $curlHandle); } - public function setRateLimit($adminAPIKey, $endUserIP, $rateLimitAPIKey) { + /** + * @param string $ip + */ + public function setForwardedFor($ip) + { + $this->endUserIP = $ip; + } + + /** + * @param string $token + */ + public function setAlgoliaUserToken($token) + { + $this->algoliaUserToken = $token; + } + + /** + * @param string $adminAPIKey + * @param string $endUserIP + * @param string $rateLimitAPIKey + */ + public function setRateLimit($adminAPIKey, $endUserIP, $rateLimitAPIKey) + { $this->adminAPIKey = $adminAPIKey; $this->endUserIP = $endUserIP; $this->rateLimitAPIKey = $rateLimitAPIKey; } - public function disableRateLimit() { - $this->adminAPIKey = NULL; - $this->endUserIP = NULL; - $this->rateLimitAPIKey = NULL; - + /** + * Disables the rate limit. + */ + public function disableRateLimit() + { + $this->adminAPIKey = null; + $this->endUserIP = null; + $this->rateLimitAPIKey = null; } - public function setExtraHeader($key, $value) { + /** + * @param string $key + * @param string $value + */ + public function setExtraHeader($key, $value) + { $this->headers[$key] = $value; } } \ No newline at end of file diff --git a/lib/AlgoliaSearch/Index.php b/lib/AlgoliaSearch/Index.php index 333d02aa..01201b2a 100644 --- a/lib/AlgoliaSearch/Index.php +++ b/lib/AlgoliaSearch/Index.php @@ -1,4 +1,5 @@ context = $context; $this->client = $client; $this->indexName = $indexName; $this->urlIndexName = urlencode($indexName); } - /* - * Perform batch operation on several objects + /** + * Perform batch operation on several objects. + * + * @param array $objects contains an array of objects to update (each object must contains an objectID + * attribute) + * @param string $objectIDKey the key in each object that contains the objectID + * @param string $objectActionKey the key in each object that contains the action to perform (addObject, updateObject, + * deleteObject or partialUpdateObject) * - * @param objects contains an array of objects to update (each object must contains an objectID attribute) - * @param objectIDKey the key in each object that contains the objectID - * @param objectActionKey the key in each object that contains the action to perform (addObject, updateObject, deleteObject or partialUpdateObject) + * @return mixed + * + * @throws \Exception */ - public function batchObjects($objects, $objectIDKey = "objectID", $objectActionKey = "objectAction") { - $requests = array(); + public function batchObjects($objects, $objectIDKey = 'objectID', $objectActionKey = 'objectAction') + { + $requests = []; + $allowedActions = [ + 'addObject', + 'updateObject', + 'deleteObject', + 'partialUpdateObject', + 'partialUpdateObjectNoCreate', + ]; foreach ($objects as $obj) { // If no or invalid action, assume updateObject - if (! isset($obj[$objectActionKey]) || ! in_array($obj[$objectActionKey], array('addObject', 'updateObject', 'deleteObject', 'partialUpdateObject', 'partialUpdateObjectNoCreate'))) { + if (!isset($obj[$objectActionKey]) || !in_array($obj[$objectActionKey], $allowedActions)) { throw new \Exception('invalid or no action detected'); } $action = $obj[$objectActionKey]; - unset($obj[$objectActionKey]); // The action key is not included in the object - $req = array("action" => $action, "body" => $obj); + // The action key is not included in the object + unset($obj[$objectActionKey]); + + $req = ['action' => $action, 'body' => $obj]; if (array_key_exists($objectIDKey, $obj)) { - $req["objectID"] = (string) $obj[$objectIDKey]; + $req['objectID'] = (string) $obj[$objectIDKey]; } $requests[] = $req; } - return $this->batch(array("requests" => $requests)); + return $this->batch(['requests' => $requests]); } - /* - * Add an object in this index + /** + * Add an object in this index. * - * @param content contains the object to add inside the index. - * The object is represented by an associative array - * @param objectID (optional) an objectID you want to attribute to this object - * (if the attribute already exist the old object will be overwrite) + * @param array $content contains the object to add inside the index. + * The object is represented by an associative array + * @param string|null $objectID (optional) an objectID you want to attribute to this object + * (if the attribute already exist the old object will be overwrite) + * + * @return mixed */ - public function addObject($content, $objectID = null) { - + public function addObject($content, $objectID = null) + { if ($objectID === null) { - return $this->client->request($this->context, "POST", "/1/indexes/" . $this->urlIndexName, array(), $content, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); - } else { - return $this->client->request($this->context, "PUT", "/1/indexes/" . $this->urlIndexName . "/" . urlencode($objectID), array(), $content, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + return $this->client->request( + $this->context, + 'POST', + '/1/indexes/'.$this->urlIndexName, + [], + $content, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } + + return $this->client->request( + $this->context, + 'PUT', + '/1/indexes/'.$this->urlIndexName.'/'.urlencode($objectID), + [], + $content, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Add several objects + /** + * Add several objects. * - * @param objects contains an array of objects to add. If the object contains an objectID + * @param array $objects contains an array of objects to add. If the object contains an objectID + * @param string $objectIDKey + * + * @return mixed */ - public function addObjects($objects, $objectIDKey = "objectID") { - $requests = $this->buildBatch("addObject", $objects, true, $objectIDKey); + public function addObjects($objects, $objectIDKey = 'objectID') + { + $requests = $this->buildBatch('addObject', $objects, true, $objectIDKey); + return $this->batch($requests); } - /* - * Get an object from this index + /** + * Get an object from this index. * - * @param objectID the unique identifier of the object to retrieve - * @param attributesToRetrieve (optional) if set, contains the list of attributes to retrieve as a string separated by "," + * @param $objectID the unique identifier of the object to retrieve + * @param $attributesToRetrieve (optional) if set, contains the list of attributes to retrieve as a string + * separated by "," + * + * @return mixed */ - public function getObject($objectID, $attributesToRetrieve = null) { + public function getObject($objectID, $attributesToRetrieve = null) + { $id = urlencode($objectID); - if ($attributesToRetrieve === null) - return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/" . $id, null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout); - else - return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/" . $id, array("attributes" => $attributesToRetrieve), null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + if ($attributesToRetrieve === null) { + return $this->client->request( + $this->context, + 'GET', + '/1/indexes/'.$this->urlIndexName.'/'.$id, + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); + } + + return $this->client->request( + $this->context, + 'GET', + '/1/indexes/'.$this->urlIndexName.'/'.$id, + ['attributes' => $attributesToRetrieve], + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Get several objects from this index + /** + * Get several objects from this index. + * + * @param array $objectIDs the array of unique identifier of objects to retrieve * - * @param objectIDs the array of unique identifier of objects to retrieve + * @return mixed + * + * @throws \Exception */ - public function getObjects($objectIDs) { + public function getObjects($objectIDs) + { if ($objectIDs == null) { throw new \Exception('No list of objectID provided'); } - $requests = array(); + + $requests = []; foreach ($objectIDs as $object) { - $req = array("indexName" => $this->indexName, "objectID" => $object); + $req = ['indexName' => $this->indexName, 'objectID' => $object]; array_push($requests, $req); } - return $this->client->request($this->context, "POST", "/1/indexes/*/objects", array(), array("requests" => $requests), $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + + return $this->client->request( + $this->context, + 'POST', + '/1/indexes/*/objects', + [], + ['requests' => $requests], + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Update partially an object (only update attributes passed in argument) + /** + * Update partially an object (only update attributes passed in argument). + * + * @param array $partialObject contains the object attributes to override, the + * object must contains an objectID attribute + * @param bool $createIfNotExists * - * @param partialObject contains the object attributes to override, the - * object must contains an objectID attribute + * @return mixed + * + * @throws AlgoliaException */ - public function partialUpdateObject($partialObject, $createIfNotExists = true) { - return $this->client->request($this->context, "POST", "/1/indexes/" . $this->urlIndexName . "/" . urlencode($partialObject["objectID"]) . "/partial" . ($createIfNotExists ? "" : "?createIfNotExists=false"), array(), $partialObject, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function partialUpdateObject($partialObject, $createIfNotExists = true) + { + $queryString = $createIfNotExists ? '' : '?createIfNotExists=false'; + + return $this->client->request( + $this->context, + 'POST', + '/1/indexes/'.$this->urlIndexName.'/'.urlencode($partialObject['objectID']).'/partial'.$queryString, + [], + $partialObject, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Partially Override the content of several objects + /** + * Partially Override the content of several objects. + * + * @param array $objects contains an array of objects to update (each object must contains a objectID attribute) + * @param string $objectIDKey + * @param bool $createIfNotExists * - * @param objects contains an array of objects to update (each object must contains a objectID attribute) + * @return mixed */ - public function partialUpdateObjects($objects, $objectIDKey = "objectID", $createIfNotExists = true) { + public function partialUpdateObjects($objects, $objectIDKey = 'objectID', $createIfNotExists = true) + { if ($createIfNotExists) { - $requests = $this->buildBatch("partialUpdateObject", $objects, true, $objectIDKey); + $requests = $this->buildBatch('partialUpdateObject', $objects, true, $objectIDKey); } else { - $requests = $this->buildBatch("partialUpdateObjectNoCreate", $objects, true, $objectIDKey); + $requests = $this->buildBatch('partialUpdateObjectNoCreate', $objects, true, $objectIDKey); } + return $this->batch($requests); } - /* - * Override the content of object + /** + * Override the content of object. + * + * @param array $object contains the object to save, the object must contains an objectID attribute * - * @param object contains the object to save, the object must contains an objectID attribute + * @return mixed */ - public function saveObject($object) { - return $this->client->request($this->context, "PUT", "/1/indexes/" . $this->urlIndexName . "/" . urlencode($object["objectID"]), array(), $object, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function saveObject($object) + { + return $this->client->request( + $this->context, + 'PUT', + '/1/indexes/'.$this->urlIndexName.'/'.urlencode($object['objectID']), + [], + $object, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Override the content of several objects + /** + * Override the content of several objects. + * + * @param array $objects contains an array of objects to update (each object must contains a objectID attribute) + * @param string $objectIDKey * - * @param objects contains an array of objects to update (each object must contains a objectID attribute) + * @return mixed */ - public function saveObjects($objects, $objectIDKey = "objectID") { - $requests = $this->buildBatch("updateObject", $objects, true, $objectIDKey); + public function saveObjects($objects, $objectIDKey = 'objectID') + { + $requests = $this->buildBatch('updateObject', $objects, true, $objectIDKey); + return $this->batch($requests); } - /* - * Delete an object from the index + /** + * Delete an object from the index. + * + * @param $objectID the unique identifier of object to delete + * + * @return mixed * - * @param objectID the unique identifier of object to delete + * @throws AlgoliaException + * @throws \Exception */ - public function deleteObject($objectID) { + public function deleteObject($objectID) + { if ($objectID == null || mb_strlen($objectID) == 0) { throw new \Exception('objectID is mandatory'); } - return $this->client->request($this->context, "DELETE", "/1/indexes/" . $this->urlIndexName . "/" . urlencode($objectID), null, null, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + + return $this->client->request( + $this->context, + 'DELETE', + '/1/indexes/'.$this->urlIndexName.'/'.urlencode($objectID), + null, + null, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Delete several objects + /** + * Delete several objects. + * + * @param array $objects contains an array of objectIDs to delete. If the object contains an objectID * - * @param objects contains an array of objectIDs to delete. If the object contains an objectID + * @return mixed */ - public function deleteObjects($objects) { - $objectIDs = array(); + public function deleteObjects($objects) + { + $objectIDs = []; foreach ($objects as $key => $id) { - $objectIDs[$key] = array('objectID' => $id); + $objectIDs[$key] = ['objectID' => $id]; } - $requests = $this->buildBatch("deleteObject", $objectIDs, true); + $requests = $this->buildBatch('deleteObject', $objectIDs, true); + return $this->batch($requests); } - /* - * Delete all objects matching a query + /** + * Delete all objects matching a query. * - * @param query the query string - * @param params the optional query parameters + * @param string $query the query string + * @param array $args the optional query parameters + * @param bool $waitLastCall + * /!\ Be safe with "waitLastCall" + * In really rare cases you can have the number of hits smaller than the hitsPerPage + * param if you trigger the timeout of the search, in that case you won't remove all the records */ - public function deleteByQuery($query, $args = array()) { - $params["attributeToRetrieve"] = array('objectID'); - $params["hitsPerPage"] = 1000; + public function deleteByQuery($query, $args = [], $waitLastCall = true) + { + $args['attributesToRetrieve'] = 'objectID'; + $args['hitsPerPage'] = 1000; + $args['distinct'] = false; + $results = $this->search($query, $args); while ($results['nbHits'] != 0) { - $objectIDs = array(); + $objectIDs = []; foreach ($results['hits'] as $elt) { array_push($objectIDs, $elt['objectID']); } $res = $this->deleteObjects($objectIDs); + if ($results['nbHits'] < $args['hitsPerPage'] && false === $waitLastCall) { + break; + } $this->waitTask($res['taskID']); $results = $this->search($query, $args); } } - /* - * Search inside the index - * - * @param query the full text query - * @param args (optional) if set, contains an associative array with query parameters: - * - page: (integer) Pagination parameter used to select the page to retrieve. - * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 - * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20. - * - attributesToRetrieve: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size). - * Attributes are separated with a comma (for example "name,address"). - * You can also use a string array encoding (for example ["name","address"]). - * By default, all attributes are retrieved. You can also use '*' to retrieve all values when an attributesToRetrieve setting is specified for your index. - * - attributesToHighlight: a string that contains the list of attributes you want to highlight according to the query. - * Attributes are separated by a comma. You can also use a string array encoding (for example ["name","address"]). - * If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted. - * You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted. - * A matchLevel is returned for each highlighted attribute and can contain: - * - full: if all the query terms were found in the attribute, - * - partial: if only some of the query terms were found, - * - none: if none of the query terms were found. - * - attributesToSnippet: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`). - * Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10). - * You can also use a string array encoding (Example: attributesToSnippet: ["name:10","content:10"]). By default no snippet is computed. - * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. Defaults to 3. - * - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos in this word. Defaults to 7. - * - getRankingInfo: if set to 1, the result hits will contain ranking information in _rankingInfo attribute. - * - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats separated by a comma). - * For example aroundLatLng=47.316669,5.016670). - * You can specify the maximum distance in meters with the aroundRadius parameter (in meters) and the precision for ranking with aroundPrecision - * (for example if you set aroundPrecision=100, two objects that are distant of less than 100m will be considered as identical for "geo" ranking parameter). - * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) - * - insideBoundingBox: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng). - * For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201). - * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) - * - numericFilters: a string that contains the list of numeric filters you want to apply separated by a comma. - * The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`. - * You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000. - * You can also use a string array encoding (for example numericFilters: ["price>100","price<1000"]). - * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas. - * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3). - * You can also use a string array encoding, for example tagFilters: ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3). - * At indexing, tags should be added in the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}). - * - facetFilters: filter the query by a list of facets. - * Facets are separated by commas and each facet is encoded as `attributeName:value`. - * For example: `facetFilters=category:Book,author:John%20Doe`. - * You can also use a string array encoding (for example `["category:Book","author:John%20Doe"]`). - * - facets: List of object attributes that you want to use for faceting. - * Attributes are separated with a comma (for example `"category,author"` ). - * You can also use a JSON string array encoding (for example ["category","author"]). - * Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter. - * You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**. - * - queryType: select how the query words are interpreted, it can be one of the following value: - * - prefixAll: all query words are interpreted as prefixes, - * - prefixLast: only the last word is interpreted as a prefix (default behavior), - * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. - * - optionalWords: a string that contains the list of words that should be considered as optional when found in the query. - * The list of words is comma separated. - * - distinct: If set to 1, enable the distinct feature (disabled by default) if the attributeForDistinct index setting is set. - * This feature is similar to the SQL "distinct" keyword: when enabled in a query with the distinct=1 parameter, - * all hits containing a duplicate value for the attributeForDistinct attribute are removed from results. - * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best - * one is kept and others are removed. - */ - public function search($query, $args = null) { + /** + * Search inside the index. + * + * @param string $query the full text query + * @param mixed $args (optional) if set, contains an associative array with query parameters: + * - page: (integer) Pagination parameter used to select the page to retrieve. + * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 + * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. + * Defaults to 20. + * - attributesToRetrieve: a string that contains the list of object attributes you want to + * retrieve (let you minimize the answer size). Attributes are separated with a comma (for + * example "name,address"). You can also use a string array encoding (for example + * ["name","address"]). By default, all attributes are retrieved. You can also use '*' to + * retrieve all values when an attributesToRetrieve setting is specified for your index. + * - attributesToHighlight: a string that contains the list of attributes you want to highlight + * according to the query. Attributes are separated by a comma. You can also use a string array + * encoding (for example ["name","address"]). If an attribute has no match for the query, the raw + * value is returned. By default all indexed text attributes are highlighted. You can use `*` if + * you want to highlight all textual attributes. Numerical attributes are not highlighted. A + * matchLevel is returned for each highlighted attribute and can contain: + * - full: if all the query terms were found in the attribute, + * - partial: if only some of the query terms were found, + * - none: if none of the query terms were found. + * - attributesToSnippet: a string that contains the list of attributes to snippet alongside the + * number of words to return (syntax is `attributeName:nbWords`). Attributes are separated by a + * comma (Example: attributesToSnippet=name:10,content:10). You can also use a string array + * encoding (Example: attributesToSnippet: ["name:10","content:10"]). By default no snippet is + * computed. + * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in + * this word. Defaults to 3. + * - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos + * in this word. Defaults to 7. + * - getRankingInfo: if set to 1, the result hits will contain ranking information in + * _rankingInfo attribute. + * - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats + * separated by a comma). For example aroundLatLng=47.316669,5.016670). You can specify the + * maximum distance in meters with the aroundRadius parameter (in meters) and the precision for + * ranking with aroundPrecision + * (for example if you set aroundPrecision=100, two objects that are distant of less than 100m + * will be considered as identical for "geo" ranking parameter). At indexing, you should specify + * geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, + * "lng":2.348800}}) + * - insideBoundingBox: search entries inside a given area defined by the two extreme points of a + * rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng). For example + * insideBoundingBox=47.3165,4.9665,47.3424,5.0201). At indexing, you should specify geoloc of an + * object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) + * - numericFilters: a string that contains the list of numeric filters you want to apply + * separated by a comma. The syntax of one filter is `attributeName` followed by `operand` + * followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`. You can have + * multiple conditions on one attribute like for example numericFilters=price>100,price<1000. You + * can also use a string array encoding (for example numericFilters: ["price>100","price<1000"]). + * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by + * commas. + * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 + * OR tag3). You can also use a string array encoding, for example tagFilters: + * ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3). At indexing, tags should be added in + * the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}). + * - facetFilters: filter the query by a list of facets. + * Facets are separated by commas and each facet is encoded as `attributeName:value`. + * For example: `facetFilters=category:Book,author:John%20Doe`. + * You can also use a string array encoding (for example + * `["category:Book","author:John%20Doe"]`). + * - facets: List of object attributes that you want to use for faceting. + * Attributes are separated with a comma (for example `"category,author"` ). + * You can also use a JSON string array encoding (for example ["category","author"]). + * Only attributes that have been added in **attributesForFaceting** index setting can be used in + * this parameter. You can also use `*` to perform faceting on all attributes specified in + * **attributesForFaceting**. + * - queryType: select how the query words are interpreted, it can be one of the following value: + * - prefixAll: all query words are interpreted as prefixes, + * - prefixLast: only the last word is interpreted as a prefix (default behavior), + * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. + * - optionalWords: a string that contains the list of words that should be considered as + * optional when found in the query. The list of words is comma separated. + * - distinct: If set to 1, enable the distinct feature (disabled by default) if the + * attributeForDistinct index setting is set. This feature is similar to the SQL "distinct" + * keyword: when enabled in a query with the distinct=1 parameter, all hits containing a + * duplicate value for the attributeForDistinct attribute are removed from results. For example, + * if the chosen attribute is show_name and several hits have the same value for show_name, then + * only the best one is kept and others are removed. + * + * @return mixed + */ + public function search($query, $args = null) + { if ($args === null) { - $args = array(); + $args = []; } - $args["query"] = $query; - return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName, $args, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->searchTimeout); + $args['query'] = $query; + + return $this->client->request( + $this->context, + 'POST', + '/1/indexes/'.$this->urlIndexName.'/query', + [], + ['params' => $this->client->buildQuery($args)], + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->searchTimeout + ); } - /* - * Perform a search with disjunctive facets generating as many queries as number of disjunctive facets + /** + * Perform a search with disjunctive facets generating as many queries as number of disjunctive facets. + * + * @param string $query the query + * @param array $disjunctive_facets the array of disjunctive facets + * @param array $params a hash representing the regular query parameters + * @param array $refinements a hash ("string" -> ["array", "of", "refined", "values"]) representing the current refinements + * ex: { "my_facet1" => ["my_value1", ["my_value2"], "my_disjunctive_facet1" => ["my_value1", "my_value2"] } * - * @param query the query - * @param disjunctive_facets the array of disjunctive facets - * @param params a hash representing the regular query parameters - * @param refinements a hash ("string" -> ["array", "of", "refined", "values"]) representing the current refinements - * ex: { "my_facet1" => ["my_value1", ["my_value2"], "my_disjunctive_facet1" => ["my_value1", "my_value2"] } + * @return mixed + * + * @throws AlgoliaException + * @throws \Exception */ - public function searchDisjunctiveFaceting($query, $disjunctive_facets, $params = array(), $refinements = array()) { - if (gettype($disjunctive_facets) != "string" && gettype($disjunctive_facets) != "array") { - throw new AlgoliaException("Argument \"disjunctive_facets\" must be a String or an Array"); + public function searchDisjunctiveFaceting($query, $disjunctive_facets, $params = [], $refinements = []) + { + if (gettype($disjunctive_facets) != 'string' && gettype($disjunctive_facets) != 'array') { + throw new AlgoliaException('Argument "disjunctive_facets" must be a String or an Array'); } - if (gettype($refinements) != "array") { - throw new AlgoliaException("Argument \"refinements\" must be a Hash of Arrays"); + + if (gettype($refinements) != 'array') { + throw new AlgoliaException('Argument "refinements" must be a Hash of Arrays'); } - if (gettype($disjunctive_facets) == "string") { - $disjunctive_facets = split(",", $disjunctive_facets); + if (gettype($disjunctive_facets) == 'string') { + $disjunctive_facets = explode(',', $disjunctive_facets); } - $disjunctive_refinements = array(); + $disjunctive_refinements = []; foreach ($refinements as $key => $value) { if (in_array($key, $disjunctive_facets)) { $disjunctive_refinements[$key] = $value; } } - $queries = array(); - $filters = array(); + $queries = []; + $filters = []; foreach ($refinements as $key => $value) { - $r = array_map(function ($val) use ($key) { return $key . ":" . $val;}, $value); + $r = array_map( + function ($val) use ($key) { + return $key.':'.$val; + }, + $value + ); if (in_array($key, $disjunctive_refinements)) { $filter = array_merge($filters, $r); @@ -334,15 +552,20 @@ public function searchDisjunctiveFaceting($query, $disjunctive_facets, $params = array_push($filters, $r); } } - $params["indexName"] = $this->indexName; - $params["query"] = $query; - $params["facetFilters"] = $filters; + $params['indexName'] = $this->indexName; + $params['query'] = $query; + $params['facetFilters'] = $filters; array_push($queries, $params); foreach ($disjunctive_facets as $disjunctive_facet) { - $filters = array(); + $filters = []; foreach ($refinements as $key => $value) { if ($key != $disjunctive_facet) { - $r = array_map(function($val) use($key) { return $key . ":" . $val;}, $value); + $r = array_map( + function ($val) use ($key) { + return $key.':'.$val; + }, + $value + ); if (in_array($key, $disjunctive_refinements)) { $filter = array_merge($filters, $r); @@ -351,22 +574,22 @@ public function searchDisjunctiveFaceting($query, $disjunctive_facets, $params = } } } - $params["indexName"] = $this->indexName; - $params["query"] = $query; - $params["facetFilters"] = $filters; - $params["page"] = 0; - $params["hitsPerPage"] = 0; - $params["attributesToRetrieve"] = array(); - $params["attributesToHighlight"] = array(); - $params["attributesToSnippet"] = array(); - $params["facets"] = $disjunctive_facet; - $params["analytics"] = false; + $params['indexName'] = $this->indexName; + $params['query'] = $query; + $params['facetFilters'] = $filters; + $params['page'] = 0; + $params['hitsPerPage'] = 0; + $params['attributesToRetrieve'] = []; + $params['attributesToHighlight'] = []; + $params['attributesToSnippet'] = []; + $params['facets'] = $disjunctive_facet; + $params['analytics'] = false; array_push($queries, $params); } $answers = $this->client->multipleQueries($queries); $aggregated_answer = $answers['results'][0]; - $aggregated_answer['disjunctiveFacets'] = array(); + $aggregated_answer['disjunctiveFacets'] = []; for ($i = 1; $i < count($answers['results']); $i++) { foreach ($answers['results'][$i]['facets'] as $key => $facet) { $aggregated_answer['disjunctiveFacets'][$key] = $facet; @@ -380,245 +603,499 @@ public function searchDisjunctiveFaceting($query, $disjunctive_facets, $params = } } } + return $aggregated_answer; } - /* - * Browse all index content + /** + * Browse all index content. + * + * @param int $page Pagination parameter used to select the page to retrieve. + * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 + * @param int $hitsPerPage : Pagination parameter used to select the number of hits per page. Defaults to 1000. + * + * @return mixed * - * @param page Pagination parameter used to select the page to retrieve. - * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 - * @param hitsPerPage: Pagination parameter used to select the number of hits per page. Defaults to 1000. + * @throws AlgoliaException */ - public function browse($page = 0, $hitsPerPage = 1000) { - return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/browse", - array("page" => $page, "hitsPerPage" => $hitsPerPage), null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + private function doBcBrowse($page = 0, $hitsPerPage = 1000) + { + return $this->client->request( + $this->context, + 'GET', + '/1/indexes/'.$this->urlIndexName.'/browse', + ['page' => $page, 'hitsPerPage' => $hitsPerPage], + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* + /** * Wait the publication of a task on the server. * All server task are asynchronous and you can check with this method that the task is published. * - * @param taskID the id of the task returned by server - * @param timeBeforeRetry the time in milliseconds before retry (default = 100ms) + * @param string $taskID the id of the task returned by server + * @param int $timeBeforeRetry the time in milliseconds before retry (default = 100ms) + * + * @return mixed */ - public function waitTask($taskID, $timeBeforeRetry = 100) { + public function waitTask($taskID, $timeBeforeRetry = 100) + { while (true) { - $res = $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/task/" . $taskID, null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout); - if ($res["status"] === "published") + $res = $this->getTaskStatus($taskID); + if ($res['status'] === 'published') { return $res; + } usleep($timeBeforeRetry * 1000); } } - /* - * Get settings of this index + /** + * get the status of a task on the server. + * All server task are asynchronous and you can check with this method that the task is published or not. + * + * @param string $taskID the id of the task returned by server * + * @return mixed */ - public function getSettings() { - return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/settings", null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function getTaskStatus($taskID) + { + return $this->client->request( + $this->context, + 'GET', + '/1/indexes/'.$this->urlIndexName.'/task/'.$taskID, + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* + /** + * Get settings of this index. + * + * @return mixed + * + * @throws AlgoliaException + */ + public function getSettings() + { + return $this->client->request( + $this->context, + 'GET', + '/1/indexes/'.$this->urlIndexName.'/settings', + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); + } + + /** * This function deletes the index content. Settings and index specific API keys are kept untouched. + * + * @return mixed + * + * @throws AlgoliaException */ - public function clearIndex() { - return $this->client->request($this->context, "POST", "/1/indexes/" . $this->urlIndexName . "/clear", null, null, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function clearIndex() + { + return $this->client->request( + $this->context, + 'POST', + '/1/indexes/'.$this->urlIndexName.'/clear', + null, + null, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Set settings for this index - * - * @param settigns the settings object that can contains : - * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3). - * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7). - * - hitsPerPage: (integer) the number of hits per page (default = 10). - * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. - * If set to null, all attributes are retrieved. - * - attributesToHighlight: (array of strings) default list of attributes to highlight. - * If set to null, all indexed attributes are highlighted. - * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number of words to return (syntax is attributeName:nbWords). - * By default no snippet is computed. If set to null, no snippet is computed. - * - attributesToIndex: (array of strings) the list of fields you want to index. - * If set to null, all textual and numerical attributes of your objects are indexed, but you should update it to get optimal results. - * This parameter has two important uses: - * - Limit the attributes to index: For example if you store a binary image in base64, you want to store it and be able to - * retrieve it but you don't want to search in the base64 string. - * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in attributes at the beginning of - * the list will be considered more important than matches in attributes further down the list. - * In one attribute, matching text at the beginning of the attribute will be considered more important than text after, you can disable - * this behavior if you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex: ["title", "unordered(text)"]. - * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. - * All strings in the attribute selected for faceting are extracted and added as a facet. If set to null, no attribute is used for faceting. - * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature is similar to the SQL "distinct" keyword: when enabled - * in query with the distinct=1 parameter, all hits containing a duplicate value for this attribute are removed from results. - * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best one is kept and others are removed. - * - ranking: (array of strings) controls the way results are sorted. - * We have six available criteria: - * - typo: sort according to number of typos, - * - geo: sort according to decreassing distance when performing a geo-location based search, - * - proximity: sort according to the proximity of query words in hits, - * - attribute: sort according to the order of attributes defined by attributesToIndex, - * - exact: - * - if the user query contains one word: sort objects having an attribute that is exactly the query word before others. - * For example if you search for the "V" TV show, you want to find it with the "V" query and avoid to have all popular TV - * show starting by the v letter before it. - * - if the user query contains multiple words: sort according to the number of words that matched exactly (and not as a prefix). - * - custom: sort according to a user defined formula set in **customRanking** attribute. - * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] - * - customRanking: (array of strings) lets you specify part of the ranking. - * The syntax of this condition is an array of strings containing attributes prefixed by asc (ascending order) or desc (descending order) operator. - * For example `"customRanking" => ["desc(population)", "asc(name)"]` - * - queryType: Select how the query words are interpreted, it can be one of the following value: - * - prefixAll: all query words are interpreted as prefixes, - * - prefixLast: only the last word is interpreted as a prefix (default behavior), - * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. - * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in the query result (default to ""). - * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in the query result (default to ""). - * - optionalWords: (array of strings) Specify a list of words that should be considered as optional when found in the query. - */ - public function setSettings($settings) { - return $this->client->request($this->context, "PUT", "/1/indexes/" . $this->urlIndexName . "/settings", array(), $settings, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + /** + * Set settings for this index. + * + * @param mixed $settings the settings object that can contains : + * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = + * 3). + * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default + * = 7). + * - hitsPerPage: (integer) the number of hits per page (default = 10). + * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. + * If set to null, all attributes are retrieved. + * - attributesToHighlight: (array of strings) default list of attributes to highlight. + * If set to null, all indexed attributes are highlighted. + * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the + * number of words to return (syntax is attributeName:nbWords). By default no snippet is computed. + * If set to null, no snippet is computed. + * - attributesToIndex: (array of strings) the list of fields you want to index. + * If set to null, all textual and numerical attributes of your objects are indexed, but you + * should update it to get optimal results. This parameter has two important uses: + * - Limit the attributes to index: For example if you store a binary image in base64, you want to + * store it and be able to retrieve it but you don't want to search in the base64 string. + * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in + * attributes at the beginning of the list will be considered more important than matches in + * attributes further down the list. In one attribute, matching text at the beginning of the + * attribute will be considered more important than text after, you can disable this behavior if + * you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex: + * ["title", "unordered(text)"]. + * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. + * All strings in the attribute selected for faceting are extracted and added as a facet. If set + * to null, no attribute is used for faceting. + * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature + * is similar to the SQL "distinct" keyword: when enabled in query with the distinct=1 parameter, + * all hits containing a duplicate value for this attribute are removed from results. For example, + * if the chosen attribute is show_name and several hits have the same value for show_name, then + * only the best one is kept and others are removed. + * - ranking: (array of strings) controls the way results are sorted. + * We have six available criteria: + * - typo: sort according to number of typos, + * - geo: sort according to decreassing distance when performing a geo-location based search, + * - proximity: sort according to the proximity of query words in hits, + * - attribute: sort according to the order of attributes defined by attributesToIndex, + * - exact: + * - if the user query contains one word: sort objects having an attribute that is exactly the + * query word before others. For example if you search for the "V" TV show, you want to find it + * with the "V" query and avoid to have all popular TV show starting by the v letter before it. + * - if the user query contains multiple words: sort according to the number of words that matched + * exactly (and not as a prefix). + * - custom: sort according to a user defined formula set in **customRanking** attribute. + * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] + * - customRanking: (array of strings) lets you specify part of the ranking. + * The syntax of this condition is an array of strings containing attributes prefixed by asc + * (ascending order) or desc (descending order) operator. For example `"customRanking" => + * ["desc(population)", "asc(name)"]` + * - queryType: Select how the query words are interpreted, it can be one of the following value: + * - prefixAll: all query words are interpreted as prefixes, + * - prefixLast: only the last word is interpreted as a prefix (default behavior), + * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. + * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in + * the query result (default to ""). + * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in + * the query result (default to ""). + * - optionalWords: (array of strings) Specify a list of words that should be considered as + * optional when found in the query. + * + * @return mixed + */ + public function setSettings($settings) + { + return $this->client->request( + $this->context, + 'PUT', + '/1/indexes/'.$this->urlIndexName.'/settings', + [], + $settings, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * List all existing user keys associated to this index with their associated ACLs + /** + * List all existing user keys associated to this index with their associated ACLs. * + * @return mixed + * + * @throws AlgoliaException */ - public function listUserKeys() { - return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/keys", null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function listUserKeys() + { + return $this->client->request( + $this->context, + 'GET', + '/1/indexes/'.$this->urlIndexName.'/keys', + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Get ACL of a user key associated to this index + /** + * Get ACL of a user key associated to this index. * + * @param string $key + * + * @return mixed + * + * @throws AlgoliaException */ - public function getUserKeyACL($key) { - return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/keys/" . $key, null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function getUserKeyACL($key) + { + return $this->client->request( + $this->context, + 'GET', + '/1/indexes/'.$this->urlIndexName.'/keys/'.$key, + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Delete an existing user key associated to this index + /** + * Delete an existing user key associated to this index. + * + * @param string $key * + * @return mixed + * + * @throws AlgoliaException */ - public function deleteUserKey($key) { - return $this->client->request($this->context, "DELETE", "/1/indexes/" . $this->urlIndexName . "/keys/" . $key, null, null, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function deleteUserKey($key) + { + return $this->client->request( + $this->context, + 'DELETE', + '/1/indexes/'.$this->urlIndexName.'/keys/'.$key, + null, + null, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Create a new user key associated to this index - * - * @param obj can be two different parameters: - * The list of parameters for this key. Defined by a NSDictionary that - * can contains the following values: - * - acl: array of string - * - indices: array of string - * - validity: int - * - referers: array of string - * - description: string - * - maxHitsPerQuery: integer - * - queryParameters: string - * - maxQueriesPerIPPerHour: integer - * Or the list of ACL for this key. Defined by an array of NSString that - * can contains the following values: - * - search: allow to search (https and http) - * - addObject: allows to add/update an object in the index (https only) - * - deleteObject : allows to delete an existing object (https only) - * - deleteIndex : allows to delete index content (https only) - * - settings : allows to get index settings (https only) - * - editSettings : allows to change index settings (https only) - * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) - * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. Defaults to 0 (no rate limit). - * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. Defaults to 0 (unlimited) - */ - public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) { - if ($obj !== array_values($obj)) { // is dict of value + /** + * Create a new user key associated to this index. + * + * @param array $obj can be two different parameters: + * The list of parameters for this key. Defined by a array that + * can contains the following values: + * - acl: array of string + * - indices: array of string + * - validity: int + * - referers: array of string + * - description: string + * - maxHitsPerQuery: integer + * - queryParameters: string + * - maxQueriesPerIPPerHour: integer + * Or the list of ACL for this key. Defined by an array of NSString that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param int $validity the number of seconds after which the key will be automatically removed (0 means + * no time limit for this key) + * @param int $maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. + * Defaults to 0 (no rate limit). + * @param int $maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. + * Defaults to 0 (unlimited) + * + * @return mixed + * + * @throws AlgoliaException + */ + public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) + { + // is dict of value + if ($obj !== array_values($obj)) { $params = $obj; - $params["validity"] = $validity; - $params["maxQueriesPerIPPerHour"] = $maxQueriesPerIPPerHour; - $params["maxHitsPerQuery"] = $maxHitsPerQuery; + $params['validity'] = $validity; + $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; + $params['maxHitsPerQuery'] = $maxHitsPerQuery; } else { - $params = array( - "acl" => $obj, - "validity" => $validity, - "maxQueriesPerIPPerHour" => $maxQueriesPerIPPerHour, - "maxHitsPerQuery" => $maxHitsPerQuery - ); + $params = [ + 'acl' => $obj, + 'validity' => $validity, + 'maxQueriesPerIPPerHour' => $maxQueriesPerIPPerHour, + 'maxHitsPerQuery' => $maxHitsPerQuery, + ]; } - return $this->client->request($this->context, "POST", "/1/indexes/" . $this->urlIndexName . "/keys", array(), $params, - $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + + return $this->client->request( + $this->context, + 'POST', + '/1/indexes/'.$this->urlIndexName.'/keys', + [], + $params, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } - /* - * Update a user key associated to this index - * - * @param obj can be two different parameters: - * The list of parameters for this key. Defined by a NSDictionary that - * can contains the following values: - * - acl: array of string - * - indices: array of string - * - validity: int - * - referers: array of string - * - description: string - * - maxHitsPerQuery: integer - * - queryParameters: string - * - maxQueriesPerIPPerHour: integer - * Or the list of ACL for this key. Defined by an array of NSString that - * can contains the following values: - * - search: allow to search (https and http) - * - addObject: allows to add/update an object in the index (https only) - * - deleteObject : allows to delete an existing object (https only) - * - deleteIndex : allows to delete index content (https only) - * - settings : allows to get index settings (https only) - * - editSettings : allows to change index settings (https only) - * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) - * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. Defaults to 0 (no rate limit). - * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. Defaults to 0 (unlimited) - */ - public function updateUserKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) { - if ($obj !== array_values($obj)) { // is dict of value + /** + * Update a user key associated to this index. + * + * @param string $key + * @param array $obj can be two different parameters: + * The list of parameters for this key. Defined by a array that + * can contains the following values: + * - acl: array of string + * - indices: array of string + * - validity: int + * - referers: array of string + * - description: string + * - maxHitsPerQuery: integer + * - queryParameters: string + * - maxQueriesPerIPPerHour: integer + * Or the list of ACL for this key. Defined by an array of NSString that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param int $validity the number of seconds after which the key will be automatically removed (0 means + * no time limit for this key) + * @param int $maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. + * Defaults to 0 (no rate limit). + * @param int $maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. + * Defaults to 0 (unlimited) + * + * @return mixed + * + * @throws AlgoliaException + */ + public function updateUserKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) + { + // is dict of value + if ($obj !== array_values($obj)) { $params = $obj; - $params["validity"] = $validity; - $params["maxQueriesPerIPPerHour"] = $maxQueriesPerIPPerHour; - $params["maxHitsPerQuery"] = $maxHitsPerQuery; + $params['validity'] = $validity; + $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; + $params['maxHitsPerQuery'] = $maxHitsPerQuery; } else { - $params = array( - "acl" => $obj, - "validity" => $validity, - "maxQueriesPerIPPerHour" => $maxQueriesPerIPPerHour, - "maxHitsPerQuery" => $maxHitsPerQuery - ); + $params = [ + 'acl' => $obj, + 'validity' => $validity, + 'maxQueriesPerIPPerHour' => $maxQueriesPerIPPerHour, + 'maxHitsPerQuery' => $maxHitsPerQuery, + ]; } - return $this->client->request($this->context, "PUT", "/1/indexes/" . $this->urlIndexName . "/keys/" . $key , array(), $params, - $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + + return $this->client->request( + $this->context, + 'PUT', + '/1/indexes/'.$this->urlIndexName.'/keys/'.$key, + [], + $params, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } /** - * Send a batch request - * @param $requests an associative array defining the batch request body + * Send a batch request. + * + * @param array $requests an associative array defining the batch request body + * + * @return mixed */ - public function batch($requests) { - return $this->client->request($this->context, "POST", "/1/indexes/" . $this->urlIndexName . "/batch", array(), $requests, - $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout); + public function batch($requests) + { + return $this->client->request( + $this->context, + 'POST', + '/1/indexes/'.$this->urlIndexName.'/batch', + [], + $requests, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); } /** - * Build a batch request - * @param $action the batch action - * @param $objects the array of objects - * @param $withObjectID set an 'objectID' attribute - * @param $objectIDKey the objectIDKey + * Build a batch request. + * + * @param string $action the batch action + * @param array $objects the array of objects + * @param string $withObjectID set an 'objectID' attribute + * @param string $objectIDKey the objectIDKey + * * @return array */ - private function buildBatch($action, $objects, $withObjectID, $objectIDKey = "objectID") { - $requests = array(); + private function buildBatch($action, $objects, $withObjectID, $objectIDKey = 'objectID') + { + $requests = []; foreach ($objects as $obj) { - $req = array("action" => $action, "body" => $obj); + $req = ['action' => $action, 'body' => $obj]; if ($withObjectID && array_key_exists($objectIDKey, $obj)) { - $req["objectID"] = (string) $obj[$objectIDKey]; + $req['objectID'] = (string) $obj[$objectIDKey]; } array_push($requests, $req); } - return array("requests" => $requests); + + return ['requests' => $requests]; + } + + /** + * @param string $query + * @param array|null $params + * + * @return IndexBrowser + */ + private function doBrowse($query, $params = null) + { + return new IndexBrowser($this, $query, $params); + } + + /** + * @param string $query + * @param array|null $params + * @param $cursor + * + * @return mixed + */ + public function browseFrom($query, $params = null, $cursor = null) + { + if ($params === null) { + $params = []; + } + foreach ($params as $key => $value) { + if (gettype($value) == 'array') { + $params[$key] = json_encode($value); + } + } + if ($query != null) { + $params['query'] = $query; + } + if ($cursor != null) { + $params['cursor'] = $cursor; + } + + return $this->client->request( + $this->context, + 'GET', + '/1/indexes/'.$this->urlIndexName.'/browse', + $params, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout + ); + } + + /** + * @param string $name + * @param array $arguments + * + * @return mixed + */ + public function __call($name, $arguments) + { + if ($name !== 'browse') { + return; + } + + if (count($arguments) >= 1 && is_string($arguments[0])) { + return call_user_func_array([$this, 'doBrowse'], $arguments); + } + + return call_user_func_array([$this, 'doBcBrowse'], $arguments); } } \ No newline at end of file diff --git a/lib/AlgoliaSearch/IndexBrowser.php b/lib/AlgoliaSearch/IndexBrowser.php new file mode 100644 index 00000000..8ee5461c --- /dev/null +++ b/lib/AlgoliaSearch/IndexBrowser.php @@ -0,0 +1,160 @@ +index = $index; + $this->query = $query; + $this->params = $params; + + $this->position = 0; + + $this->doQuery($cursor); + } + + /** + * @return mixed + */ + public function current() + { + return $this->hit; + } + + /** + * @return mixed + */ + public function next() + { + return $this->hit; + } + + /** + * @return int + */ + public function key() + { + return $this->position; + } + + /** + * @return bool + */ + public function valid() + { + do { + if ($this->position < count($this->answer['hits'])) { + $this->hit = $this->answer['hits'][$this->position]; + $this->position++; + + return true; + } + + if (isset($this->answer['cursor']) && $this->answer['cursor']) { + $this->position = 0; + + $this->doQuery($this->answer['cursor']); + + continue; + } + + return false; + } while (true); + } + + public function rewind() + { + $this->cursor = null; + $this->position = 0; + } + + /** + * @return int + */ + public function cursor() + { + return $this->answer['cursor']; + } + + /** + * @param int $cursor + */ + private function doQuery($cursor = null) + { + if ($cursor !== null) { + $this->params['cursor'] = $cursor; + } + + $this->answer = $this->index->browseFrom($this->query, $this->params, $cursor); + } +} \ No newline at end of file diff --git a/lib/AlgoliaSearch/PlacesIndex.php b/lib/AlgoliaSearch/PlacesIndex.php new file mode 100644 index 00000000..039b4985 --- /dev/null +++ b/lib/AlgoliaSearch/PlacesIndex.php @@ -0,0 +1,81 @@ +context = $context; + $this->client = $client; + } + + /** + * @param string $query + * @param array|null $args + * + * @return mixed + * + * @throws AlgoliaException + */ + public function search($query, $args = null) + { + if ($args === null) { + $args = []; + } + $args['query'] = $query; + + return $this->client->request( + $this->context, + 'POST', + '/1/places/query', + [], + ['params' => $this->client->buildQuery($args)], + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->searchTimeout + ); + } + + public function setExtraHeader($key, $value) + { + $this->context->setExtraHeader($key, $value); + } + + public function getContext() + { + return $this->context; + } +} \ No newline at end of file diff --git a/lib/AlgoliaSearch/Version.php b/lib/AlgoliaSearch/Version.php index 83834d24..9144c029 100644 --- a/lib/AlgoliaSearch/Version.php +++ b/lib/AlgoliaSearch/Version.php @@ -1,4 +1,5 @@ Date: Fri, 22 Apr 2016 17:12:28 +0200 Subject: [PATCH 20/30] include new client files --- code/Helper/Algoliahelper.php | 2 ++ code/Helper/Data.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/code/Helper/Algoliahelper.php b/code/Helper/Algoliahelper.php index fe9867ba..849d9dae 100644 --- a/code/Helper/Algoliahelper.php +++ b/code/Helper/Algoliahelper.php @@ -7,6 +7,8 @@ require_once Mage::getBaseDir('lib').'/AlgoliaSearch/ClientContext.php'; require_once Mage::getBaseDir('lib').'/AlgoliaSearch/Client.php'; require_once Mage::getBaseDir('lib').'/AlgoliaSearch/Index.php'; + require_once Mage::getBaseDir('lib').'/AlgoliaSearch/PlacesIndex.php'; + require_once Mage::getBaseDir('lib').'/AlgoliaSearch/IndexBrowser.php'; } class Algolia_Algoliasearch_Helper_Algoliahelper extends Mage_Core_Helper_Abstract diff --git a/code/Helper/Data.php b/code/Helper/Data.php index 5e291f5c..16426594 100644 --- a/code/Helper/Data.php +++ b/code/Helper/Data.php @@ -7,6 +7,8 @@ require_once Mage::getBaseDir('lib').'/AlgoliaSearch/ClientContext.php'; require_once Mage::getBaseDir('lib').'/AlgoliaSearch/Client.php'; require_once Mage::getBaseDir('lib').'/AlgoliaSearch/Index.php'; + require_once Mage::getBaseDir('lib').'/AlgoliaSearch/PlacesIndex.php'; + require_once Mage::getBaseDir('lib').'/AlgoliaSearch/IndexBrowser.php'; } class Algolia_Algoliasearch_Helper_Data extends Mage_Core_Helper_Abstract From 39eee64be925b8a24dc9efc87541d7e764925185 Mon Sep 17 00:00:00 2001 From: Maxime Locqueville Date: Fri, 22 Apr 2016 17:19:20 +0200 Subject: [PATCH 21/30] fix crt path --- lib/AlgoliaSearch/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/AlgoliaSearch/Client.php b/lib/AlgoliaSearch/Client.php index a0b302e7..3d336ae3 100644 --- a/lib/AlgoliaSearch/Client.php +++ b/lib/AlgoliaSearch/Client.php @@ -82,7 +82,7 @@ public function __construct($applicationID, $apiKey, $hostsArray = null, $option throw new \Exception('AlgoliaSearch requires the JSON PHP extension.'); } - $this->caInfoPath = __DIR__.'/../../resources/ca-bundle.crt'; + $this->caInfoPath = __DIR__.'/resources/ca-bundle.crt'; foreach ($options as $option => $value) { switch ($option) { case self::CAINFO: From e6663708b79ba49c74782c93125171cf1bffa0b3 Mon Sep 17 00:00:00 2001 From: Maxime Locqueville Date: Fri, 22 Apr 2016 17:56:57 +0200 Subject: [PATCH 22/30] Use secured api keys to only retrieve one group price in the frontend. Fixes #319 --- code/Helper/Algoliahelper.php | 6 ++++ code/Helper/Config.php | 30 +++++++++++++++++++ .../frontend/template/beforetopsearch.phtml | 4 ++- design/frontend/template/topsearch.phtml | 2 +- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/code/Helper/Algoliahelper.php b/code/Helper/Algoliahelper.php index 849d9dae..2267a9c9 100644 --- a/code/Helper/Algoliahelper.php +++ b/code/Helper/Algoliahelper.php @@ -13,6 +13,7 @@ class Algolia_Algoliasearch_Helper_Algoliahelper extends Mage_Core_Helper_Abstract { + /** @var \AlgoliaSearch\Client */ protected $client; protected $config; @@ -28,6 +29,11 @@ public function resetCredentialsFromConfig() $this->client = new \AlgoliaSearch\Client($this->config->getApplicationID(), $this->config->getAPIKey()); } + public function generateSearchSecuredApiKey($key, $params = array()) + { + return $this->client->generateSecuredApiKey($key, $params); + } + public function getIndex($name) { return $this->client->initIndex($name); diff --git a/code/Helper/Config.php b/code/Helper/Config.php index 9c086872..582eb806 100644 --- a/code/Helper/Config.php +++ b/code/Helper/Config.php @@ -315,6 +315,36 @@ public function getIndexPrefix($storeId = NULL) return trim(Mage::getStoreConfig(self::INDEX_PREFIX, $storeId)); } + public function getAttributesToRetrieve($group_id) + { + if (false === $this->isCustomerGroupsEnabled()) { + return []; + } + + $attributes = array(); + foreach ($this->getProductAdditionalAttributes() as $attribute) { + if ($attribute['attribute'] !== 'price') { + $attributes[] = $attribute['attribute']; + } + } + + $attributes = array_merge($attributes, ['objectID', 'name', 'url', 'visibility_search', 'visibility_catalog', 'categories', 'categories_without_path', 'thumbnail_url', 'image_url', 'in_stock', 'type_id']); + + $currencies = Mage::getModel('directory/currency')->getConfigAllowCurrencies(); + + foreach ($currencies as $currency) { + $attributes[] = 'price.'.$currency.'.default'; + $attributes[] = 'price.'.$currency.'.default_formated'; + $attributes[] = 'price.'.$currency.'.group_'.$group_id; + $attributes[] = 'price.'.$currency.'.group_'.$group_id.'_formated'; + $attributes[] = 'price.'.$currency.'.special_from_date'; + $attributes[] = 'price.'.$currency.'.special_to_date'; + } + + + return ['attributesToRetrieve' => $attributes]; + } + public function getCategoryAdditionalAttributes($storeId = NULL) { $attrs = unserialize(Mage::getStoreConfig(self::CATEGORY_ATTRIBUTES, $storeId)); diff --git a/design/frontend/template/beforetopsearch.phtml b/design/frontend/template/beforetopsearch.phtml index 734d8bce..f067f708 100644 --- a/design/frontend/template/beforetopsearch.phtml +++ b/design/frontend/template/beforetopsearch.phtml @@ -4,6 +4,7 @@ $config = Mage::helper('algoliasearch/config'); $catalogSearchHelper = $this->helper('catalogsearch'); /** @var $catalogSearchHelper Mage_CatalogSearch_Helper_Data */ $algoliaSearchHelper = $this->helper('algoliasearch'); /** @var $algoliaSearchHelper Algolia_Algoliasearch_Helper_Data */ $product_helper = Mage::helper('algoliasearch/entity_producthelper'); +$algolia_helper = Mage::helper('algoliasearch/algoliahelper'); $base_url = Mage::getBaseUrl(); @@ -12,6 +13,7 @@ $isCategoryPage = false; $currency_code = Mage::app()->getStore()->getCurrentCurrencyCode(); $group_id = Mage::getSingleton('customer/session')->getCustomerGroupId(); + $price_key = $config->isCustomerGroupsEnabled(Mage::app()->getStore()->getStoreId()) ? '.'.$currency_code.'.group_'.$group_id : '.'.$currency_code.'.default'; $allDepartments = "All departments"; @@ -106,7 +108,7 @@ if ($config->isInstantEnabled() && $isSearchPage) { }, applicationId: 'getApplicationID() ?>', indexName: 'getBaseIndexName(); ?>', - apiKey: 'getSearchOnlyAPIKey() ?>', + apiKey: 'generateSearchSecuredApiKey($config->getSearchOnlyAPIKey(), $config->getAttributesToRetrieve($group_id)) ?>', facets: getFacets()); ?>, hitsPerPage: getNumberOfProductResults() ?>, sortingIndices: getSortingIndices())); ?>, diff --git a/design/frontend/template/topsearch.phtml b/design/frontend/template/topsearch.phtml index a019db4a..56ff1ed9 100644 --- a/design/frontend/template/topsearch.phtml +++ b/design/frontend/template/topsearch.phtml @@ -9,7 +9,7 @@ helper('catalogsearch'); -$group_id = Mage::getSingleton('customer/session')->getCustomer()->getGroupId(); +$group_id = Mage::getSingleton('customer/session')->getCustomerGroupId(); $currency_code = Mage::app()->getStore()->getCurrentCurrencyCode(); $price_key = $config->isCustomerGroupsEnabled(Mage::app()->getStore()->getStoreId()) ? '.'.$currency_code.'.group_'.$group_id : '.'.$currency_code.'.default'; From 6a78d34a315a5ce34459a0738a0010c7053c72b5 Mon Sep 17 00:00:00 2001 From: Maxime Locqueville Date: Mon, 25 Apr 2016 16:27:34 +0200 Subject: [PATCH 23/30] Better handling of include in navigation config. Fixes #321 --- code/Helper/Config.php | 6 ++++++ code/Helper/Entity/Categoryhelper.php | 20 ++++++++++--------- code/Helper/Entity/Producthelper.php | 1 - code/Model/Indexer/Algoliacategories.php | 2 +- code/etc/config.xml | 1 + code/etc/system.xml | 14 +++++++++++++ .../frontend/template/beforetopsearch.phtml | 5 +++++ 7 files changed, 38 insertions(+), 11 deletions(-) diff --git a/code/Helper/Config.php b/code/Helper/Config.php index 582eb806..d1145e13 100644 --- a/code/Helper/Config.php +++ b/code/Helper/Config.php @@ -38,6 +38,7 @@ class Algolia_Algoliasearch_Helper_Config extends Mage_Core_Helper_Abstract const CATEGORY_ATTRIBUTES = 'algoliasearch/categories/category_additional_attributes2'; const INDEX_PRODUCT_COUNT = 'algoliasearch/categories/index_product_count'; const CATEGORY_CUSTOM_RANKING = 'algoliasearch/categories/custom_ranking_category_attributes'; + const SHOW_CATS_NOT_INCLUDED_IN_NAVIGATION = 'algoliasearch/categories/show_cats_not_included_in_navigation'; const IS_ACTIVE = 'algoliasearch/queue/active'; @@ -60,6 +61,11 @@ class Algolia_Algoliasearch_Helper_Config extends Mage_Core_Helper_Abstract protected $_productTypeMap = array(); + public function showCatsNotIncludedInNavigation($storeId = null) + { + return Mage::getStoreConfigFlag(self::SHOW_CATS_NOT_INCLUDED_IN_NAVIGATION, $storeId); + } + public function isDefaultSelector($storeId = null) { return '.algolia-search-input' === $this->getAutocompleteSelector($storeId); diff --git a/code/Helper/Entity/Categoryhelper.php b/code/Helper/Entity/Categoryhelper.php index 3a18609c..f4352086 100644 --- a/code/Helper/Entity/Categoryhelper.php +++ b/code/Helper/Entity/Categoryhelper.php @@ -66,13 +66,14 @@ public function getCategoryCollectionQuery($storeId, $categoryIds = null) foreach ($unserializedCategorysAttrs as $attr) $additionalAttr[] = $attr['attribute']; + $additionalAttr[] = 'include_in_menu'; + $categories ->addPathFilter($storeRootCategoryPath) ->addNameToResult() ->addUrlRewriteToResult() ->addIsActiveFilter() ->setStoreId($storeId) - ->addAttributeToFilter('include_in_menu', '1') ->addAttributeToSelect(array_merge(array('name'), $additionalAttr)) ->addFieldToFilter('level', array('gt' => 1)); @@ -140,14 +141,15 @@ public function getObject(Mage_Catalog_Model_Category $category) } catch (Exception $e) { /* no image, no default: not fatal */ } $data = array( - 'objectID' => $category->getId(), - 'name' => $category->getName(), - 'path' => $path, - 'level' => $category->getLevel(), - 'url' => $category->getUrl(), - '_tags' => array('category'), - 'popularity' => 1, - 'product_count' => $category->getProductCount() + 'objectID' => $category->getId(), + 'name' => $category->getName(), + 'path' => $path, + 'level' => $category->getLevel(), + 'url' => $category->getUrl(), + 'include_in_menu' => $category->getIncludeInMenu(), + '_tags' => array('category'), + 'popularity' => 1, + 'product_count' => $category->getProductCount() ); if ( ! empty($image_url)) { diff --git a/code/Helper/Entity/Producthelper.php b/code/Helper/Entity/Producthelper.php index d885f780..f175b879 100644 --- a/code/Helper/Entity/Producthelper.php +++ b/code/Helper/Entity/Producthelper.php @@ -526,7 +526,6 @@ public function getObject(Mage_Catalog_Model_Product $product) $categoryCollection = Mage::getResourceModel('catalog/category_collection') ->addAttributeToSelect('name') ->addAttributeToFilter('entity_id', $_categoryIds) - ->addAttributeToFilter('include_in_menu', '1') ->addFieldToFilter('level', array('gt' => 1)) ->addIsActiveFilter(); diff --git a/code/Model/Indexer/Algoliacategories.php b/code/Model/Indexer/Algoliacategories.php index 1dc86667..ea8a683f 100644 --- a/code/Model/Indexer/Algoliacategories.php +++ b/code/Model/Indexer/Algoliacategories.php @@ -70,7 +70,7 @@ protected function _registerCatalogCategoryEvent(Mage_Index_Model_Event $event) $category = $event->getDataObject(); $productIds = $category->getAffectedProductIds(); - if (! $category->getData('is_active') || ! $category->getData('include_in_menu')) + if (! $category->getData('is_active')) { $event->addNewData('catalogsearch_delete_category_id', array_merge(array($category->getId()), $category->getAllChildren(TRUE))); diff --git a/code/etc/config.xml b/code/etc/config.xml index 5ee43393..938b8cd2 100644 --- a/code/etc/config.xml +++ b/code/etc/config.xml @@ -172,6 +172,7 @@ 5 a:7:{s:18:"_1427960339954_954";a:4:{s:9:"attribute";s:4:"name";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}s:18:"_1427960354437_437";a:4:{s:9:"attribute";s:4:"path";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}s:18:"_1427961004989_989";a:4:{s:9:"attribute";s:11:"description";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:9:"unordered";}s:18:"_1427961205511_511";a:4:{s:9:"attribute";s:10:"meta_title";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}s:18:"_1427961216134_134";a:4:{s:9:"attribute";s:13:"meta_keywords";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}s:18:"_1427961216916_916";a:4:{s:9:"attribute";s:16:"meta_description";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:9:"unordered";}s:18:"_1427977778338_338";a:4:{s:9:"attribute";s:13:"product_count";s:10:"searchable";s:1:"0";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}} a:1:{s:18:"_1427961035192_192";a:2:{s:9:"attribute";s:13:"product_count";s:5:"order";s:4:"desc";}} + 0 0 diff --git a/code/etc/system.xml b/code/etc/system.xml index 2ac52a2d..a01ab386 100644 --- a/code/etc/system.xml +++ b/code/etc/system.xml @@ -474,6 +474,20 @@ ]]> + + + select + adminhtml/system_config_source_yesno + 40 + 1 + 1 + 1 + + + + diff --git a/design/frontend/template/beforetopsearch.phtml b/design/frontend/template/beforetopsearch.phtml index f067f708..5cf38e93 100644 --- a/design/frontend/template/beforetopsearch.phtml +++ b/design/frontend/template/beforetopsearch.phtml @@ -126,6 +126,7 @@ if ($config->isInstantEnabled() && $isSearchPage) { refinement_value: '', path: '' }, + show_cats_not_included_in_navigation: showCatsNotIncludedInNavigation() ? "true" : "false"; ?>, showSuggestionsOnNoResultsPage: showSuggestionsOnNoResultsPage() ? "true" : "false"; ?>, baseUrl: '', popularQueries: getPopularQueries()); ?> @@ -306,6 +307,10 @@ if ($config->isInstantEnabled() && $isSearchPage) { } else if (section.name === "categories" || section.name === "pages") { + if (section.name === "categories" && algoliaConfig.show_cats_not_included_in_navigation == false) { + options.numericFilters = 'include_in_menu=1'; + } + source = { source: $.fn.autocomplete.sources.hits(algolia_client.initIndex(algoliaConfig.indexName + "_" + section.name), options), name: i, From f92caa4b1f11dc3e056a3d346915b6549224bf52 Mon Sep 17 00:00:00 2001 From: Maxime Locqueville Date: Mon, 25 Apr 2016 17:35:52 +0200 Subject: [PATCH 24/30] add an option to include data from out-of-stock sub products. Fixes #358 --- code/Helper/Config.php | 6 ++++++ code/Helper/Entity/Producthelper.php | 13 ++++++++----- code/etc/config.xml | 1 + code/etc/system.xml | 14 ++++++++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/code/Helper/Config.php b/code/Helper/Config.php index d1145e13..842160d9 100644 --- a/code/Helper/Config.php +++ b/code/Helper/Config.php @@ -34,6 +34,7 @@ class Algolia_Algoliasearch_Helper_Config extends Mage_Core_Helper_Abstract const PRODUCT_CUSTOM_RANKING = 'algoliasearch/products/custom_ranking_product_attributes'; const RESULTS_LIMIT = 'algoliasearch/products/results_limit'; const SHOW_SUGGESTIONS_NO_RESULTS = 'algoliasearch/products/show_suggestions_on_no_result_page'; + const INDEX_OUT_OF_STOCK_OPTIONS = 'algoliasearch/products/index_out_of_stock_options'; const CATEGORY_ATTRIBUTES = 'algoliasearch/categories/category_additional_attributes2'; const INDEX_PRODUCT_COUNT = 'algoliasearch/categories/index_product_count'; @@ -61,6 +62,11 @@ class Algolia_Algoliasearch_Helper_Config extends Mage_Core_Helper_Abstract protected $_productTypeMap = array(); + public function indexOutOfStockOptions($storeId = null) + { + return Mage::getStoreConfigFlag(self::INDEX_OUT_OF_STOCK_OPTIONS, $storeId); + } + public function showCatsNotIncludedInNavigation($storeId = null) { return Mage::getStoreConfigFlag(self::SHOW_CATS_NOT_INCLUDED_IN_NAVIGATION, $storeId); diff --git a/code/Helper/Entity/Producthelper.php b/code/Helper/Entity/Producthelper.php index f175b879..cc4a5a93 100644 --- a/code/Helper/Entity/Producthelper.php +++ b/code/Helper/Entity/Producthelper.php @@ -82,14 +82,17 @@ public function getProductCollectionQuery($storeId, $productIds = null, $only_vi $products = $products->setStoreId($storeId) ->addStoreFilter($storeId); - if ($only_visible) + if ($only_visible) { $products = $products->addAttributeToFilter('visibility', array('in' => Mage::getSingleton('catalog/product_visibility')->getVisibleInSiteIds())); + $products = $products->addFinalPrice(); + } + - if (false === $this->config->getShowOutOfStock($storeId)) + if (false === $this->config->getShowOutOfStock($storeId) && $only_visible == true) { Mage::getSingleton('cataloginventory/stock')->addInStockFilterToCollection($products); + } - $products = $products->addFinalPrice() - ->addAttributeToSelect('special_from_date') + $products = $products->addAttributeToSelect('special_from_date') ->addAttributeToSelect('special_to_date') ->addAttributeToFilter('status', Mage_Catalog_Model_Product_Status::STATUS_ENABLED); @@ -726,7 +729,7 @@ public function getObject(Mage_Catalog_Model_Product $product) { $isInStock = (int) $sub_product->getStockItem()->getIsInStock(); - if ($isInStock == false) + if ($isInStock == false && $this->config->indexOutOfStockOptions($product->getStoreId()) == false) continue; $all_sub_products_out_of_stock = false; diff --git a/code/etc/config.xml b/code/etc/config.xml index 938b8cd2..899401be 100644 --- a/code/etc/config.xml +++ b/code/etc/config.xml @@ -151,6 +151,7 @@ a:1:{s:18:"_1427960305274_274";a:2:{s:9:"attribute";s:11:"ordered_qty";s:5:"order";s:4:"desc";}} 1000 1 + 0 1 diff --git a/code/etc/system.xml b/code/etc/system.xml index a01ab386..7593f573 100644 --- a/code/etc/system.xml +++ b/code/etc/system.xml @@ -428,6 +428,20 @@ ]]> + + + select + adminhtml/system_config_source_yesno + 60 + 1 + 1 + 1 + + + + From 81337f92db03eb8b4697f106c18aca3ce8abcb95 Mon Sep 17 00:00:00 2001 From: Tom Richards Date: Wed, 27 Apr 2016 16:37:30 -0400 Subject: [PATCH 25/30] Correct typos in system config comments --- code/etc/system.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/etc/system.xml b/code/etc/system.xml index 7593f573..03773d59 100644 --- a/code/etc/system.xml +++ b/code/etc/system.xml @@ -32,7 +32,7 @@ 10 1 1 - 1 + 1 1 1 top.search and content block. + If set to Yes, the search box will display a search-as-you-type drop-down menu. It requires your theme to expose a top.search and content block. ]]> @@ -531,7 +531,7 @@ 1 1 - If your images are already present in some size, eg. 265x265, Algolia's index job may not have to resize those, potentially saving time and resources.]]> + If your images are already present in some size, e.g. 265x265, Algolia's index job may not have to resize those, potentially saving time and resources.]]> @@ -564,7 +564,7 @@ 1 + If enabled, all indexing operations (add, remove & update operations) will be done asynchronously using the CRON mechanism.

To schedule the run you need to add this in your crontab:
*/5 * * * * php -f /absolute/path/to/magento/shell/indexer.php -- -reindex algolia_queue_runner From bf68464e862c5365e8f0cd3ce32477274e66a922 Mon Sep 17 00:00:00 2001 From: Tom Richards Date: Wed, 27 Apr 2016 17:08:46 -0400 Subject: [PATCH 26/30] Process template directives for CMS pages --- code/Helper/Entity/Pagehelper.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/code/Helper/Entity/Pagehelper.php b/code/Helper/Entity/Pagehelper.php index 57038427..4287be9a 100644 --- a/code/Helper/Entity/Pagehelper.php +++ b/code/Helper/Entity/Pagehelper.php @@ -45,14 +45,15 @@ public function getPages($storeId) if (! $page->getId()) continue; - $page_obj['objectID'] = $page->getId(); + $tmplProc = Mage::helper('cms')->getPageTemplateProcessor(); + $page_obj['objectID'] = $page->getId(); $page_obj['url'] = Mage::helper('cms/page')->getPageUrl($page->getId()); - $page_obj['content'] = $this->strip($page->getContent()); + $page_obj['content'] = $this->strip($tmplProc->filter($page->getContent())); $pages[] = $page_obj; } return $pages; } -} \ No newline at end of file +} From 12064ea3ab41c73bb96ac306aee6a702da36c562 Mon Sep 17 00:00:00 2001 From: Tom Richards Date: Wed, 27 Apr 2016 17:53:56 -0400 Subject: [PATCH 27/30] Add system config option for template directives --- code/etc/config.xml | 3 ++- code/etc/system.xml | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/code/etc/config.xml b/code/etc/config.xml index 899401be..52c8c126 100644 --- a/code/etc/config.xml +++ b/code/etc/config.xml @@ -168,6 +168,7 @@ a:1:{s:18:"_1450089283397_397";a:3:{s:4:"name";s:5:"pages";s:5:"label";s:5:"Pages";s:11:"hitsPerPage";s:1:"2";}} 1000 2 + 1 5 @@ -201,4 +202,4 @@ - + diff --git a/code/etc/system.xml b/code/etc/system.xml index 7593f573..22ec14a2 100644 --- a/code/etc/system.xml +++ b/code/etc/system.xml @@ -32,7 +32,7 @@ 10 1 1 - 1 + 1 1 + + + select + adminhtml/system_config_source_yesno + 30 + 1 + 1 + 1 + + {{{{block type="core/template" ...}}) will be processed by default. Set this to "No" if you don't want to have template content indexed. + ]]> + + From 48da12e335a7ef16663a9b499a65fd58fefb0a73 Mon Sep 17 00:00:00 2001 From: Tom Richards Date: Wed, 27 Apr 2016 18:02:57 -0400 Subject: [PATCH 28/30] Conditionally render template directives --- code/Helper/Config.php | 6 ++++++ code/Helper/Entity/Pagehelper.php | 8 ++++++-- code/etc/system.xml | 6 +++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/code/Helper/Config.php b/code/Helper/Config.php index 842160d9..8133875a 100644 --- a/code/Helper/Config.php +++ b/code/Helper/Config.php @@ -28,6 +28,7 @@ class Algolia_Algoliasearch_Helper_Config extends Mage_Core_Helper_Abstract const EXCLUDED_PAGES = 'algoliasearch/autocomplete/excluded_pages'; const MIN_POPULARITY = 'algoliasearch/autocomplete/min_popularity'; const MIN_NUMBER_OF_RESULTS = 'algoliasearch/autocomplete/min_number_of_results'; + const RENDER_TEMPLATE_DIRECTIVES = 'algoliasearch/autocomplete/render_template_directives'; const NUMBER_OF_PRODUCT_RESULTS = 'algoliasearch/products/number_product_results'; const PRODUCT_ATTRIBUTES = 'algoliasearch/products/product_additional_attributes'; @@ -271,6 +272,11 @@ public function getExcludedPages($storeId = NULL) return array(); } + public function getRenderTemplateDirectives($storeId = NULL) + { + return Mage::getStoreConfigFlag(self::RENDER_TEMPLATE_DIRECTIVES, $storeId); + } + public function getSortingIndices($storeId = NULL) { $product_helper = Mage::helper('algoliasearch/entity_producthelper'); diff --git a/code/Helper/Entity/Pagehelper.php b/code/Helper/Entity/Pagehelper.php index 4287be9a..a6e85763 100644 --- a/code/Helper/Entity/Pagehelper.php +++ b/code/Helper/Entity/Pagehelper.php @@ -45,11 +45,15 @@ public function getPages($storeId) if (! $page->getId()) continue; - $tmplProc = Mage::helper('cms')->getPageTemplateProcessor(); + $content = $page->getContent(); + if ($this->config->getRenderTemplateDirectives()) { + $tmplProc = Mage::helper('cms')->getPageTemplateProcessor(); + $content = $tmplProc->filter($content); + } $page_obj['objectID'] = $page->getId(); $page_obj['url'] = Mage::helper('cms/page')->getPageUrl($page->getId()); - $page_obj['content'] = $this->strip($tmplProc->filter($page->getContent())); + $page_obj['content'] = $this->strip($content); $pages[] = $page_obj; } diff --git a/code/etc/system.xml b/code/etc/system.xml index 22ec14a2..f7e21952 100644 --- a/code/etc/system.xml +++ b/code/etc/system.xml @@ -258,8 +258,7 @@ 1
- Do not forget to trigger the Algolia Search indexers whenever you modify those settings. + Configure here the pages you don't want to search in. ]]>
@@ -273,7 +272,8 @@ 1 {{{{block type="core/template" ...}}) will be processed by default. Set this to "No" if you don't want to have template content indexed. + For CMS pages, template directives (e.g. {{block type="core/template" ...}}) will be processed by default. Set this to "No" if you don't want to have template content indexed.

+ Do not forget to trigger the Algolia Search indexers whenever you modify those settings. ]]>
From d2e0138beb221b724bf0ecff8321dae2d5776103 Mon Sep 17 00:00:00 2001 From: Maxime Locqueville Date: Thu, 28 Apr 2016 16:03:28 +0200 Subject: [PATCH 29/30] bump to 1.5.5 --- Algolia_Algoliasearch.xml | 2 +- CHANGELOG.md | 16 +++++++++++++++- code/Helper/Data.php | 2 +- code/etc/config.xml | 2 +- code/etc/system.xml | 2 +- doc/_config.yml | 2 +- 6 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Algolia_Algoliasearch.xml b/Algolia_Algoliasearch.xml index 107e7479..475267c8 100644 --- a/Algolia_Algoliasearch.xml +++ b/Algolia_Algoliasearch.xml @@ -4,7 +4,7 @@ true community - 1.5.4 + 1.5.5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f37fe17..cc009904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ ## Change Log +### 1.5.5 + +- NEW: Add an option to include data from out-of-stock sub products +- NEW: Use secured api keys to only retrieve one group price in the frontend +- NEW: Better update strategy to simplify the indexer code and to avoid missing deleted products event +- UPDATE: Better handling of include in navigation config +- UPDATE: underlying php client +- UPDATE: Conditionally render template directives +- UPDATE: Make sub product skus searchable +- FIX: slaves creation issue +- FIX: small price issue +- FIX: fallback to default search in case there is a error from the api + + ### 1.5.4 - UPDATED: instantsearch.js update @@ -198,6 +212,6 @@ - Upgrade the underlying Algolia PHP API client to 1.5.5 (high available DNS) ### 1.0.3 - - Upgrade the underlying PHP API client to 1.5.4 + - Upgrade the underlying PHP API client to 1.5.5 - Fix deadlock that may occur with order processing - Fix results saved every search (remove flag probably added for debugging). diff --git a/code/Helper/Data.php b/code/Helper/Data.php index 16426594..8ec3d5ad 100644 --- a/code/Helper/Data.php +++ b/code/Helper/Data.php @@ -27,7 +27,7 @@ class Algolia_Algoliasearch_Helper_Data extends Mage_Core_Helper_Abstract public function __construct() { - \AlgoliaSearch\Version::$custom_value = " Magento (1.5.4)"; + \AlgoliaSearch\Version::$custom_value = " Magento (1.5.5)"; $this->algolia_helper = Mage::helper('algoliasearch/algoliahelper'); diff --git a/code/etc/config.xml b/code/etc/config.xml index 899401be..a2614c0f 100644 --- a/code/etc/config.xml +++ b/code/etc/config.xml @@ -2,7 +2,7 @@ - 1.5.4 + 1.5.5 diff --git a/code/etc/system.xml b/code/etc/system.xml index 7593f573..1b136182 100644 --- a/code/etc/system.xml +++ b/code/etc/system.xml @@ -4,7 +4,7 @@