diff --git a/Block/Category.php b/Block/Category.php index dac638abb..d7a294557 100644 --- a/Block/Category.php +++ b/Block/Category.php @@ -39,7 +39,7 @@ use Magento\Framework\Registry; use Magento\Framework\View\Element\Template; use Magento\Framework\View\Element\Template\Context; -use Nosto\Model\Category as NostoCategory; +use Nosto\Model\Category\Category as NostoCategory; use Nosto\Tagging\Helper\Account as NostoHelperAccount; use Nosto\Tagging\Helper\Scope as NostoHelperScope; use Nosto\Tagging\Model\Category\Builder as NostoCategoryBuilder; @@ -104,7 +104,7 @@ public function __construct( /** * Returns the current category as a slash delimited string * - * @return string|null the current category as a slash delimited string + * @return NostoCategory|null the current category as a slash delimited string */ private function getNostoCategory() { @@ -120,12 +120,10 @@ private function getNostoCategory() /** * Returns the HTML to render categories * - * @return NostoCategory + * @return NostoCategory|null */ public function getAbstractObject() { - $category = new NostoCategory(); - $category->setCategoryString($this->getNostoCategory()); - return $category; + return $this->getNostoCategory(); } } diff --git a/Block/Product.php b/Block/Product.php index 7224344d4..5eb3d8a81 100644 --- a/Block/Product.php +++ b/Block/Product.php @@ -142,19 +142,6 @@ public function getAbstractObject() ); } - /** - * Returns the Nosto category DTO. - * - * @return string|null the current category as a slash-delimited string - */ - public function getNostoCategory() - { - /** @phan-suppress-next-line PhanDeprecatedFunction */ - $category = $this->_coreRegistry->registry('current_category'); - $store = $this->nostoHelperScope->getStore(); - return $category !== null ? $this->categoryBuilder->build($category, $store) : null; - } - /** * Formats a price e.g. "1234.56". * diff --git a/CHANGELOG.md b/CHANGELOG.md index cc6de06a2..7aab3cefc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ All notable changes to this project will be documented in this file. This project adheres to Semantic Versioning. +### 7.3.0 +* Update tagging with category listing support +* Add category export ### 7.2.6 * Convert price in cart tagging item to avoid rounding errors diff --git a/Controller/Export/Category.php b/Controller/Export/Category.php new file mode 100644 index 000000000..12603a246 --- /dev/null +++ b/Controller/Export/Category.php @@ -0,0 +1,99 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Controller\Export; + +use Magento\Framework\App\Action\Context; +use Magento\Store\Model\Store; +use Nosto\Model\AbstractCollection; +use Nosto\Model\Category\CategoryCollection; +use Nosto\NostoException; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Model\Category\CollectionBuilder; + +/** + * Category export controller used to export category tree to Nosto. + * This controller will be called by Nosto when a new account has been created + * from the Magento backend. The controller is public, but the information is + * encrypted with AES, and only Nosto can decrypt it. + */ +class Category extends Base +{ + public const PARAM_PREVIEW = 'preview'; + + /** @var CollectionBuilder */ + private CollectionBuilder $nostoCollectionBuilder; + + /** + * @param Context $context + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperAccount $nostoHelperAccount + * @param CollectionBuilder $collectionBuilder + */ + public function __construct( + Context $context, + NostoHelperScope $nostoHelperScope, + NostoHelperAccount $nostoHelperAccount, + CollectionBuilder $collectionBuilder + ) { + parent::__construct($context, $nostoHelperScope, $nostoHelperAccount); + $this->nostoCollectionBuilder = $collectionBuilder; + } + + /** + * @param Store $store + * @param int $limit + * @param int $offset + * @return AbstractCollection|CategoryCollection + * @throws NostoException + */ + public function buildExportCollection(Store $store, int $limit = 100, int $offset = 0) + { + return $this->nostoCollectionBuilder->buildMany($store, $limit, $offset); + } + + /** + * @param Store $store + * @param $id + * @return AbstractCollection|CategoryCollection + * @throws NostoException + */ + public function buildSingleExportCollection(Store $store, $id) + { + return $this->nostoCollectionBuilder->buildSingle($store, $id); + } +} diff --git a/Model/Category/Builder.php b/Model/Category/Builder.php index 435166884..13b34730d 100644 --- a/Model/Category/Builder.php +++ b/Model/Category/Builder.php @@ -37,33 +37,46 @@ namespace Nosto\Tagging\Model\Category; use Exception; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Category; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\Store; -use Nosto\Model\Category as NostoCategory; +use Nosto\Model\Category\Category as NostoCategory; use Nosto\Tagging\Logger\Logger as NostoLogger; use Nosto\Tagging\Model\Service\Product\Category\CategoryServiceInterface as NostoCategoryService; class Builder { - private NostoLogger $logger; + /** @var CategoryRepositoryInterface */ + private CategoryRepositoryInterface $categoryRepository; + + /** @var ManagerInterface */ private ManagerInterface $eventManager; + + /** @var NostoCategoryService */ private NostoCategoryService $nostoCategoryService; + /** @var NostoLogger */ + private NostoLogger $logger; + /** * Builder constructor. - * @param NostoLogger $logger + * @param CategoryRepositoryInterface $categoryRepository * @param ManagerInterface $eventManager * @param NostoCategoryService $nostoCategoryService + * @param NostoLogger $logger */ public function __construct( - NostoLogger $logger, + CategoryRepositoryInterface $categoryRepository, ManagerInterface $eventManager, - NostoCategoryService $nostoCategoryService + NostoCategoryService $nostoCategoryService, + NostoLogger $logger ) { - $this->logger = $logger; + $this->categoryRepository = $categoryRepository; $this->eventManager = $eventManager; $this->nostoCategoryService = $nostoCategoryService; + $this->logger = $logger; } /** @@ -77,18 +90,16 @@ public function build(Category $category, Store $store) try { $nostoCategory->setId($category->getId()); $nostoCategory->setParentId($category->getParentId()); - $nostoCategory->setImageUrl($category->getImageUrl()); - $nostoCategory->setLevel($category->getLevel()); + $nostoCategory->setTitle($this->getCategoryNameById($category->getId(), $store->getId())); + $path = $this->nostoCategoryService->getCategory($category, $store); + $nostoCategory->setPath($path); + $nostoCategory->setCategoryString($path); $nostoCategory->setUrl($category->getUrl()); - $nostoCategory->setVisibleInMenu($this->getCategoryVisibleInMenu($category)); - $nostoCategory->setCategoryString( - $this->nostoCategoryService->getCategory($category, $store) - ); - $nostoCategory->setName($category->getName()); + $nostoCategory->setAvailable($category->getIsActive() ?? false); } catch (Exception $e) { $this->logger->exception($e); } - if (empty($nostoCategory)) { + if (empty($nostoCategory->getId())) { $nostoCategory = null; } else { $this->eventManager->dispatch( @@ -101,12 +112,13 @@ public function build(Category $category, Store $store) } /** - * @param Category $category - * @return bool + * @param int $id + * @param int $storeId + * @return string + * @throws NoSuchEntityException */ - private function getCategoryVisibleInMenu(Category $category) + private function getCategoryNameById(int $id, int $storeId) { - $visibleInMenu = $category->getIncludeInMenu(); - return $visibleInMenu === "1"; + return $this->categoryRepository->get($id, $storeId)->getName(); } } diff --git a/Model/Category/CollectionBuilder.php b/Model/Category/CollectionBuilder.php new file mode 100644 index 000000000..c408fee7b --- /dev/null +++ b/Model/Category/CollectionBuilder.php @@ -0,0 +1,148 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Category; + +use Exception; +use Magento\Catalog\Model\Category; +use Magento\Store\Model\Store; +use Nosto\NostoException; +use Nosto\Model\Category\CategoryCollection as NostoCategoryCollection; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Magento\Catalog\Model\ResourceModel\Category\Collection as MagentoCategoryCollection; +use Nosto\Tagging\Model\Category\Builder as NostoCategoryBuilder; +use Nosto\Tagging\Model\ResourceModel\Magento\Category\CollectionBuilder as CategoryCollectionBuilder; +use Nosto\Tagging\Model\Service\Product\Category\CategoryServiceInterface; +use Traversable; + +/** + * A builder class for building collection containing Nosto categories + */ +class CollectionBuilder +{ + /** @var NostoCategoryBuilder */ + private NostoCategoryBuilder $categoryBuilder; + + /** @var CategoryCollectionBuilder */ + private CategoryCollectionBuilder $categoryCollectionBuilder; + + /** @var CategoryServiceInterface */ + private CategoryServiceInterface $categoryService; + + /** @var NostoLogger */ + private NostoLogger $logger; + + /** + * Collection constructor. + * @param NostoCategoryBuilder $categoryBuilder + * @param CategoryCollectionBuilder $categoryCollectionBuilder + * @param CategoryServiceInterface $categoryService + * @param NostoLogger $logger + */ + public function __construct( + NostoCategoryBuilder $categoryBuilder, + CategoryCollectionBuilder $categoryCollectionBuilder, + CategoryServiceInterface $categoryService, + NostoLogger $logger + ) { + $this->categoryBuilder = $categoryBuilder; + $this->categoryCollectionBuilder = $categoryCollectionBuilder; + $this->categoryService = $categoryService; + $this->logger = $logger; + } + + /** + * @param Store $store + * @param $id + * @return NostoCategoryCollection + * @throws NostoException + */ + public function buildSingle(Store $store, $id) + { + return $this->load( + $store, + $this->categoryCollectionBuilder->buildSingle($store, $id) + ); + } + + /** + * @param Store $store + * @param int $limit + * @param int $offset + * @return NostoCategoryCollection + * @throws NostoException + */ + public function buildMany(Store $store, int $limit = 100, int $offset = 0) + { + return $this->load( + $store, + $this->categoryCollectionBuilder->buildMany($store, $limit, $offset) + ); + } + + /** + * @param Store $store + * @param $collection + * @return NostoCategoryCollection + * @throws NostoException + */ + private function load(Store $store, $collection) + { + /** @var MagentoCategoryCollection $collection */ + $categories = new NostoCategoryCollection(); + $items = $collection->load(); + if ($items instanceof Traversable === false && !is_array($items)) { + throw new NostoException( + sprintf('Invalid collection type %s for category export', get_class($collection)) + ); + } + foreach ($items as $category) { + /** @var Category $category */ + try { + $nostoCategory = $this->categoryBuilder->build( + $category, + $store + ); + if ($nostoCategory !== null) { + $categories->append($nostoCategory); + } + } catch (Exception $e) { + $this->logger->exception($e); + } + } + return $categories; + } +} diff --git a/Model/ResourceModel/Magento/Category/CollectionBuilder.php b/Model/ResourceModel/Magento/Category/CollectionBuilder.php new file mode 100644 index 000000000..bc670925a --- /dev/null +++ b/Model/ResourceModel/Magento/Category/CollectionBuilder.php @@ -0,0 +1,213 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\ResourceModel\Magento\Category; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Sales\Api\Data\EntityInterface; +use Magento\Store\Model\Store; +use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; +use Zend_Db_Select; + +/** + * A builder class for building product collection with the most common filters + */ +class CollectionBuilder +{ + /** @var CategoryCollection */ + private CategoryCollection $categoryCollection; + + /** + * Collection constructor. + * @param CategoryCollection $categoryCollection, + */ + public function __construct( + CategoryCollection $categoryCollection + ) { + $this->categoryCollection = $categoryCollection; + } + + /** + * @return CategoryCollection + */ + public function build() + { + return $this->categoryCollection; + } + + /** + * Sets the store filter + * + * @param Store $store + * @return $this + */ + public function withStore(Store $store) + { + $this->categoryCollection->setProductStoreId($store->getId()); + $this->categoryCollection->setStore($store); + return $this; + } + + /** + * Defines all attributes to be included into the collection items + * + * @return $this + * @throws LocalizedException + */ + public function withAllAttributes() + { + $this->categoryCollection->addAttributeToSelect('*'); + return $this; + } + + /** + * Sets filter for only given product ids + * + * @param array $ids + * @return $this + */ + public function withIds(array $ids) + { + $this->categoryCollection->addIdFilter($ids); + return $this; + } + + /** + * Sets the sort for the collection + * + * @param string $field + * @param string $sortOrder + * @return $this + */ + public function setSort(string $field, string $sortOrder) + { + $this->categoryCollection->setOrder($field, $sortOrder); + return $this; + } + + /** + * Sets the page size + * + * @param int $pageSize + * @return $this + */ + public function setPageSize(int $pageSize) + { + $this->categoryCollection->setPageSize($pageSize); + return $this; + } + + /** + * Sets the current page + * + * @param int $currentPage + * @return $this + */ + public function setCurrentPage(int $currentPage) + { + $this->categoryCollection->setCurPage($currentPage); + return $this; + } + + /** + * Resets the data and filters in collection + * @return $this + */ + public function reset() + { + return $this->init(); + } + + /** + * Initializes the collection + * + * @return $this + */ + public function init() + { + $this->categoryCollection->clear()->getSelect()->reset(Zend_Db_Select::WHERE); + return $this; + } + + /** + * Initializes the collection with store filter and defaults + * + * @param Store $store + * @return CollectionBuilder + */ + public function initDefault(Store $store) + { + /** @var CategoryCollection $collection */ + return $this + ->reset() + ->withStore($store) + ->setSort(EntityInterface::CREATED_AT, $this->categoryCollection::SORT_ORDER_DESC); + } + + /** + * Builds and returns the collection with single item (if found) + * + * @param Store $store + * @param int $id + * @return CategoryCollection + */ + public function buildSingle(Store $store, int $id) + { + return $this + ->initDefault($store) + ->withIds([$id]) + ->build(); + } + + /** + * Builds collection with default visibility filter and given limit + * and offset. + * + * @param Store $store + * @param int $limit + * @param int $offset + * @return CategoryCollection + */ + public function buildMany(Store $store, int $limit = 100, int $offset = 0) + { + $currentPage = ($offset / $limit) + 1; + return $this + ->initDefault($store) + ->setPageSize($limit) + ->setCurrentPage($currentPage) + ->build(); + } +} diff --git a/Observer/Category/Update.php b/Observer/Category/Update.php new file mode 100644 index 000000000..43b3ead85 --- /dev/null +++ b/Observer/Category/Update.php @@ -0,0 +1,148 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Observer\Category; + +use Exception; +use Magento\Catalog\Model\Category; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Module\Manager as ModuleManager; +use Magento\Store\Model\Store; +use Nosto\Operation\Category\CategoryUpdate as NostoCategoryUpdate; +use Nosto\Request\Http\HttpRequest; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Helper\Url as NostoHelperUrl; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Category\Builder as NostoCategoryBuilder; +use Nosto\Types\Signup\AccountInterface; + +class Update implements ObserverInterface +{ + private NostoHelperData $nostoHelperData; + private NostoHelperAccount $nostoHelperAccount; + private NostoCategoryBuilder $nostoCategoryBuilder; + private NostoLogger $logger; + private ModuleManager $moduleManager; + private NostoHelperUrl $nostoHelperUrl; + + /** + * Save constructor. + * @param NostoHelperData $nostoHelperData + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoCategoryBuilder $nostoCategoryBuilder + * @param NostoLogger $logger + * @param ModuleManager $moduleManager + * @param NostoHelperUrl $nostoHelperUrl + */ + public function __construct( + NostoHelperData $nostoHelperData, + NostoHelperAccount $nostoHelperAccount, + NostoCategoryBuilder $nostoCategoryBuilder, + NostoLogger $logger, + ModuleManager $moduleManager, + NostoHelperUrl $nostoHelperUrl + ) { + $this->nostoHelperData = $nostoHelperData; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->nostoCategoryBuilder = $nostoCategoryBuilder; + $this->logger = $logger; + $this->moduleManager = $moduleManager; + $this->nostoHelperUrl = $nostoHelperUrl; + } + + /** + * Event handler for the "catalog_category_save_after" and event. + * Sends a category update API call to Nosto. + * + * @param Observer $observer + * @return void + * @suppress PhanDeprecatedFunction + * @suppress PhanTypeMismatchArgument + */ + public function execute(Observer $observer) + { + if ($this->moduleManager->isEnabled(NostoHelperData::MODULE_NAME)) { + HttpRequest::buildUserAgent( + 'Magento', + $this->nostoHelperData->getPlatformVersion(), + $this->nostoHelperData->getModuleVersion() + ); + + /* @var Category $category */ + /** @noinspection PhpUndefinedMethodInspection */ + $category = $observer->getCategory(); + + $store = $category->getStore(); + $nostoAccount = $this->nostoHelperAccount->findAccount( + $store + ); + if ($nostoAccount !== null) { + $this->updateCategory($category, $nostoAccount, $store); + } + } + } + + /** + * Send a category update to Nosto + * + * @param Category $category + * @param AccountInterface $nostoAccount + * @param Store $store + */ + private function updateCategory(Category $category, AccountInterface $nostoAccount, Store $store) + { + $nostoCategory = $this->nostoCategoryBuilder->build($category, $store); + try { + $categoryService = new NostoCategoryUpdate( + $nostoCategory, + $nostoAccount, + $this->nostoHelperUrl->getActiveDomain($store) + ); + $categoryService->execute(); + } catch (Exception $e) { + $this->logger->error( + sprintf( + 'Failed to update categoeries with id %s. + Message was: %s', + $category->getId(), + $e->getMessage() + ) + ); + } + } +} diff --git a/composer.json b/composer.json index a228e59fe..d7b05b19c 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "nosto/module-nostotagging", "description": "Increase your conversion rate and average order value by delivering your customers personalized product recommendations throughout their shopping journey.", "type": "magento2-module", - "version": "7.2.6", + "version": "7.3.0", "require-dev": { "phpmd/phpmd": "^2.5", "sebastian/phpcpd": "*", @@ -41,7 +41,7 @@ "php": ">=7.4.0", "magento/framework": ">=101.0.6|~104.0", "ext-json": "*", - "nosto/php-sdk": "^6.2" + "nosto/php-sdk": "^7.1" }, "repositories": [ { diff --git a/composer.lock b/composer.lock index 5db72dc16..ab769840f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e48b5a6cae8345900601f3e5cc5f90c3", + "content-hash": "aa6d7af5994cf533b8e395bf62d5c06d", "packages": [ { "name": "brick/math", @@ -2866,16 +2866,16 @@ }, { "name": "nosto/php-sdk", - "version": "6.2.2", + "version": "7.1.1", "source": { "type": "git", "url": "https://github.com/Nosto/nosto-php-sdk.git", - "reference": "386b6731987b9d6ff4f7849bc4ddec836080a9e0" + "reference": "8f14a91acf7f60f6fb48eab1dfdd61a7f97eef4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nosto/nosto-php-sdk/zipball/386b6731987b9d6ff4f7849bc4ddec836080a9e0", - "reference": "386b6731987b9d6ff4f7849bc4ddec836080a9e0", + "url": "https://api.github.com/repos/Nosto/nosto-php-sdk/zipball/8f14a91acf7f60f6fb48eab1dfdd61a7f97eef4f", + "reference": "8f14a91acf7f60f6fb48eab1dfdd61a7f97eef4f", "shasum": "" }, "require": { @@ -2916,9 +2916,9 @@ "description": "PHP SDK for developing Nosto modules for e-commerce platforms", "support": { "issues": "https://github.com/Nosto/nosto-php-sdk/issues", - "source": "https://github.com/Nosto/nosto-php-sdk/tree/6.2.2" + "source": "https://github.com/Nosto/nosto-php-sdk/tree/7.1.1" }, - "time": "2023-05-29T12:10:57+00:00" + "time": "2023-06-27T11:31:54+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -12263,5 +12263,5 @@ "ext-json": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.2.0" } diff --git a/etc/events.xml b/etc/events.xml index 79d76cdfe..ef27a780e 100644 --- a/etc/events.xml +++ b/etc/events.xml @@ -55,6 +55,9 @@ + + + diff --git a/etc/module.xml b/etc/module.xml index b56064ecd..44ed5f640 100755 --- a/etc/module.xml +++ b/etc/module.xml @@ -37,5 +37,5 @@ - +