From fef9dc7ccd5a1fcb79be9d2af44e7f3a5146526c Mon Sep 17 00:00:00 2001 From: Leo Gumbo Date: Thu, 19 May 2022 13:46:48 +0100 Subject: [PATCH] Upgrade to 6.0.0 --- Api/BaseRepositoryInterface.php | 51 + Api/CustomerRepositoryInterface.php | 72 + Api/Data/CustomerInterface.php | 112 +- Api/Data/CustomerSearchResultInterface.php | 57 + Api/Data/ProductUpdateQueueInterface.php | 197 + Api/ProductUpdateQueueRepositoryInterface.php | 64 + Block/Addtocart.php | 117 + Block/Adminhtml/Account/Config.php | 88 + Block/Adminhtml/Account/Iframe.php | 174 +- Block/Adminhtml/Form/Field/Tokens.php | 114 + Block/Category.php | 117 +- Block/Element.php | 79 +- Block/Email/ImageUrl.php | 100 + Block/Email/Visible.php | 84 + Block/Embed.php | 102 +- Block/Knockout.php | 145 +- Block/Meta.php | 89 +- Block/Order.php | 138 +- Block/PageType.php | 99 + Block/Product.php | 158 +- Block/Search.php | 94 +- Block/Stub.php | 127 + Block/TaggingTrait.php | 100 + Block/Variation.php | 165 + CHANGELOG.md | 570 + .../Command/NostoAccountConnectCommand.php | 254 + Console/Command/NostoAccountRemoveCommand.php | 200 + .../NostoGenerateCustomerReferenceCommand.php | 119 + Controller/Adminhtml/Account/Base.php | 52 + Controller/Adminhtml/Account/Connect.php | 103 +- Controller/Adminhtml/Account/Create.php | 255 +- Controller/Adminhtml/Account/Delete.php | 165 +- Controller/Adminhtml/Account/Index.php | 129 +- Controller/Adminhtml/Account/Proxy.php | 90 +- Controller/Adminhtml/Account/Sync.php | 117 +- Controller/Checkout/Cart/Add.php | 169 + Controller/Export/Base.php | 174 +- Controller/Export/Order.php | 116 +- Controller/Export/Product.php | 125 +- Controller/Frontend/Cart.php | 197 + Controller/Frontend/RestoreCart.php | 46 + Controller/Oauth/Index.php | 303 +- Cron/RatesCron.php | 83 + CustomerData/ActiveVariationTagging.php | 118 + CustomerData/CartTagging.php | 193 +- CustomerData/CustomerTagging.php | 87 +- CustomerData/HashedTagging.php | 59 + Exception/ParentProductDisabledException.php | 49 + Helper/Account.php | 401 +- Helper/Cache.php | 127 + Helper/Currency.php | 157 +- Helper/Customer.php | 111 + Helper/Data.php | 627 +- Helper/Format.php | 94 - Helper/Item.php | 61 - Helper/NewRelic.php | 68 + Helper/Price.php | 456 +- Helper/Ratings.php | 308 + Helper/RatingsFactory.php | 67 + Helper/Scope.php | 182 + Helper/Url.php | 374 +- Helper/Variation.php | 132 + LICENSE_AFL.txt | 48 - Logger/Logger.php | 127 + Magento2.iml | 16 + Model/Cache/Type/ProductData.php | 64 + Model/Cache/Type/ProductDataInterface.php | 44 + Model/Cart/Builder.php | 189 +- Model/Cart/Factory.php | 57 - Model/Cart/Item/Builder.php | 197 + Model/Cart/Item/Bundle.php | 78 + Model/Cart/Item/Configurable.php | 71 + Model/Cart/Item/Factory.php | 57 - Model/Cart/Item/Grouped.php | 102 + Model/Cart/Item/Simple.php | 56 + Model/Cart/Restore/Builder.php | 191 + Model/Category/Builder.php | 131 +- Model/Category/Factory.php | 57 - Model/Config/Backend/MultiCurrency.php | 102 + Model/Config/Source/Brand.php | 66 + Model/Config/Source/GoogleCategory.php | 64 + Model/Config/Source/Gtin.php | 64 + Model/Config/Source/Image.php | 56 + Model/Config/Source/Margin.php | 56 + Model/Config/Source/Memory.php | 57 + Model/Config/Source/MultiCurrency.php | 62 + Model/Config/Source/Ratings.php | 84 + Model/Config/Source/Selector.php | 92 + Model/Config/Source/Tags.php | 56 + Model/Customer.php | 129 - Model/Customer/Customer.php | 156 + Model/Customer/CustomerSearchResults.php | 44 + Model/Customer/Repository.php | 189 + Model/Email/Repository.php | 94 + Model/Indexer/AbstractIndexer.php | 378 + .../AbstractDimensionModeConfiguration.php | 89 + .../Dimensions/ModeSwitcherInterface.php | 47 + .../Queue/DimensionModeConfiguration.php | 66 + .../Indexer/Dimensions/Queue/ModeSwitcher.php | 97 + .../Queue/ModeSwitcherConfiguration.php | 86 + .../DimensionModeConfiguration.php | 66 + .../QueueProcessor/ModeSwitcher.php | 97 + .../ModeSwitcherConfiguration.php | 87 + .../Dimensions/StoreDimensionProvider.php | 95 + Model/Indexer/IndexerUtil.php | 75 + Model/Indexer/QueueIndexer.php | 191 + Model/Indexer/QueueProcessorIndexer.php | 151 + Model/Item/Bundle.php | 45 + Model/Item/Configurable.php | 45 + Model/Item/Downloadable.php | 45 + Model/Item/Giftcard.php | 43 + Model/Item/Grouped.php | 45 + Model/Item/Simple.php | 95 + Model/Item/Virtual.php | 45 + Model/Meta/Account/Billing/Builder.php | 75 +- Model/Meta/Account/Builder.php | 159 +- Model/Meta/Account/Iframe/Builder.php | 138 +- Model/Meta/Account/Owner/Builder.php | 87 +- Model/Meta/Account/Settings/Builder.php | 151 + .../Account/Settings/Currencies/Builder.php | 281 + Model/Meta/Account/Settings/Service.php | 93 + Model/Meta/Account/Sso/Builder.php | 90 +- Model/Meta/Oauth/Builder.php | 104 +- Model/Mview/ChangeLog.php | 62 + Model/Mview/ChangeLogInterface.php | 48 + Model/Mview/Mview.php | 49 + Model/Mview/MviewInterface.php | 49 + Model/Order/Builder.php | 344 +- Model/Order/Buyer/Builder.php | 107 + Model/Order/Collection.php | 119 + Model/Order/Item/Builder.php | 221 + Model/Order/Item/Bundle.php | 77 + Model/Order/Item/Configurable.php | 70 + Model/Order/Item/Grouped.php | 96 + Model/Order/Item/Simple.php | 60 + Model/Order/Status/Builder.php | 104 + Model/Person/Builder.php | 175 + Model/Person/Tagging/Builder.php | 219 + Model/Product/Builder.php | 520 +- Model/Product/CollectionBuilder.php | 141 + Model/Product/Queue/QueueBuilder.php | 111 + Model/Product/Queue/QueueRepository.php | 113 + Model/Product/Ratings.php | 75 + Model/Product/Repository.php | 346 + Model/Product/Sku/Builder.php | 206 + Model/Product/Sku/Collection.php | 121 + Model/Product/Tags/LowStock.php | 69 + Model/Product/Update/Queue.php | 208 + Model/Product/Url/Builder.php | 113 + Model/Product/Variation/Builder.php | 251 + Model/Product/Variation/Collection.php | 105 + Model/Rates/Builder.php | 109 + Model/Rates/Service.php | 123 + Model/ResourceModel/Customer.php | 64 +- Model/ResourceModel/Customer/Collection.php | 53 +- .../Magento/Product/Collection.php | 69 + .../Magento/Product/CollectionBuilder.php | 267 + Model/ResourceModel/Product/Update/Queue.php | 55 + .../Product/Update/Queue/QueueCollection.php | 184 + .../Update/Queue/QueueCollectionBuilder.php | 208 + Model/ResourceModel/Sku.php | 84 + Model/Service/AbstractService.php | 234 + Model/Service/Cache/CacheService.php | 157 + .../Service/Indexer/IndexerStatusService.php | 112 + .../Indexer/IndexerStatusServiceInterface.php | 58 + .../Attribute/AbstractAttributeService.php | 195 + .../Attribute/AttributeProviderInterface.php | 58 + .../Attribute/AttributeServiceInterface.php | 70 + .../Attribute/CachingAttributeService.php | 168 + .../Attribute/DefaultAttributeProvider.php | 120 + .../Attribute/DefaultAttributeService.php | 83 + Model/Service/Product/AvailabilityService.php | 89 + .../Service/Product/CachingProductService.php | 98 + .../Category/CachingCategoryService.php | 88 + .../Category/CategoryServiceInterface.php | 58 + .../Category/DefaultCategoryService.php | 140 + .../Product/DefaultProductComparator.php | 56 + .../Product/DefaultProductSerializer.php | 63 + .../Service/Product/DefaultProductService.php | 111 + Model/Service/Product/ImageService.php | 110 + .../Product/ProductComparatorInterface.php | 49 + .../Product/ProductSerializerInterface.php | 55 + .../Product/ProductServiceInterface.php | 51 + .../Product/SanitizingProductService.php | 85 + .../Stock/Provider/CachingStockProvider.php | 282 + .../Stock/Provider/DefaultStockProvider.php | 140 + .../Stock/Provider/StockProviderInterface.php | 115 + .../Stock/Provider/StockRegistryProvider.php | 100 + Model/Service/Stock/StockService.php | 192 + Model/Service/Store/MissingStoreException.php | 43 + Model/Service/Sync/AbstractBulkConsumer.php | 134 + Model/Service/Sync/AbstractBulkPublisher.php | 221 + Model/Service/Sync/BulkConsumerInterface.php | 52 + Model/Service/Sync/BulkPublisherInterface.php | 47 + .../Service/Sync/Delete/AsyncBulkConsumer.php | 93 + .../Sync/Delete/AsyncBulkPublisher.php | 77 + Model/Service/Sync/Delete/DeleteService.php | 134 + .../Service/Sync/Upsert/AsyncBulkConsumer.php | 108 + .../Sync/Upsert/AsyncBulkPublisher.php | 78 + Model/Service/Sync/Upsert/SyncService.php | 168 + .../Service/Update/QueueProcessorService.php | 296 + Model/Service/Update/QueueService.php | 186 + .../Message/Notification/InvalidAccount.php | 128 + Model/User/Builder.php | 88 + Observer/Adminhtml/Config.php | 174 + Observer/Cart/Add.php | 173 + Observer/Customer/Save.php | 83 + .../Customer/UpdateMarketingPermission.php | 124 + Observer/Order/Save.php | 371 +- Observer/Product/Base.php | 190 +- Observer/Product/Delete.php | 58 - .../Product/MassProductAttributeUpdate.php | 129 + Observer/Product/Review.php | 68 + Observer/Product/Update.php | 58 - Observer/Rates/Update.php | 100 + Observer/Settings/Update.php | 100 + Plugin/ProductQueueUpdate.php | 89 + Plugin/ProductUpdate.php | 142 + Plugin/Sales/OrderRepository.php | 74 + README.md | 55 +- Setup/InstallSchema.php | 104 - Setup/Patch/Data/AddCustomerReference.php | 162 + .../AlterCustomerReferenceNonEditable.php | 125 + .../Patch/Data/PopulateCustomerReference.php | 149 + Test/Unit/Helper/AccountTest.php | 132 + Test/Unit/Util/UrlTest.php | 64 + Util/Benchmark.php | 205 + Util/Customer.php | 57 + Util/PagingIterator.php | 138 + Util/Repository.php | 88 + Util/StringUtil.php | 59 + Util/Url.php | 57 + build.xml | 103 + compile.sh | 15 + composer.json | 104 +- composer.lock | 12138 +++++++++++++++- default.conf | 14 + entrypoint.sh | 8 + etc/acl.xml | 62 +- etc/adminhtml/di.xml | 46 + etc/adminhtml/events.xml | 60 +- etc/adminhtml/menu.xml | 56 +- etc/adminhtml/routes.xml | 48 +- etc/adminhtml/system.xml | 297 + etc/cache.xml | 42 + etc/communication.xml | 44 + etc/config.xml | 70 + etc/crontab.xml | 45 + etc/csp_whitelist.xml | 41 + etc/db_schema.xml | 40 + etc/di.xml | 261 +- etc/events.xml | 71 +- etc/frontend/di.xml | 47 +- etc/frontend/routes.xml | 50 +- etc/frontend/sections.xml | 43 +- etc/indexer.xml | 45 + etc/module.xml | 52 +- etc/mview.xml | 73 + etc/queue.xml | 50 + etc/queue_consumer.xml | 50 + etc/queue_publisher.xml | 44 + etc/queue_topology.xml | 42 + inspect.sh | 3 + phan.php | 90 + phpunit.xml | 41 + registration.php | 58 +- ruleset.xml | 81 +- supervisord.conf | 13 + view/adminhtml/layout/nosto_account_index.xml | 60 +- view/adminhtml/requirejs-config.js | 60 +- view/adminhtml/templates/config.phtml | 48 + view/adminhtml/templates/iframe.phtml | 57 +- view/adminhtml/templates/tokens.phtml | 96 + view/adminhtml/web/js/iframe_handler.js | 354 +- .../frontend/layout/catalog_category_view.xml | 76 +- view/frontend/layout/catalog_product_view.xml | 89 +- .../layout/catalogsearch_result_index.xml | 76 +- view/frontend/layout/checkout_cart_index.xml | 84 +- view/frontend/layout/checkout_index_index.xml | 61 + .../layout/checkout_onepage_success.xml | 76 +- view/frontend/layout/cms_index_index.xml | 73 + view/frontend/layout/cms_noroute_index.xml | 84 +- view/frontend/layout/default.xml | 78 +- view/frontend/requirejs-config.js | 58 +- view/frontend/templates/addtocart.phtml | 49 + view/frontend/templates/cart.phtml | 93 +- view/frontend/templates/category.phtml | 34 - view/frontend/templates/customer.phtml | 98 +- view/frontend/templates/element.phtml | 66 +- view/frontend/templates/embed.phtml | 71 +- view/frontend/templates/jsstub.phtml | 78 +- view/frontend/templates/meta.phtml | 70 +- view/frontend/templates/order.phtml | 68 - view/frontend/templates/product.phtml | 72 - view/frontend/templates/search.phtml | 32 - view/frontend/templates/variation.phtml | 80 + view/frontend/web/js/nostojs.js | 65 +- view/frontend/web/js/recobuy.js | 126 + view/frontend/web/js/view/cart-tagging.js | 116 +- view/frontend/web/js/view/customer-tagging.js | 95 +- .../frontend/web/js/view/variation-tagging.js | 69 + 301 files changed, 41460 insertions(+), 4815 deletions(-) create mode 100644 Api/BaseRepositoryInterface.php create mode 100644 Api/CustomerRepositoryInterface.php create mode 100644 Api/Data/CustomerSearchResultInterface.php create mode 100644 Api/Data/ProductUpdateQueueInterface.php create mode 100644 Api/ProductUpdateQueueRepositoryInterface.php create mode 100644 Block/Addtocart.php create mode 100755 Block/Adminhtml/Account/Config.php create mode 100644 Block/Adminhtml/Form/Field/Tokens.php create mode 100644 Block/Email/ImageUrl.php create mode 100644 Block/Email/Visible.php create mode 100644 Block/PageType.php create mode 100644 Block/Stub.php create mode 100644 Block/TaggingTrait.php create mode 100644 Block/Variation.php create mode 100644 CHANGELOG.md create mode 100644 Console/Command/NostoAccountConnectCommand.php create mode 100644 Console/Command/NostoAccountRemoveCommand.php create mode 100644 Console/Command/NostoGenerateCustomerReferenceCommand.php create mode 100644 Controller/Adminhtml/Account/Base.php create mode 100644 Controller/Checkout/Cart/Add.php create mode 100755 Controller/Frontend/Cart.php create mode 100644 Controller/Frontend/RestoreCart.php create mode 100644 Cron/RatesCron.php create mode 100644 CustomerData/ActiveVariationTagging.php create mode 100644 CustomerData/HashedTagging.php create mode 100644 Exception/ParentProductDisabledException.php create mode 100755 Helper/Cache.php create mode 100644 Helper/Customer.php delete mode 100644 Helper/Format.php delete mode 100644 Helper/Item.php create mode 100644 Helper/NewRelic.php create mode 100644 Helper/Ratings.php create mode 100644 Helper/RatingsFactory.php create mode 100644 Helper/Scope.php create mode 100644 Helper/Variation.php delete mode 100644 LICENSE_AFL.txt create mode 100644 Logger/Logger.php create mode 100644 Magento2.iml create mode 100644 Model/Cache/Type/ProductData.php create mode 100644 Model/Cache/Type/ProductDataInterface.php delete mode 100644 Model/Cart/Factory.php create mode 100644 Model/Cart/Item/Builder.php create mode 100644 Model/Cart/Item/Bundle.php create mode 100644 Model/Cart/Item/Configurable.php delete mode 100644 Model/Cart/Item/Factory.php create mode 100644 Model/Cart/Item/Grouped.php create mode 100644 Model/Cart/Item/Simple.php create mode 100644 Model/Cart/Restore/Builder.php delete mode 100644 Model/Category/Factory.php create mode 100644 Model/Config/Backend/MultiCurrency.php create mode 100644 Model/Config/Source/Brand.php create mode 100644 Model/Config/Source/GoogleCategory.php create mode 100644 Model/Config/Source/Gtin.php create mode 100644 Model/Config/Source/Image.php create mode 100644 Model/Config/Source/Margin.php create mode 100644 Model/Config/Source/Memory.php create mode 100644 Model/Config/Source/MultiCurrency.php create mode 100644 Model/Config/Source/Ratings.php create mode 100644 Model/Config/Source/Selector.php create mode 100644 Model/Config/Source/Tags.php delete mode 100644 Model/Customer.php create mode 100644 Model/Customer/Customer.php create mode 100644 Model/Customer/CustomerSearchResults.php create mode 100644 Model/Customer/Repository.php create mode 100644 Model/Email/Repository.php create mode 100644 Model/Indexer/AbstractIndexer.php create mode 100644 Model/Indexer/Dimensions/AbstractDimensionModeConfiguration.php create mode 100644 Model/Indexer/Dimensions/ModeSwitcherInterface.php create mode 100644 Model/Indexer/Dimensions/Queue/DimensionModeConfiguration.php create mode 100644 Model/Indexer/Dimensions/Queue/ModeSwitcher.php create mode 100644 Model/Indexer/Dimensions/Queue/ModeSwitcherConfiguration.php create mode 100644 Model/Indexer/Dimensions/QueueProcessor/DimensionModeConfiguration.php create mode 100644 Model/Indexer/Dimensions/QueueProcessor/ModeSwitcher.php create mode 100644 Model/Indexer/Dimensions/QueueProcessor/ModeSwitcherConfiguration.php create mode 100644 Model/Indexer/Dimensions/StoreDimensionProvider.php create mode 100644 Model/Indexer/IndexerUtil.php create mode 100644 Model/Indexer/QueueIndexer.php create mode 100644 Model/Indexer/QueueProcessorIndexer.php create mode 100644 Model/Item/Bundle.php create mode 100644 Model/Item/Configurable.php create mode 100644 Model/Item/Downloadable.php create mode 100644 Model/Item/Giftcard.php create mode 100644 Model/Item/Grouped.php create mode 100644 Model/Item/Simple.php create mode 100644 Model/Item/Virtual.php create mode 100644 Model/Meta/Account/Settings/Builder.php create mode 100644 Model/Meta/Account/Settings/Currencies/Builder.php create mode 100644 Model/Meta/Account/Settings/Service.php create mode 100644 Model/Mview/ChangeLog.php create mode 100644 Model/Mview/ChangeLogInterface.php create mode 100644 Model/Mview/Mview.php create mode 100644 Model/Mview/MviewInterface.php create mode 100644 Model/Order/Buyer/Builder.php create mode 100644 Model/Order/Collection.php create mode 100644 Model/Order/Item/Builder.php create mode 100644 Model/Order/Item/Bundle.php create mode 100644 Model/Order/Item/Configurable.php create mode 100644 Model/Order/Item/Grouped.php create mode 100644 Model/Order/Item/Simple.php create mode 100644 Model/Order/Status/Builder.php create mode 100644 Model/Person/Builder.php create mode 100644 Model/Person/Tagging/Builder.php create mode 100644 Model/Product/CollectionBuilder.php create mode 100644 Model/Product/Queue/QueueBuilder.php create mode 100644 Model/Product/Queue/QueueRepository.php create mode 100644 Model/Product/Ratings.php create mode 100644 Model/Product/Repository.php create mode 100644 Model/Product/Sku/Builder.php create mode 100644 Model/Product/Sku/Collection.php create mode 100644 Model/Product/Tags/LowStock.php create mode 100644 Model/Product/Update/Queue.php create mode 100644 Model/Product/Url/Builder.php create mode 100644 Model/Product/Variation/Builder.php create mode 100644 Model/Product/Variation/Collection.php create mode 100644 Model/Rates/Builder.php create mode 100644 Model/Rates/Service.php create mode 100644 Model/ResourceModel/Magento/Product/Collection.php create mode 100644 Model/ResourceModel/Magento/Product/CollectionBuilder.php create mode 100644 Model/ResourceModel/Product/Update/Queue.php create mode 100644 Model/ResourceModel/Product/Update/Queue/QueueCollection.php create mode 100644 Model/ResourceModel/Product/Update/Queue/QueueCollectionBuilder.php create mode 100644 Model/ResourceModel/Sku.php create mode 100644 Model/Service/AbstractService.php create mode 100644 Model/Service/Cache/CacheService.php create mode 100644 Model/Service/Indexer/IndexerStatusService.php create mode 100644 Model/Service/Indexer/IndexerStatusServiceInterface.php create mode 100644 Model/Service/Product/Attribute/AbstractAttributeService.php create mode 100644 Model/Service/Product/Attribute/AttributeProviderInterface.php create mode 100644 Model/Service/Product/Attribute/AttributeServiceInterface.php create mode 100644 Model/Service/Product/Attribute/CachingAttributeService.php create mode 100644 Model/Service/Product/Attribute/DefaultAttributeProvider.php create mode 100644 Model/Service/Product/Attribute/DefaultAttributeService.php create mode 100644 Model/Service/Product/AvailabilityService.php create mode 100644 Model/Service/Product/CachingProductService.php create mode 100644 Model/Service/Product/Category/CachingCategoryService.php create mode 100644 Model/Service/Product/Category/CategoryServiceInterface.php create mode 100644 Model/Service/Product/Category/DefaultCategoryService.php create mode 100644 Model/Service/Product/DefaultProductComparator.php create mode 100644 Model/Service/Product/DefaultProductSerializer.php create mode 100644 Model/Service/Product/DefaultProductService.php create mode 100644 Model/Service/Product/ImageService.php create mode 100644 Model/Service/Product/ProductComparatorInterface.php create mode 100644 Model/Service/Product/ProductSerializerInterface.php create mode 100644 Model/Service/Product/ProductServiceInterface.php create mode 100644 Model/Service/Product/SanitizingProductService.php create mode 100644 Model/Service/Stock/Provider/CachingStockProvider.php create mode 100644 Model/Service/Stock/Provider/DefaultStockProvider.php create mode 100644 Model/Service/Stock/Provider/StockProviderInterface.php create mode 100644 Model/Service/Stock/Provider/StockRegistryProvider.php create mode 100644 Model/Service/Stock/StockService.php create mode 100644 Model/Service/Store/MissingStoreException.php create mode 100644 Model/Service/Sync/AbstractBulkConsumer.php create mode 100644 Model/Service/Sync/AbstractBulkPublisher.php create mode 100644 Model/Service/Sync/BulkConsumerInterface.php create mode 100644 Model/Service/Sync/BulkPublisherInterface.php create mode 100644 Model/Service/Sync/Delete/AsyncBulkConsumer.php create mode 100644 Model/Service/Sync/Delete/AsyncBulkPublisher.php create mode 100644 Model/Service/Sync/Delete/DeleteService.php create mode 100644 Model/Service/Sync/Upsert/AsyncBulkConsumer.php create mode 100644 Model/Service/Sync/Upsert/AsyncBulkPublisher.php create mode 100644 Model/Service/Sync/Upsert/SyncService.php create mode 100644 Model/Service/Update/QueueProcessorService.php create mode 100644 Model/Service/Update/QueueService.php create mode 100644 Model/System/Message/Notification/InvalidAccount.php create mode 100644 Model/User/Builder.php create mode 100644 Observer/Adminhtml/Config.php create mode 100644 Observer/Cart/Add.php create mode 100644 Observer/Customer/Save.php create mode 100644 Observer/Customer/UpdateMarketingPermission.php delete mode 100644 Observer/Product/Delete.php create mode 100644 Observer/Product/MassProductAttributeUpdate.php create mode 100644 Observer/Product/Review.php delete mode 100644 Observer/Product/Update.php create mode 100644 Observer/Rates/Update.php create mode 100644 Observer/Settings/Update.php create mode 100644 Plugin/ProductQueueUpdate.php create mode 100644 Plugin/ProductUpdate.php create mode 100644 Plugin/Sales/OrderRepository.php delete mode 100644 Setup/InstallSchema.php create mode 100644 Setup/Patch/Data/AddCustomerReference.php create mode 100644 Setup/Patch/Data/AlterCustomerReferenceNonEditable.php create mode 100644 Setup/Patch/Data/PopulateCustomerReference.php create mode 100644 Test/Unit/Helper/AccountTest.php create mode 100644 Test/Unit/Util/UrlTest.php create mode 100644 Util/Benchmark.php create mode 100644 Util/Customer.php create mode 100644 Util/PagingIterator.php create mode 100644 Util/Repository.php create mode 100644 Util/StringUtil.php create mode 100644 Util/Url.php create mode 100644 build.xml create mode 100755 compile.sh create mode 100644 default.conf create mode 100755 entrypoint.sh create mode 100644 etc/adminhtml/di.xml create mode 100644 etc/adminhtml/system.xml create mode 100644 etc/cache.xml create mode 100644 etc/communication.xml create mode 100644 etc/config.xml create mode 100644 etc/crontab.xml create mode 100644 etc/csp_whitelist.xml create mode 100644 etc/db_schema.xml create mode 100644 etc/indexer.xml create mode 100644 etc/mview.xml create mode 100644 etc/queue.xml create mode 100644 etc/queue_consumer.xml create mode 100644 etc/queue_publisher.xml create mode 100644 etc/queue_topology.xml create mode 100755 inspect.sh create mode 100644 phan.php create mode 100644 phpunit.xml create mode 100644 supervisord.conf create mode 100644 view/adminhtml/templates/config.phtml create mode 100644 view/adminhtml/templates/tokens.phtml create mode 100644 view/frontend/layout/checkout_index_index.xml create mode 100644 view/frontend/layout/cms_index_index.xml create mode 100644 view/frontend/templates/addtocart.phtml delete mode 100644 view/frontend/templates/category.phtml delete mode 100644 view/frontend/templates/order.phtml delete mode 100644 view/frontend/templates/product.phtml delete mode 100644 view/frontend/templates/search.phtml create mode 100644 view/frontend/templates/variation.phtml create mode 100644 view/frontend/web/js/recobuy.js create mode 100644 view/frontend/web/js/view/variation-tagging.js diff --git a/Api/BaseRepositoryInterface.php b/Api/BaseRepositoryInterface.php new file mode 100644 index 000000000..47a203a15 --- /dev/null +++ b/Api/BaseRepositoryInterface.php @@ -0,0 +1,51 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Api; + +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\Data\SearchResultInterface; + +interface BaseRepositoryInterface +{ + /** + * Get search result + * + * @param SearchCriteriaInterface $searchCriteria + * @return SearchResultInterface + */ + public function search(SearchCriteriaInterface $searchCriteria); +} diff --git a/Api/CustomerRepositoryInterface.php b/Api/CustomerRepositoryInterface.php new file mode 100644 index 000000000..a5caf67e4 --- /dev/null +++ b/Api/CustomerRepositoryInterface.php @@ -0,0 +1,72 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Api; + +use Nosto\Tagging\Api\Data\CustomerInterface; + +interface CustomerRepositoryInterface extends BaseRepositoryInterface +{ + /** + * Save Queue entry + * + * @param CustomerInterface $customer + * + * @return CustomerInterface + */ + public function save(CustomerInterface $customer); + + /** + * Get customer entry by nosto id and quote id. If multiple entries + * are found first one will be returned. + * + * @param string $nostoId + * @param int $quoteId + * + * @return CustomerInterface|null + */ + public function getOneByNostoIdAndQuoteId(string $nostoId, int $quoteId); + + /** + * Get customer entry by restore cart hash. If multiple entries + * are found first one will be returned. + * + * @param string $hash + * + * @return CustomerInterface|null + */ + public function getOneByRestoreCartHash(string $hash); +} diff --git a/Api/Data/CustomerInterface.php b/Api/Data/CustomerInterface.php index 5eac151e2..f8a453adf 100644 --- a/Api/Data/CustomerInterface.php +++ b/Api/Data/CustomerInterface.php @@ -1,97 +1,143 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Api\Data; +use DateTime; + interface CustomerInterface { - const CUSTOMER_ID = 'customer_id'; - const QUOTE_ID = 'quote_id'; - const NOSTO_ID = 'nosto_id'; - const CREATED_AT = 'created_at'; - const UPDATED_AT = 'updated_at'; + public const CUSTOMER_ID = 'customer_id'; + public const QUOTE_ID = 'quote_id'; + public const NOSTO_ID = 'nosto_id'; + public const CREATED_AT = 'created_at'; + public const UPDATED_AT = 'updated_at'; + public const RESTORE_CART_HASH = 'restore_cart_hash'; + + /** + * @var int The length of the restore cart attribute + */ + public const NOSTO_TAGGING_RESTORE_CART_ATTRIBUTE_LENGTH = 64; /** * Get customer id + * * @return int|null */ public function getCustomerId(); /** * Get quote id + * * @return int|null */ public function getQuoteId(); /** * Get Nosto Id + * * @return string */ public function getNostoId(); /** * Get created at time - * @return \DateTime + * + * @return DateTime */ public function getCreatedAt(); /** * Get updated at time - * @return \DateTime + * + * @return DateTime */ public function getUpdatedAt(); - + + /** + * Get restore cart hash + * + * @return string restore cart hash + */ + public function getRestoreCartHash(); + /** * Set customer id + * * @param int $customerId + * @return self */ - public function setCustomerId($customerId); + public function setCustomerId(int $customerId); /** * Set quote id + * * @param int $quoteId */ - public function setQuoteId($quoteId); + public function setQuoteId(int $quoteId); /** * Set Nosto Id + * * @param string $nostoId + * @return self */ - public function setNostoId($nostoId); + public function setNostoId(string $nostoId); /** * Set created at time - * @param \DateTime $createdAt + * + * @param DateTime $createdAt + * @return self */ - public function setCreatedAt(\DateTime $createdAt); + public function setCreatedAt(DateTime $createdAt); /** * Set updated at time - * @param \DateTime $updatedAt + * + * @param DateTime $updatedAt + * @return self + */ + public function setUpdatedAt(DateTime $updatedAt); + + /** + * Set restore cart hash + * + * @param string $restoreCartHash + * @return self */ - public function setUpdatedAt(\DateTime $updatedAt); + public function setRestoreCartHash(string $restoreCartHash); } diff --git a/Api/Data/CustomerSearchResultInterface.php b/Api/Data/CustomerSearchResultInterface.php new file mode 100644 index 000000000..f3336ab65 --- /dev/null +++ b/Api/Data/CustomerSearchResultInterface.php @@ -0,0 +1,57 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Api\Data; + +use Magento\Framework\Data\SearchResultInterface; + +interface CustomerSearchResultInterface extends SearchResultInterface +{ + /** + * Get items from search results + * + * @return CustomerInterface[] + */ + public function getItems(); + + /** + * Set items to the search results + * + * @param CustomerInterface[] $items + * @return $this + */ + public function setItems(array $items); +} diff --git a/Api/Data/ProductUpdateQueueInterface.php b/Api/Data/ProductUpdateQueueInterface.php new file mode 100644 index 000000000..d141c4e13 --- /dev/null +++ b/Api/Data/ProductUpdateQueueInterface.php @@ -0,0 +1,197 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Api\Data; + +use DateTime; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Interface ProductUpdateQueueInterface + */ +interface ProductUpdateQueueInterface +{ + public const ID = 'id'; + public const CREATED_AT = 'created_at'; + public const STARTED_AT = 'started_at'; + public const COMPLETED_AT = 'completed_at'; + public const STORE_ID = 'store_id'; + public const PRODUCT_IDS = 'product_ids'; + public const PRODUCT_ID_COUNT = 'product_id_count'; + public const ACTION = 'action'; + public const ACTION_VALUE_UPSERT = 'upsert'; + public const ACTION_VALUE_DELETE = 'delete'; + public const STATUS = 'status'; + public const STATUS_VALUE_NEW = 'new'; + public const STATUS_VALUE_PROCESSING = 'processing'; + public const STATUS_VALUE_DONE = 'done'; + + /** + * Get row id + * + * @return int|null + */ + public function getId(); + + /** + * Get created at time + * + * @return DateTime + */ + public function getCreatedAt(); + + /** + * Get started at time + * + * @return DateTime + */ + public function getStartedAt(); + + /** + * Get completed at time + * + * @return DateTime + */ + public function getCompletedAt(); + + /** + * Get store id + * + * @return int + */ + public function getStoreId(); + + /** + * Get product data + * + * @return string|null + */ + public function getProductIds(); + + /** + * Get the queue status + * + * @return string + */ + public function getStatus(); + + /** + * Get the queue action + * + * @return string + */ + public function getAction(); + + /** + * Get the count of product ids in entry + * + * @return int + */ + public function getProductIdCount(); + + /** + * Set id + * + * @param int $id + * @return self + * @SuppressWarnings(PHPMD.ShortVariable) + */ + public function setId(int $id); + + /** + * Set product id + * + * @param array $productIds + * @return self + */ + public function setProductIds(array $productIds); + + /** + * Set store id + * + * @param int $storeId + * @return self + */ + public function setStoreId(int $storeId); + + /** + * Set created at time + * + * @param DateTime $createdAt + * @return self + */ + public function setCreatedAt(DateTime $createdAt); + + /** + * Set started at time + * + * @param DateTime $startedAt + * @return self + */ + public function setStartedAt(DateTime $startedAt); + + /** + * Set completed at time + * + * @param DateTime $completedAt + * @return self + */ + public function setCompletedAt(DateTime $completedAt); + + /** + * @param StoreInterface $store + * @return self + */ + public function setStore(StoreInterface $store); + + /** + * @param string $status + * @return self + */ + public function setStatus(string $status); + + /** + * @param string $action + * @return self + */ + public function setAction(string $action); + + /** + * @param int $count + * @return self + */ + public function setProductIdCount(int $count); +} diff --git a/Api/ProductUpdateQueueRepositoryInterface.php b/Api/ProductUpdateQueueRepositoryInterface.php new file mode 100644 index 000000000..d978486cb --- /dev/null +++ b/Api/ProductUpdateQueueRepositoryInterface.php @@ -0,0 +1,64 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Api; + +use Magento\Store\Api\Data\StoreInterface; +use Nosto\Tagging\Api\Data\ProductUpdateQueueInterface; + +interface ProductUpdateQueueRepositoryInterface +{ + /** + * Save Queue entry + * + * @param ProductUpdateQueueInterface $entry + * @return ProductUpdateQueueInterface + */ + public function save(ProductUpdateQueueInterface $entry); + + /** + * Delete productIndex + * + * @param ProductUpdateQueueInterface $entry + */ + public function delete(ProductUpdateQueueInterface $entry); + + /** + * @param StoreInterface $store + * @return ProductUpdateQueueInterface|null + */ + public function getByStore(StoreInterface $store); +} diff --git a/Block/Addtocart.php b/Block/Addtocart.php new file mode 100644 index 000000000..bd2380b4a --- /dev/null +++ b/Block/Addtocart.php @@ -0,0 +1,117 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Block; + +use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Url\EncoderInterface; +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; + +/** + * Embed script block that includes the Nosto script in the page to be included on all pages. + */ +class Addtocart extends Template +{ + use TaggingTrait { + TaggingTrait::__construct as taggingConstruct; // @codingStandardsIgnoreLine + } + + private EncoderInterface $urlEncoder; + private NostoHelperData $nostoHelperData; + + /** + * Constructor. + * + * @param Context $context + * @param EncoderInterface $urlEncoder + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperData $nostoHelperData + * @param array $data + */ + public function __construct( + Context $context, + EncoderInterface $urlEncoder, + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, + NostoHelperData $nostoHelperData, + array $data = [] + ) { + parent::__construct($context, $data); + + $this->taggingConstruct($nostoHelperAccount, $nostoHelperScope); + $this->urlEncoder = $urlEncoder; + $this->nostoHelperData = $nostoHelperData; + } + + /** + * Retrieve url for add product to cart + * + * @return string + */ + public function getSubmitUrl() + { + $continueUrl = $this->urlEncoder->encode($this->_urlBuilder->getCurrentUrl()); + $activeStore = $this->nostoHelperScope->getStore(true); + $routeParams = [ActionInterface::PARAM_NAME_URL_ENCODED => $continueUrl]; + $routeParams['_secure'] = $this->getRequest()->isSecure(); + $routeParams['_scope'] = $activeStore->getCode(); + $routeParams['_scope_to_url'] = $this->nostoHelperData->getStoreCodeToUrl($activeStore); + $request = $this->getRequest(); + if ($request instanceof Http + && $request->getRouteName() === 'checkout' + && $request->getControllerName() === 'cart' + ) { + $routeParams['in_cart'] = 1; + } + return $this->_urlBuilder->getUrl('checkout/cart/add', $routeParams); + } + + /** + * + * @return null + */ + public function getAbstractObject() + { + return null; + } +} diff --git a/Block/Adminhtml/Account/Config.php b/Block/Adminhtml/Account/Config.php new file mode 100755 index 000000000..5fd454552 --- /dev/null +++ b/Block/Adminhtml/Account/Config.php @@ -0,0 +1,88 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Block\Adminhtml\Account; + +use Exception; +use Magento\Backend\Block\Template as BlockTemplate; +use Magento\Backend\Block\Template\Context as BlockContext; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Helper\Url as NostoHelperUrl; +use Psr\Log\LoggerInterface; + +class Config extends BlockTemplate +{ + private NostoHelperUrl $urlHelper; + private NostoHelperScope $nostoHelperScope; + private LoggerInterface $logger; + + /** + * Constructor. + * + * @param BlockContext $context the context. + * @param NostoHelperUrl $urlHelper + * @param NostoHelperScope $nostoHelperScope + * @param array $data + */ + public function __construct( + BlockContext $context, + NostoHelperUrl $urlHelper, + NostoHelperScope $nostoHelperScope, + array $data = [] + ) { + parent::__construct($context, $data); + + $this->urlHelper = $urlHelper; + $this->nostoHelperScope = $nostoHelperScope; + $this->logger = $context->getLogger(); + } + + /** + * Get the admin url. Used in the template file + * + * @return string + */ + public function getConfigurationUrl() + { + try { + $store = $this->nostoHelperScope->getSelectedStore($this->getRequest()); + return $this->urlHelper->getAdminNostoConfigurationUrl($store); + } catch (Exception $e) { + $this->logger->error($e->getMessage()); + } + return ''; + } +} diff --git a/Block/Adminhtml/Account/Iframe.php b/Block/Adminhtml/Account/Iframe.php index edfbd618e..29b394ce5 100755 --- a/Block/Adminhtml/Account/Iframe.php +++ b/Block/Adminhtml/Account/Iframe.php @@ -1,38 +1,54 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Block\Adminhtml\Account; +use Exception; use Magento\Backend\Block\Template as BlockTemplate; use Magento\Backend\Block\Template\Context as BlockContext; use Magento\Backend\Model\Auth\Session; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NotFoundException; -use Magento\Store\Api\Data\StoreInterface; -use Nosto\Tagging\Helper\Account; +use Nosto\Mixins\IframeTrait; +use Nosto\Nosto; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Meta\Account\Iframe\Builder as NostoIframeMetaBuilder; +use Nosto\Tagging\Model\User\Builder as NostoCurrentUserBuilder; /** * Iframe block for displaying the Nosto account management iframe. @@ -41,40 +57,47 @@ */ class Iframe extends BlockTemplate { - /** - * Default iframe origin regexp for validating window.postMessage() calls. - */ - const DEFAULT_IFRAME_ORIGIN_REGEXP = '(https:\/\/(.*)\.hub\.nosto\.com)|(https:\/\/my\.nosto\.com)'; + use IframeTrait; - /** - * @inheritdoc - */ - protected $_template = 'iframe.phtml'; + public const IFRAME_VERSION = 1; - /** - * @var Account account helper. - */ - protected $_accountHelper; - private $_backendAuthSession; + private NostoHelperAccount $nostoHelperAccount; + private Session $backendAuthSession; + private NostoIframeMetaBuilder $iframeMetaBuilder; + private NostoCurrentUserBuilder $currentUserBuilder; + private NostoHelperScope $nostoHelperScope; + private NostoLogger $logger; /** * Constructor. * * @param BlockContext $context the context. - * @param Account $accountHelper the account helper. + * @param NostoHelperAccount $nostoHelperAccount the account helper. * @param Session $backendAuthSession - * @param array $data optional data. + * @param NostoIframeMetaBuilder $iframeMetaBuilder + * @param NostoCurrentUserBuilder $currentUserBuilder + * @param NostoHelperScope $nostoHelperScope + * @param NostoLogger $logger + * @param array $data */ public function __construct( BlockContext $context, - Account $accountHelper, + NostoHelperAccount $nostoHelperAccount, Session $backendAuthSession, + NostoIframeMetaBuilder $iframeMetaBuilder, + NostoCurrentUserBuilder $currentUserBuilder, + NostoHelperScope $nostoHelperScope, + NostoLogger $logger, array $data = [] ) { parent::__construct($context, $data); - $this->_accountHelper = $accountHelper; - $this->_backendAuthSession = $backendAuthSession; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->backendAuthSession = $backendAuthSession; + $this->iframeMetaBuilder = $iframeMetaBuilder; + $this->currentUserBuilder = $currentUserBuilder; + $this->nostoHelperScope = $nostoHelperScope; + $this->logger = $logger; } /** @@ -88,24 +111,24 @@ public function __construct( */ public function getIframeUrl() { - $params = array(); + $params = []; + $params['v'] = self::IFRAME_VERSION; // Pass any error/success messages we might have to the iframe. // These can be available when getting redirect back from the OAuth // front controller after connecting a Nosto account to a store. - $nostoMessage = $this->_backendAuthSession->getData('nosto_message'); + $nostoMessage = $this->backendAuthSession->getData('nosto_message'); if (is_array($nostoMessage) && !empty($nostoMessage)) { foreach ($nostoMessage as $key => $value) { if (is_string($key) && !empty($value)) { $params[$key] = $value; } } - $this->_backendAuthSession->setData('nosto_message', null); + /** @noinspection PhpUndefinedMethodInspection */ + $this->backendAuthSession->setData('nosto_message', null); } - $store = $this->getSelectedStore(); - $account = $this->_accountHelper->findAccount($store); - return $this->_accountHelper->getIframeUrl($store, $account, $params); + return $this->buildURL($params); } /** @@ -113,16 +136,16 @@ public function getIframeUrl() * This config can be converted into JSON in the view file. * * @return array the config. + * @throws NotFoundException + * @throws LocalizedException */ public function getIframeConfig() { - $get = [ - 'store' => $this->getSelectedStore()->getId(), - 'isAjax' => true - ]; + $store = $this->nostoHelperScope->getSelectedStore($this->getRequest()); + $get = ['store' => $store->getId(), 'isAjax' => true]; return [ 'iframe_handler' => [ - 'origin' => $this->getIframeOrigin(), + 'origin' => Nosto::getIframeOriginRegex(), 'xhrParams' => [ 'form_key' => $this->formKey->getFormKey() ], @@ -137,41 +160,40 @@ public function getIframeConfig() } /** - * Returns the valid origin url regexp from where the iframe should accept - * postMessage calls. - * This is configurable to support different origins based on $_ENV. - * - * @return string the origin url regexp. + * @inheritDoc */ - public function getIframeOrigin() + public function getIframe() { - return (isset($_ENV['NOSTO_IFRAME_ORIGIN_REGEXP'])) - ? $_ENV['NOSTO_IFRAME_ORIGIN_REGEXP'] - : self::DEFAULT_IFRAME_ORIGIN_REGEXP; + try { + $store = $this->nostoHelperScope->getSelectedStore($this->getRequest()); + return $this->iframeMetaBuilder->build($store); + } catch (Exception $e) { + $this->logger->exception($e); + } + + return null; } /** - * Returns the currently selected store. - * Nosto can only be configured on a store basis, and if we cannot find a - * store, an exception is thrown. - * - * @return StoreInterface the store. - * - * @throws NotFoundException store not found. + * @inheritDoc + */ + public function getUser() + { + return $this->currentUserBuilder->build(); + } + + /** + * @inheritDoc */ - public function getSelectedStore() + public function getAccount() { - $store = null; - if ($this->_storeManager->isSingleStoreMode()) { - $store = $this->_storeManager->getStore(true); - } elseif (($storeId = $this->_request->getParam('store'))) { - $store = $this->_storeManager->getStore($storeId); - } elseif (($this->_storeManager->getStore())) { - $store = $this->_storeManager->getStore(); - } else { - throw new NotFoundException(__('Store not found.')); + try { + $store = $this->nostoHelperScope->getSelectedStore($this->getRequest()); + return $this->nostoHelperAccount->findAccount($store); + } catch (Exception $e) { + $this->logger->exception($e); } - return $store; + return null; } } diff --git a/Block/Adminhtml/Form/Field/Tokens.php b/Block/Adminhtml/Form/Field/Tokens.php new file mode 100644 index 000000000..02d843dbf --- /dev/null +++ b/Block/Adminhtml/Form/Field/Tokens.php @@ -0,0 +1,114 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Block\Adminhtml\Form\Field; + +use Magento\Backend\Block\Template\Context; +use Magento\Config\Block\System\Config\Form\Field; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Data\Form\Element\AbstractElement; +use Nosto\Model\Signup\Account as SignupAccount; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; + +class Tokens extends Field +{ + /** @var NostoHelperAccount $nostoHelperAccount */ + public NostoHelperAccount $nostoHelperAccount; + + /** @var NostoHelperScope $nostoHelperScope */ + public NostoHelperScope $nostoHelperScope; + + /** @var Http $request */ + public Http $request; + + /** + * Tokens constructor. + * @param Context $context + * @param Http $request + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperAccount $nostoHelperAccount + * @param array $data + */ + public function __construct( + Context $context, + Http $request, + NostoHelperScope $nostoHelperScope, + NostoHelperAccount $nostoHelperAccount, + array $data = [] + ) { + parent::__construct($context, $data); + $this->nostoHelperAccount = $nostoHelperAccount; + $this->nostoHelperScope = $nostoHelperScope; + $this->request = $request; + } + + /** + * Get the Nosto account details + * + * @return SignupAccount|null + */ + public function getAccountDetails() + { + /** @SuppressWarnings(PHPMD.ShortVariable) */ + $id = (int) $this->request->getParam('store'); + $store = $this->nostoHelperScope->getStore($id); + return $this->nostoHelperAccount->findAccount($store); + } + + /** + * @param AbstractElement $element + * + * @return string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.CamelCaseMethodName) + */ + protected function _getElementHtml(AbstractElement $element) //@codingStandardsIgnoreLine + { + return $this->toHtml(); + } + + /** + * @return $this|Field + * @SuppressWarnings(PHPMD.CamelCaseMethodName) + */ + protected function _prepareLayout() //@codingStandardsIgnoreLine + { + parent::_prepareLayout(); + $this->setTemplate('tokens.phtml'); + return $this; + } +} diff --git a/Block/Category.php b/Block/Category.php index 75bd9f66a..dac638abb 100644 --- a/Block/Category.php +++ b/Block/Category.php @@ -1,35 +1,48 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Block; use Magento\Framework\Registry; use Magento\Framework\View\Element\Template; -use Nosto\Tagging\Model\Category\Builder as CategoryBuilder; +use Magento\Framework\View\Element\Template\Context; +use Nosto\Model\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; /** * Category block used for outputting meta-data on the stores category pages. @@ -38,49 +51,81 @@ */ class Category extends Template { + use TaggingTrait { + TaggingTrait::__construct as taggingConstruct; // @codingStandardsIgnoreLine + } + + /** + * @var Registry + */ + private Registry $registry; + /** - * @inheritdoc + * @var NostoCategoryBuilder */ - protected $_template = 'category.phtml'; + private NostoCategoryBuilder $categoryBuilder; /** - * @var Registry the framework registry. + * @var NostoHelperScope */ - protected $_registry; + private NostoHelperScope $nostoHelperScope; /** - * @var CategoryBuilder the category meta model builder. + * @var NostoHelperAccount */ - protected $_categoryBuilder; + private NostoHelperAccount $nostoHelperAccount; /** * Constructor. * - * @param Template\Context $context + * @param Context $context * @param Registry $registry - * @param CategoryBuilder $categoryBuilder + * @param NostoCategoryBuilder $categoryBuilder + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperAccount $nostoHelperAccount * @param array $data */ public function __construct( - Template\Context $context, + Context $context, Registry $registry, - CategoryBuilder $categoryBuilder, + NostoCategoryBuilder $categoryBuilder, + NostoHelperScope $nostoHelperScope, + NostoHelperAccount $nostoHelperAccount, array $data = [] ) { parent::__construct($context, $data); - $this->_registry = $registry; - $this->_categoryBuilder = $categoryBuilder; + $this->registry = $registry; + $this->categoryBuilder = $categoryBuilder; + $this->nostoHelperScope = $nostoHelperScope; + $this->nostoHelperAccount = $nostoHelperAccount; + } + + /** + * Returns the current category as a slash delimited string + * + * @return string|null the current category as a slash delimited string + */ + private function getNostoCategory() + { + /** @phan-suppress-next-line PhanDeprecatedFunction */ + $category = $this->registry->registry('current_category'); + $store = $this->nostoHelperScope->getStore(); + if ($category) { + return $this->categoryBuilder->build($category, $store); + } + return null; } /** - * Returns the Nosto category meta-data model. + * Returns the HTML to render categories * - * @return \NostoCategory the category meta data model. + * @return NostoCategory */ - public function getNostoCategory() + public function getAbstractObject() { - $category = $this->_registry->registry('current_category'); - return $this->_categoryBuilder->build($category); + $category = new NostoCategory(); + $category->setCategoryString($this->getNostoCategory()); + return $category; } } diff --git a/Block/Element.php b/Block/Element.php index 82e555535..2556d3f43 100644 --- a/Block/Element.php +++ b/Block/Element.php @@ -1,33 +1,45 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Block; use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; /** * Element block used for outputting a recommendation placeholders on the stores pages. @@ -36,10 +48,28 @@ */ class Element extends Template { + use TaggingTrait { + TaggingTrait::__construct as taggingConstruct; // @codingStandardsIgnoreLine + } + /** - * @inheritdoc + * Constructor. + * + * @param Context $context the context. + * @param NostoHelperAccount $nostoHelperAccount the account helper. + * @param NostoHelperScope $nostoHelperScope + * @param array $data optional data. */ - protected $_template = 'element.phtml'; + public function __construct( + Context $context, + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, + array $data = [] + ) { + parent::__construct($context, $data); + + $this->taggingConstruct($nostoHelperAccount, $nostoHelperScope); + } /** * Returns the Nosto recommendation placeholder ID. @@ -52,4 +82,13 @@ public function getElementId() { return $this->getData('nostoId'); } + + /** + * + * @return null + */ + public function getAbstractObject() + { + return null; + } } diff --git a/Block/Email/ImageUrl.php b/Block/Email/ImageUrl.php new file mode 100644 index 000000000..88227749a --- /dev/null +++ b/Block/Email/ImageUrl.php @@ -0,0 +1,100 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Block\Email; + +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; +use Nosto\Model\Email\ImageUrl as NostoImageUrl; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Logger\Logger as NostoLogger; + +/** + * ImageUrl block used for getting the image url + */ +class ImageUrl extends Template +{ + private NostoHelperScope $nostoHelperScope; + private NostoHelperAccount $nostoHelperAccount; + private NostoLogger $logger; + + /** + * Constructor. + * + * @param Context $context + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoLogger $logger + * @param array $data + */ + public function __construct( + Context $context, + NostoHelperScope $nostoHelperScope, + NostoHelperAccount $nostoHelperAccount, + NostoLogger $logger, + array $data = [] + ) { + parent::__construct($context, $data); + $this->nostoHelperScope = $nostoHelperScope; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->logger = $logger; + } + + /** + * Format the image url + * + * @return string + * @SuppressWarnings(PHPMD.CamelCaseMethodName) + */ + public function _toHtml() + { + $store = $this->nostoHelperScope->getStore(true); + $account = $this->nostoHelperAccount->findAccount($store); + + if (!$account || !$account->getName()) { + return ''; + } + + $urlTemplate = $this->getData(NostoImageUrl::URL_TEMPLATE); + $customerEmail = urlencode($this->getData(NostoImageUrl::CUSTOMER_EMAIL)); + $campaignId = urlencode($this->getData(NostoImageUrl::CAMPAIGN_ID)); + + $url = new NostoImageUrl($urlTemplate, $account->getName(), $customerEmail, $campaignId); + + return $url->format(); + } +} diff --git a/Block/Email/Visible.php b/Block/Email/Visible.php new file mode 100644 index 000000000..b562aa61d --- /dev/null +++ b/Block/Email/Visible.php @@ -0,0 +1,84 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Block\Email; + +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; + +/** + * Visible block used for hide the div in the email template + * if the nosto account is not available in current store view + */ +class Visible extends Template +{ + private NostoHelperScope $nostoHelperScope; + private NostoHelperAccount $nostoHelperAccount; + + /** + * Constructor. + * + * @param Context $context + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperAccount $nostoHelperAccount + * @param array $data + */ + public function __construct( + Context $context, + NostoHelperScope $nostoHelperScope, + NostoHelperAccount $nostoHelperAccount, + array $data = [] + ) { + parent::__construct($context, $data); + $this->nostoHelperScope = $nostoHelperScope; + $this->nostoHelperAccount = $nostoHelperAccount; + } + + /** + * Returns "" if current store view has nosto, otherwise 'style="display:none"' + * + * @return string + * @SuppressWarnings(PHPMD.CamelCaseMethodName) + */ + public function _toHtml() + { + $store = $this->nostoHelperScope->getStore(true); + $account = $this->nostoHelperAccount->findAccount($store); + return $account === null ? 'style="display:none"' : ''; + } +} diff --git a/Block/Embed.php b/Block/Embed.php index 9e7fe53ed..d74bbe400 100644 --- a/Block/Embed.php +++ b/Block/Embed.php @@ -1,37 +1,46 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Block; use Magento\Framework\View\Element\Template; use Magento\Framework\View\Element\Template\Context; -use Magento\Store\Model\Store; -use Nosto\Tagging\Helper\Account; -use Nosto\Tagging\Helper\Data; +use Nosto\Nosto; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; /** * Embed script block that includes the Nosto script in the page . @@ -39,36 +48,35 @@ */ class Embed extends Template { - /** - * The default Nosto server address to use if none is configured. - */ - const DEFAULT_SERVER_ADDRESS = 'connect.nosto.com'; - - /** - * @inheritdoc - */ - protected $_template = 'embed.phtml'; - private $_accountHelper; - private $_dataHelper; + use TaggingTrait { + TaggingTrait::__construct as taggingConstruct; // @codingStandardsIgnoreLine + } /** * Constructor. * * @param Context $context the context. - * @param Account $accountHelper the account helper. - * @param Data $dataHelper the data helper. + * @param NostoHelperAccount $nostoHelperAccount the account helper. + * @param NostoHelperScope $nostoHelperScope * @param array $data optional data. */ public function __construct( Context $context, - Account $accountHelper, - Data $dataHelper, + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, array $data = [] ) { parent::__construct($context, $data); - $this->_accountHelper = $accountHelper; - $this->_dataHelper = $dataHelper; + $this->taggingConstruct($nostoHelperAccount, $nostoHelperScope); + } + + public function getNostoScriptUrl() + { + if (Nosto::getServerUrl() && $this->getAccountName()) { + return '//' . Nosto::getServerUrl() . '/include/' . $this->getAccountName(); + } + return null; } /** @@ -78,23 +86,17 @@ public function __construct( */ public function getAccountName() { - /** @var Store $store */ - $store = $this->_storeManager->getStore(true); - $account = $this->_accountHelper->findAccount($store); - return !is_null($account) ? $account->getName() : ''; + $store = $this->nostoHelperScope->getStore(true); + $account = $this->nostoHelperAccount->findAccount($store); + return $account !== null ? $account->getName() : ''; } /** - * Returns the Nosto server address. - * This is taken from the local environment if it is set, or else it - * defaults to "connect.nosto.com". * - * @return string the url. + * @return null */ - public function getServerAddress() + public function getAbstractObject() { - return isset($_ENV['NOSTO_SERVER_URL']) - ? $_ENV['NOSTO_SERVER_URL'] - : self::DEFAULT_SERVER_ADDRESS; + return null; } } diff --git a/Block/Knockout.php b/Block/Knockout.php index a51204f58..4fd57f3fe 100755 --- a/Block/Knockout.php +++ b/Block/Knockout.php @@ -1,65 +1,101 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Block; use Magento\Framework\View\Element\Template; -use Nosto\Tagging\Helper\Account as NostoAccountHelper; +use Magento\Framework\View\Element\Template\Context; +use Magento\Store\Model\StoreManagerInterface; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Logger\Logger; +use Exception; /** * Cart block used for cart tagging. */ class Knockout extends Template { + /** @var NostoHelperAccount */ + private NostoHelperAccount $nostoHelperAccount; - /** - * @var NostoAccountHelper - */ - protected $nostoAccountHelper; + /** @var NostoHelperScope */ + private NostoHelperScope $nostoHelperScope; + + /** @var NostoHelperData */ + private NostoHelperData $nostoHelperData; + + /** @var StoreManagerInterface */ + private StoreManagerInterface $storeManager; + + /** @var Logger */ + private Logger $logger; /** - * Constructor - * - * @param Template\Context $context - * @param NostoAccountHelper $accountHelper + * Knockout constructor. + * @param Context $context + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperData $nostoHelperData + * @param Logger $logger * @param array $data */ public function __construct( - Template\Context $context, - NostoAccountHelper $accountHelper, + Context $context, + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, + NostoHelperData $nostoHelperData, + Logger $logger, array $data = [] - ) - { - + ) { parent::__construct($context, $data); - $this->nostoAccountHelper = $accountHelper; + $this->storeManager = $context->getStoreManager(); + $this->nostoHelperAccount = $nostoHelperAccount; + $this->nostoHelperScope = $nostoHelperScope; + $this->nostoHelperData = $nostoHelperData; + $this->logger = $logger; } - + /** + * Get relevant path to template + * + * @return string + * @suppress PhanTypeMismatchReturn + */ public function getTemplate() { $template = null; @@ -70,9 +106,27 @@ public function getTemplate() return $template; } + private function nostoEnabled() + { + $enabled = false; + if ($this->nostoHelperAccount->nostoInstalledAndEnabled( + $this->nostoHelperScope->getStore() + ) + ) { + $enabled = true; + } + + return $enabled; + } + + /** + * Retrieve serialized JS layout configuration ready to use in template + * + * @return string + */ public function getJsLayout() { - $jsLayout = null; + $jsLayout = ''; if ($this->nostoEnabled()) { $jsLayout = parent::getJsLayout(); } @@ -80,14 +134,19 @@ public function getJsLayout() return $jsLayout; } - private function nostoEnabled() { - $enabled = false; - if ($this->nostoAccountHelper->nostoInstalledAndEnabled( - $this->_storeManager->getStore() - )) { - $enabled= true; + /** + * @return int + */ + public function isReloadRecsAfterAtcEnabled() + { + $reload = 0; + try { + $store = $this->storeManager->getStore(); + $reload = $this->nostoHelperData->isReloadRecsAfterAtcEnabled($store); + } catch (Exception $e) { + $this->logger->debug("Could not get value for reloading recs after ATC"); + } finally { + return $reload; } - - return $enabled; } } diff --git a/Block/Meta.php b/Block/Meta.php index 80cf050b1..8fc84b76d 100644 --- a/Block/Meta.php +++ b/Block/Meta.php @@ -1,36 +1,46 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Block; use Magento\Framework\View\Element\Template; use Magento\Framework\View\Element\Template\Context; -use Magento\Store\Model\Store; -use Nosto\Tagging\Helper\Data as DataHelper; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; /** * Meta data block for outputting elements in the page . @@ -38,31 +48,32 @@ */ class Meta extends Template { - /** - * @inheritdoc - */ - protected $_template = 'meta.phtml'; + use TaggingTrait { + TaggingTrait::__construct as taggingConstruct; // @codingStandardsIgnoreLine + } - /** - * @var DataHelper the module data helper. - */ - protected $_dataHelper; + private NostoHelperData $nostoHelperData; /** * Constructor. * * @param Context $context the context. - * @param DataHelper $dataHelper the data helper. + * @param NostoHelperData $nostoHelperData the data helper. + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope * @param array $data optional data. */ public function __construct( Context $context, - DataHelper $dataHelper, + NostoHelperData $nostoHelperData, + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, array $data = [] ) { parent::__construct($context, $data); - $this->_dataHelper = $dataHelper; + $this->taggingConstruct($nostoHelperAccount, $nostoHelperScope); + $this->nostoHelperData = $nostoHelperData; } /** @@ -72,7 +83,7 @@ public function __construct( */ public function getModuleVersion() { - return $this->_dataHelper->getModuleVersion(); + return $this->nostoHelperData->getModuleVersion(); } /** @@ -82,7 +93,7 @@ public function getModuleVersion() */ public function getInstallationId() { - return $this->_dataHelper->getInstallationId(); + return $this->nostoHelperData->getInstallationId(); } /** @@ -92,8 +103,16 @@ public function getInstallationId() */ public function getLanguageCode() { - /** @var Store $store */ - $store = $this->_storeManager->getStore(true); + $store = $this->nostoHelperScope->getStore(true); return substr($store->getConfig('general/locale/code'), 0, 2); } + + /** + * + * @return null + */ + public function getAbstractObject() + { + return null; + } } diff --git a/Block/Order.php b/Block/Order.php index 99a9f40c3..cd20da95e 100644 --- a/Block/Order.php +++ b/Block/Order.php @@ -1,41 +1,50 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Block; use Magento\Checkout\Block\Success; use Magento\Checkout\Model\Session; -use Magento\Framework\Registry; -use Magento\Framework\View\Element\Template; -use /** @noinspection PhpUndefinedClassInspection */ - Magento\Sales\Model\OrderFactory; -use Nosto\Tagging\Helper\Format; -use Nosto\Tagging\Model\Order\Builder as OrderBuilder; -use NostoPrice; +use Magento\Framework\View\Element\Template\Context; +use Magento\Sales\Model\OrderFactory; +use Nosto\Helper\DateHelper; +use Nosto\Helper\PriceHelper; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Model\Order\Builder as NostoOrderBuilder; /** * Category block used for outputting meta-data on the stores category pages. @@ -44,93 +53,70 @@ */ class Order extends Success { - /** - * @inheritdoc - */ - protected $_template = 'order.phtml'; - - /** - * @var OrderBuilder the order meta model builder. - */ - protected $_orderBuilder; - - /** - * @var Registry the framework registry. - */ - protected $_registry; + use TaggingTrait { + TaggingTrait::__construct as taggingConstruct; // @codingStandardsIgnoreLine + } - /** - * @var Format the format helper. - */ - protected $_formatHelper; - /** - * @var Session - */ - protected $_checkoutSession; + private NostoOrderBuilder $nostoOrderBuilder; + private Session $checkoutSession; - /** @noinspection PhpUndefinedClassInspection */ /** * Constructor. * - * @param Template\Context $context + * @param Context $context * @param OrderFactory $orderFactory - * @param OrderBuilder $orderBuilder - * @param Format $formatHelper + * @param NostoOrderBuilder $orderBuilder * @param Session $checkoutSession + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope * @param array $data - * @internal param Registry $registry - * @internal param CategoryBuilder $categoryBuilder */ public function __construct( - Template\Context $context, - /** @noinspection PhpUndefinedClassInspection */ + Context $context, OrderFactory $orderFactory, - OrderBuilder $orderBuilder, - Format $formatHelper, + NostoOrderBuilder $orderBuilder, Session $checkoutSession, + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, array $data = [] ) { - parent::__construct( - $context, - $orderFactory, - $data - ); + parent::__construct($context, $orderFactory, $data); - $this->_formatHelper = $formatHelper; - $this->_checkoutSession = $checkoutSession; - $this->_orderBuilder = $orderBuilder; + $this->taggingConstruct($nostoHelperAccount, $nostoHelperScope); + $this->checkoutSession = $checkoutSession; + $this->nostoOrderBuilder = $orderBuilder; } /** * Returns the Nosto order meta-data model. * - * @return \NostoOrder the order meta data model. + * @return \Nosto\Model\Order\Order the order meta data model. */ - public function getNostoOrder() + public function getAbstractObject() { /** @var \Magento\Sales\Model\Order $order */ - return $this->_orderBuilder->build($this->_checkoutSession->getLastRealOrder()); + return $this->nostoOrderBuilder->build($this->checkoutSession->getLastRealOrder()); } /** - * Formats a \NostoPrice object, e.g. "1234.56". + * Formats a price e.g. "1234.56". * - * @param NostoPrice $price the price to format. + * @param int|float|string $price the price to format. * @return string the formatted price. */ - public function formatNostoPrice(NostoPrice $price) + public function formatNostoPrice($price) { - return $this->_formatHelper->formatPrice($price); + return PriceHelper::format($price); } /** - * Formats a \NostoDate object, e.g. "2015-12-24"; + * Formats a date, e.g. "2015-12-24"; * - * @param \NostoDate $date the date to format. + * @param string $date the date to format. * @return string the formatted date. */ - public function formatNostoDate(\NostoDate $date) + public function formatNostoDate(string $date) { - return $this->_formatHelper->formatDate($date); + return DateHelper::format($date); } } diff --git a/Block/PageType.php b/Block/PageType.php new file mode 100644 index 000000000..039382009 --- /dev/null +++ b/Block/PageType.php @@ -0,0 +1,99 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Block; + +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; +use Nosto\Model\PageType as NostoPageType; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; + +/** + * Page type block used for outputting page-type on the different pages. + */ +class PageType extends Template +{ + use TaggingTrait { + TaggingTrait::__construct as taggingConstruct; // @codingStandardsIgnoreLine + } + + /** + * Default type assigned to the page if none is set in the layout xml. + */ + public const DEFAULT_TYPE = 'unknown'; + + /** + * Constructor. + * + * @param Context $context + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope + * @param array $data + */ + public function __construct( + Context $context, + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, + array $data = [] + ) { + parent::__construct($context, $data); + $this->taggingConstruct($nostoHelperAccount, $nostoHelperScope); + } + + /** + * Return the page-type of the current page. If none is defined in the layout xml, + * then set a default one. + * + * @return string + */ + private function getPageTypeName() + { + return $this->getData('page_type') ?: self::DEFAULT_TYPE; + } + + /** + * Returns the HTML to render PageTypes + * + * @return string + */ + public function getAbstractObject() + { + return (new NostoPageType( + $this->getPageTypeName() + )); + } +} diff --git a/Block/Product.php b/Block/Product.php index f9449cfef..7224344d4 100644 --- a/Block/Product.php +++ b/Block/Product.php @@ -1,32 +1,42 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Block; +use Exception; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Block\Product\Context; use Magento\Catalog\Block\Product\View; @@ -37,11 +47,13 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Stdlib\StringUtils; use Magento\Framework\Url\EncoderInterface as UrlEncoder; -use Magento\Store\Model\Store; -use Nosto\Tagging\Helper\Data; -use Nosto\Tagging\Helper\Format; -use Nosto\Tagging\Model\Category\Builder as CategoryBuilder; -use Nosto\Tagging\Model\Product\Builder as ProductBuilder; +use Nosto\Helper\DateHelper; +use Nosto\Helper\PriceHelper; +use Nosto\Model\Product\Product as NostoProduct; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Model\Category\Builder as NostoCategoryBuilder; +use Nosto\Tagging\Model\Service\Product\ProductServiceInterface; /** * Product block used for outputting meta-data on the stores product pages. @@ -50,30 +62,15 @@ */ class Product extends View { - /** - * @inheritdoc - */ - protected $_template = 'product.phtml'; + use TaggingTrait { + TaggingTrait::__construct as taggingConstruct; // @codingStandardsIgnoreLine + } - /** - * @var ProductBuilder the product meta model builder. - */ - protected $_productBuilder; + /** @var NostoCategoryBuilder */ + private NostoCategoryBuilder $categoryBuilder; - /** - * @var CategoryBuilder the category meta model builder. - */ - protected $_categoryBuilder; - - /** - * @var Data the data helper. - */ - protected $_dataHelper; - - /** - * @var Format the format helper. - */ - protected $_formatHelper; + /** @var ProductServiceInterface */ + private ProductServiceInterface $productService; /** * Constructor. @@ -88,11 +85,12 @@ class Product extends View * @param Session $customerSession the user session. * @param ProductRepositoryInterface $productRepository th product repository. * @param PriceCurrencyInterface $priceCurrency the price currency. - * @param ProductBuilder $productBuilder the product meta model builder. - * @param CategoryBuilder $categoryBuilder the category meta model builder. - * @param Data $dataHelper the data helper. - * @param Format $formatHelper the format helper. + * @param NostoCategoryBuilder $categoryBuilder the category meta model builder. + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope + * @param ProductServiceInterface $productService * @param array $data optional data. + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( Context $context, @@ -105,10 +103,10 @@ public function __construct( Session $customerSession, ProductRepositoryInterface $productRepository, PriceCurrencyInterface $priceCurrency, - ProductBuilder $productBuilder, - CategoryBuilder $categoryBuilder, - Data $dataHelper, - Format $formatHelper, + NostoCategoryBuilder $categoryBuilder, + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, + ProductServiceInterface $productService, array $data = [] ) { parent::__construct( @@ -125,59 +123,69 @@ public function __construct( $data ); - $this->_productBuilder = $productBuilder; - $this->_categoryBuilder = $categoryBuilder; - $this->_dataHelper = $dataHelper; - $this->_formatHelper = $formatHelper; + $this->taggingConstruct($nostoHelperAccount, $nostoHelperScope); + $this->categoryBuilder = $categoryBuilder; + $this->productService = $productService; } /** * Returns the Nosto product DTO. * - * @return \NostoProduct the product meta data model. + * @return NostoProduct + * @throws Exception */ - public function getNostoProduct() + public function getAbstractObject() { - /** @var Store $store */ - $store = $this->_storeManager->getStore(); - return $this->_productBuilder->build( + return $this->productService->getProduct( $this->getProduct(), - $store + $this->nostoHelperScope->getStore() ); } /** * Returns the Nosto category DTO. * - * @return \NostoCategory the category meta data model. + * @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'); - return !is_null($category) - ? $this->_categoryBuilder->build($category) - : null; + $store = $this->nostoHelperScope->getStore(); + return $category !== null ? $this->categoryBuilder->build($category, $store) : null; } /** - * Formats a \NostoPrice object, e.g. "1234.56". + * Formats a price e.g. "1234.56". * - * @param \NostoPrice $price the price to format. + * @param int|float|string $price the price to format. * @return string the formatted price. */ - public function formatNostoPrice(\NostoPrice $price) + public function formatNostoPrice($price) { - return $this->_formatHelper->formatPrice($price); + return PriceHelper::format($price); } /** - * Formats a \NostoDate object, e.g. "2015-12-24"; + * Formats a date, e.g. "2015-12-24"; * - * @param \NostoDate $date the date to format. + * @param string $date the date to format. * @return string the formatted date. */ - public function formatNostoDate(\NostoDate $date) + public function formatNostoDate(string $date) + { + return DateHelper::format($date); + } + + /** + * Checks if store uses more than one currency in order to decide whether to hide or show the + * nosto_variation tagging. + * + * @return bool a boolean value indicating whether the store has more than one currency + */ + public function hasMultipleCurrencies() { - return $this->_formatHelper->formatDate($date); + $store = $this->nostoHelperScope->getStore(true); + return count($store->getAvailableCurrencyCodes(true)) > 1; } } diff --git a/Block/Search.php b/Block/Search.php index 38663c91f..9b14eda2e 100644 --- a/Block/Search.php +++ b/Block/Search.php @@ -1,33 +1,50 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Block; +use Magento\Catalog\Model\Layer\Resolver as LayerResolver; use Magento\CatalogSearch\Block\Result; +use Magento\CatalogSearch\Helper\Data; +use Magento\Framework\View\Element\Template\Context; +use Magento\Search\Model\QueryFactory; +use Nosto\Model\MarkupableString; +use Nosto\Model\SearchTerm; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; /** * Search block used for outputting meta-data on the stores search pages. @@ -36,10 +53,33 @@ */ class Search extends Result { + use TaggingTrait { + TaggingTrait::__construct as taggingConstruct; // @codingStandardsIgnoreLine + } + /** - * @inheritdoc + * Search constructor. + * @param Context $context + * @param LayerResolver $layerResolver + * @param Data $catalogSearchData + * @param QueryFactory $queryFactory + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope + * @param array $data */ - protected $_template = 'search.phtml'; + public function __construct( + Context $context, + LayerResolver $layerResolver, + Data $catalogSearchData, + QueryFactory $queryFactory, + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, + array $data = [] + ) { + parent::__construct($context, $layerResolver, $catalogSearchData, $queryFactory, $data); + + $this->taggingConstruct($nostoHelperAccount, $nostoHelperScope); + } /** * Returns the current escaped search term @@ -50,4 +90,18 @@ public function getNostoSearchTerm() { return $this->catalogSearchData->getEscapedQueryText(); } + + /** + * Returns the HTML to render search blocks + * + * @return MarkupableString + */ + public function getAbstractObject() + { + $searchTerm = new SearchTerm( + $this->getNostoSearchTerm() + ); + $searchTerm->disableAutoEncodeAll(); + return $searchTerm; + } } diff --git a/Block/Stub.php b/Block/Stub.php new file mode 100644 index 000000000..f5808ed06 --- /dev/null +++ b/Block/Stub.php @@ -0,0 +1,127 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Block; + +use Magento\Framework\View\Element\Template; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Customer as NostoHelperCustomer; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Helper\Variation as NostoHelperVariation; + +/** + * Nosto JS stub block + * + * @author Nosto Solutions Ltd + */ +class Stub extends Template +{ + use TaggingTrait { + TaggingTrait::__construct as taggingConstruct; // @codingStandardsIgnoreLine + } + + /** + * @var NostoHelperData + */ + private NostoHelperData $nostoHelperData; + + /** + * @var NostoHelperCustomer + */ + private NostoHelperCustomer $nostoHelperCustomer; + + /** + * @var NostoHelperVariation + */ + private NostoHelperVariation $nostoHelperVariation; + + /** + * Stub constructor. + * @param Template\Context $context + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperData $nostoHelperData + * @param NostoHelperCustomer $nostoHelperCustomer + * @param NostoHelperVariation $nostoHelperVariation + * @param array $data + */ + public function __construct( + Template\Context $context, + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, + NostoHelperData $nostoHelperData, + NostoHelperCustomer $nostoHelperCustomer, + NostoHelperVariation $nostoHelperVariation, + array $data = [] + ) { + parent::__construct($context, $data); + $this->taggingConstruct($nostoHelperAccount, $nostoHelperScope); + $this->nostoHelperData = $nostoHelperData; + $this->nostoHelperCustomer = $nostoHelperCustomer; + $this->nostoHelperVariation = $nostoHelperVariation; + } + + /** + * + * @return null + */ + public function getAbstractObject() + { + return null; + } + + /** + * Returns if autoloading recommendations is disabled or not. + * + * @return boolean + */ + public function isRecoAutoloadDisabled() + { + $store = $this->getNostoHelperScope()->getStore(true); + // If price variations are used and the variation something else than + // the default one we disable the autoload. For default variation + // the sections are not loaded and loadRecommendations() is not called + if ($this->nostoHelperData->isPricingVariationEnabled($store) + && !$this->nostoHelperVariation->isDefaultVariationCode( + $this->nostoHelperCustomer->getGroupCode() + ) + ) { + return true; + } + return false; + } +} diff --git a/Block/TaggingTrait.php b/Block/TaggingTrait.php new file mode 100644 index 000000000..bc6efd252 --- /dev/null +++ b/Block/TaggingTrait.php @@ -0,0 +1,100 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Block; + +use Nosto\AbstractObject; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; + +trait TaggingTrait +{ + private NostoHelperAccount $nostoHelperAccount; + private NostoHelperScope $nostoHelperScope; + + /** + * TaggingTrait constructor. + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope + */ + public function __construct( + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope + ) { + $this->nostoHelperAccount = $nostoHelperAccount; + $this->nostoHelperScope = $nostoHelperScope; + } + + /** + * Overridden method that only outputs any markup if the extension is enabled and an account + * exists for the current store view. + * + * @return string the markup or an empty string (if an account doesn't exist) + * @suppress PhanTraitParentReference + */ + public function _toHtml() + { + if ($this->nostoHelperAccount->nostoInstalledAndEnabled($this->nostoHelperScope->getStore())) { + $abstractObject = $this->getAbstractObject(); + if ($abstractObject instanceof AbstractObject) { + return $abstractObject->toHtml(); + } + return parent::_toHtml(); + } + return ''; + } + + /** + * @return NostoHelperScope + */ + public function getNostoHelperScope() + { + return $this->nostoHelperScope; + } + + /** + * @return NostoHelperAccount + */ + public function getNostoHelperAccount() + { + return $this->nostoHelperAccount; + } + + /** + * @return AbstractObject + */ + abstract public function getAbstractObject(); +} diff --git a/Block/Variation.php b/Block/Variation.php new file mode 100644 index 000000000..032f58dde --- /dev/null +++ b/Block/Variation.php @@ -0,0 +1,165 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Block; + +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; +use Nosto\Model\MarkupableString; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Currency as NostoHelperCurrency; +use Nosto\Tagging\Helper\Customer as NostoHelperCustomer; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Helper\Variation as NostoHelperVariation; + +/** + * Page type block used for outputting the variation identifier on the different pages. + */ +class Variation extends Template +{ + use TaggingTrait { + TaggingTrait::__construct as taggingConstruct; // @codingStandardsIgnoreLine + } + + /** + * @var NostoHelperCurrency + */ + private NostoHelperCurrency $nostoHelperCurrency; + + /** + * @var NostoHelperData + */ + private NostoHelperData $nostoHelperData; + + /** + * @var NostoHelperCustomer + */ + private NostoHelperCustomer $nostoHelperCustomer; + + /** + * @var NostoHelperVariation + */ + private NostoHelperVariation $nostoHelperVariation; + + /** + * Variation constructor. + * + * @param Context $context + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperCurrency $nostoHelperCurrency + * @param NostoHelperData $nostoHelperData + * @param NostoHelperCustomer $nostoHelperCustomer + * @param NostoHelperVariation $nostoHelperVariation + * @param array $data + */ + public function __construct( + Context $context, + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, + NostoHelperCurrency $nostoHelperCurrency, + NostoHelperData $nostoHelperData, + NostoHelperCustomer $nostoHelperCustomer, + NostoHelperVariation $nostoHelperVariation, + array $data = [] + ) { + parent::__construct($context, $data); + + $this->taggingConstruct($nostoHelperAccount, $nostoHelperScope); + $this->nostoHelperCurrency = $nostoHelperCurrency; + $this->nostoHelperData = $nostoHelperData; + $this->nostoHelperCustomer = $nostoHelperCustomer; + $this->nostoHelperVariation = $nostoHelperVariation; + } + + /** + * Return the current variation id + * + * @return string + */ + public function getVariationId() + { + $store = $this->nostoHelperScope->getStore(true); + if ($this->nostoHelperData->isMultiCurrencyDisabled($store) + && $this->nostoHelperData->isPricingVariationEnabled($store) + ) { + return $this->nostoHelperCustomer->getGroupCode(); + } + + return $store->getCurrentCurrencyCode(); + } + + /** + * Checks if store uses more than one currency in order to decide whether to hide or show the + * nosto_variation tagging. + * + * @return bool a boolean value indicating whether the store has more than one currency + */ + public function hasMultipleCurrencies() + { + $store = $this->nostoHelperScope->getStore(true); + return $this->nostoHelperCurrency->getCurrencyCount($store) > 1; + } + + /** + * Returns the HTML to render variation blocks + * + * @return MarkupableString|string + */ + public function getAbstractObject() + { + $store = $this->nostoHelperScope->getStore(true); + + // We inject the active variation tag if the exchange rates are used or + // if the price variations are used and the active variation is the + // default one + if ($this->nostoHelperCurrency->exchangeRatesInUse($store) + || ($this->nostoHelperData->isPricingVariationEnabled($store) + && $this->nostoHelperVariation->isDefaultVariationCode( + $this->nostoHelperCustomer->getGroupCode() + ) + + ) + ) { + return new MarkupableString( + $this->getVariationId(), + 'nosto_variation' + ); + } + return ''; + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..79cf9df28 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,570 @@ +All notable changes to this project will be documented in this file. This project adheres to Semantic Versioning. + +### 6.0.0 +* Compatibility with Magento 2.4.4 +* Bump minimum PHP version to 7.4 +* Use of XML schemas and data patches +* Remove personally identifiable information from the module + +### 5.4.3 +* Add page-type tagging for checkout page + +### 5.4.2 +* Send all active currencies formating to Nosto only when multi-currency is enabled +* Fix failing product data tagging for configurable product with no SKUs + +### 5.4.1 +* Fix product availability building for products with OOS threshold + +### 5.4.0 +* Add functionality to send disabled products to Nosto +* Add ttl for nosto_product_cache +* Refactor BuilderTrait into service classes + +### 5.3.2 +* Improve exception during product build by passing previos exception + +### 5.3.1 +* Fix to use placeholder thumbnail image if product has no image + +### 5.3.0 +* Index products to Nosto after bulk updates + +### 5.2.10 +* Improve message text for mixed nosto accounts +* Remove old changelog tables when upgrading to v5 + +### 5.2.9 +* Upgrade sdk version to update phpseclib dependency + +### 5.2.8 +* Fix simple products not being reindex issue + +### 5.2.7 +* Add configuration to reload recs after adding product to cart + +### 5.2.6 +* Get the correct product data by emulating the store + +### 5.2.5 +* Fix product update consumer running out of memory issue + +### 5.2.4 +* Fix order tagging rendering in case variation tags were missing + +### 5.2.3 +* Pass correct product id when adding grouped product to cart + +### 5.2.2 +* Fix bug where disabled parent product ids are added to reindex queue + +### 5.2.1 +* Sort categories inside breadcrumb based on their level + +### 5.2.0 +* Remove synchronous product indexing +* Improve queue consumer message + +### 5.1.2 +* Update PHP-SDK dependency version for better http exception logging + +### 5.1.1 +* Check that order payment is instance of payment interface + +### 5.1.0 +* Fix addMultipleProductsToCart issue happening in M2 cloud + +### 5.0.8 +* Bump dependencies to be compatible with Nosto CMP module + +### 5.0.7 +* User serializer provided by the PHP SDK to keep the module compatible with Magento EQP + +### 5.0.6 +* Add Content Security Policy (CSP) whitelist + +### 5.0.5 +* Fix an issue where if deleted user with ID 1, the indexer will throw foreign keys constraints errors + +### 5.0.4 +* Fix an issue with incorrect prices when different base currencies are used in websites and taxes are included in display prices + +### 5.0.3 +* Bump the PHP SDK version to be compatible with Nosto CMP module (no functional changes) + +### 5.0.2 +* Fix an issue where custom tags (tag1) were overridden by default tags + +### 5.0.1 +* Fix an issue with configurable product prices being zero in Nosto product data when taxes are included in display prices + +### 5.0.0 +* Refactor the indexing logic to use batched queues & decouple the caching logic from product updates +* Use Magento's built-in caching logic for caching Nosto product data +* Add google category as customisable attribute +* Change the namespaces to comply with PHP SDK 5.0.0 +* Add check for an empty array before trying to get min price for bundled product price +* Remove mview subscription / trigger to catalog_product_entity_media_gallery + +### 4.0.9 +* Fix an issue with configurable product prices not being set when using MSI + +### 4.0.8 +* Add null guard for caching product service in case the product data building fails for dirty product + +### 4.0.7 +* Handle empty / invalid product cache entries and possible failures in product data building gracefully + +### 4.0.6 +* Fix issue with non-generated proxy classes during di compilation + +### 4.0.5 +* Fix an issue where product cache table was not created during upgrade + +### 4.0.4 +* Store cached Nosto product data as a base64 encoded string in database to avoid problems with character sets and collations +* Alter the type of cached product to be longtext to allow saving large product data sets + +### 4.0.3 +* `setup:upgrade` for customer now saves only customer reference instead of entire customer object + +### 4.0.2 +* Fix an issue where setup:upgrade could crash if customer migration is faulty + +### 4.0.1 +* Make the new order detection more fault tolerant by comparing also updated at and created at timestamps + +### 4.0.0 +**New features (performance improvements)** +* Introduce cache for Nosto product data to speedup the product tagging added to the product pages +* Introduce Nosto product data change detection to avoid redundant API calls to Nosto +* Utilize bulk operations for product updates +* Add support for indexing in parallel mode +* Introduce caching for building product attribute values +* Introduce caching layer for building categories + +**Bug fixes and improvements** +* Generate customer reference for all registered customers automatically during setup upgrade +* Cleanup the change log database table after indexer run +* Prevent redundant full reindex on Nosto indexers when running `setup:upgrade` +* Fix GTIN attribute being set with margin value + +**Removed features / functionalities** +* Remove logic for sending cart updates to Nosto from server side + +### 3.10.5 +* Hide Nosto customer reference for registered customers in account edit view + +### 3.10.4 +* Bump SDK version to 4.0.10 to fix OrderStatus Handlers throwing exceptions + +### 3.10.3 +* Fix an issue where product url contains the category breadcrumbs if shortest url is not first entry in database table + +### 3.10.2 +* Fix an issue where the indexer page size was not set properly (Credit goes to `Deepak Upadhyay` (https://github.com/dupadhyay3)) + +### 3.10.1 +* Fix an issue with sending order confirmations via API when customer details could not be resolved + +### 3.10.0 +* Speed-up sku collection building by using native magento method to fetch configurable product SKU's +* Speed-up category name building by using Magento's category collection + +Credits also goes to `Ivan Chepurnyi` (https://github.com/IvanChepurnyi) for his performance improvement feedback + +### 3.9.0 +* Remove category personalization features (separate plug-in) +* Use graphql for sending order confirmations and order status updates +* Speedup the SKU price lookups by using price index table (catalog_product_index_price) +* Speedup the inventory level lookups by using `StockRegistryProvider` +* Add constraint for minimum supported Magento version (2.2.6) + +### 3.8.8 +* Fix the compatibility issue with Magento 2.3.3 in ratings & reviews building + +### 3.8.7 +* Fix product availability when in single store mode + +### 3.8.6 +* Fix add to cart array comparison + +### 3.8.5 +* Remove proxy classes from constructors to be in accordance with Magento marketplace code review + +### 3.8.4 +* Fix Nosto indexer's full reindex logic + +### 3.8.3 +* Fix parent product resolving for SKUs in product service + +### 3.8.2 +* Fix redirect_uri for multi-store setup with different domains + +### 3.8.1 +* Fix version comparison on data upgrading + +### 3.8.0 +* Use mock product in category personalisation when preview mode is enabled +* Generate customer_reference value automatically when new customer is created +* Create command line to generate customer_reference value for all customers missing it +* Add inventory level data to SKUs + +### 3.7.5 +* Remove product cache flushing after upsert to avoid failures in altering product categories + +### 3.7.4 +* Fix issue with trying to call method on null when order confirmation observer fails + +### 3.7.3 +* Fix issue with add to cart controller where product from request could be null +* Fetch the customer group directly from the session since the logged in check seems to fail when full page cahce is enabled + +### 3.7.2 +* Get current store from store code param when connecting existing Nosto account +* Fix unclosed javascript method when Nosto autoload is enabled + +### 3.7.1 +* Fix issue in setting the marketing permission in customer tagging + +### 3.7.0 +* Add customer reference field when installing the extension +* Ignore non-numeric product ids in Graphql responses + +### 3.6.1 +* Use sections for active variation tagging when price variations are enabled +* Exclude base variation from variation collection + +### 3.6.0 +* Add support for persistent customer reference +* Enrich the customer tagging to contain more fields + +### 3.5.0 +* Refactor console commands to use proxy dependencies to avoid redundant dependency injection chain reaction +* Remove redundant injected dependencies from block classes +* Enrich the category tagging to contain url, image, etc. + +### 3.4.1 +* Fix issue with price variations in case catalog rules are specified + +### 3.4.0 +* Add category personalisation for sorting products +* Fix issue with sending orders when user is not logged in +* Fix issue with sending unmatched orders +* Add date published to product tagging +* Fix issue with redirect url +* Fix issue with reconnecting same account for same scope + +### 3.3.0 +* Fix an issue with configurable products that were added to cart had no link or image +* Handle exceptions in line cart line item building and order line item building +* Remove support for PHP < 7.0.0 + +### 3.2.3 +* Fix issue with scope when saving the domain + +### 3.2.2 +* Fix issue with missing storefront domain when upgrading module + +### 3.2.1 +* Fix issue calculating bundle product price with no options + +### 3.2.0 +* Disable price variation when multicurrency is enabled +* Skip product attributes that contains arrays with non-scalar values +* Exclude non-scalar arrays from tags selector +* Refactor method being called many times + +### 3.1.0 +* Define percentage of PHP memory that indexer is allowed to use +* Prevent indexing in case Nosto is not connected +* Improve memory usage during indexing proccess +* Encode HTML characters automatically +* Save storefront domain when creating or connecting Nosto account +* Display warning in case of mismatching live domain with stored domain +* Include active domain and Nosto account in API calls +* Display Nosto tokens in store configuration + +### 3.0.4 +* Fix issue populating custom fields from Nosto product tags + +### 3.0.3 +* Fix error that may happen when order and cart items has no parent product associated + +### 3.0.2 +* Bump Nosto SDK version to fix the double encoded Oauth redirect URL +* Remove redundant module manager dependency from rating helper + +### 3.0.1 +* Bump Nosto SDK version to support HTTP 2 + +### 3.0.0 +**New features** +* Add support for using customer group pricing in Nosto recommendations +* Introduce a cli command for connecting Nosto account via command line +* Support using Yotpo ratings and reviews in Nosto recommendations +* Support using same nosto email widget snippet for multiple Nosto accounts +* Update marketing permission to Nosto in real-time when newsletter subscription is changed +* Support adding multiple products to cart from Nosto recommendations + +**Fixes & improvements** +* Improve performance for generating tagging (@hostep) +* Fix the issue with product building when no custom fields are found (@hostep) +* Improve error handling for Nosto dashboard in store admin area +* Code style fixes & refactoring + +### 2.11.8 +* Fix an issue that could prevent the extension to be installed in Magento 2.3 + +### 2.11.7 +* Fix wrong category translation + +### 2.11.6 +* Add batching for scheduled indexer + +### 2.11.5 +* Fix bundled product throwing exceptions when option has no selections + +### 2.11.4 +* Improve the stock status check for Nosto prouducts and SKUs + +### 2.11.3 +* Fix check causing all SKU’s to have invisible availability + +### 2.11.2 +* Ensure that the product is available in given store scope before building Nosto product object + +### 2.11.1 +* Use IframeTrait for URL building in order to make the possible errors more visible + +### 2.11.0 +* Add possibility to remove “/pub/” directory from product image URLs +* Add possibility to define quantity for Nosto’s add to cart +* Obey the alternative image tagging feature flag + +### 2.10.6 +* Fix add to cart (Recobuy) javascript errors when Magento 2 is configured to minimize, merge or bundle javascript files + +### 2.10.5 +* Fix product availability check for non visible products in the website + +### 2.10.4 +* Fix issue that send null prices when configurable product has no available SKU + +### 2.10.3 +* Fix Nosto product prices to obey the tax display setting +* Fix product indexer database issue with large catalogs + +### 2.10.2 +* Add fallback for product URL builder in case the rewrites are missing + +### 2.10.1 +* Fix issue with product service not handling model filter when Nosto product builder returns null + +### 2.10.0 +* Add possibility to disable Nosto's multi currency features +* Fix Nosto iframe loading bug + +### 2.9.0 +* Add advanced setting to disable sending customer data to Nosto servers + +### 2.8.0 +* Add marketing permission for customer tagging and for buyer (GDPR compatibility) +* Fix the Nosto account installation screen if no products are attached in a store view + +### 2.7.0 +Improvements +* Prefix order numbers to avoid collision with already used order numbers when migrating from Magento 1 to Magento 2 +* Add possibility to exclude products from Nosto index +* Improve the add to cart popup trigger + +Bug fixes +* Fix the list price for bundled products when all selections are optional + +Refactoring +* Use repositories instead of factories where applicable +* Render Nosto tagging programmatically without templates + +### 2.6.1 +* Improve add to cart popup trigger +* Add support for removing / discontinuing products in Nosto +* Apply catalog price rules for product API calls + +### 2.6.0 +* Add support for sending cart updates to nosto when product is added to the shopping cart + +### 2.5.0 +* Add setting for hiding store codes from URLs +* Add a button links to the nosto configuration page +* Add sku id in the cart and order tagging +* Add custom fields tagging to product +* Fix the issue that order and product importing to nosto did not work in php strict mode + +### 2.4.0 +* Add CI definitions +* Fix doc blocks & coding standard issues +* Clear page cache and layout cache after Nosto account is installed, reconnected or removed +* Introduce repository for Nosto Customer +* Fix infinite redirect loop on Nosto admin page +* Rename price helper methods to avoid confusion whether the taxes are included or not + +### 2.3.10 +* Fix the issue with sku availability being always in stock + +### 2.3.9 +* Remove debug logging for database queries + +### 2.3.8 +* Fix the issue that current currency tagging is missing + +### 2.3.7 +* Enable sku tagging by default + +### 2.3.6 +* Update related products by indexer if catalog price rule, review and rating or inventory level get updated +* Update parent product by indexer if its child product has been deleted + +### 2.3.5 +* Fix indexer bugs + +### 2.3.4 +* Fix tracking issue of adding sku to cart + +### 2.3.3 +* Fix exception handling bug + +### 2.3.2 +* Fix Nosto customer saving + +### 2.3.1 +* Define arguments for custom logger +* Handle non-existent categories in product category builder +* Move the second recommendation slot under the search results on search results page + +### 2.3.0 +Improvements +* Introduce update queue and custom indexer for Nosto product updates +* Add cron job to update advanced scheduled pricing (Community edition only) +* Add a feature flag for low stock notifications +* Report Nosto exception to New Relic if available +* Send more contact details to Nosto during the order confirmation +* Omit inventory level and margin determination when product model is used for tagging + +### 2.2.3 +Bug fixes +* Ignore disabled variations from price calculation and SKU tagging + +### 2.2.2 +Bug fixes +* Fix the discount calculation in order items + +### 2.2.1 +Bug fixes +* Fix the price handling for configurable products with no SKUs +* Fix product observer to fetch the parent configurable product +* Check if product is scheduled / staged before calling Nosto API + +### 2.2.0 +Improvements +* Fix deprecated layout definitions +* Display more descriptive errors when account opening fails +* Rename restore cart controller to avoid routing issues with case sensitive setup +* Add missing positioning attributes to layout definitions +* Add "Do not edit" notifications to template files +* Send product updates to Nosto when ratings and reviews are updated or added + +Bug fixes +* Fix Nosto product handling in multi-store setup +* Recover from invalid account opening requests +* Remove default variation id from account opening if multiple currencies are not used + +### 2.1.0 +Improvements +* Add support for restore cart link +* Add possibility to add product attributes to Nosto tags +* Add support for indicating low stock for a product +* Add support for using thumb url +* Include Magento's object for events dispatched by Nosto + +Bug fixes +* Set area code outside constructor in product sync command +* Remove multi-currency check from product template +* Check Nosto account before rendering the javascript stub +* Add null checks item builders + +### 2.0.1 +* Fix the multi-currency variation issue when only single currency is used + +### 2.0.0 +* Add possibility to use following attributes in Nosto + * GTIN + * brand + * inventory level + * supplier cost + * rating + * review count + * alternative image URLs +* Add possibility to extend / override product data +* Add support for multi-currency stores +* Add possibility to choose which image version is tagged +* Add support for handling the qualification UI +* Add check if Nosto account is installed before outputting Nosto tags & scripts +* Add page type tagging +* Add support for customer reference +* Implement support for "Add to cart" button for recommendations +* Update account settings over the API to Nosto +* Fix product price issue with special prices +* Fix product price to obey tax rules +* Fix product URL sent via API to Nosto +* Fix list price issue configurable products +* Update to the latest Nosto PHP SDK + +### 1.2.0 +* Stable release +* Stable release + +### 1.2.0-RC2 +* Updated the composer lock +* Removed the minimum stability flag + +### 1.2.0-RC1 +* Bump to SDK version 2.5.2 +* Make compatible with MEQP + +### 1.1.1 +* Fix the errors when running Magento compiler +* Change the block definition of referenceContainer to referenceBlock + +### 1.1.0 +* Use Knockout.js for dynamic cart and customer tagging in order to handle full page cache correctly + +### 1.0.1 +* Add "js stub" for Nosto script +* Fix issue with orders when Nosto module is installed but Nosto account is not connected + +### 1.0.0 +* Make the plug-in compatible with Magento 2.1.0 + +### 1.0.0-RC4 +* Remove variation tagging + +### 1.0.0-RC3 +* Fix store resolving issue(#18) + +### 1.0.0-RC2 +* Fix javascript include issue (#16) +* Fix multi store issue (#15) + +### 1.0.0-RC +* Rename the package to nosto/module-nostotagging + +### 0.2.0 +* Dispatch event after Nosto product is loaded +* Improve exception handling +* Fix acl issues + +### 0.1.1 +* Fix the composer files to autoload Nosto PHP SDK correctly + +### 0.1.0 +* First implementation of Magento 2 extension diff --git a/Console/Command/NostoAccountConnectCommand.php b/Console/Command/NostoAccountConnectCommand.php new file mode 100644 index 000000000..e2fad76cc --- /dev/null +++ b/Console/Command/NostoAccountConnectCommand.php @@ -0,0 +1,254 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Console\Command; + +use Nosto\NostoException; +use Nosto\Model\Signup\Account as NostoSignupAccount; +use Nosto\Request\Api\Token; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class NostoAccountConnectCommand extends Command +{ + public const NOSTO_ACCOUNT_ID = 'account-id'; + public const TOKEN_SUFFIX = '_token'; + public const SCOPE_CODE = 'scope-code'; + + /** + * @var NostoHelperAccount + */ + private NostoHelperAccount $nostoHelperAccount; + + /** + * @var NostoHelperScope + */ + private NostoHelperScope $nostoHelperScope; + + /** + * @var bool + */ + private bool $isInteractive = true; + + /** + * NostoConfigConnectCommand constructor. + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope + */ + public function __construct( + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope + ) { + $this->nostoHelperAccount = $nostoHelperAccount; + $this->nostoHelperScope = $nostoHelperScope; + parent::__construct(); + } + + /** + * @inheritDoc + */ + protected function configure() + { + $this->setName('nosto:account:connect') + ->setDescription('Reconnect Nosto Account Via CLI') + ->addOption( + self::NOSTO_ACCOUNT_ID, + null, + InputOption::VALUE_REQUIRED, + 'Nosto Account ID to be reconnected (Should exist already)' + )->addOption( + Token::API_SSO . self::TOKEN_SUFFIX, + null, + InputOption::VALUE_REQUIRED, + 'SSO token' + )->addOption( + Token::API_PRODUCTS . self::TOKEN_SUFFIX, + null, + InputOption::VALUE_REQUIRED, + 'Products Token' + )->addOption( + Token::API_SETTINGS . self::TOKEN_SUFFIX, + null, + InputOption::VALUE_REQUIRED, + 'Settings token' + )->addOption( + Token::API_EXCHANGE_RATES . self::TOKEN_SUFFIX, + null, + InputOption::VALUE_REQUIRED, + 'API exchange rates token' + )->addOption( + Token::API_EMAIL . self::TOKEN_SUFFIX, + null, + InputOption::VALUE_REQUIRED, + 'Email token' + )->addOption( + Token::API_GRAPHQL . self::TOKEN_SUFFIX, + null, + InputOption::VALUE_REQUIRED, + 'Apps Token' + )->addOption( + self::SCOPE_CODE, + null, + InputOption::VALUE_REQUIRED, + 'Store view code' + ); + parent::configure(); + } + + /** + * @inheritDoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->isInteractive = !$input->getOption('no-interaction'); + $io = new SymfonyStyle($input, $output); + $accountId = $input->getOption(self::NOSTO_ACCOUNT_ID) ?: + $io->ask('Enter Nosto Account Id: '); + $scopeCode = $input->getOption(self::SCOPE_CODE) ?: + $io->ask('Enter Store Scope Code'); + + try { + $tokens = $this->getTokensFromInput($input, $io); + if ($this->updateNostoTokens($tokens, $accountId, $io, $scopeCode)) { + $io->success('Tokens successfully Configured'); + return 0; + } else { + $io->error('Could not complete operation'); + return 1; + } + } catch (NostoException $e) { + $io->error('An error occurred'); + return 1; + } + } + + /** + * Set or override tokens for the given account id. + * If a local account is not found, will create a new one. + * + * @param array $tokens + * @param $accountId + * @param SymfonyStyle $io + * @param $scopeCode + * @return bool + * @throws NostoException + */ + private function updateNostoTokens(array $tokens, $accountId, SymfonyStyle $io, $scopeCode) + { + $store = $this->nostoHelperScope->getStoreByCode($scopeCode); + if (!$store) { + $io->error('Store not found. Check your input.'); + return false; + } + $storeAccountId = $store->getConfig(NostoHelperAccount::XML_PATH_ACCOUNT); + $account = $this->nostoHelperAccount->findAccount($store); + if ($account && $storeAccountId === $accountId) { + // If the script is non-interactive, do not ask for confirmation + $confirmOverride = $this->isInteractive ? + $confirmOverride = $io->confirm( + 'Local Nosto account found for this store view. Override tokens?', + false + ) : + true; + if ($confirmOverride) { + $account->setTokens($tokens); + return $this->nostoHelperAccount->saveAccount($account, $store); + } else { + return false; + } + } else { + $io->note('Local account not found. Saving local account...'); + $account = new NostoSignupAccount($accountId); + $account->setTokens($tokens); + return $this->nostoHelperAccount->saveAccount($account, $store); + } + } + + /** + * Check if required arguments passed by command line are present, + * if not, will ask for the remaining parameters. + * + * @param InputInterface $input + * @param SymfonyStyle $io + * @return Token[] + * @throws NostoException + */ + private function getTokensFromInput(InputInterface $input, SymfonyStyle $io) + { + $tokens = []; + + $ssoToken = $input->getOption(Token::API_SSO . self::TOKEN_SUFFIX) ?: + $io->ask('Enter SSO Token: '); + $tokens[] = new Token(Token::API_SSO, $ssoToken); + + $productsToken = $input->getOption(Token::API_PRODUCTS . self::TOKEN_SUFFIX) ?: + $io->ask('Enter Products Token: '); + $tokens[] = new Token(Token::API_PRODUCTS, $productsToken); + + $ratesToken = $input->getOption(Token::API_EXCHANGE_RATES . self::TOKEN_SUFFIX) ?: + $io->ask('Enter Exchange Rates Token: '); + $tokens[] = new Token(Token::API_EXCHANGE_RATES, $ratesToken); + + $settingsToken = $input->getOption(Token::API_SETTINGS . self::TOKEN_SUFFIX) ?: + $io->ask('Enter Settings Token: '); + $tokens[] = new Token(Token::API_SETTINGS, $settingsToken); + + $emailToken = $input->getOption(Token::API_EMAIL . self::TOKEN_SUFFIX); + if (!$emailToken && $this->isInteractive) { + $emailToken = $io->ask( + 'Enter Email Token (Optional): ' + ); + } + if ($emailToken) { + $tokens[] = new Token(Token::API_EMAIL, $emailToken); + } + $appsToken = $input->getOption(Token::API_GRAPHQL . self::TOKEN_SUFFIX); + if (!$appsToken && $this->isInteractive) { + $appsToken = $io->ask( + 'Enter Apps Token (Optional): ' + ); + } + if ($appsToken) { + $tokens[] = new Token(Token::API_GRAPHQL, $appsToken); + } + return $tokens; + } +} diff --git a/Console/Command/NostoAccountRemoveCommand.php b/Console/Command/NostoAccountRemoveCommand.php new file mode 100644 index 000000000..93f7a8192 --- /dev/null +++ b/Console/Command/NostoAccountRemoveCommand.php @@ -0,0 +1,200 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Console\Command; + +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Cache as NostoHelperCache; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class NostoAccountRemoveCommand extends Command +{ + public const SCOPE_CODE = 'scope-code'; + + /** + * @var NostoHelperAccount + */ + private NostoHelperAccount $nostoHelperAccount; + + /** + * @var NostoHelperScope + */ + private NostoHelperScope $nostoHelperScope; + + /** + * @var NostoHelperCache + */ + private NostoHelperCache $nostoHelperCache; + + /** + * @var bool + */ + private bool $isInteractive = true; + + /** + * @var WriterInterface + */ + private WriterInterface $config; + + /** + * NostoAccountRemoveCommand constructor. + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope + * @param WriterInterface $appConfig + * @param NostoHelperCache $nostoHelperCache + */ + public function __construct( + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, + WriterInterface $appConfig, + NostoHelperCache $nostoHelperCache + ) { + $this->nostoHelperAccount = $nostoHelperAccount; + $this->nostoHelperScope = $nostoHelperScope; + $this->config = $appConfig; + $this->nostoHelperCache = $nostoHelperCache; + parent::__construct(); + } + + /** + * Configure the command and the arguments + */ + public function configure() + { + $this->setName('nosto:account:remove') + ->setDescription('Remove Nosto Account Via CLI') + ->addOption( + self::SCOPE_CODE, + null, + InputOption::VALUE_REQUIRED, + 'Store view code' + ); + parent::configure(); + } + + /** + * @inheritDoc + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $this->isInteractive = !$input->getOption('no-interaction'); + $io = new SymfonyStyle($input, $output); + $scopeCode = $input->getOption(self::SCOPE_CODE); + if (!$scopeCode) { + $scopeCode = $io->ask('Enter Store Scope Code'); + } + + if ($this->removeNostoAccount($io, $scopeCode)) { + $io->success('Nosto account removed successfully'); + return 0; + } else { + $io->error('Could not complete operation'); + return 1; + } + } + + /** + * Removes the local installation for the specific store + * + * @param SymfonyStyle $io + * @param $scopeCode + * @return bool + */ + private function removeNostoAccount(SymfonyStyle $io, $scopeCode) + { + $store = $this->nostoHelperScope->getStoreByCode($scopeCode); + if (!$store) { + $io->error('Store not found. Check your input.'); + return false; + } + if (!$this->nostoHelperAccount->nostoInstalledAndEnabled($store)) { + $io->warning('Store is not connected with any Nosto account.'); + return false; + } + // If the script is non-interactive, do not ask for confirmation + if ($this->isInteractive) { + $confirmOverride = $io->confirm( + 'Local Nosto account found for this store view. Remove account?', + false + ); + } else { + $confirmOverride = true; + } + + if ($confirmOverride) { + $deleted = $this->deleteAccount($store); + $this->nostoHelperCache->flushCache(); + return $deleted; + } else { + $io->error('Removal was cancelled'); + return false; + } + } + + /** + * @param Store $store + * @return bool + */ + private function deleteAccount(Store $store) + { + if ((int)$store->getId() < 1) { + return false; + } + + $this->config->delete( + NostoHelperAccount::XML_PATH_ACCOUNT, + ScopeInterface::SCOPE_STORES, + $store->getId() + ); + $this->config->delete( + NostoHelperAccount::XML_PATH_TOKENS, + ScopeInterface::SCOPE_STORES, + $store->getId() + ); + + $store->resetConfig(); + + return true; + } +} diff --git a/Console/Command/NostoGenerateCustomerReferenceCommand.php b/Console/Command/NostoGenerateCustomerReferenceCommand.php new file mode 100644 index 000000000..2d6b99735 --- /dev/null +++ b/Console/Command/NostoGenerateCustomerReferenceCommand.php @@ -0,0 +1,119 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Console\Command; + +use Exception; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Util\Customer as CustomerUtil; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class NostoGenerateCustomerReferenceCommand extends Command +{ + /** + * @var CollectionFactory + */ + private CollectionFactory $collectionFactory; + + /** + * NostoGenerateCustomerReferenceCommand constructor. + * @param CollectionFactory $collectionFactory + */ + public function __construct( + CollectionFactory $collectionFactory + ) { + $this->collectionFactory = $collectionFactory; + parent::__construct(); + } + + /** + * @inheritDoc + */ + public function configure() + { + $this->setName('nosto:generate:customer-reference') + ->setDescription('Generate automatically customer_reference for all customer missing it'); + parent::configure(); + } + + /** + * @inheritDoc + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + try { + $customerCollection = $this->collectionFactory->create() + ->addAttributeToSelect([ + 'entity_id', + NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME]) + ->addAttributeToFilter( + NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME, + ["null" => true] + ) + ->load(); + + $customers = $customerCollection->getItems(); + /** @var CustomerInterface $customer */ + foreach ($customers as $customer) { + /** + * Argument is of type \Magento\Framework\DataObject + * but CustomerInterface|\Magento\Customer\Model\Backend\Customer\Interceptor is expected + */ + $customerUtil = new CustomerUtil(); + /** @phan-suppress-next-line PhanTypeMismatchArgumentSuperType */ + $customerReference = $customerUtil->generateCustomerReference($customer); + /** @noinspection PhpUndefinedMethodInspection */ + $customer->setData( + NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME, + $customerReference + ); + /** @noinspection PhpUndefinedMethodInspection */ + $customer->save(); + } + $io->success('Operation finished with success'); + return 0; + } catch (Exception $e) { + $io->error($e->getMessage()); + return 1; + } + } +} diff --git a/Controller/Adminhtml/Account/Base.php b/Controller/Adminhtml/Account/Base.php new file mode 100644 index 000000000..3e87b91c1 --- /dev/null +++ b/Controller/Adminhtml/Account/Base.php @@ -0,0 +1,52 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Controller\Adminhtml\Account; + +use Magento\Backend\App\Action; + +abstract class Base extends Action +{ + /** + * Is the user allowed to view Nosto account settings + * + * @return bool + */ + public function _isAllowed() + { + return $this->_authorization->isAllowed(self::ADMIN_RESOURCE); + } +} diff --git a/Controller/Adminhtml/Account/Connect.php b/Controller/Adminhtml/Account/Connect.php index acc61d0d1..20cf49a3d 100644 --- a/Controller/Adminhtml/Account/Connect.php +++ b/Controller/Adminhtml/Account/Connect.php @@ -1,67 +1,71 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Controller\Adminhtml\Account; -use Magento\Backend\App\Action; use Magento\Backend\App\Action\Context; use Magento\Framework\Controller\Result\Json; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use Nosto\Tagging\Model\Meta\Oauth\Builder; +use Nosto\Helper\OAuthHelper; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Model\Meta\Oauth\Builder as NostoOauthBuilder; -class Connect extends Action +class Connect extends Base { - const ADMIN_RESOURCE = 'Nosto_Tagging::system_nosto_account'; - - /** - * @var Json - */ - protected $_result; - private $_oauthMetaBuilder; - private $_storeManager; + public const ADMIN_RESOURCE = 'Nosto_Tagging::system_nosto_account'; + private Json $result; + private NostoOauthBuilder $oauthMetaBuilder; + private NostoHelperScope $nostoHelperScope; /** * @param Context $context - * @param Builder $oauthMetaBuilder - * @param StoreManagerInterface $storeManager + * @param NostoOauthBuilder $oauthMetaBuilder + * @param NostoHelperScope $nostoHelperScope * @param Json $result */ public function __construct( Context $context, - Builder $oauthMetaBuilder, - StoreManagerInterface $storeManager, + NostoOauthBuilder $oauthMetaBuilder, + NostoHelperScope $nostoHelperScope, Json $result ) { parent::__construct($context); - $this->_oauthMetaBuilder = $oauthMetaBuilder; - $this->_storeManager = $storeManager; - $this->_result = $result; + $this->oauthMetaBuilder = $oauthMetaBuilder; + $this->result = $result; + $this->nostoHelperScope = $nostoHelperScope; } /** @@ -72,27 +76,14 @@ public function execute() $response = ['success' => false]; $storeId = $this->_request->getParam('store'); - /** @var Store $store */ - $store = $this->_storeManager->getStore($storeId); - - if (!is_null($store)) { - $metaData = $this->_oauthMetaBuilder->build($store); - $client = new \NostoOAuthClient($metaData); + $store = $this->nostoHelperScope->getStore($storeId); + if ($store !== null) { + $metaData = $this->oauthMetaBuilder->build($store); $response['success'] = true; - $response['redirect_url'] = $client->getAuthorizationUrl(); + $response['redirect_url'] = OAuthHelper::getAuthorizationUrl($metaData); } - return $this->_result->setData($response); - } - - /** - * Is the user allowed to view Nosto account settings - * - * @return bool - */ - protected function _isAllowed() - { - return $this->_authorization->isAllowed(self::ADMIN_RESOURCE); + return $this->result->setData($response); } } diff --git a/Controller/Adminhtml/Account/Create.php b/Controller/Adminhtml/Account/Create.php index 7c49b2f37..cb1116973 100644 --- a/Controller/Adminhtml/Account/Create.php +++ b/Controller/Adminhtml/Account/Create.php @@ -1,146 +1,213 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Controller\Adminhtml\Account; -use Magento\Backend\App\Action; +use Exception; use Magento\Backend\App\Action\Context; use Magento\Framework\Controller\Result\Json; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use Nosto\Tagging\Helper\Account; -use Nosto\Tagging\Model\Meta\Account\Builder; -use Psr\Log\LoggerInterface; +use Magento\Framework\Exception\LocalizedException; +use Nosto\Helper\IframeHelper; +use Nosto\Nosto; +use Nosto\NostoException; +use Nosto\Operation\AccountSignup; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Cache as NostoHelperCache; +use Nosto\Tagging\Helper\Currency as NostoCurrencyHelper; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Meta\Account\Builder as NostoSignupBuilder; +use Nosto\Tagging\Model\Meta\Account\Iframe\Builder as NostoIframeMetaBuilder; +use Nosto\Tagging\Model\Meta\Account\Owner\Builder as NostoOwnerBuilder; +use Nosto\Tagging\Model\Rates\Service as NostoRatesService; +use Nosto\Tagging\Model\User\Builder as NostoCurrentUserBuilder; +use Zend_Validate; +use Zend_Validate_Exception; -class Create extends Action +class Create extends Base { - const ADMIN_RESOURCE = 'Nosto_Tagging::system_nosto_account'; - - /** - * @var Json - */ - protected $_result; - private $_accountHelper; - private $_accountMetaBuilder; - private $_storeManager; - private $_accountService; - private $_logger; + public const ADMIN_RESOURCE = 'Nosto_Tagging::system_nosto_account'; + private Json $result; + private NostoHelperAccount $nostoHelperAccount; + private NostoCurrentUserBuilder $nostoCurrentUserBuilder; + private NostoIframeMetaBuilder $nostoIframeMetaBuilder; + private NostoRatesService $nostoRatesService; + private NostoCurrencyHelper $nostoCurrencyHelper; + private NostoOwnerBuilder $nostoOwnerBuilder; + private NostoSignupBuilder $nostoSignupBuilder; + private NostoLogger $logger; + private NostoHelperScope $nostoHelperScope; + private NostoHelperCache $nostoHelperCache; /** * @param Context $context - * @param Account $accountHelper - * @param Builder $accountMetaBuilder - * @param StoreManagerInterface $storeManager + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoSignupBuilder $nostoSignupBuilder + * @param NostoIframeMetaBuilder $nostoIframeMetaBuilder + * @param NostoCurrentUserBuilder $nostoCurrentUserBuilder + * @param NostoOwnerBuilder $nostoOwnerBuilder + * @param NostoHelperScope $nostoHelperScope * @param Json $result - * @param LoggerInterface $logger - * @param \NostoServiceAccount $accountService + * @param NostoLogger $logger + * @param NostoRatesService $nostoRatesService + * @param NostoCurrencyHelper $nostoCurrencyHelper + * @param NostoHelperCache $nostoHelperCache + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( Context $context, - Account $accountHelper, - Builder $accountMetaBuilder, - StoreManagerInterface $storeManager, + NostoHelperAccount $nostoHelperAccount, + NostoSignupBuilder $nostoSignupBuilder, + NostoIframeMetaBuilder $nostoIframeMetaBuilder, + NostoCurrentUserBuilder $nostoCurrentUserBuilder, + NostoOwnerBuilder $nostoOwnerBuilder, + NostoHelperScope $nostoHelperScope, Json $result, - LoggerInterface $logger, - \NostoServiceAccount $accountService + NostoLogger $logger, + NostoRatesService $nostoRatesService, + NostoCurrencyHelper $nostoCurrencyHelper, + NostoHelperCache $nostoHelperCache ) { parent::__construct($context); - $this->_accountHelper = $accountHelper; - $this->_accountMetaBuilder = $accountMetaBuilder; - $this->_storeManager = $storeManager; - $this->_result = $result; - $this->_logger = $logger; - $this->_accountService = $accountService; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->nostoSignupBuilder = $nostoSignupBuilder; + $this->nostoIframeMetaBuilder = $nostoIframeMetaBuilder; + $this->nostoOwnerBuilder = $nostoOwnerBuilder; + $this->nostoCurrentUserBuilder = $nostoCurrentUserBuilder; + $this->result = $result; + $this->logger = $logger; + $this->nostoRatesService = $nostoRatesService; + $this->nostoCurrencyHelper = $nostoCurrencyHelper; + $this->nostoHelperScope = $nostoHelperScope; + $this->nostoHelperCache = $nostoHelperCache; } /** * @return Json + * @throws LocalizedException + * @throws Zend_Validate_Exception + * @suppress PhanTypeMismatchArgument + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @phpcs:disable Generic.Metrics.CyclomaticComplexity + * @phpcs:disable Generic.Metrics.NestingLevel + * @throws Zend_Validate_Exception */ public function execute() { $response = ['success' => false]; $storeId = $this->_request->getParam('store'); - /** @var Store $store */ - $store = $this->_storeManager->getStore($storeId); - - if (!is_null($store)) { + $store = $this->nostoHelperScope->getStore($storeId); + $messageText = null; + if ($store !== null) { try { - $emailAddress = $this->_request->getParam('email'); - $metaData = $this->_accountMetaBuilder->build($store); - // todo: how to handle this class, DI? - if (\Zend_Validate::is($emailAddress, 'EmailAddress')) { - /** @var \NostoOwner $owner */ - $owner = $metaData->getOwner(); - $owner->setEmail($emailAddress); + /** @var string $signupDetails */ + $signupDetails = $this->_request->getParam('details'); + if (!empty($signupDetails)) { + $signupDetails = json_decode($signupDetails, true); } - $account = $this->_accountService->create($metaData); - - if ($this->_accountHelper->saveAccount($account, $store)) { - // todo - //$this->_accountHelper->updateCurrencyExchangeRates($account, $store); - $response['success'] = true; - $response['redirect_url'] = $this->_accountHelper->getIframeUrl( + $emailAddress = $this->_request->getParam('email'); + $accountOwner = $this->nostoOwnerBuilder->build(); + if (Zend_Validate::is($emailAddress, 'EmailAddress')) { + $accountOwner->setFirstName(''); + $accountOwner->setLastName(''); + $accountOwner->setEmail($emailAddress); + /** @var array $signupDetails */ + $signupParams = $this->nostoSignupBuilder->build( $store, - $account, - [ - 'message_type' => \NostoMessage::TYPE_SUCCESS, - 'message_code' => \NostoMessage::CODE_ACCOUNT_CREATE, - ] + $accountOwner, + $signupDetails ); + $operation = new AccountSignup($signupParams); + $account = $operation->create(); + + if ($this->nostoHelperAccount->saveAccount($account, $store)) { + $response['success'] = true; + $response['redirect_url'] = IframeHelper::getUrl( + $this->nostoIframeMetaBuilder->build($store), + $account, + $this->nostoCurrentUserBuilder->build(), + [ + 'message_type' => Nosto::TYPE_SUCCESS, + 'message_code' => Nosto::CODE_ACCOUNT_CREATE, + ] + ); + + // Note that we will send the exchange rates even if the multi currency + // is not set. This is mostly for debugging purposes. + if ($this->nostoCurrencyHelper->getCurrencyCount($store) > 1) { + try { + $this->nostoRatesService->update($store); + } catch (Exception $e) { + $this->logger->exception($e); + } + } + + //invalidate page cache and layout cache + $this->nostoHelperCache->invalidatePageCache(); + $this->nostoHelperCache->invalidateLayoutCache(); + } + } else { + $this->logger->exception(new NostoException('Invalid email address ' . $emailAddress)); + $messageText = 'Invalid email address ' . $emailAddress; } - } catch (\NostoException $e) { - $this->_logger->error($e, ['exception' => $e]); + } catch (NostoException $e) { + $this->logger->exception($e); + $messageText = $e->getMessage(); } } if (!$response['success']) { - $response['redirect_url'] = $this->_accountHelper->getIframeUrl( - $store, + $params = [ + 'message_type' => Nosto::TYPE_ERROR, + 'message_code' => Nosto::CODE_ACCOUNT_CREATE, + ]; + if ($messageText) { + $params['message_text'] = $messageText; + } + $response['redirect_url'] = IframeHelper::getUrl( + $this->nostoIframeMetaBuilder->build($store), null, // account creation failed, so we have none. - [ - 'message_type' => \NostoMessage::TYPE_ERROR, - 'message_code' => \NostoMessage::CODE_ACCOUNT_CREATE, - ] + $this->nostoCurrentUserBuilder->build(), + $params ); } - return $this->_result->setData($response); - } - - /** - * Is the user allowed to view Nosto account settings - * - * @return bool - */ - protected function _isAllowed() - { - return $this->_authorization->isAllowed(self::ADMIN_RESOURCE); + return $this->result->setData($response); } } diff --git a/Controller/Adminhtml/Account/Delete.php b/Controller/Adminhtml/Account/Delete.php index 7fd8b461e..ed7a48dfc 100644 --- a/Controller/Adminhtml/Account/Delete.php +++ b/Controller/Adminhtml/Account/Delete.php @@ -1,118 +1,137 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Controller\Adminhtml\Account; -use Magento\Backend\App\Action; +use Exception; use Magento\Backend\App\Action\Context; use Magento\Framework\Controller\Result\Json; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use Nosto\Tagging\Helper\Account; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Phrase; +use Nosto\Helper\IframeHelper; +use Nosto\Nosto; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Cache as NostoHelperCache; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Model\Meta\Account\Iframe\Builder as NostoIframeMetaBuilder; +use Nosto\Tagging\Model\User\Builder as NostoCurrentUserBuilder; -class Delete extends Action +class Delete extends Base { - const ADMIN_RESOURCE = 'Nosto_Tagging::system_nosto_account'; - - /** - * @var Json - */ - protected $_result; - private $_storeManager; - private $_accountHelper; + public const ADMIN_RESOURCE = 'Nosto_Tagging::system_nosto_account'; + private Json $result; + private NostoHelperAccount $nostoHelperAccount; + private NostoCurrentUserBuilder $nostoCurrentUserBuilder; + private NostoIframeMetaBuilder $nostoIframeMetaBuilder; + private NostoHelperScope $nostoHelperScope; + private NostoHelperCache $nostoHelperCache; /** * @param Context $context - * @param Account $accountHelper - * @param StoreManagerInterface $storeManager + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoIframeMetaBuilder $nostoIframeMetaBuilder + * @param NostoCurrentUserBuilder $nostoCurrentUserBuilder + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperCache $nostoHelperCache * @param Json $result */ public function __construct( Context $context, - Account $accountHelper, - StoreManagerInterface $storeManager, + NostoHelperAccount $nostoHelperAccount, + NostoIframeMetaBuilder $nostoIframeMetaBuilder, + NostoCurrentUserBuilder $nostoCurrentUserBuilder, + NostoHelperScope $nostoHelperScope, + NostoHelperCache $nostoHelperCache, Json $result ) { parent::__construct($context); - $this->_accountHelper = $accountHelper; - $this->_storeManager = $storeManager; - $this->_result = $result; + $this->nostoIframeMetaBuilder = $nostoIframeMetaBuilder; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->result = $result; + $this->nostoCurrentUserBuilder = $nostoCurrentUserBuilder; + $this->nostoHelperScope = $nostoHelperScope; + $this->nostoHelperCache = $nostoHelperCache; } /** * @return Json + * @throws Exception */ public function execute() { - $response = ['success' => false]; - $storeId = $this->_request->getParam('store'); - /** @var Store $store */ - $store = $this->_storeManager->getStore($storeId); - $account = !is_null($store) - ? $this->_accountHelper->findAccount($store) - : null; + $store = $this->nostoHelperScope->getStore($storeId); + if ($store === null) { + throw new LocalizedException(new Phrase('No account found')); + } + + $account = $this->nostoHelperAccount->findAccount($store); + if ($account !== null) { + $currentUser = $this->nostoCurrentUserBuilder->build(); + if ($this->nostoHelperAccount->deleteAccount($account, $store, $currentUser)) { + //Invalidate the cache + $this->nostoHelperCache->invalidatePageCache(); + $this->nostoHelperCache->invalidateLayoutCache(); - if (!is_null($store) && !is_null($account)) { - if ($this->_accountHelper->deleteAccount($account, $store)) { + $response = []; $response['success'] = true; - $response['redirect_url'] = $this->_accountHelper->getIframeUrl( - $store, + $response['redirect_url'] = IframeHelper::getUrl( + $this->nostoIframeMetaBuilder->build($store), null, // we don't have an account anymore + $this->nostoCurrentUserBuilder->build(), [ - 'message_type' => \NostoMessage::TYPE_SUCCESS, - 'message_code' => \NostoMessage::CODE_ACCOUNT_DELETE, + 'message_type' => Nosto::TYPE_SUCCESS, + 'message_code' => Nosto::CODE_ACCOUNT_DELETE, ] ); + return $this->result->setData($response); } } - if (!$response['success']) { - $response['redirect_url'] = $this->_accountHelper->getIframeUrl( - $store, - $account, - [ - 'message_type' => \NostoMessage::TYPE_ERROR, - 'message_code' => \NostoMessage::CODE_ACCOUNT_DELETE, - ] - ); - } - - return $this->_result->setData($response); - } - - /** - * Is the user allowed to view Nosto account settings - * - * @return bool - */ - protected function _isAllowed() - { - return $this->_authorization->isAllowed(self::ADMIN_RESOURCE); + $response = []; + $response['redirect_url'] = IframeHelper::getUrl( + $this->nostoIframeMetaBuilder->build($store), + null, + $this->nostoCurrentUserBuilder->build(), + [ + 'message_type' => Nosto::TYPE_ERROR, + 'message_code' => Nosto::CODE_ACCOUNT_DELETE, + ] + ); + return $this->result->setData($response); } } diff --git a/Controller/Adminhtml/Account/Index.php b/Controller/Adminhtml/Account/Index.php index 305029817..e17d4bba8 100755 --- a/Controller/Adminhtml/Account/Index.php +++ b/Controller/Adminhtml/Account/Index.php @@ -1,82 +1,86 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Controller\Adminhtml\Account; -use Magento\Backend\App\Action; use Magento\Backend\App\Action\Context; use Magento\Backend\Model\View\Result\Page; use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\Phrase; use Magento\Framework\View\Result\PageFactory; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; -/** - * - */ -class Index extends Action +class Index extends Base { - const ADMIN_RESOURCE = 'Nosto_Tagging::system_nosto_account'; - - /** - * @var PageFactory - */ - protected $_resultPageFactory; - - /** - * @var StoreManagerInterface - */ - protected $_storeManager; + public const ADMIN_RESOURCE = 'Nosto_Tagging::system_nosto_account'; + private PageFactory $resultPageFactory; + private NostoHelperScope $nostoHelperScope; /** * @param Context $context * @param PageFactory $resultPageFactory - * @param StoreManagerInterface $storeManager + * @param NostoHelperScope $nostoHelperScope */ public function __construct( Context $context, PageFactory $resultPageFactory, - StoreManagerInterface $storeManager + NostoHelperScope $nostoHelperScope ) { parent::__construct($context); - $this->_resultPageFactory = $resultPageFactory; - $this->_storeManager = $storeManager; + $this->resultPageFactory = $resultPageFactory; + $this->nostoHelperScope = $nostoHelperScope; } /** * @return Page | Redirect + * @throws LocalizedException + * @throws NotFoundException */ public function execute() { - if (!$this->getSelectedStore()) { + $store = $this->nostoHelperScope->getSelectedStore($this->getRequest()); + + if (!($store && $store->getId())) { // If we are not under a store view, then redirect to the first // found one. Nosto is configured per store. - foreach ($this->_storeManager->getWebsites() as $website) { + foreach ($this->nostoHelperScope->getWebsites() as $website) { + /** @noinspection PhpUndefinedMethodInspection */ $storeId = $website->getDefaultGroup()->getDefaultStoreId(); if (!empty($storeId)) { return $this->resultRedirectFactory->create() @@ -85,43 +89,12 @@ public function execute() } } - /** @var Page $result */ - $result = $this->_resultPageFactory->create(); - $result->setActiveMenu(self::ADMIN_RESOURCE); - $result->getConfig()->getTitle()->prepend( - __('Nosto - Account Settings') - ); - - return $result; - } - - /** - * Returns the currently selected store. - * If it is single store setup, then just return the default store. - * If it is a multi store setup, the expect a store id to passed in the - * request params and return that store as the current one. - * - * @return Store|null the store or null if not found. - */ - protected function getSelectedStore() - { - $store = null; - if ($this->_storeManager->isSingleStoreMode()) { - $store = $this->_storeManager->getStore(true); - } elseif (($storeId = $this->_storeManager->getStore()->getId())) { - $store = $this->_storeManager->getStore($storeId); + $result = $this->resultPageFactory->create(); + if ($result instanceof Page) { + $result->setActiveMenu(self::ADMIN_RESOURCE); + $result->getConfig()->getTitle()->prepend(new Phrase('Nosto - Account Settings')); } - return $store; - } - - /** - * Is the user allowed to view Nosto account settings - * - * @return bool - */ - protected function _isAllowed() - { - return $this->_authorization->isAllowed(self::ADMIN_RESOURCE); + return $result; } } diff --git a/Controller/Adminhtml/Account/Proxy.php b/Controller/Adminhtml/Account/Proxy.php index a4fab2e4d..607baa022 100644 --- a/Controller/Adminhtml/Account/Proxy.php +++ b/Controller/Adminhtml/Account/Proxy.php @@ -1,50 +1,53 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Controller\Adminhtml\Account; -use Magento\Backend\App\Action; use Magento\Backend\App\Action\Context; use Magento\Backend\Model\Auth\Session; use Magento\Framework\Controller\Result\Redirect; -/** - * - */ -class Proxy extends Action +class Proxy extends Base { - const ADMIN_RESOURCE = 'Nosto_Tagging::system_nosto_account'; + public const ADMIN_RESOURCE = 'Nosto_Tagging::system_nosto_account'; /** - * @inheritdoc + * @var Session */ - protected $_publicActions = ['proxy']; - /** @var Session */ - private $_backendAuthSession; + private Session $backendAuthSession; /** * @param Context $context @@ -56,7 +59,8 @@ public function __construct( ) { parent::__construct($context); - $this->_backendAuthSession = $backendAuthSession; + $this->_publicActions = ['proxy']; + $this->backendAuthSession = $backendAuthSession; } /** @@ -66,16 +70,16 @@ public function __construct( * This is a workaround as you cannot redirect directly to a protected * action in the backend end from the front end. The action also handles * passing along any error/success messages. - * @return Redirect */ public function execute() { - $type = $this->_request->getParam('message_type'); - $code = $this->_request->getParam('message_code'); - $text = $this->_request->getParam('message_text'); - if (!is_null($type) && !is_null($code)) { - $this->_backendAuthSession->setData( + $type = $this->getRequest()->getParam('message_type'); + $code = $this->getRequest()->getParam('message_code'); + $text = $this->getRequest()->getParam('message_text'); + if ($type !== null && $code !== null) { + /** @noinspection PhpUndefinedMethodInspection */ + $this->backendAuthSession->setData( 'nosto_message', [ 'message_type' => $type, @@ -85,22 +89,12 @@ public function execute() ); } - if (($storeId = (int)$this->_request->getParam('store')) !== 0) { + if (($storeId = (int)$this->getRequest()->getParam('store')) !== 0) { return $this->resultRedirectFactory->create() ->setPath('*/*/index', ['store' => $storeId]); - } else { - return $this->resultRedirectFactory->create() - ->setPath('*/*/index', []); } - } - /** - * Is the user allowed to view Nosto account settings - * - * @return bool - */ - protected function _isAllowed() - { - return $this->_authorization->isAllowed(self::ADMIN_RESOURCE); + return $this->resultRedirectFactory->create() + ->setPath('*/*/index', []); } } diff --git a/Controller/Adminhtml/Account/Sync.php b/Controller/Adminhtml/Account/Sync.php index 280105497..576fbdc6d 100644 --- a/Controller/Adminhtml/Account/Sync.php +++ b/Controller/Adminhtml/Account/Sync.php @@ -1,72 +1,76 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Controller\Adminhtml\Account; -use Magento\Backend\App\Action; use Magento\Backend\App\Action\Context; use Magento\Framework\Controller\Result\Json; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use Nosto\Tagging\Helper\Account; -use Nosto\Tagging\Model\Meta\Oauth\Builder; +use Nosto\Helper\OAuthHelper; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Model\Meta\Oauth\Builder as NostoOauthBuilder; -class Sync extends Action +class Sync extends Base { - const ADMIN_RESOURCE = 'Nosto_Tagging::system_nosto_account'; - - /** - * @var Json - */ - protected $_result; - private $_accountHelper; - private $_oauthMetaBuilder; - private $_storeManager; + public const ADMIN_RESOURCE = 'Nosto_Tagging::system_nosto_account'; + private Json $result; + private NostoHelperAccount $nostoHelperAccount; + private NostoOauthBuilder $oauthMetaBuilder; + private NostoHelperScope $nostoHelperScope; /** * @param Context $context - * @param Account $accountHelper - * @param Builder $oauthMetaBuilder - * @param StoreManagerInterface $storeManager + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoOauthBuilder $oauthMetaBuilder + * @param NostoHelperScope $nostoHelperScope * @param Json $result */ public function __construct( Context $context, - Account $accountHelper, - Builder $oauthMetaBuilder, - StoreManagerInterface $storeManager, + NostoHelperAccount $nostoHelperAccount, + NostoOauthBuilder $oauthMetaBuilder, + NostoHelperScope $nostoHelperScope, Json $result ) { parent::__construct($context); - $this->_accountHelper = $accountHelper; - $this->_oauthMetaBuilder = $oauthMetaBuilder; - $this->_storeManager = $storeManager; - $this->_result = $result; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->oauthMetaBuilder = $oauthMetaBuilder; + $this->result = $result; + $this->nostoHelperScope = $nostoHelperScope; } /** @@ -77,30 +81,15 @@ public function execute() $response = ['success' => false]; $storeId = $this->_request->getParam('store'); - /** @var Store $store */ - $store = $this->_storeManager->getStore($storeId); - $account = !is_null($store) - ? $this->_accountHelper->findAccount($store) - : null; - - if (!is_null($store) && !is_null($account)) { - $metaData = $this->_oauthMetaBuilder->build($store, $account); - $client = new \NostoOAuthClient($metaData); + $store = $this->nostoHelperScope->getStore($storeId); + $account = $store !== null ? $this->nostoHelperAccount->findAccount($store) : null; + if ($store !== null && $account !== null) { + $metaData = $this->oauthMetaBuilder->build($store, $account); $response['success'] = true; - $response['redirect_url'] = $client->getAuthorizationUrl(); + $response['redirect_url'] = OAuthHelper::getAuthorizationUrl($metaData); } - return $this->_result->setData($response); - } - - /** - * Is the user allowed to view Nosto account settings - * - * @return bool - */ - protected function _isAllowed() - { - return $this->_authorization->isAllowed(self::ADMIN_RESOURCE); + return $this->result->setData($response); } } diff --git a/Controller/Checkout/Cart/Add.php b/Controller/Checkout/Cart/Add.php new file mode 100644 index 000000000..7ef5c94d8 --- /dev/null +++ b/Controller/Checkout/Cart/Add.php @@ -0,0 +1,169 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Controller\Checkout\Cart; + +use Exception; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute as MageAttribute; +use Magento\Catalog\Model\ResourceModel\Product as ProductResourceModel; +use Magento\Checkout\Controller\Cart\Add as MageAdd; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableType; +use Magento\Store\Model\StoreManager; + +/** + * Class Add + * Implements around method to Magento's native add to cart controller. + * Modify the request to add a super_attribute key if it is missing. + */ +class Add +{ + /** @var ProductRepositoryInterface */ + private ProductRepositoryInterface $productRepository; + + /** @var StoreManager */ + private StoreManager $storeManager; + + /** @var ProductResourceModel */ + private ProductResourceModel $productResourceModel; + + /** + * Add constructor. + * @param ProductRepositoryInterface $productRepository + * @param StoreManager $storeManager + * @param ProductResourceModel $productResourceModel + */ + public function __construct( + ProductRepositoryInterface $productRepository, + StoreManager $storeManager, + ProductResourceModel $productResourceModel + ) { + $this->productRepository = $productRepository; + $this->storeManager = $storeManager; + $this->productResourceModel = $productResourceModel; + } + + /** + * Method executed before magento's native add to cart controller + * Checks if request has product attributes and set them before + * returning to Magento's controller + * + * @param MageAdd $add + * @param callable $proceed + * @return mixed + * @throws Exception + * @noinspection PhpUnused + */ + public function aroundExecute(MageAdd $add, callable $proceed) + { + $params = $add->getRequest()->getParams(); + // Skip if request already has product attributes or doesn't have product array key + if (isset($params['super_attribute']) || !isset($params['product'])) { + return $proceed(); + } + + $productId = $params['product']; + $product = $this->initProduct($productId); + if (!$product) { + return $proceed(); + } + + $parentType = $product->getTypeInstance(); + $skuId = $add->getRequest()->getParam('sku'); + if ($parentType instanceof ConfigurableType && !empty($skuId)) { + $skuProduct = $this->initProduct($skuId); + if ($skuProduct instanceof Product) { + $attributeOptions = $this->getAttributeOptions($product, $skuProduct, $parentType); + if (!empty($attributeOptions)) { + $params['super_attribute'] = $attributeOptions; + $add->getRequest()->setParams($params); + } + } + } + + return $proceed(); + } + + /** + * Returns an array with a list of super_attributes for a parent product and his SKU + * + * @param Product $product + * @param Product $skuProduct + * @param ConfigurableType $configurableType + * @return array + * @throws Exception + */ + private function getAttributeOptions(Product $product, Product $skuProduct, ConfigurableType $configurableType) + { + $configurableAttributes = $configurableType->getConfigurableAttributesAsArray($product); + $attributeOptions = []; + $skuResource = $this->productResourceModel->load($skuProduct, $skuProduct->getId()); + foreach ($configurableAttributes as $configurableAttribute) { + $attributeCode = $configurableAttribute['attribute_code']; + $attribute = $skuResource->getAttribute($attributeCode); + if ($attribute instanceof MageAttribute) { + $attributeId = $attribute->getId(); + $attributeValueId = $skuProduct->getData($attributeCode); + if ($attributeId && $attributeValueId) { + $attributeOptions[$attributeId] = $attributeValueId; + } + } + } + return $attributeOptions; + } + + /** + * Initialize product instance from request data + * + * @param $productId + * @return null|ProductInterface|Product + */ + private function initProduct($productId) + { + try { + $store = $this->storeManager->getStore(); + if (!$store) { + return null; + } + $storeId = $store->getId(); + return $this->productRepository->getById($productId, false, $storeId); + } catch (Exception $e) { + return null; + } + } +} diff --git a/Controller/Export/Base.php b/Controller/Export/Base.php index 78b9971cc..7ab915989 100644 --- a/Controller/Export/Base.php +++ b/Controller/Export/Base.php @@ -1,28 +1,38 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Controller\Export; @@ -32,61 +42,41 @@ use Magento\Framework\Controller\Result\Raw; use Magento\Framework\Controller\ResultFactory; use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use Nosto\Tagging\Helper\Account as AccountHelper; -use NostoExportCollectionInterface; +use Nosto\Helper\ExportHelper; +use Nosto\Model\AbstractCollection; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; /** * Export base controller that all export controllers must extend. */ +// @phan-suppress-next-next-line PhanDeprecatedClass - recommended to avoid code migration till 2.5.0 +// https://community.magento.com/t5/Magento-DevBlog/Decomposition-of-Magento-Controllers/ba-p/430883 abstract class Base extends Action { - const ID = 'id'; - const LIMIT = 'limit'; - const OFFSET = 'offset'; - const CREATED_AT = 'created_at'; - const ENTITY_ID = 'entity_id'; + public const ID = 'id'; + public const LIMIT = 'limit'; + public const OFFSET = 'offset'; - protected $_storeManager; - protected $_accountHelper; + private NostoHelperAccount $nostoHelperAccount; + private NostoHelperScope $nostoHelperScope; /** * Constructor. * * @param Context $context - * @param StoreManagerInterface $storeManager - * @param AccountHelper $accountHelper + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperAccount $nostoHelperAccount */ public function __construct( Context $context, - StoreManagerInterface $storeManager, - AccountHelper $accountHelper + NostoHelperScope $nostoHelperScope, + NostoHelperAccount $nostoHelperAccount ) { parent::__construct($context); - $this->_storeManager = $storeManager; - $this->_accountHelper = $accountHelper; - } - - /** - * Encrypts the export collection and outputs it to the browser. - * - * @param \NostoExportCollectionInterface $collection the data collection to export. - * - * @return Raw - */ - protected function export(\NostoExportCollectionInterface $collection) - { - /** @var Raw $result */ - $result = $this->resultFactory->create(ResultFactory::TYPE_RAW); - /** @var Store $store */ - $store = $this->_storeManager->getStore(true); - $account = $this->_accountHelper->findAccount($store); - if ($account !== null) { - $cipherText = \NostoExporter::export($account, $collection); - $result->setContents($cipherText); - } - return $result; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->nostoHelperScope = $nostoHelperScope; } /** @@ -94,48 +84,66 @@ protected function export(\NostoExportCollectionInterface $collection) * encrypts the JSON and returns the result * * @return Raw + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute() { - /** @var Store $store */ - $store = $this->_storeManager->getStore(true); - /** @var \Magento\Sales\Model\ResourceModel\Order\Collection $collection */ - $collection = $this->getCollection($store); - $collection->addAttributeToSelect('*'); - + $store = $this->nostoHelperScope->getStore(true); $id = $this->getRequest()->getParam(self::ID, false); if (!empty($id)) { - $collection->addFieldToFilter(self::ENTITY_ID, $id); - } else { - $pageSize = (int)$this->getRequest()->getParam(self::LIMIT, 100); - $currentOffset = (int)$this->getRequest()->getParam(self::OFFSET, 0); - $currentPage = ($currentOffset / $pageSize) + 1; - $collection->getSelect()->limitPage($currentPage, $pageSize); - $collection->setOrder(self::CREATED_AT, $collection::SORT_ORDER_DESC); + return $this->export($this->buildSingleExportCollection($store, $id)); } - $collection->load(); - - /** @var NostoExportCollectionInterface $exportCollection */ - $exportCollection = $this->buildExportCollection($collection, $store); - return $this->export($exportCollection); + $pageSize = (int)$this->getRequest()->getParam(self::LIMIT, 100); + $currentOffset = (int)$this->getRequest()->getParam(self::OFFSET, 0); + return $this->export($this->buildExportCollection($store, $pageSize, $currentOffset)); } /** - * Abstract function that should be implemented to return the correct - * collection object with the controller specific filters applied + * Abstract function that should be implemented to return the correct collection object with + * the controller specific filters applied * * @param Store $store The store object for the current store - * @return \Magento\Sales\Model\ResourceModel\Order\Collection The collection + * @param $id + * @return AbstractCollection The collection */ - abstract protected function getCollection(Store $store); + abstract public function buildSingleExportCollection(Store $store, $id); /** - * Abstract function that should be implemented to return the built export - * collection object with all the items added + * Abstract function that should be implemented to return the built export collection object + * with all the items added * - * @param \Magento\Sales\Model\ResourceModel\Order\Collection $collection - * @param Store $store The store object for the current store - * @return \Magento\Sales\Model\ResourceModel\Order\Collection The collection + * @param Store $store + * @param int $limit + * @param int $offset + * @return AbstractCollection the collection with the items to export + */ + abstract public function buildExportCollection(Store $store, int $limit = 100, int $offset = 0); + + /** + * Encrypts the export collection and outputs it to the browser. + * + * @param AbstractCollection $collection the data collection to export. + * @return Raw + */ + public function export(AbstractCollection $collection) + { + $result = $this->resultFactory->create(ResultFactory::TYPE_RAW); + $store = $this->nostoHelperScope->getStore(true); + $account = $this->nostoHelperAccount->findAccount($store); + if ($account !== null) { + $cipherText = (new ExportHelper())->export($account, $collection); + if ($result instanceof Raw) { + $result->setContents($cipherText); + } + } + return $result; + } + + /** + * @return NostoHelperScope */ - abstract protected function buildExportCollection($collection, Store $store); + public function getNostoHelperScope(): NostoHelperScope + { + return $this->nostoHelperScope; + } } diff --git a/Controller/Export/Order.php b/Controller/Export/Order.php index 7b610c921..c39750638 100755 --- a/Controller/Export/Order.php +++ b/Controller/Export/Order.php @@ -1,40 +1,49 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Controller\Export; use Magento\Framework\App\Action\Context; -use /** @noinspection PhpUndefinedClassInspection */ - Magento\Sales\Model\ResourceModel\Order\CollectionFactory as OrderCollectionFactory; use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use Nosto\Tagging\Helper\Account as AccountHelper; -use Nosto\Tagging\Model\Order\Builder as OrderBuilder; -use NostoExportCollectionOrder; +use Nosto\NostoException; +use Nosto\Model\AbstractCollection; +use Nosto\Model\Order\OrderCollection; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Model\Order\Collection as NostoOrderCollection; /** * Order export controller used to export order history to Nosto in order to @@ -45,56 +54,49 @@ */ class Order extends Base { + private NostoOrderCollection $nostoOrderCollection; - private $_orderCollectionFactory; - private $_orderBuilder; - - /** @noinspection PhpUndefinedClassInspection */ /** * Constructor. * * @param Context $context - * @param OrderCollectionFactory $orderCollectionFactory - * @param StoreManagerInterface $storeManager - * @param AccountHelper $accountHelper - * @param OrderBuilder $orderBuilder + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoOrderCollection $nostoOrderCollection */ public function __construct( Context $context, - /** @noinspection PhpUndefinedClassInspection */ - OrderCollectionFactory $orderCollectionFactory, - StoreManagerInterface $storeManager, - AccountHelper $accountHelper, - OrderBuilder $orderBuilder + NostoHelperScope $nostoHelperScope, + NostoHelperAccount $nostoHelperAccount, + NostoOrderCollection $nostoOrderCollection ) { - parent::__construct($context, $storeManager, $accountHelper); - - $this->_orderCollectionFactory = $orderCollectionFactory; - $this->_orderBuilder = $orderBuilder; + parent::__construct($context, $nostoHelperScope, $nostoHelperAccount); + $this->nostoOrderCollection = $nostoOrderCollection; } /** - * @inheritdoc + * + * @suppress PhanParamSignatureMismatch + * @param Store $store + * @param int $limit + * @param int $offset + * @return AbstractCollection|OrderCollection + * @throws NostoException */ - protected function getCollection(Store $store) + public function buildExportCollection(Store $store, int $limit = 100, int $offset = 0) { - /** @var \Magento\Sales\Model\ResourceModel\Order\Collection $collection */ - $collection = $this->_orderCollectionFactory->create(); - $collection->addAttributeToFilter('store_id', ['eq' => $store->getId()]); - return $collection; + return $this->nostoOrderCollection->buildMany($store, $limit, $offset); } /** - * @inheritdoc + * @suppress PhanParamSignatureMismatch + * @param Store $store + * @param int $id + * @return AbstractCollection|OrderCollection + * @throws NostoException */ - protected function buildExportCollection($collection, Store $store) + public function buildSingleExportCollection(Store $store, $id) { - /** @var \Magento\Sales\Model\ResourceModel\Order\Collection $collection */ - $exportCollection = new NostoExportCollectionOrder(); - foreach ($collection->getItems() as $order) { - /** @var \Magento\Sales\Model\Order $order */ - $exportCollection[] = $this->_orderBuilder->build($order); - } - return $exportCollection; + return $this->nostoOrderCollection->buildSingle($store, $id); } -} \ No newline at end of file +} diff --git a/Controller/Export/Product.php b/Controller/Export/Product.php index 1c4dce479..347ec4b16 100755 --- a/Controller/Export/Product.php +++ b/Controller/Export/Product.php @@ -1,42 +1,49 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Controller\Export; -use Magento\Catalog\Model\Product\Visibility as ProductVisibility; -use /** @noinspection PhpUndefinedClassInspection */ - Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; -use Magento\Catalog\Model\ResourceModel\ProductFactory; use Magento\Framework\App\Action\Context; use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use Nosto\Tagging\Helper\Account as AccountHelper; -use Nosto\Tagging\Model\Product\Builder as ProductBuilder; -use NostoExportCollectionProduct; +use Nosto\Model\AbstractCollection; +use Nosto\Model\Product\ProductCollection; +use Nosto\NostoException; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Model\Product\CollectionBuilder; /** * Product export controller used to export product history to Nosto in order to @@ -47,63 +54,47 @@ */ class Product extends Base { + public const PARAM_PREVIEW = 'preview'; - private $_productCollectionFactory; - private $_productVisibility; - private $_productBuilder; + /** @var CollectionBuilder */ + private CollectionBuilder $nostoCollectionBuilder; - /** @noinspection PhpUndefinedClassInspection */ /** - * Constructor. - * * @param Context $context - * @param ProductCollectionFactory $productCollectionFactory - * @param ProductVisibility $productVisibility - * @param StoreManagerInterface $storeManager - * @param AccountHelper $accountHelper - * @param ProductBuilder $productBuilder + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperAccount $nostoHelperAccount + * @param CollectionBuilder $collectionBuilder */ public function __construct( Context $context, - /** @noinspection PhpUndefinedClassInspection */ - ProductCollectionFactory $productCollectionFactory, - ProductVisibility $productVisibility, - StoreManagerInterface $storeManager, - AccountHelper $accountHelper, - ProductBuilder $productBuilder + NostoHelperScope $nostoHelperScope, + NostoHelperAccount $nostoHelperAccount, + CollectionBuilder $collectionBuilder ) { - parent::__construct($context, $storeManager, $accountHelper); - - $this->_productCollectionFactory = $productCollectionFactory; - $this->_productVisibility = $productVisibility; - $this->_productBuilder = $productBuilder; + parent::__construct($context, $nostoHelperScope, $nostoHelperAccount); + $this->nostoCollectionBuilder = $collectionBuilder; } /** - * @inheritdoc + * @param Store $store + * @param int $limit + * @param int $offset + * @return AbstractCollection|ProductCollection + * @throws NostoException */ - protected function getCollection(Store $store) + public function buildExportCollection(Store $store, int $limit = 100, int $offset = 0) { - /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ - $collection = $this->_productCollectionFactory->create(); - $collection->setVisibility($this->_productVisibility->getVisibleInSiteIds()); - $collection->addAttributeToFilter('status', ['eq' => '1']); - $collection->addStoreFilter($store->getId()); - return $collection; + return $this->nostoCollectionBuilder->buildMany($store, $limit, $offset); } /** - * @inheritdoc + * @param Store $store + * @param $id + * @return AbstractCollection|ProductCollection + * @throws NostoException */ - protected function buildExportCollection($collection, Store $store) + public function buildSingleExportCollection(Store $store, $id) { - /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ - $exportCollection = new NostoExportCollectionProduct(); - $items = $collection->loadData(); - foreach ($items as $product) { - /** @var \Magento\Catalog\Model\Product $product */ - $exportCollection[] = $this->_productBuilder->build($product, $store); - } - return $exportCollection; + return $this->nostoCollectionBuilder->buildSingle($store, $id); } -} \ No newline at end of file +} diff --git a/Controller/Frontend/Cart.php b/Controller/Frontend/Cart.php new file mode 100755 index 000000000..2d5e53832 --- /dev/null +++ b/Controller/Frontend/Cart.php @@ -0,0 +1,197 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Controller\Frontend; + +use Exception; +use Magento\Checkout\Model\Session; +use Magento\Framework\App\Action\Action; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Module\Manager as ModuleManager; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Nosto\NostoException; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Helper\Url as NostoHelperUrl; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Customer\Repository as NostoCustomerRepository; +use Zend_Uri_Exception; + +/* + * Controller class for handling cart restoration + */ +// @phan-suppress-next-next-line PhanDeprecatedClass - recommended to avoid code migration till 2.5.0 +// https://community.magento.com/t5/Magento-DevBlog/Decomposition-of-Magento-Controllers/ba-p/430883 +class Cart extends Action +{ + /** + * The name of the hash parameter to look from URL + */ + public const HASH_PARAM = 'h'; + + private Context $context; + private ModuleManager $moduleManager; + private Session $checkoutSession; + private NostoLogger $logger; + private NostoHelperUrl $nostoUrlHelper; + private NostoHelperScope $nostoScopeHelper; + private NostoCustomerRepository $nostoCustomerRepository; + private CartRepositoryInterface $cartRepository; + + /** + * Cart constructor. + * @param Context $context + * @param ModuleManager $moduleManager + * @param Session $checkoutSession + * @param NostoLogger $logger + * @param NostoHelperUrl $nostoUrlHelper + * @param NostoHelperScope $nostoScopeHelper + * @param NostoCustomerRepository $nostoCustomerRepository + * @param CartRepositoryInterface $cartRepository + */ + public function __construct( + Context $context, + ModuleManager $moduleManager, + Session $checkoutSession, + NostoLogger $logger, + NostoHelperUrl $nostoUrlHelper, + NostoHelperScope $nostoScopeHelper, + NostoCustomerRepository $nostoCustomerRepository, + CartRepositoryInterface $cartRepository + ) { + parent::__construct($context); + $this->context = $context; + $this->moduleManager = $moduleManager; + $this->checkoutSession = $checkoutSession; + $this->logger = $logger; + $this->nostoUrlHelper = $nostoUrlHelper; + $this->nostoScopeHelper = $nostoScopeHelper; + $this->nostoCustomerRepository = $nostoCustomerRepository; + $this->cartRepository = $cartRepository; + } + + /** + * @return ResponseInterface|ResultInterface + * @throws NoSuchEntityException + * @throws Zend_Uri_Exception + */ + public function execute() + { + $store = $this->nostoScopeHelper->getStore(); + $redirectUrl = $store->getBaseUrl(); + + $url = $this->context->getUrl(); + $currentUrl = $url->getCurrentUrl(); + + if ($this->moduleManager->isEnabled(NostoHelperData::MODULE_NAME)) { + if (!$this->checkoutSession->getQuoteId()) { + $restoreCartHash = $this->getRequest()->getParam(self::HASH_PARAM); + try { + if ($restoreCartHash) { + $quote = $this->resolveQuote($restoreCartHash); + if ($quote !== null) { + $this->checkoutSession->setQuoteId($quote->getId()); + $redirectUrl = $this->nostoUrlHelper->getUrlCart($store, $currentUrl); + } else { + throw new NostoException('Could not resolve quote for the given restore cart hash'); + } + } else { + throw new NostoException('No hash provided for restore cart'); + } + } catch (Exception $e) { + $this->logger->exception($e); + $this->messageManager->addErrorMessage('Sorry, we could not find your cart'); + } + } else { + $redirectUrl = $this->nostoUrlHelper->getUrlCart($store, $currentUrl); + } + } + return $this->_redirect($redirectUrl); + } + + /** + * Resolves the cart (quote) by the given hash + * + * @param $restoreCartHash + * @return CartInterface|null + * @throws NostoException + * @throws NoSuchEntityException + */ + private function resolveQuote($restoreCartHash) + { + $customer = $this->nostoCustomerRepository->getOneByRestoreCartHash($restoreCartHash); + if ($customer === null || !$customer->getCustomerId()) { + throw new NostoException( + sprintf( + 'No nosto customer found for hash %s', + $restoreCartHash + ) + ); + } + + if ($customer->getQuoteId() === null) { + throw new NostoException( + sprintf( + 'Found customer without quote for hash %s', + $restoreCartHash + ) + ); + } + + $quote = $this->cartRepository->get($customer->getQuoteId()); + if ($quote === null || !$quote->getId()) { + throw new NostoException( + sprintf( + 'No quote found for id %d', + $customer->getQuoteId() + ) + ); + } + // Note - we reactivate the cart if it's not active. + // This would happen for example when the cart was bought. + if (!$quote->getIsActive()) { + $quote->setIsActive(true); + + $this->cartRepository->save($quote); + } + + return $quote; + } +} diff --git a/Controller/Frontend/RestoreCart.php b/Controller/Frontend/RestoreCart.php new file mode 100644 index 000000000..bc01c6777 --- /dev/null +++ b/Controller/Frontend/RestoreCart.php @@ -0,0 +1,46 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Controller\Frontend; + +/** + * Class for backwards compatibility with Nosto Magento 2 extension version 2.1.0 + * + * @deprecated + */ +class RestoreCart extends Cart // @codingStandardsIgnoreLine +{ +} diff --git a/Controller/Oauth/Index.php b/Controller/Oauth/Index.php index c8f37b58d..ea0815282 100644 --- a/Controller/Oauth/Index.php +++ b/Controller/Oauth/Index.php @@ -1,180 +1,233 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Controller\Oauth; +use Exception; use Magento\Backend\Model\UrlInterface; use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; -use Magento\Framework\Exception\CouldNotSaveException; -use Magento\Framework\Exception\State\InputMismatchException; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\Response\Http; use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use Nosto\Tagging\Helper\Account; -use Nosto\Tagging\Model\Meta\Oauth\Builder; -use Psr\Log\LoggerInterface; +use Magento\Store\Model\StoreRepository; +use Nosto\Mixins\OauthTrait; +use Nosto\NostoException; +use Nosto\OAuth; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Cache as NostoHelperCache; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Helper\Url as NostoHelperUrl; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Meta\Oauth\Builder as NostoOauthBuilder; +use Nosto\Types\Signup\AccountInterface; +// @phan-suppress-next-next-line PhanDeprecatedClass - recommended to avoid code migration till 2.5.0 +// https://community.magento.com/t5/Magento-DevBlog/Decomposition-of-Magento-Controllers/ba-p/430883 class Index extends Action { - private $_logger; - private $_backendUrlBuilder; - private $_accountHelper; - private $_oauthMetaBuilder; - private $_accountService; - private $_storeManager; + use OauthTrait; + + private NostoLogger $logger; + private UrlInterface $urlBuilder; + private NostoHelperAccount $nostoHelperAccount; + private NostoOauthBuilder $oauthMetaBuilder; + private NostoHelperScope $nostoHelperScope; + private NostoHelperCache $nostoHelperCache; + private StoreRepository $storeRepository; + private RequestInterface $request; /** + * Index constructor. * @param Context $context - * @param LoggerInterface $logger - * @param StoreManagerInterface $storeManager - * @param UrlInterface $backendUrlBuilder - * @param Account $accountHelper - * @param Builder $oauthMetaBuilder - * @param \NostoServiceAccount $accountService + * @param NostoLogger $logger + * @param NostoHelperScope $nostoHelperScope + * @param UrlInterface $urlBuilder + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperCache $nostoHelperCache + * @param NostoOauthBuilder $oauthMetaBuilder + * @param StoreRepository $storeRepository */ public function __construct( Context $context, - LoggerInterface $logger, - StoreManagerInterface $storeManager, - UrlInterface $backendUrlBuilder, - Account $accountHelper, - Builder $oauthMetaBuilder, - \NostoServiceAccount $accountService + NostoLogger $logger, + NostoHelperScope $nostoHelperScope, + UrlInterface $urlBuilder, + NostoHelperAccount $nostoHelperAccount, + NostoHelperCache $nostoHelperCache, + NostoOauthBuilder $oauthMetaBuilder, + StoreRepository $storeRepository ) { parent::__construct($context); - $this->_logger = $logger; - $this->_storeManager = $storeManager; - $this->_backendUrlBuilder = $backendUrlBuilder; - $this->_accountHelper = $accountHelper; - $this->_oauthMetaBuilder = $oauthMetaBuilder; - $this->_accountService = $accountService; + $this->logger = $logger; + $this->urlBuilder = $urlBuilder; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->oauthMetaBuilder = $oauthMetaBuilder; + $this->nostoHelperScope = $nostoHelperScope; + $this->nostoHelperCache = $nostoHelperCache; + $this->storeRepository = $storeRepository; + $this->request = $context->getRequest(); } /** - * Handles the redirect from Nosto oauth2 authorization server when an - * existing account is connected to a store. - * This is handled in the front end as the oauth2 server validates the - * "return_url" sent in the first step of the authorization cycle, and - * requires it to be from the same domain that the account is configured - * for and only redirects to that domain. + * Handles the redirect from Nosto oauth2 authorization server when an existing account is + * connected to a store. This is handled in the front end as the oauth2 server validates the + * "return_url" sent in the first step of the authorization cycle, and requires it to be from + * the same domain that the account is configured for and only redirects to that domain. * * @return void */ public function execute() { - $request = $this->getRequest(); + $this->connect(); + } + + /** + * Implemented trait method that is responsible for fetching the OAuth parameters used for all + * OAuth operations + * + * @return Oauth the OAuth parameters for the operations + */ + public function getMeta() + { + $account = $this->nostoHelperAccount->findAccount($this->nostoHelperScope->getStore()); + return $this->oauthMetaBuilder->build($this->nostoHelperScope->getStore(), $account); + } + + /** + * Implemented trait method that is responsible for saving an account with the all tokens for + * the current store view (as defined by the parameter.) + * + * @param AccountInterface $account the account to save + * @return boolean a boolean value indicating whether the account was saved + * @throws NostoException + * @suppress PhanTypeMismatchArgument + */ + public function save(AccountInterface $account) + { + $stores = $this->storeRepository->getList(); + $storeCode = $this->request->getParam( + NostoHelperUrl::MAGENTO_URL_PARAMETER_STORE + ); + + if ($storeCode !== null) { + $currentStore = $this->nostoHelperScope->getStoreByCode($storeCode); + } else { + $currentStore = $this->nostoHelperScope->getStore(); + } + /** @var Store $store */ - $store = $this->_storeManager->getStore(); - if (($authCode = $request->getParam('code')) !== null) { - try { - $this->connectAccount($authCode, $store); - $params = [ - 'message_type' => \NostoMessage::TYPE_SUCCESS, - 'message_code' => \NostoMessage::CODE_ACCOUNT_CONNECT, - 'store' => (int)$store->getId(), - ]; - } catch (\Exception $e) { - $this->_logger->error($e, ['exception' => $e]); - $params = [ - 'message_type' => \NostoMessage::TYPE_ERROR, - 'message_code' => \NostoMessage::CODE_ACCOUNT_CONNECT, - 'store' => (int)$store->getId(), - ]; - } - $this->redirectBackend('nosto/account/proxy', $params); - } elseif (($error = $request->getParam('error')) !== null) { - $logMsg = $error; - if (($reason = $request->getParam('error_reason')) !== null) { - $logMsg .= ' - ' . $reason; + foreach ($stores as $store) { + $existingAccount = $this->nostoHelperAccount->findAccount($store); + if ($existingAccount !== null + && $existingAccount->getName() === $account->getName() + && $currentStore->getId() !== $store->getId() + ) { + throw new NostoException( + sprintf( + 'This account is already being used by "%s". + Please create a new account for each store view', + $store->getName() + ) + ); } - if (($desc = $request->getParam('error_description')) !== null) { - $logMsg .= ' - ' . $desc; - } - $this->_logger->error($logMsg); - $this->redirectBackend( - 'nosto/account/proxy', - [ - 'message_type' => \NostoMessage::TYPE_ERROR, - 'message_code' => \NostoMessage::CODE_ACCOUNT_CONNECT, - 'message_text' => $desc, - 'store' => (int)$store->getId(), - ] - ); - } else { - // todo - /** @var \Magento\Framework\App\Response\Http $response */ - $response = $this->getResponse(); - $response->setHttpResponseCode(404); } + + $success = $this->nostoHelperAccount->saveAccount( + $account, + $currentStore + ); + + // Invalidate cache after reconnected nosto account + if ($success) { + $this->nostoHelperCache->invalidatePageCache(); + $this->nostoHelperCache->invalidateLayoutCache(); + } + + return $success; } /** - * Redirects the user to the Magento backend. - * - * @param string $path the backend path to redirect to. - * @param array $args the url arguments. + * Implemented trait method that redirects the user with the authentication params to the + * admin controller. * - * @return \Magento\Framework\App\ResponseInterface the response. + * @param array $params the parameters to be used when building the redirect */ - private function redirectBackend($path, $args = []) + public function redirect(array $params) { - /** @var \Magento\Framework\App\Response\Http $response */ $response = $this->getResponse(); - $response->setRedirect($this->_backendUrlBuilder->getUrl($path, $args)); - return $response; + if ($response instanceof Http) { + $params['store'] = (int)$this->nostoHelperScope->getStore()->getId(); + $response->setRedirect($this->urlBuilder->getUrl('nosto/account/proxy', $params)); + } } /** - * Tries to connect the Nosto account and saves the account details to the - * store config. + * Implemented trait method that is a utility responsible for fetching a specified query + * parameter from the GET request. * - * @param string $authCode the OAuth authorization code by which to get the account details from Nosto. - * @param Store $store the store the account is connect for. - * @throws \Exception if the connection fails. + * @param string $name the name of the query parameter to fetch + * @return string the value of the specified query parameter */ - protected function connectAccount($authCode, $store) + public function getParam($name) { - $oldAccount = $this->_accountHelper->findAccount($store); - $meta = $this->_oauthMetaBuilder->build($store, $oldAccount); - $newAccount = $this->_accountService->sync($meta, $authCode); - - // If we are updating an existing account, - // double check that we got the same account back from Nosto. - if (!is_null($oldAccount) && !$newAccount->equals($oldAccount)) { - throw new InputMismatchException(__('Failed to synchronise Nosto account details, account mismatch.')); - } + return $this->getRequest()->getParam($name); + } - if (!$this->_accountHelper->saveAccount($newAccount, $store)) { - throw new CouldNotSaveException(__('Failed to save Nosto account.')); - } + /** + * Implemented trait method that is responsible for logging an exception to the Magento error + * log when an error occurs. + * + * @param Exception $e the exception to be logged + */ + public function logError(Exception $e) + { + $this->logger->exception($e); + } - // todo -// $this->_accountHelper->updateCurrencyExchangeRates($newAccount, $store); -// $this->_accountHelper->updateAccount($newAccount, $store); + /** + * Implemented trait method that is responsible for redirecting the user to a 404 page when + * the authorization code is invalid. + */ + public function notFound() + { + $response = $this->getResponse(); + if ($response instanceof Http) { + $response->setHttpResponseCode(404); + } } } diff --git a/Cron/RatesCron.php b/Cron/RatesCron.php new file mode 100644 index 000000000..1b3a33383 --- /dev/null +++ b/Cron/RatesCron.php @@ -0,0 +1,83 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Cron; + +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Rates\Service as NostoRatesService; + +/** + * Cronjob class that periodically updates exchange-rates to Nosto for each of the store views, + * provided that multiple-currencies are configured for that store view. + */ +class RatesCron +{ + protected NostoLogger $logger; + private NostoRatesService $nostoRatesService; + private NostoHelperScope $nostoHelperScope; + + /** + * Rates constructor. + * + * @param NostoLogger $logger + * @param NostoHelperScope $nostoHelperScope + * @param NostoRatesService $nostoRatesService + */ + public function __construct( + NostoLogger $logger, + NostoHelperScope $nostoHelperScope, + NostoRatesService $nostoRatesService + ) { + $this->logger = $logger; + $this->nostoRatesService = $nostoRatesService; + $this->nostoHelperScope = $nostoHelperScope; + } + + public function execute() + { + $this->logger->info('Updating exchange rates to Nosto for all store views'); + foreach ($this->nostoHelperScope->getStores(false) as $store) { + $this->logger->info('Updating exchange rates for ' . $store->getName()); + if ($this->nostoRatesService->update($store)) { + $this->logger->info('Successfully updated the exchange rates for the store view'); + } else { + $this->logger->warning('Unable to update the exchange rates for the store view'); + } + } + } +} diff --git a/CustomerData/ActiveVariationTagging.php b/CustomerData/ActiveVariationTagging.php new file mode 100644 index 000000000..daf26b017 --- /dev/null +++ b/CustomerData/ActiveVariationTagging.php @@ -0,0 +1,118 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\CustomerData; + +use Exception; +use Magento\Customer\CustomerData\SectionSourceInterface; +use Nosto\Tagging\Helper\Customer as NostoHelperCustomer; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Helper\Variation as NostoHelperVariation; +use Nosto\Tagging\Logger\Logger as NostoLogger; + +class ActiveVariationTagging implements SectionSourceInterface +{ + /** + * @var NostoHelperData + */ + private NostoHelperData $nostoHelperData; + + /** + * @var NostoHelperCustomer + */ + private NostoHelperCustomer $nostoHelperCustomer; + + /** + * @var NostoHelperScope + */ + private NostoHelperScope $nostoHelperScope; + + /** + * @var NostoHelperVariation + */ + private NostoHelperVariation $nostoHelperVariation; + + /** + * @var NostoLogger + */ + private NostoLogger $nostoLogger; + + /** + * ActiveVariationTagging constructor. + * @param NostoHelperData $nostoHelperData + * @param NostoHelperCustomer $nostoHelperCustomer + * @param NostoHelperScope $nostoHelperScope + * @param NostoHelperVariation $nostoHelperVariation + * @param NostoLogger $nostoLogger + */ + public function __construct( + NostoHelperData $nostoHelperData, + NostoHelperCustomer $nostoHelperCustomer, + NostoHelperScope $nostoHelperScope, + NostoHelperVariation $nostoHelperVariation, + NostoLogger $nostoLogger + ) { + $this->nostoHelperData = $nostoHelperData; + $this->nostoHelperCustomer = $nostoHelperCustomer; + $this->nostoHelperScope = $nostoHelperScope; + $this->nostoHelperVariation = $nostoHelperVariation; + $this->nostoLogger = $nostoLogger; + } + + /** + * @inheritDoc + */ + public function getSectionData() + { + $data = []; + $store = $this->nostoHelperScope->getStore(true); + if ($this->nostoHelperData->isPricingVariationEnabled($store) + && !$this->nostoHelperVariation->isDefaultVariationCode( + $this->nostoHelperCustomer->getGroupCode() + ) + ) { + try { + $data['active_variation'] = $this->nostoHelperCustomer->getGroupCode(); + } catch (Exception $e) { + $this->nostoLogger->exception($e); + } + } + + return $data; + } +} diff --git a/CustomerData/CartTagging.php b/CustomerData/CartTagging.php index bb1bf7745..e29da77f8 100644 --- a/CustomerData/CartTagging.php +++ b/CustomerData/CartTagging.php @@ -1,109 +1,130 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * */ + namespace Nosto\Tagging\CustomerData; -use Magento\Customer\CustomerData\SectionSourceInterface; +use Exception; use Magento\Checkout\Helper\Cart as CartHelper; -use Magento\Store\Api\StoreManagementInterface; -use Magento\Store\Model\StoreManagerInterface; -use Nosto\Tagging\Model\Cart\Builder as NostoCartBuilder; +use Magento\Customer\CustomerData\SectionSourceInterface; use Magento\Framework\Stdlib\CookieManagerInterface; -use Nosto\Tagging\Model\Customer as NostoCustomer; -use Nosto\Tagging\Model\CustomerFactory as NostoCustomerFactory; +use Magento\Quote\Model\Quote; +use Nosto\Model\Cart\LineItem; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Cart\Builder as NostoCartBuilder; +use Nosto\Tagging\Model\Cart\Restore\Builder as NostoRestoreCartUrlBuilder; +use Nosto\Tagging\Model\Customer\Customer as NostoCustomer; -class CartTagging implements SectionSourceInterface +class CartTagging extends HashedTagging implements SectionSourceInterface { - - /** - * @var \Magento\Checkout\Helper\Cart - */ - protected $cartHelper; - - /** - * @var \Nosto\Tagging\Model\Cart\Builder - */ - protected $nostoCartBuilder; - - /** - * @var StoreManagementInterface - */ - protected $storeManager; - - /** - * @var CookieManagerInterface - */ - protected $cookieManager; - - /** - * @var NostoCustomerFactory - */ - protected $nostoCustomerFactory; - - /** - * @var \Magento\Quote\Model\Quote|null - */ - protected $quote = null; + private CartHelper $cartHelper; + private CookieManagerInterface $cookieManager; + private NostoLogger $logger; + private Quote $quote; + private NostoHelperScope $nostoScopeHelper; + private NostoCartBuilder $nostoCartBuilder; + private NostoRestoreCartUrlBuilder $nostoRestoreCartUrlBuilder; /** * @param CartHelper $cartHelper - * @param NostoCartBuilder $nostoCartBuilder - * @param StoreManagerInterface $storeManager * @param CookieManagerInterface $cookieManager - * @param NostoCustomerFactory $nostoCustomerFactory + * @param NostoLogger $logger + * @param NostoCartBuilder $nostoCartBuilder + * @param NostoHelperScope $nostoScopeHelper + * @param NostoRestoreCartUrlBuilder $nostoRestoreCartUrlBuilder */ public function __construct( CartHelper $cartHelper, - NostoCartBuilder $nostoCartBuilder, - StoreManagerInterface $storeManager, CookieManagerInterface $cookieManager, - NostoCustomerFactory $nostoCustomerFactory + NostoLogger $logger, + NostoCartBuilder $nostoCartBuilder, + NostoHelperScope $nostoScopeHelper, + NostoRestoreCartUrlBuilder $nostoRestoreCartUrlBuilder ) { - $this->cartHelper= $cartHelper; - $this->nostoCartBuilder = $nostoCartBuilder; - $this->storeManager = $storeManager; + $this->cartHelper = $cartHelper; + $this->logger = $logger; $this->cookieManager = $cookieManager; - $this->nostoCustomerFactory = $nostoCustomerFactory; + $this->nostoScopeHelper = $nostoScopeHelper; + $this->nostoCartBuilder = $nostoCartBuilder; + $this->nostoRestoreCartUrlBuilder = $nostoRestoreCartUrlBuilder; + $this->quote = $this->cartHelper->getCart()->getQuote(); } /** - * @inheritdoc + * @inheritDoc */ public function getSectionData() { + $nostoCustomerId = $this->cookieManager->getCookie(NostoCustomer::COOKIE_NAME); $data = [ - "items" => [], - "itemCount" => 0, + 'hcid' => $this->generateVisitorChecksum($nostoCustomerId), + 'items' => [], + 'restore_cart_url' => '' ]; $cart = $this->cartHelper->getCart(); - $items = $this->getQuote()->getAllVisibleItems(); $nostoCart = $this->nostoCartBuilder->build( - $items, - $this->storeManager->getStore() + $this->getQuote(), + $this->nostoScopeHelper->getStore() ); $itemCount = $cart->getItemsCount(); - $data["itemCount"] = $itemCount; + $data['itemCount'] = $itemCount; $addedCount = 0; - /* @var \NostoCartItemInterface $item */ + /* @var LineItem $item */ foreach ($nostoCart->getItems() as $item) { $addedCount++; - $data["items"][] = [ - 'product_id' => $item->getItemId(), + $data['items'][] = [ + 'product_id' => $item->getProductId(), + 'sku_id' => $item->getSkuId(), 'quantity' => $item->getQuantity(), 'name' => $item->getName(), - 'unit_price' => $item->getUnitPrice()->getPrice(), - 'price_currency_code' => $item->getCurrency()->getCode(), + 'unit_price' => $item->getUnitPrice(), + 'price_currency_code' => $item->getPriceCurrencyCode(), 'total_count' => $itemCount, 'index' => $addedCount ]; } - if ($data["itemCount"] > 0) { - $this->updateNostoId(); + if ($data['itemCount'] > 0) { + $store = $this->nostoScopeHelper->getStore(); + try { + $data['restore_cart_url'] = $this->nostoRestoreCartUrlBuilder + ->build($this->getQuote(), $store); + } catch (Exception $e) { + $this->logger->exception($e); + } } return $data; @@ -112,9 +133,9 @@ public function getSectionData() /** * Get active quote * - * @return \Magento\Quote\Model\Quote + * @return Quote */ - protected function getQuote() + public function getQuote() { if (!$this->quote) { $cart = $this->cartHelper->getCart(); @@ -127,42 +148,10 @@ protected function getQuote() /** * Return customer quote items * - * @return \Magento\Quote\Model\Quote\Item[] + * @return Quote\Item[] */ - protected function getAllQuoteItems() + public function getAllQuoteItems() { - - $quote = $this->getQuote(); - return $quote->getAllVisibleItems(); - } - - private function updateNostoId() { - // Handle the Nosto customer & quote mapping - $nostoCustomerId = $this->cookieManager->getCookie(NostoCustomer::COOKIE_NAME); - $quoteId = $this->getQuote()->getId(); - if (!empty($quoteId) && !empty($nostoCustomerId)) { - $nostoCustomer = $this->nostoCustomerFactory - ->create() - ->getCollection() - ->addFieldToFilter(NostoCustomer::QUOTE_ID, $quoteId) - ->addFieldToFilter(NostoCustomer::NOSTO_ID, $nostoCustomerId) - ->setPageSize(1) - ->setCurPage(1) - ->getFirstItem(); - if ($nostoCustomer->hasData(NostoCustomer::CUSTOMER_ID)) { - $nostoCustomer->setUpdatedAt(new \DateTime('now')); - } else { - $nostoCustomer = $this->nostoCustomerFactory->create(); - $nostoCustomer->setQuoteId($quoteId); - $nostoCustomer->setNostoId($nostoCustomerId); - $nostoCustomer->setCreatedAt(new \DateTime('now')); - $nostoCustomer->setUpdatedAt(new \DateTime('now')); - } - try { - $nostoCustomer->save(); - } catch (\Exception $e) { - //Todo - handle errors, maybe log? - } - } + return $this->getQuote()->getAllVisibleItems(); } } diff --git a/CustomerData/CustomerTagging.php b/CustomerData/CustomerTagging.php index 4ce83241d..e853cd9da 100644 --- a/CustomerData/CustomerTagging.php +++ b/CustomerData/CustomerTagging.php @@ -1,62 +1,95 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * */ + namespace Nosto\Tagging\CustomerData; use Magento\Customer\CustomerData\SectionSourceInterface; use Magento\Customer\Helper\Session\CurrentCustomer; use Magento\Framework\Stdlib\CookieManagerInterface; -use Nosto\Tagging\Helper\Data as NostoDataHelper; -use Nosto\Tagging\Model\Customer as NostoCustomer; +use Nosto\Model\Customer; +use Nosto\Tagging\Model\Customer\Customer as NostoCustomer; +use Nosto\Tagging\Model\Person\Tagging\Builder as NostoPersonBuilder; -class CustomerTagging implements SectionSourceInterface +class CustomerTagging extends HashedTagging implements SectionSourceInterface { - /* - * @var CurrentCustomer - */ - protected $currentCustomer; - - /* - * @var CookieManagerInterface - */ - protected $cookieManager; + private CurrentCustomer $currentCustomer; + private CookieManagerInterface $cookieManager; + private NostoPersonBuilder $personBuilder; /** - * Constructor - * + * CustomerTagging constructor. * @param CurrentCustomer $currentCustomer * @param CookieManagerInterface $cookieManager + * @param NostoPersonBuilder $personBuilder */ public function __construct( CurrentCustomer $currentCustomer, - CookieManagerInterface $cookieManager + CookieManagerInterface $cookieManager, + NostoPersonBuilder $personBuilder ) { $this->currentCustomer = $currentCustomer; $this->cookieManager = $cookieManager; + $this->personBuilder = $personBuilder; } /** - * @inheritdoc + * @return array */ public function getSectionData() { - $data = []; - if ( - $this->currentCustomer instanceof CurrentCustomer + if ($this->currentCustomer instanceof CurrentCustomer && $this->currentCustomer->getCustomerId() ) { - $customer = $this->currentCustomer->getCustomer(); + /** @var Customer $customer */ + $customer = $this->personBuilder->fromSession($this->currentCustomer); + if ($customer === null) { + return []; + } $nostoCustomerId = $this->cookieManager->getCookie(NostoCustomer::COOKIE_NAME); $data = [ - 'first_name' => $customer->getFirstname(), - 'last_name' => $customer->getLastname(), + 'first_name' => $customer->getFirstName(), + 'last_name' => $customer->getLastName(), 'email' => $customer->getEmail(), - 'hcid' => NostoDataHelper::generateVisitorChecksum($nostoCustomerId), + 'hcid' => $this->generateVisitorChecksum($nostoCustomerId), + 'marketing_permission' => $customer->getMarketingPermission(), + 'customer_reference' => $customer->getCustomerReference(), + 'customer_group' => $customer->getCustomerGroup(), + 'gender' => $customer->getGender(), + 'date_of_birth' => $customer->getDateOfBirth() ]; } diff --git a/CustomerData/HashedTagging.php b/CustomerData/HashedTagging.php new file mode 100644 index 000000000..2b1c8efb4 --- /dev/null +++ b/CustomerData/HashedTagging.php @@ -0,0 +1,59 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\CustomerData; + +abstract class HashedTagging +{ + + /** + * @var string the algorithm to use for hashing visitor id. + */ + public const VISITOR_HASH_ALGO = 'sha256'; + + /** + * Return the checksum for for the customer tagging i.e hashed cookie identifier or HCID for + * short. This is used to sign the tagging so that if it is in fact cached, the cookie and + * tagging signature won't match and the data will be discarded + * + * @param string $string + * @return string + */ + public function generateVisitorChecksum(string $string) + { + return hash(self::VISITOR_HASH_ALGO, $string); + } +} diff --git a/Exception/ParentProductDisabledException.php b/Exception/ParentProductDisabledException.php new file mode 100644 index 000000000..7ad994d4c --- /dev/null +++ b/Exception/ParentProductDisabledException.php @@ -0,0 +1,49 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Exception; + +use Nosto\NostoException; +use Throwable; + +class ParentProductDisabledException extends NostoException +{ + public function __construct(int $productId, $code = 0, Throwable $previous = null) + { + $message = "Parent product is disabled for SKU with id: " . $productId; + parent::__construct($message, $code, $previous); + } +} diff --git a/Helper/Account.php b/Helper/Account.php index 50b90403d..dc8c13677 100644 --- a/Helper/Account.php +++ b/Helper/Account.php @@ -1,46 +1,64 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Helper; +use Exception; use Magento\Framework\App\Config\Storage\WriterInterface; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; -use Magento\Store\Api\Data\StoreInterface; +use Magento\Backend\Model\UrlInterface; +use Magento\Framework\Module\Manager; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; -use Nosto\Tagging\Model\Meta\Account\Iframe\Builder as IframeMetaBuilder; -use Nosto\Tagging\Model\Meta\Account\Sso\Builder as SsoMetaBuilder; -use Magento\Framework\Module\Manager as ModuleManager; +use Nosto\NostoException; +use Nosto\Model\Signup\Account as NostoSignupAccount; +use Nosto\Model\User; +use Nosto\Operation\UninstallAccount; +use Nosto\Request\Api\Token; use Nosto\Tagging\Helper\Data as NostoHelper; - +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Helper\Url as NostoHelperUrl; +use Nosto\Types\Signup\AccountInterface; +use Psr\Log\LoggerInterface; +use RuntimeException; /** - * Account helper class for common tasks related to Nosto accounts. + * NostoHelperAccount helper class for common tasks related to Nosto accounts. * Everything related to saving/updating/deleting accounts happens in here. */ class Account extends AbstractHelper @@ -48,135 +66,86 @@ class Account extends AbstractHelper /** * Path to store config nosto account name. */ - const XML_PATH_ACCOUNT = 'nosto_tagging/settings/account'; + public const XML_PATH_ACCOUNT = 'nosto_tagging/settings/account'; /** * Path to store config nosto account tokens. */ - const XML_PATH_TOKENS = 'nosto_tagging/settings/tokens'; - - /** - * Platform UI version - */ - const IFRAME_VERSION = 0; - - /** - * @var SsoMetaBuilder the builder for sso meta models. - */ - protected $_ssoMetaBuilder; - - /** - * @var IframeMetaBuilder the builder for iframe meta models. - */ - protected $_iframeMetaBuilder; - - /** - * @var \NostoHelperIframe the Nosto SDK iframe helper. - */ - protected $_iframeHelper; + public const XML_PATH_TOKENS = 'nosto_tagging/settings/tokens'; /** - * @var WriterInterface the app config writer. + * Path to store config store domain. */ - protected $_config; + public const XML_PATH_DOMAIN = 'nosto_tagging/settings/domain'; - /** - * @var ModuleManager - */ - protected $_moduleManager; + private WriterInterface $config; + private Manager $moduleManager; + private LoggerInterface $logger; + private Scope $nostoHelperScope; + private Url $nostoHelperUrl; + private UrlInterface $urlBuilder; /** - * Constructor. - * - * @param Context $context the context. - * @param SsoMetaBuilder $ssoMetaBuilder the builder for sso meta models. - * @param IframeMetaBuilder $iframeMetaBuilder the builder for iframe meta models. - * @param \NostoHelperIframe $iframeHelper - * @param WriterInterface $appConfig the app config writer. - * @param ModuleManager $moduleManager + * Account constructor. + * @param Context $context + * @param WriterInterface $appConfig + * @param Scope $nostoHelperScope + * @param Url $nostoHelperUrl + * @param UrlInterface $urlInterface */ public function __construct( Context $context, - SsoMetaBuilder $ssoMetaBuilder, - IframeMetaBuilder $iframeMetaBuilder, - \NostoHelperIframe $iframeHelper, WriterInterface $appConfig, - ModuleManager $moduleManager + NostoHelperScope $nostoHelperScope, + NostoHelperUrl $nostoHelperUrl, + UrlInterface $urlInterface ) { parent::__construct($context); - $this->_ssoMetaBuilder = $ssoMetaBuilder; - $this->_iframeMetaBuilder = $iframeMetaBuilder; - $this->_iframeHelper = $iframeHelper; - $this->_config = $appConfig; - $this->_moduleManager = $moduleManager; - } - - /** - * Returns the account with associated api tokens for the store. - * - * @param StoreInterface $store the store. - * - * @return \NostoAccount|null the account or null if not found. - */ - public function findAccount(StoreInterface $store) - { - $accountName = $store->getConfig(self::XML_PATH_ACCOUNT); - - if (!empty($accountName)) { - $account = new \NostoAccount($accountName); - $tokens = json_decode( - $store->getConfig(self::XML_PATH_TOKENS), - true - ); - if (is_array($tokens) && !empty($tokens)) { - foreach ($tokens as $name => $value) { - try { - $account->addApiToken( - new \NostoApiToken($name, $value) - ); - } catch (\NostoInvalidArgumentException $e) { - - } - } - } - return $account; - } - - return null; + $this->config = $appConfig; + $this->moduleManager = $context->getModuleManager(); + $this->logger = $context->getLogger(); + $this->nostoHelperScope = $nostoHelperScope; + $this->nostoHelperUrl = $nostoHelperUrl; + $this->urlBuilder = $urlInterface; } /** * Saves the account and the associated api tokens for the store. * - * @param \NostoAccountMetaInterface $account the account to save. + * @param AccountInterface $account the account to save. * @param Store $store the store. - * * @return bool true on success, false otherwise. */ - public function saveAccount(\NostoAccountMetaInterface $account, Store $store) + public function saveAccount(AccountInterface $account, Store $store) { if ((int)$store->getId() < 1) { return false; } - $tokens = array(); + $tokens = []; foreach ($account->getTokens() as $token) { $tokens[$token->getName()] = $token->getValue(); } - $this->_config->save( + $this->config->save( self::XML_PATH_ACCOUNT, $account->getName(), ScopeInterface::SCOPE_STORES, $store->getId() ); - $this->_config->save( + $this->config->save( self::XML_PATH_TOKENS, json_encode($tokens), ScopeInterface::SCOPE_STORES, $store->getId() ); + $this->config->save( + self::XML_PATH_DOMAIN, + $this->nostoHelperUrl->getActiveDomain($store), + ScopeInterface::SCOPE_STORES, + $store->getId() + ); $store->resetConfig(); @@ -186,35 +155,42 @@ public function saveAccount(\NostoAccountMetaInterface $account, Store $store) /** * Removes an account with associated api tokens for the store. * - * @param \NostoAccount $account the account to remove. + * @param NostoSignupAccount $account the account to remove. * @param Store $store the store. - * + * @param User $currentUser * @return bool true on success, false otherwise. */ - public function deleteAccount(\NostoAccount $account, Store $store) - { + public function deleteAccount( + NostoSignupAccount $account, + Store $store, + User $currentUser + ) { if ((int)$store->getId() < 1) { return false; } - $this->_config->delete( + $this->config->delete( self::XML_PATH_ACCOUNT, ScopeInterface::SCOPE_STORES, $store->getId() ); - $this->_config->delete( + $this->config->delete( self::XML_PATH_TOKENS, ScopeInterface::SCOPE_STORES, $store->getId() ); + $this->config->delete( + self::XML_PATH_DOMAIN, + ScopeInterface::SCOPE_STORES, + $store->getId() + ); try { // Notify Nosto that the account was deleted. - $service = new \NostoServiceAccount(); - $service->delete($account); - } catch (\NostoException $e) { - // Failures are logged but not shown to the user. - $this->_logger->error($e, ['exception' => $e]); + $service = new UninstallAccount($account); + $service->delete($currentUser); + } catch (Exception $e) { + $this->logger->error($e->__toString()); } $store->resetConfig(); @@ -223,47 +199,174 @@ public function deleteAccount(\NostoAccount $account, Store $store) } /** - * Returns the account administration iframe url. - * If there is no account, the "front page" url will be returned where an - * account can be created from. + * Checks if Nosto module is enabled and Nosto account is set * - * @param StoreInterface $store the store to get the url for. - * @param \NostoAccount $account the account to get the iframe url for. - * @param array $params optional extra params for the url. + * @param Store $store + * @return bool + */ + public function nostoInstalledAndEnabled(Store $store) + { + return $this->moduleManager->isEnabled(NostoHelper::MODULE_NAME) + && $this->findAccount($store); + } + + /** + * Returns the account with associated api tokens for the store. * - * @return string the iframe url. + * @param Store $store the store. + * @return NostoSignupAccount|null the account or null if not found. */ - public function getIframeUrl( - StoreInterface $store, - \NostoAccount $account = null, - array $params = [] - ) { - if (self::IFRAME_VERSION > 0) { - $params['v'] = self::IFRAME_VERSION; + public function findAccount(Store $store) + { + $accountName = $store->getConfig(self::XML_PATH_ACCOUNT); + + if ($accountName !== null) { + try { + $account = new NostoSignupAccount($accountName); + } catch (NostoException $e) { + throw new RuntimeException($e->getMessage()); + } + $tokens = json_decode( + $store->getConfig(self::XML_PATH_TOKENS), + true + ); + if (is_array($tokens) && !empty($tokens)) { + foreach ($tokens as $name => $value) { + try { + $account->addApiToken(new Token($name, $value)); + } catch (Exception $e) { + $this->logger->error($e->__toString()); + } + } + } + $missingTokens = false; + foreach ($this->forgeMissingApiTokens($account) as $token) { + $account->addApiToken($token); + $missingTokens = true; + } + if ($missingTokens) { + $this->saveAccount($account, $store); + } + + return $account; } - return $this->_iframeHelper->getUrl( - $this->_ssoMetaBuilder->build(), - $this->_iframeMetaBuilder->build($store), - $account, - $params - ); + + return null; } /** - * Checks if Nosto module is enabled and Nosto account is set + * Creates tokens for settings and rates if those are missing * - * @param StoreInterface $store - * @return bool + * @param AccountInterface $account + * @return Token[] */ - public function nostoInstalledAndEnabled(StoreInterface $store) { + private function forgeMissingApiTokens(AccountInterface $account) + { + $tokens = []; + $ssoToken = $account->getApiToken(Token::API_SSO); + if ($ssoToken instanceof Token) { + if (!$account->getApiToken(Token::API_EXCHANGE_RATES)) { + try { + $ratesToken = new Token( + Token::API_EXCHANGE_RATES, + $ssoToken->getValue() + ); + $tokens[] = $ratesToken; + } catch (NostoException $e) { + $this->logger->error($e->getMessage()); + } + } + if (!$account->getApiToken(Token::API_SETTINGS)) { + try { + $settingsToken = new Token( + Token::API_SETTINGS, + $ssoToken->getValue() + ); + $tokens[] = $settingsToken; + } catch (NostoException $e) { + $this->logger->error($e->getMessage()); + } + } + } + + return $tokens; + } - $enabled = false; - if ($this->_moduleManager->isEnabled(NostoHelper::MODULE_NAME)) { - if ($this->findAccount($store)) { - $enabled = true; + /** + * Returns an array of stores where Nosto is installed + * + * @return Store[] + */ + public function getStoresWithNosto() + { + $stores = $this->nostoHelperScope->getStores(); + $storesWithNosto = []; + foreach ($stores as $store) { + $nostoAccount = $this->findAccount($store); + if ($nostoAccount instanceof NostoSignupAccount) { + $storesWithNosto[] = $store; } } - return $enabled; + return $storesWithNosto; + } + + /** + * Returns the stored storefront domain + * + * @param Store $store + * @return string the domain + */ + public function getStoreFrontDomain(Store $store) + { + return $store->getConfig(self::XML_PATH_DOMAIN); + } + + /** + * Returns the Nosto account name for the store + * + * @param Store $store + * @return string account name + */ + public function getAccountName(Store $store) + { + return $store->getConfig(self::XML_PATH_ACCOUNT); + } + + /** + * Returns bool value that represent validity of domain + * + * @param Store $store + * @return bool + */ + public function isDomainValid(Store $store) + { + $storedDomain = $this->getStoreFrontDomain($store); + $realDomain = $this->nostoHelperUrl->getActiveDomain($store); + return ($realDomain === $storedDomain); + } + + /** + * Returns the list of invalid Nosto accounts + * + * @return array + */ + public function getInvalidAccounts() + { + $stores = $this->getStoresWithNosto(); + $invalidAccounts = []; + + foreach ($stores as $store) { + if (!$this->isDomainValid($store)) { + $invalidAccounts[] = [ + 'storeName' => $store->getName(), + 'nostoAccount' => $this->getAccountName($store), + 'currentDomain' => $this->nostoHelperUrl->getActiveDomain($store), + 'storedDomain' => $this->getStoreFrontDomain($store), + 'resetUrl' => $this->urlBuilder->getUrl('nosto/account/index', ['store' => $store->getId()]) + ]; + } + } + return $invalidAccounts; } } diff --git a/Helper/Cache.php b/Helper/Cache.php new file mode 100755 index 000000000..9f37f6a48 --- /dev/null +++ b/Helper/Cache.php @@ -0,0 +1,127 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Helper; + +use Exception; +use Magento\Framework\App\Cache\Frontend\Pool; +use Magento\Framework\App\Cache\StateInterface; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Helper\AbstractHelper; +use Magento\Framework\App\Helper\Context; +use Magento\PageCache\Model\Cache\Type; +use Nosto\Tagging\Logger\Logger as NostoLogger; + +/** + * Cache helper class for cache related tasks. + */ +class Cache extends AbstractHelper +{ + public const CACHE_ID_LAYOUT = 'layout'; + + /** @var TypeListInterface $typeList */ + private TypeListInterface $typeList; + + private StateInterface $cacheState; + + /** @var NostoLogger */ + private NostoLogger $logger; + + /** @var Pool */ + private Pool $cacheFrontendPool; + + /** + * Cache constructor. + * @param Context $context + * @param TypeListInterface $typeList + * @param StateInterface $cacheState + * @param Pool $cacheFrontendPool + * @param NostoLogger $logger + */ + public function __construct( + Context $context, + TypeListInterface $typeList, + StateInterface $cacheState, + Pool $cacheFrontendPool, + NostoLogger $logger + ) { + parent::__construct($context); + $this->typeList = $typeList; + $this->cacheState = $cacheState; + $this->logger = $logger; + $this->cacheFrontendPool = $cacheFrontendPool; + } + + /** + * Invalidate full page cache + */ + public function invalidatePageCache() + { + if ($this->cacheState->isEnabled(Type::TYPE_IDENTIFIER)) { + $this->typeList->invalidate(Type::TYPE_IDENTIFIER); + } + } + + /** + * Invalidate layout cache + */ + public function invalidateLayoutCache() + { + if ($this->cacheState->isEnabled(self::CACHE_ID_LAYOUT)) { + $this->typeList->invalidate(self::CACHE_ID_LAYOUT); + } + } + + /** + * Flush all cache types + */ + public function flushCache() + { + try { + $caches = $this->typeList->getTypes(); + foreach ($caches as $cache) { + $id = $cache->getId(); + $this->typeList->cleanType($id); + } + + foreach ($this->cacheFrontendPool as $cacheFrontend) { + $cacheFrontend->getBackend()->clean(); + } + } catch (Exception $e) { + $this->logger->exception($e); + } + } +} diff --git a/Helper/Currency.php b/Helper/Currency.php index f3c168773..90de61640 100644 --- a/Helper/Currency.php +++ b/Helper/Currency.php @@ -1,75 +1,158 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Helper; +use Exception; +use Magento\Directory\Model\Currency as MagentoCurrency; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; -use /** @noinspection PhpUndefinedClassInspection */ - Zend_Currency; +use Magento\Store\Model\Store; +use Nosto\Tagging\Helper\Data as NostoHelperData; /** * Currency helper used for currency related tasks. */ class Currency extends AbstractHelper { - /** - * @var \NostoHelperCurrency the Nosto currency helper. - */ - protected $_currencyHelper; + private Data $nostoHelperData; /** * Constructor. * * @param Context $context the context. - * @param \NostoHelperCurrency $currencyHelper the Nosto currency helper. + * @param NostoHelperData $nostoHelperData */ public function __construct( Context $context, - \NostoHelperCurrency $currencyHelper + NostoHelperData $nostoHelperData ) { parent::__construct($context); - $this->_currencyHelper = $currencyHelper; + $this->nostoHelperData = $nostoHelperData; + } + + /** + * If the store uses multiple currencies the prices are converted from base + * currency into given currency. Otherwise the given price is returned. + * + * @param float $basePrice The price of a product in base currency + * @param Store $store + * @return float + * @throws Exception + */ + public function convertToTaggingPrice(float $basePrice, Store $store) + { + // If multi currency is disabled or exchange rates are used + // we don't do any processing / conversions for the price + if ($this->nostoHelperData->isMultiCurrencyDisabled($store) + || $this->nostoHelperData->isMultiCurrencyExchangeRatesEnabled($store) + ) { + return $basePrice; + } + + $taggingPrice = $basePrice; + $taggingCurrency = $this->getTaggingCurrency($store); + $baseCurrency = $store->getBaseCurrency(); + + if ($taggingCurrency->getCurrencyCode() !== $baseCurrency->getCurrencyCode()) { + $taggingPrice = $baseCurrency->convert($basePrice, $taggingCurrency); + } + + return $taggingPrice; + } + + /** + * Returns the currency that must be used in tagging + * + * @param Store $store + * @return MagentoCurrency + */ + public function getTaggingCurrency(Store $store) + { + // If multi currency is disabled or exhange rates are used + // we always use the base currency for tagging + if ($this->nostoHelperData->isMultiCurrencyExchangeRatesEnabled($store) + || $this->nostoHelperData->isMultiCurrencyDisabled($store) + ) { + $taggingCurrency = $store->getBaseCurrency(); + } else { + $taggingCurrency = $store->getDefaultCurrency(); + } + + return $taggingCurrency; + } + + /** + * Returns the amount of currencies used in given store + * + * @param Store $store + * @return int + */ + public function getCurrencyCount(Store $store) + { + $currencies = $store->getAvailableCurrencyCodes(true); + + return count($currencies); } /** - * Parses the format for a currency into a Nosto currency object. + * Returns the info if exchange rates are used * - * @param string $locale the locale to get the currency format in. - * @param string $currencyCode the currency ISO 4217 code to get the currency in. - * @return \NostoCurrency the parsed currency. + * @param Store $store + * @return boolean */ - public function getCurrencyObject($locale, $currencyCode) + public function exchangeRatesInUse(Store $store) { - /** @noinspection PhpUndefinedClassInspection */ - return $this->_currencyHelper->parseZendCurrencyFormat( - $currencyCode, - new Zend_Currency($locale, $currencyCode) - ); + if ($this->nostoHelperData->isMultiCurrencyExchangeRatesEnabled($store)) { + return true; + } + $method = $this->nostoHelperData->getMultiCurrencyMethod($store); + // Determine the value for MC setting if it's undefined + if ($method === Data::SETTING_VALUE_MC_UNDEFINED) { + if ($this->getCurrencyCount($store) > 1) { + $this->nostoHelperData->saveMultiCurrencyMethod(Data::SETTING_VALUE_MC_EXCHANGE_RATE, $store); + $this->nostoHelperData->clearMagentoCache('config'); + return true; + } + if ($this->getCurrencyCount($store) === 1) { + $this->nostoHelperData->saveMultiCurrencyMethod(Data::SETTING_VALUE_MC_SINGLE, $store); + $this->nostoHelperData->clearMagentoCache('config'); + } + } + return false; } } diff --git a/Helper/Customer.php b/Helper/Customer.php new file mode 100644 index 000000000..6e6bdeaa3 --- /dev/null +++ b/Helper/Customer.php @@ -0,0 +1,111 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Helper; + +use Magento\Customer\Api\GroupRepositoryInterface as GroupRepository; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Framework\App\Helper\AbstractHelper; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Nosto\Tagging\Logger\Logger; + +/** + * Customer helper + */ +class Customer extends AbstractHelper +{ + + private CustomerSession $customerSession; + private GroupRepository $groupRepository; + private Logger $logger; + + /** + * Customer constructor. + * + * @param Context $context + * @param Logger $logger + * @param CustomerSession $customerSession + * @param GroupRepository $groupRepository + */ + public function __construct( + Context $context, + Logger $logger, + CustomerSession $customerSession, // @codingStandardsIgnoreLine + GroupRepository $groupRepository + ) { + parent::__construct($context); + $this->customerSession = $customerSession; + $this->groupRepository = $groupRepository; + $this->logger = $logger; + } + + /** + * @return string + */ + public function getGroupCode() + { + try { + $customerGroupId = $this->getGroupId(); + if ($customerGroupId) { + $group = $this->groupRepository->getById($customerGroupId); + return $group->getCode(); + } + return $this->groupRepository->getById(Variation::DEFAULT_CUSTOMER_GROUP_ID)->getCode(); + } catch (NoSuchEntityException $e) { + $this->logger->exception($e); + return 'missing'; + } catch (LocalizedException $e) { + $this->logger->exception($e); + return 'missing'; + } + } + + /** + * @return int|null + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function getGroupId() + { + $groupId = $this->customerSession->getCustomerGroupId(); + if ($groupId && $groupId !== 0) { + return $groupId; + } + return null; + } +} diff --git a/Helper/Data.php b/Helper/Data.php index bda96b721..1091f1b18 100644 --- a/Helper/Data.php +++ b/Helper/Data.php @@ -1,118 +1,239 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Helper; +use Magento\Framework\App\Cache\Manager as CacheManager; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Config\Storage\WriterInterface; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; +use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\AppInterface; use Magento\Framework\Module\ModuleListInterface; +use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\App\ProductMetadataInterface; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use phpseclib3\Crypt\Random; /** - * Data helper used for common tasks, mainly configurations. + * NostoHelperData helper used for common tasks, mainly configurations. */ class Data extends AbstractHelper { /** * Path to store config installation ID. */ - const XML_PATH_INSTALLATION_ID = 'nosto_tagging/installation/id'; + public const XML_PATH_INSTALLATION_ID = 'nosto_tagging/installation/id'; /** * Path to store config product image version setting. */ - const XML_PATH_IMAGE_VERSION = 'nosto_tagging/image_options/image_version'; + public const XML_PATH_IMAGE_VERSION = 'nosto/images/version'; /** - * @var string the algorithm to use for hashing visitor id. + * Path to store config for removing "pub/" directory from image URLs */ - const VISITOR_HASH_ALGO = 'sha256'; + public const XML_PATH_IMAGE_REMOVE_PUB_FROM_URL = 'nosto/images/remove_pub_directory'; /** - * @var StoreManagerInterface the store manager. + * Path to the configuration object that store's the brand attribute */ - protected $_storeManager; + public const XML_PATH_BRAND_ATTRIBUTE = 'nosto/optional/brand'; /** - * @var ModuleListInterface the module listing + * Path to the configuration object that store's the margin attribute */ - protected $_moduleListing; + public const XML_PATH_MARGIN_ATTRIBUTE = 'nosto/optional/margin'; /** - * @var WriterInterface the config writer. + * Path to the configuration object that store's the GTIN attribute */ - protected $_configWriter; + public const XML_PATH_GTIN_ATTRIBUTE = 'nosto/optional/gtin'; /** - * @var ProductMetadataInterface $_productMetaData. + * Path to the configuration object that store's the google_category attribute */ - protected $_productMetaData; + public const XML_PATH_GOOGLE_CATEGORY_ATTRIBUTE = 'nosto/optional/google_category'; - const MODULE_NAME = 'Nosto_Tagging'; + /** + * Path to the configuration object that stores the preference to tag variation data + */ + public const XML_PATH_VARIATION_TAGGING = 'nosto/flags/variation_tagging'; /** - * Constructor. - * - * @param Context $context the context. - * @param StoreManagerInterface $storeManager the store manager. + * Path to store config for custom fields + */ + public const XML_PATH_USE_CUSTOM_FIELDS = 'nosto/flags/use_custom_fields'; + + /** + * Path to the configuration object that stores the preference to tag alt. image data + */ + public const XML_PATH_ALTIMG_TAGGING = 'nosto/flags/altimg_tagging'; + + /** + * Path to the configuration object that stores the preference to tag rating and review data + */ + public const XML_PATH_RATING_TAGGING = 'nosto/flags/rating_tagging'; + + /** + * Path to the configuration object that stores the preference to tag inventory data + */ + public const XML_PATH_INVENTORY_TAGGING = 'nosto/flags/inventory_tagging'; + + /** + * Path to the configuration object that stores the preference for real time product updates + */ + public const XML_PATH_PRODUCT_UPDATES = 'nosto/flags/product_updates'; + + /** + * Path to store config for sending customer data to Nosto or not + */ + public const XML_PATH_SEND_CUSTOMER_DATA = 'nosto/flags/send_customer_data'; + + /** + * Path to the configuration object that stores the preference for low stock tagging + */ + public const XML_PATH_LOW_STOCK_INDICATION = 'nosto/flags/low_stock_indication'; + + /** + * Path to the configuration object that stores the percentage of PHP available memory for indexer + */ + public const XML_PATH_INDEXER_MEMORY = 'nosto/flags/indexer_memory'; + + /** + * Path to the configuration object that stores the preference for indexing disabled products + */ + public const XML_PATH_INDEX_DISABLED_PRODUCTS = 'nosto/flags/indexer_disabled_products'; + + /* + * Path to the configuration object for tagging the date a product has beed added to Magento's catalog + */ + public const XML_PATH_TAG_DATE_PUBLISHED = 'nosto/flags/tag_date_published'; + + /** + * Path to the configuration object that stores customer reference + */ + public const XML_PATH_TRACK_MULTI_CHANNEL_ORDERS = 'nosto/flags/track_multi_channel_orders'; + + /** + * Path to the configuration object that stores preference for reloading recs after adding product to cart + */ + public const XML_PATH_RELOAD_RECS_AFTER_ATC = 'nosto/flags/reload_recs_after_atc'; + + /** + * Path to the configuration object for pricing variations + */ + public const XML_PATH_PRICING_VARIATION = 'nosto/multicurrency/pricing_variation'; + + /** + * Path to the configuration object that stores the preference for adding store code to URL + */ + public const XML_PATH_STORE_CODE_TO_URL = 'nosto/url/store_code_to_url'; + + /** + * Path to the configuration object for customized tags + */ + public const XML_PATH_TAG = 'nosto/attributes/'; + + /** + * Path to the configuration object for multi currency + */ + public const XML_PATH_MULTI_CURRENCY = 'nosto/multicurrency/method'; + + /** + * @var string Nosto customer reference attribute name + */ + public const NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME = 'nosto_customer_reference'; + + /** + * Values for ratings settings + */ + public const SETTING_VALUE_YOTPO_RATINGS = '2'; + public const SETTING_VALUE_MAGENTO_RATINGS = '1'; + public const SETTING_VALUE_NO_RATINGS = '0'; + + /** + * Values of the multi currency settings + */ + public const SETTING_VALUE_MC_EXCHANGE_RATE = 'exchangerates'; + public const SETTING_VALUE_MC_SINGLE = 'single'; + public const SETTING_VALUE_MC_DISABLED = 'disabled'; + public const SETTING_VALUE_MC_UNDEFINED = 'undefined'; + + /** + * Name of the module + */ + public const MODULE_NAME = 'Nosto_Tagging'; + + /** + * Name of the platform + */ + public const PLATFORM_NAME = 'Magento'; + + private ModuleListInterface $moduleListing; + private WriterInterface $configWriter; + private ProductMetadataInterface $productMetaData; + private Scope $nostoHelperScope; + private CacheManager $cacheManager; + + /** + * Data constructor. + * @param Context $context + * @param Scope $nostoHelperScope * @param ModuleListInterface $moduleListing * @param WriterInterface $configWriter * @param ProductMetadataInterface $productMetadataInterface + * @param CacheManager $cacheManager */ public function __construct( Context $context, - StoreManagerInterface $storeManager, + NostoHelperScope $nostoHelperScope, ModuleListInterface $moduleListing, WriterInterface $configWriter, - ProductMetadataInterface $productMetadataInterface + ProductMetadataInterface $productMetadataInterface, + CacheManager $cacheManager ) { parent::__construct($context); - $this->_storeManager = $storeManager; - $this->_moduleListing = $moduleListing; - $this->_configWriter = $configWriter; - $this->_productMetaData = $productMetadataInterface; - } - - /** - * @param string $path - * @param Store|null $store - * @return mixed|null - */ - public function getStoreConfig($path, Store $store = null) - { - if (is_null($store)) { - $store = $this->_storeManager->getStore(true); - } - return $store->getConfig($path); + $this->moduleListing = $moduleListing; + $this->configWriter = $configWriter; + $this->productMetaData = $productMetadataInterface; + $this->nostoHelperScope = $nostoHelperScope; + $this->cacheManager = $cacheManager; } /** @@ -129,28 +250,331 @@ public function getInstallationId() ); if (empty($installationId)) { // Running bin2hex() will make the ID string length 64 characters. - $installationId = bin2hex(\phpseclib_Crypt_Random::string(32)); - $this->_configWriter->save( + $installationId = bin2hex(Random::string(32)); + $this->configWriter->save( self::XML_PATH_INSTALLATION_ID, $installationId ); - // todo: clear cache. } return $installationId; } /** - * Return the product image version to include in product tagging. - * - * @param \Magento\Store\Model\Store|null $store the store model or null. + * Returns the value of the selected image version option from the configuration table * - * @return string + * @param StoreInterface|null $store the store model or null. + * @return string the configuration value */ - public function getProductImageVersion(Store $store = null) + public function getProductImageVersion(StoreInterface $store = null) { return $this->getStoreConfig(self::XML_PATH_IMAGE_VERSION, $store); } + /** + * Returns boolean if "pub/" directory should be removed from product image + * URLs. This is needed because M2 CLI doesn't know if the docroot is pointing to + * "pub/" directory or Magento root. + * + * @param StoreInterface|null $store the store model or null. + * @return boolean + */ + public function getRemovePubDirectoryFromProductImageUrl(StoreInterface $store = null) + { + return $this->getStoreConfig(self::XML_PATH_IMAGE_REMOVE_PUB_FROM_URL, $store); + } + + /** + * Returns the value of the selected brand attribute from the configuration table + * + * @param StoreInterface|null $store the store model or null. + * @return string the configuration value + */ + public function getBrandAttribute(StoreInterface $store = null) + { + return $this->getStoreConfig(self::XML_PATH_BRAND_ATTRIBUTE, $store); + } + + /** + * Returns the value of the selected margin attribute from the configuration table + * + * @param StoreInterface|null $store the store model or null. + * @return string the configuration value + */ + public function getMarginAttribute(StoreInterface $store = null) + { + return $this->getStoreConfig(self::XML_PATH_MARGIN_ATTRIBUTE, $store); + } + + /** + * Returns the value of the selected GTIN attribute from the configuration table + * + * @param StoreInterface|null $store the store model or null. + * @return string the configuration value + */ + public function getGtinAttribute(StoreInterface $store = null) + { + return $this->getStoreConfig(self::XML_PATH_GTIN_ATTRIBUTE, $store); + } + + /** + * Returns the value of the selected google category attribute from the configuration table + * + * @param StoreInterface|null $store the store model or null. + * @return string the configuration value + */ + public function getGoogleCategoryAttribute(StoreInterface $store = null) + { + return $this->getStoreConfig(self::XML_PATH_GOOGLE_CATEGORY_ATTRIBUTE, $store); + } + + /** + * Returns if variation data tagging is enabled from the configuration table + * + * @param StoreInterface|null $store the store model or null. + * @return bool the configuration value + */ + public function isVariationTaggingEnabled(StoreInterface $store = null) + { + return (bool)$this->getStoreConfig(self::XML_PATH_VARIATION_TAGGING, $store); + } + + /** + * Returns on/off setting for custom fields + * + * @param StoreInterface|null $store the store model or null. + * @return boolean + */ + public function isCustomFieldsEnabled(StoreInterface $store = null) + { + return (bool)$this->getStoreConfig(self::XML_PATH_USE_CUSTOM_FIELDS, $store); + } + + /** + * Returns if alt. image data tagging is enabled from the configuration table + * + * @param StoreInterface|null $store the store model or null. + * @return bool the configuration value + */ + public function isAltimgTaggingEnabled(StoreInterface $store = null) + { + return (bool)$this->getStoreConfig(self::XML_PATH_ALTIMG_TAGGING, $store); + } + + /** + * Returns if rating and review data tagging is enabled from the configuration table + * + * @param StoreInterface|null $store the store model or null. + * @return bool the configuration value + */ + public function isRatingTaggingEnabled(StoreInterface $store = null) + { + $providerCode = $this->getStoreConfig(self::XML_PATH_RATING_TAGGING, $store); + + if ((int)$providerCode === 0) { + return false; + } + + return true; + } + + /** + * Returns the provider used for ratings and reviews + * + * @param StoreInterface|null $store + * @return mixed|null + */ + public function getRatingTaggingProvider(StoreInterface $store = null) + { + return $this->getStoreConfig(self::XML_PATH_RATING_TAGGING, $store); + } + + /** + * Returns if inventory data tagging is enabled from the configuration table + * + * @param StoreInterface|null $store the store model or null. + * @return bool the configuration value + */ + public function isInventoryTaggingEnabled(StoreInterface $store = null) + { + return (bool)$this->getStoreConfig(self::XML_PATH_INVENTORY_TAGGING, $store); + } + + /** + * Returns if real time product updates are enabled from the configuration table + * + * @param StoreInterface|null $store the store model or null. + * @return bool the configuration value + */ + public function isProductUpdatesEnabled(StoreInterface $store = null) + { + return (bool)$this->getStoreConfig(self::XML_PATH_PRODUCT_UPDATES, $store); + } + + /** + * Returns if customer data should be sent to Nosto + * + * @param StoreInterface|null $store the store model or null. + * @return bool the configuration value + */ + public function isSendCustomerDataToNostoEnabled(StoreInterface $store = null) + { + return (bool)$this->getStoreConfig(self::XML_PATH_SEND_CUSTOMER_DATA, $store); + } + + /** + * Returns if orders want to be tracked from various channels + * + * @param StoreInterface|null $store + * @return bool + */ + public function isMultiChannelOrderTrackingEnabled(StoreInterface $store = null) + { + return (bool)$this->getStoreConfig(self::XML_PATH_TRACK_MULTI_CHANNEL_ORDERS, $store); + } + + /** + * Returns if recs should be reloaded after adding product to cart + * + * @param StoreInterface|null $store + * @return int + */ + public function isReloadRecsAfterAtcEnabled(StoreInterface $store = null) + { + return $this->getStoreConfig(self::XML_PATH_RELOAD_RECS_AFTER_ATC, $store); + } + + /** + * Returns if low stock indication should be tagged + * + * @param StoreInterface|null $store the store model or null. + * @return bool the configuration value + */ + public function isLowStockIndicationEnabled(StoreInterface $store = null) + { + return (bool)$this->getStoreConfig(self::XML_PATH_LOW_STOCK_INDICATION, $store); + } + + /** + * Returns maximum percentage of PHP available memory that indexer should use + * + * @param StoreInterface|null $store the store model or null. + * @return int the configuration value + */ + public function getIndexerMemory(StoreInterface $store = null) + { + return $this->getStoreConfig(self::XML_PATH_INDEXER_MEMORY, $store); + } + + /** + * Returns maximum percentage of PHP available memory that indexer should use + * + * @param StoreInterface|null $store the store model or null. + * @return bool the configuration value + */ + public function canIndexDisabledProducts(StoreInterface $store = null) + { + return $this->getStoreConfig(self::XML_PATH_INDEX_DISABLED_PRODUCTS, $store); + } + + /** + * Returns on/off setting for tagging product's date published + * + * @param StoreInterface|null $store the store model or null. + * @return bool the configuration value + */ + public function isTagDatePublishedEnabled(StoreInterface $store = null) + { + return $this->getStoreConfig(self::XML_PATH_TAG_DATE_PUBLISHED, $store); + } + + /** + * Returns if pricing variation is enabled + * + * @param StoreInterface|null $store the store model or null. + * @return bool the configuration value + */ + public function isPricingVariationEnabled(StoreInterface $store = null) + { + return (bool)$this->getStoreConfig(self::XML_PATH_PRICING_VARIATION, $store); + } + + /** + * Returns if multi currency is disabled + * + * @param StoreInterface|null $store the store model or null. + * @return bool the configuration value + */ + public function isMultiCurrencyDisabled(StoreInterface $store = null) + { + $storeConfig = $this->getMultiCurrencyMethod($store); + return ($storeConfig === self::SETTING_VALUE_MC_DISABLED); + } + + /** + * Returns if multi currency is enabled + * + * @param StoreInterface|null $store the store model or null. + * @return bool the configuration value + */ + public function isMultiCurrencyExchangeRatesEnabled(StoreInterface $store = null) + { + $storeConfig = $this->getMultiCurrencyMethod($store); + return ($storeConfig === self::SETTING_VALUE_MC_EXCHANGE_RATE); + } + + /** + * Returns the multi currency setup value / multi currency method + * + * @param StoreInterface|null $store the store model or null. + * @return string the configuration value + */ + public function getMultiCurrencyMethod(StoreInterface $store = null) + { + return $this->getStoreConfig(self::XML_PATH_MULTI_CURRENCY, $store); + } + + /** + * Saves the multi currency setup value / multi currency method + * + * @param string $value the value of the multi currency setting. + * @param StoreInterface|null $store the store model or null. + * @return string|null the configuration value + */ + public function saveMultiCurrencyMethod($value, StoreInterface $store = null) + { + return $this->saveStoreConfig(self::XML_PATH_MULTI_CURRENCY, $value, $store); + } + + /** + * @param string $path + * @param StoreInterface|Store|null $store + * @return mixed|null + */ + public function getStoreConfig($path, StoreInterface $store = null) + { + if ($store === null) { + $store = $this->nostoHelperScope->getStore(true); + } + return $store->getConfig($path); + } + + /** + * @param string $path + * @param mixed $value + * @param StoreInterface|Store|null $store + */ + public function saveStoreConfig($path, $value, StoreInterface $store = null) + { + $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT; + $storeId = 0; + if ($store !== null) { + $scope = 'stores'; // No const found for this one in M2.2.2 + $storeId = $store->getStoreId(); // No const found for this one in M2.2.2 + } + + $this->configWriter->save($path, $value, $scope, $storeId); + } + /** * Returns the module version number of the currently installed module. * @@ -158,26 +582,25 @@ public function getProductImageVersion(Store $store = null) */ public function getModuleVersion() { - $nostoModule = $this->_moduleListing->getOne('Nosto_Tagging'); + $nostoModule = $this->moduleListing->getOne('Nosto_Tagging'); if (!empty($nostoModule['setup_version'])) { - return $nostoModule['setup_version']; - } else { - - return 'unknown'; } + return 'unknown'; } /** * Returns the version number of the platform the e-commerce installation * * @return string the platforms's version + * @suppress PhanUndeclaredConstantOfClass + * @noinspection PhpUndefinedClassConstantInspection */ public function getPlatformVersion() { $version = 'unknown'; - if ($this->_productMetaData->getVersion()) { - $version = $this->_productMetaData->getVersion(); + if ($this->productMetaData->getVersion()) { + $version = $this->productMetaData->getVersion(); } elseif (defined(AppInterface::VERSION)) { $version = AppInterface::VERSION; } @@ -185,16 +608,66 @@ public function getPlatformVersion() } /** - * Return the checksum for string + * Returns the edition (community/enterprise) of the platform the e-commerce installation * - * @param string $string + * @return string the platforms's edition + */ + public function getPlatformEdition() + { + $edition = 'unknown'; + if ($this->productMetaData->getEdition()) { + $edition = $this->productMetaData->getEdition(); + } + + return $edition; + } + + /** + * Get tag1 mapping attributes * - * @return string + * @param string $tagId tag1, tag2 or tag3 + * @param StoreInterface|null $store the store model or null. + * @return null|array of attributes */ - public static function generateVisitorChecksum($string) + public function getTagAttributes($tagId, StoreInterface $store = null) { + $attributesConfig = $this->getStoreConfig(self::XML_PATH_TAG . $tagId, $store); + /** @noinspection TypeUnsafeComparisonInspection */ + if ($attributesConfig == null) { + return null; + } + + return explode(',', $attributesConfig); + } - return hash(self::VISITOR_HASH_ALGO, $string); + /** + * Returns the value if store codes should be added to Nosto URLs + * + * @param StoreInterface|null $store the store model or null. + * @return boolean the configuration value + */ + public function getStoreCodeToUrl(StoreInterface $store = null) + { + return (bool)$this->getStoreConfig(self::XML_PATH_STORE_CODE_TO_URL, $store); } + /** + * Clears Magento cache for given type (config, layout, block_html, etc.) + * @see http://devdocs.magento.com/guides/v2.2/config-guide/cli/config-cli-subcommands-cache.html + * + * @param string $type give "all" to clear all + */ + public function clearMagentoCache($type) + { + $types = $this->cacheManager->getAvailableTypes(); + $clearTypes = []; + if ($type === 'all') { + $clearTypes = $types; + } elseif (in_array($type, $types, false)) { + $clearTypes[] = $type; + } + if (!empty($clearTypes)) { + $this->cacheManager->clean($clearTypes); + } + } } diff --git a/Helper/Format.php b/Helper/Format.php deleted file mode 100644 index 02187d1d0..000000000 --- a/Helper/Format.php +++ /dev/null @@ -1,94 +0,0 @@ - - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) - */ - -namespace Nosto\Tagging\Helper; - -/** @noinspection PhpIncludeInspection */ -use Magento\Framework\App\Helper\AbstractHelper; -use Magento\Framework\App\Helper\Context; - -/** - * Format helper used for common formatting tasks. - */ -class Format extends AbstractHelper -{ - /** - * @var \NostoFormatterPrice the nosto price formatter. - */ - protected $_priceFormatter; - - /** - * @var \NostoFormatterDate the nosto date formatter. - */ - protected $_dateFormatter; - - /** - * Constructor. - * - * @param Context $context the context. - * @param \NostoFormatterPrice $priceFormatter the nosto price formatter. - * @param \NostoFormatterDate $dateFormatter the nosto date formatter. - */ - public function __construct( - Context $context, - \NostoFormatterPrice $priceFormatter, - \NostoFormatterDate $dateFormatter - ) { - parent::__construct($context); - - $this->_priceFormatter = $priceFormatter; - $this->_dateFormatter = $dateFormatter; - } - - /** - * Formats a \NostoPrice object, e.g. "1234.56". - * - * @param \NostoPrice $price the price to format. - * @return string the formatted price. - */ - public function formatPrice(\NostoPrice $price) - { - return $this->_priceFormatter->format( - $price, - new \NostoPriceFormat(2, '.', '') - ); - } - - /** - * Formats a \NostoDate object, e.g. "2015-12-24"; - * - * @param \NostoDate $date the date to format. - * @return string the formatted date. - */ - public function formatDate(\NostoDate $date) - { - return $this->_dateFormatter->format( - $date, - new \NostoDateFormat(\NostoDateFormat::YMD) - ); - } -} diff --git a/Helper/Item.php b/Helper/Item.php deleted file mode 100644 index 0a6a09c7e..000000000 --- a/Helper/Item.php +++ /dev/null @@ -1,61 +0,0 @@ - - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) - */ - -namespace Nosto\Tagging\Helper; - -use Magento\Framework\App\Helper\AbstractHelper; -use Magento\Catalog\Model\Product\Type; - -/** - * Item helper class for common tasks related to cart and order items. - */ -class Item extends AbstractHelper -{ - /** - * @param \Magento\Quote\Model\Quote\Item | \Magento\Sales\Model\Order\Item $item - * @return string - */ - public function buildProductId($item) - { - $parent = $item->getProductOptionByCode('super_product_config'); - if (isset($parent['product_id'])) { - return $parent['product_id']; - } elseif ($item->getProductType() === Type::TYPE_SIMPLE) { - $type = $item->getProduct()->getTypeInstance(); - $parentIds = $type->getParentIdsByChild($item->getProductId()); - $attributes = $item->getBuyRequest()->getData('super_attribute'); - // If the product has a configurable parent, we assume we should tag - // the parent. If there are many parent IDs, we are safer to tag the - // products own ID. - if (count($parentIds) === 1 && !empty($attributes)) { - return $parentIds[0]; - } - } - - return $item->getProductId(); - } -} diff --git a/Helper/NewRelic.php b/Helper/NewRelic.php new file mode 100644 index 000000000..7806884ff --- /dev/null +++ b/Helper/NewRelic.php @@ -0,0 +1,68 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Helper; + +use Throwable; + +/** + * New Relic wrapper utility + */ +class NewRelic +{ + /** + * Checks if New Relic extension is loaded + * + * @return bool + */ + public function newRelicAvailable() + { + return extension_loaded('newrelic'); + } + + /** + * Reports an exception to new relic + * + * @param Throwable $throwable + */ + public function reportException(Throwable $throwable) + { + if ($this->newRelicAvailable()) { + /** @noinspection PhpComposerExtensionStubsInspection */ + newrelic_notice_error($throwable->getMessage(), $throwable); + } + } +} diff --git a/Helper/Price.php b/Helper/Price.php index d2ce069ac..1625298c6 100644 --- a/Helper/Price.php +++ b/Helper/Price.php @@ -1,181 +1,427 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Helper; +use Exception; +use Magento\Bundle\Model\Option; use Magento\Bundle\Model\Product\Price as BundlePrice; +use Magento\Bundle\Model\Product\Type as BundleType; use Magento\Catalog\Helper\Data as CatalogHelper; use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Type as ProductType; +use Magento\CatalogRule\Model\ResourceModel\RuleFactory; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableType; +use Magento\Customer\Model\GroupManagement; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\GroupedProduct\Model\Product\Type\Grouped as GroupedType; -use Magento\Sales\Model\Order\Item; +use Magento\Store\Model\Store; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Tax\Model\Config as TaxConfig; +use Nosto\NostoException; +use Nosto\Tagging\Model\Product\Repository as NostoProductRepository; /** * Price helper used for product price related tasks. */ class Price extends AbstractHelper { - private $_catalogHelper; + private CatalogHelper $catalogHelper; + private RuleFactory $priceRuleFactory; + private TimezoneInterface $localeDate; + private NostoProductRepository $nostoProductRepository; + private TaxHelper $taxHelper; + + public const KEY_SKU_FINAL_PRICE = 'final_price'; + public const KEY_SKU_PRICE = 'price'; + public const KEY_SKU_PRODUCT_ID = 'entity_id'; /** * Constructor. * * @param Context $context the context. * @param CatalogHelper $catalogHelper the catalog helper. + * @param RuleFactory $ruleFactory + * @param TimezoneInterface $localeDate + * @param NostoProductRepository $nostoProductRepository + * @param TaxHelper $taxHelper */ public function __construct( Context $context, - CatalogHelper $catalogHelper + CatalogHelper $catalogHelper, + RuleFactory $ruleFactory, + TimezoneInterface $localeDate, + NostoProductRepository $nostoProductRepository, + TaxHelper $taxHelper ) { parent::__construct($context); - - $this->_catalogHelper = $catalogHelper; + $this->catalogHelper = $catalogHelper; + $this->priceRuleFactory = $ruleFactory; + $this->localeDate = $localeDate; + $this->nostoProductRepository = $nostoProductRepository; + $this->taxHelper = $taxHelper; } /** * Gets the unit price for a product model including taxes. * * @param Product $product the product model. - * + * @param Store $store * @return float + * @throws LocalizedException + * @throws NostoException */ - public function getProductPriceInclTax($product) + public function getProductDisplayPrice(Product $product, Store $store) { - return $this->_getProductPrice($product, false, true); + return $this->getProductPrice( + $product, + $store, + $this->includeTaxes($store), + false + ); + } + + /** + * Get unit/final price for a product model. + * + * @param Product $product the product model. + * @param Store $store + * @param bool $inclTax if tax is to be included. + * @param bool $finalPrice if final price. + * @return float + * @throws LocalizedException + * @throws NoSuchEntityException + * @throws NostoException + * @suppress PhanTypeMismatchArgument + * @suppress PhanDeprecatedFunction + */ + public function getProductPrice(// @codingStandardsIgnoreLine + Product $product, + Store $store, + bool $inclTax = true, + bool $finalPrice = false + ) { + switch ($product->getTypeId()) { + // Get the bundle product "from" price. + case BundleType::TYPE_CODE: + return $this->getBundleProductPrice($product, $finalPrice, $inclTax, $store); + + // Get the grouped product "minimal" price. + case GroupedType::TYPE_CODE: + return $this->getGroupedProductPrice($product, $finalPrice, $inclTax); + + // We will use the SKU that has the lowest final price + case ConfigurableType::TYPE_CODE: + return $this->getConfigurableProductPrice($product, $finalPrice, $inclTax, $store); + + default: + $date = $this->localeDate->scopeDate(); + $wid = $product->getStore()->getWebsiteId(); + $gid = GroupManagement::NOT_LOGGED_IN_ID; + $pid = $product->getId(); + if ($finalPrice) { + $currentProductPrice = $product->getFinalPrice(); + /** @noinspection UnnecessaryCastingInspection */ + $pricesToCompare = [(float)$currentProductPrice, (float)$product->getPrice()]; + foreach ($product->getTierPrices() as $tierPrice) { + if ((int)$tierPrice->getCustomerGroupId() === $gid) { + $pricesToCompare[] = $tierPrice->getValue(); + break; + } + } + try { + $currentRulePrice = $this->priceRuleFactory->create()->getRulePrice($date, $wid, $gid, $pid); + } catch (Exception $e) { + $currentRulePrice = $product->getFinalPrice(); + } + if (is_numeric($currentRulePrice)) { + $pricesToCompare[] = $currentRulePrice; + } + $price = min($pricesToCompare); + } else { + $price = $product->getPrice(); + } + if ($inclTax) { + $price = $this->addTaxes($product, $store, $price); + } + return $price; + } } /** * Get the final price for a product model including taxes. * * @param Product $product the product model. + * @param Store $store + * @return float + * @throws LocalizedException + * @throws NostoException + */ + public function getProductFinalDisplayPrice(Product $product, Store $store) + { + return $this->getProductPrice( + $product, + $store, + $this->includeTaxes($store), + true + ); + } + + /** + * Tells if taxes should be added to the prices. + * We need this method due to the bugs in Magento's store emulation that + * are not setting the tax display settings correctly for the API calls. + * + * If the store is configured to show prices with and without taxes we will + * use the price without taxes. + * + * @param Store $store + * @return bool + */ + private function includeTaxes(Store $store) + { + return ($this->taxHelper->getPriceDisplayType($store) === TaxConfig::DISPLAY_TYPE_INCLUDING_TAX); + } + + /** + * Adds taxes to the product based on product and store + * + * @param Product $product + * @param Store $store + * @param float $price * * @return float */ - public function getProductFinalPriceInclTax($product) + public function addTaxes(Product $product, Store $store, float $price) { - return $this->_getProductPrice($product, true, true); + return $this->catalogHelper->getTaxPrice( + $product, + $price, + true, + null, + null, + null, + $store + ); } /** - * Get unit/final price for a product model. + * Adds taxes to the price if the store view is configured to display the prices with taxes. + * Otherwise returns the price without taxes. * - * @param Product $product the product model. - * @param bool $finalPrice if final price. - * @param bool $inclTax if tax is to be included. + * @param Product $product + * @param Store $store + * @param float $price * * @return float */ - protected function _getProductPrice($product, $finalPrice = false, $inclTax = true) + public function addTaxDisplayPriceIfApplicable(Product $product, Store $store, float $price) { - switch ($product->getTypeId()) { - // Get the bundle product "from" price. - case ProductType::TYPE_BUNDLE: - /** @var BundlePrice $priceModel */ - $priceModel = $product->getPriceModel(); - // todo: from price discount? - $price = $priceModel->getTotalPrices($product, 'min', $inclTax); - break; - - // No constant for this value was found (Magento ver. 1.0.0-beta). - // Get the grouped product "minimal" price. - case 'grouped': - /* @var $typeInstance GroupedType */ - $typeInstance = $product->getTypeInstance(); - $associatedProducts = $typeInstance - ->setStoreFilter($product->getStore(), $product) - ->getAssociatedProducts($product); - $cheapestAssociatedProduct = null; - $minimalPrice = 0; - foreach ($associatedProducts as $associatedProduct) { - /** @var Product $associatedProduct */ - $tmpPrice = $finalPrice - ? $associatedProduct->getFinalPrice() - : $associatedProduct->getPrice(); - if ($minimalPrice === 0 || $minimalPrice > $tmpPrice) { - $minimalPrice = $tmpPrice; - $cheapestAssociatedProduct = $associatedProduct; - } - } - $price = $minimalPrice; - if ($inclTax && $cheapestAssociatedProduct) { - $price = $this->_catalogHelper->getTaxPrice( - $cheapestAssociatedProduct, - $price, - true - ); - } - break; + if ($this->includeTaxes($store)) { + return $this->catalogHelper->getTaxPrice( + $product, + $price, + true, + null, + null, + null, + $store + ); + } - // No constant for this value was found (Magento ver. 1.0.0-beta). - // The configurable product has the tax already applied in the - // "final" price, but not in the regular price. - case 'configurable': - if ($finalPrice) { - $price = $product->getFinalPrice(); - } elseif ($inclTax) { - $price = $this->_catalogHelper->getTaxPrice( - $product, - $product->getPrice(), - true - ); - } else { - $price = $product->getPrice(); - } - break; + return $price; + } - default: - $price = $finalPrice - ? $product->getFinalPrice() - : $product->getPrice(); - if ($inclTax) { - $price = $this->_catalogHelper->getTaxPrice( - $product, - $price, - true - ); + /** + * Calculates the price for Product of type Bundle + * + * @param Product $product + * @param $finalPrice + * @param $inclTax + * @param Store $store + * @return array|float|int|mixed + * @throws LocalizedException + * @throws NostoException + */ + private function getBundleProductPrice(Product $product, $finalPrice, $inclTax, Store $store) + { + $priceModel = $product->getPriceModel(); + $price = 0.0; + if (!$priceModel instanceof BundlePrice) { + return $price; + } + $productType = $product->getTypeInstance(); + if ($finalPrice) { + $price = $priceModel->getTotalPrices( + $product, + 'min', + $inclTax + ); + } elseif ($productType instanceof BundleType) { + $options = $productType->getOptions($product); + $allOptional = true; + $minPrices = []; + $requiredMinPrices = []; + /** @var Option $option */ + foreach ($options as $option) { + $selectionMinPrice = null; + $optionSelections = $option->getSelections(); + if ($optionSelections === null) { + continue; + } + foreach ($optionSelections as $selection) { + /** @var Product $selection */ + $selectionPrice + = $this->getProductDisplayPrice($selection, $store); + if ($selectionMinPrice === null + || $selectionPrice < $selectionMinPrice + ) { + $selectionMinPrice = $selectionPrice; + } + } + $minPrices[] = $selectionMinPrice; + if ($option->getRequired()) { + $allOptional = false; + $requiredMinPrices[] = $selectionMinPrice; } - break; + } + // If all products are optional use the price for the cheapest option + $price = $allOptional && !empty($minPrices) ? min($minPrices) : array_sum($requiredMinPrices); } - return $price; } /** - * Get the final price in base currency for an ordered item including - * taxes as discounts. + * Calculates the price for Product of type Grouped * - * @param Item $item the item model. + * @param Product $product + * @param $finalPrice + * @param $inclTax + * @return float + */ + private function getGroupedProductPrice(Product $product, $finalPrice, $inclTax) + { + $price = 0.0; + $typeInstance = $product->getTypeInstance(); + if (!$typeInstance instanceof GroupedType) { + return $price; + } + $associatedProducts = $typeInstance + ->setStoreFilter($product->getStore(), $product) + ->getAssociatedProducts($product); + $cheapestAssociatedProduct = null; + $minimalPrice = 0.0; + foreach ($associatedProducts as $associatedProduct) { + /** @var Product $associatedProduct */ + $tmpPrice = $finalPrice + ? $associatedProduct->getFinalPrice() + : $associatedProduct->getPrice(); + if ($minimalPrice === 0.0 || $minimalPrice > $tmpPrice) { + $minimalPrice = $tmpPrice; + $cheapestAssociatedProduct = $associatedProduct; + } + } + $price = $minimalPrice; + if ($inclTax && $cheapestAssociatedProduct !== null) { + $price = $this->catalogHelper->getTaxPrice( + $cheapestAssociatedProduct, + $price, + true + ); + } + return $price; + } + + /** + * Calculates the price for Product of type Configurable * + * @param Product $product + * @param $finalPrice + * @param $inclTax + * @param Store $store * @return float + * @throws LocalizedException + * @throws NoSuchEntityException + * @throws NostoException */ - public function getItemFinalPriceInclTax(Item $item) + private function getConfigurableProductPrice(Product $product, $finalPrice, $inclTax, Store $store) { - return $item->getPriceInclTax() - $item->getBaseDiscountAmount(); + $price = 0.00; + if (!$product->getTypeInstance() instanceof ConfigurableType) { + return $price; + } + $skuPrices = $this->nostoProductRepository->getSkusAsArray($product, $store); + if (count($skuPrices) === 0) { + return $price; + } + $priceColumn = array_column($skuPrices, self::KEY_SKU_FINAL_PRICE); + array_multisort($priceColumn, SORT_ASC, $skuPrices); + $minSku = reset($skuPrices); + if ($finalPrice && isset($minSku[self::KEY_SKU_FINAL_PRICE])) { + $price = $minSku[self::KEY_SKU_FINAL_PRICE]; + } elseif (isset($minSku[self::KEY_SKU_PRICE])) { + $price = $minSku[self::KEY_SKU_PRICE]; + } + if ($inclTax === true) { + if (!isset($minSku[self::KEY_SKU_PRODUCT_ID])) { + // Since print_r is discouraged we use this + $arrayContents = ''; + foreach ($minSku as $key => $val) { + $arrayContents .= sprintf('%s => %s, ', $key, $val); + } + throw new NostoException( + sprintf( + 'No %s key in sku prices array. Array content: %s', + self::KEY_SKU_PRODUCT_ID, + $arrayContents + ) + ); + } + $skuProduct = $this->nostoProductRepository->reloadProduct( + $minSku[self::KEY_SKU_PRODUCT_ID], + $store->getId() + ); + if ($skuProduct instanceof Product) { + $price = $this->getProductPrice($skuProduct, $store, true, $finalPrice); + } + } + return $price; } } diff --git a/Helper/Ratings.php b/Helper/Ratings.php new file mode 100644 index 000000000..6ae7f7754 --- /dev/null +++ b/Helper/Ratings.php @@ -0,0 +1,308 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Helper; + +use Exception; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\Helper\AbstractHelper; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\DataObject; +use Magento\Framework\Module\Manager; +use Magento\Framework\Registry; +use Magento\Review\Model\ReviewFactory; +use Magento\Store\Model\Store; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Product\Ratings as ProductRatings; + +/** + * Rating helper used for product rating related tasks. + */ +class Ratings extends AbstractHelper +{ + public const REVIEW_COUNT = 'reviews_count'; + public const AVERAGE_SCORE = 'average_score'; + public const CURRENT_PRODUCT = 'current_product'; + + private Manager $moduleManager; + private Data $nostoDataHelper; + private NostoLogger $logger; + private ReviewFactory $reviewFactory; + private RatingsFactory $ratingsFactory; + private Registry $registry; + private ?ProductInterface $originalProduct; + + /** + * Ratings constructor. + * @param Context $context + * @param NostoHelperData $nostoHelperData + * @param ReviewFactory $reviewFactory + * @param NostoLogger $logger + * @param RatingsFactory $ratingsFactory + * @param Registry $registry + * + * @suppress PhanUndeclaredTypeParameter + */ + public function __construct( + Context $context, + NostoHelperData $nostoHelperData, + ReviewFactory $reviewFactory, + NostoLogger $logger, + RatingsFactory $ratingsFactory, + Registry $registry + ) { + parent::__construct($context); + $this->moduleManager = $context->getModuleManager(); + $this->nostoDataHelper = $nostoHelperData; + $this->logger = $logger; + $this->reviewFactory = $reviewFactory; + $this->ratingsFactory = $ratingsFactory; + $this->registry = $registry; + } + + /** + * Get ratings + * + * @param Product $product + * @param Store $store + * @return ProductRatings|null + */ + public function getRatings(Product $product, Store $store) + { + $ratings = $this->getRatingsFromProviders($product, $store); + if ($ratings === null) { + return null; + } + + $productRatings = new ProductRatings(); + $productRatings->setReviewCount($ratings[self::REVIEW_COUNT]); + $productRatings->setRating($ratings[self::AVERAGE_SCORE]); + return $productRatings; + } + + /** + * Get Ratings of product from different providers + * + * @param Product $product + * @param Store $store + * @return array|null + * + * @suppress PhanUndeclaredClassMethod + */ + private function getRatingsFromProviders(Product $product, Store $store) + { + if ($this->nostoDataHelper->isRatingTaggingEnabled($store)) { + $provider = $this->nostoDataHelper->getRatingTaggingProvider($store); + + if ($provider === NostoHelperData::SETTING_VALUE_YOTPO_RATINGS) { + if (!$this->canUseYotpo()) { + return null; + } + + try { + $this->setRegistryProduct($product); + + $ratings = $this->ratingsFactory->create()->getRichSnippet(); + } catch (Exception $e) { + $this->resetRegistryProduct(); + $this->logger->exception($e); + return null; + } + + $this->resetRegistryProduct(); + + if (empty($ratings)) { + return null; + } + + return [ + self::AVERAGE_SCORE => $ratings[self::AVERAGE_SCORE], + self::REVIEW_COUNT => $ratings[self::REVIEW_COUNT] + ]; + } + + if ($provider === NostoHelperData::SETTING_VALUE_MAGENTO_RATINGS && + $this->canUseMagentoRatingsAndReviews()) { + return [ + self::AVERAGE_SCORE => $this->buildRatingValue($product, $store), + self::REVIEW_COUNT => $this->buildReviewCount($product, $store) + ]; + } + } + + return null; + } + + /** + * Helper method to fetch and return the normalised rating value for a product. The rating is + * normalised to a 0-5 value. + * + * @param Product $product the product whose rating value to fetch + * @param Store $store the store scope in which to fetch the rating + * @return float the normalized rating value of the product + * @noinspection PhpPossiblePolymorphicInvocationInspection + */ + private function buildRatingValue(Product $product, Store $store) + { + try { + /** @noinspection PhpUndefinedMethodInspection */ + if (!$product->getRatingSummary()) { + /** @phan-suppress-next-line PhanDeprecatedFunction */ + $this->reviewFactory->create()->getEntitySummary($product, $store->getId()); + } + /** @noinspection PhpUndefinedMethodInspection */ + $ratingSummary = $product->getRatingSummary(); + $ratingValue = null; + // As of Magento 2.3.3 rating summary returns directly the sum of ratings rather + // than DataObject + if ($ratingSummary instanceof DataObject) { + if ($ratingSummary->getReviewsCount() > 0 + && $ratingSummary->getRatingSummary() > 0 + ) { + $ratingValue = $ratingSummary->getRatingSummary(); + } + } elseif (is_numeric($ratingSummary)) { + $ratingValue = $ratingSummary; + } + if ($ratingValue !== null) { + return (float)round($ratingValue / 20, 1); + } + } catch (Exception $e) { + $this->logger->exception($e); + } + + return 0; + } + + /** + * Helper method to fetch and return the total review count for a product. The review counts are + * returned as is. + * + * @param Product $product the product whose rating value to fetch + * @param Store $store the store scope in which to fetch the rating + * @return int the normalized rating value of the product + */ + private function buildReviewCount(Product $product, Store $store) + { + try { + /** @noinspection PhpUndefinedMethodInspection */ + if (!$product->getRatingSummary()) { + /** @phan-suppress-next-line PhanDeprecatedFunction */ + $this->reviewFactory->create()->getEntitySummary($product, $store->getId()); + } + /** @noinspection PhpUndefinedMethodInspection */ + $ratingSummary = $product->getRatingSummary(); + /** @noinspection PhpUndefinedMethodInspection */ + $reviewCount = $product->getReviewsCount(); + // As of Magento 2.3.3 rating summary returns directly the amount of + // than DataObject + if ($ratingSummary instanceof DataObject) { + /** @noinspection PhpPossiblePolymorphicInvocationInspection */ + if ($ratingSummary->getReviewsCount() > 0) { + /** @noinspection PhpPossiblePolymorphicInvocationInspection */ + return (int)$ratingSummary->getReviewsCount(); + } + } elseif (is_numeric($reviewCount)) { + return (int)$reviewCount; + } + return 0; + } catch (Exception $e) { + $this->logger->exception($e); + } + return 0; + } + + /** + * Check if Yopto module is enabled and has getRichSnippet method + * + * @return bool + */ + public function canUseYotpo() + { + if ($this->moduleManager->isEnabled('Yotpo_Yotpo') && + // phpcs:ignore + class_exists('Yotpo\Yotpo\Helper\RichSnippets') && + method_exists($this->ratingsFactory->create(), 'getRichSnippet') + ) { + return true; + } + + return false; + } + + /** + * Check if the Review module is enabled, review tables are present + * + * @return bool + */ + public function canUseMagentoRatingsAndReviews() + { + return $this->moduleManager->isEnabled('Magento_Review'); + } + + /** + * Sets product to Magento registry + * + * @param Product $product + */ + private function setRegistryProduct(Product $product) + { + /** @phan-suppress-next-line PhanDeprecatedFunction */ + $this->originalProduct = $this->registry->registry(self::CURRENT_PRODUCT); + if ($this->originalProduct !== null) { + /** @phan-suppress-next-line PhanDeprecatedFunction */ + $this->registry->unregister(self::CURRENT_PRODUCT); + /** @phan-suppress-next-line PhanDeprecatedFunction */ + $this->registry->register(self::CURRENT_PRODUCT, $product); + } else { + /** @phan-suppress-next-line PhanDeprecatedFunction */ + $this->registry->register(self::CURRENT_PRODUCT, $product); + } + } + + /** + * Resets the product to Magento registry + */ + private function resetRegistryProduct() + { + /** @phan-suppress-next-line PhanDeprecatedFunction */ + $this->registry->unregister(self::CURRENT_PRODUCT); + /** @phan-suppress-next-line PhanDeprecatedFunction */ + $this->registry->register(self::CURRENT_PRODUCT, $this->originalProduct); + } +} diff --git a/Helper/RatingsFactory.php b/Helper/RatingsFactory.php new file mode 100644 index 000000000..2b1093a27 --- /dev/null +++ b/Helper/RatingsFactory.php @@ -0,0 +1,67 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Helper; + +use Magento\Framework\ObjectManagerInterface; +use Yotpo\Yotpo\Helper\RichSnippets; + +class RatingsFactory +{ + /** + * @var ObjectManagerInterface + */ + private ObjectManagerInterface $objectManager; + + /** + * @param ObjectManagerInterface $objectManager + */ + public function __construct(ObjectManagerInterface $objectManager) + { + $this->objectManager = $objectManager; + } + + /** + * @return RichSnippets + * @suppress PhanUndeclaredTypeReturnType + */ + public function create() + { + /** @phan-suppress-next-line PhanUndeclaredTypeReturnType */ + return $this->objectManager->create('Yotpo\Yotpo\Helper\RichSnippets'); // @codingStandardsIgnoreLine + } +} diff --git a/Helper/Scope.php b/Helper/Scope.php new file mode 100644 index 000000000..83bca32f8 --- /dev/null +++ b/Helper/Scope.php @@ -0,0 +1,182 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Helper; + +use Magento\Framework\App\Helper\AbstractHelper; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\Phrase; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Website; +use Nosto\Tagging\Model\Service\Store\MissingStoreException; + +class Scope extends AbstractHelper +{ + private StoreManagerInterface $storeManager; + + /** + * Scope constructor. + * @param Context $context + * @param StoreManagerInterface $storeManager + */ + public function __construct( + Context $context, + StoreManagerInterface $storeManager + ) { + parent::__construct($context); + $this->storeManager = $storeManager; + } + + /** + * @param null|string|bool|int|StoreInterface $storeId + * @return Store + */ + public function getStore($storeId = null) + { + try { + /** + * Returning StoreInterface but declared to return Store + */ + /** @phan-suppress-next-next-line PhanTypeMismatchReturnSuperType */ + /** @noinspection PhpIncompatibleReturnTypeInspection */ + return $this->storeManager->getStore($storeId); + } catch (NoSuchEntityException $e) { + throw new MissingStoreException($e); + } + } + + /** + * @param bool $withDefault + * @param bool $codeKey + * @return Store[] + */ + public function getStores($withDefault = false, $codeKey = false) + { + /** + * Returning StoreInterface[] but declared to return Store[] + */ + /** @phan-suppress-next-next-line PhanTypeMismatchReturn */ + /** @noinspection PhpIncompatibleReturnTypeInspection */ + return $this->storeManager->getStores($withDefault, $codeKey); + } + + /** + * Return the store by store code + * + * @param $scopeCode + * @return mixed + */ + public function getStoreByCode($scopeCode) + { + $stores = $this->getStores(); + foreach ($stores as $store) { + if ($store->getCode() === $scopeCode) { + return $store; + } + } + return null; + } + + /** + * @return bool + */ + public function isSingleStoreMode() + { + return $this->storeManager->isSingleStoreMode(); + } + + /** + * Get loaded websites + * + * @param bool $withDefault + * @param bool $codeKey + * @return Website[] + */ + public function getWebsites($withDefault = false, $codeKey = false) + { + /** + * Returning WebsiteInterface[] but declared to return Website[] + */ + /** @phan-suppress-next-next-line PhanTypeMismatchReturn */ + /** @noinspection PhpIncompatibleReturnTypeInspection */ + return $this->storeManager->getWebsites($withDefault, $codeKey); + } + + /** + * Get specified website + * + * @param null|bool|int|string|WebsiteInterface $websiteId + * @return WebsiteInterface|Website + * @throws LocalizedException + */ + public function getWebsite($websiteId) + { + return $this->storeManager->getWebsite($websiteId); + } + + /** + * Returns the currently selected store. + * If it is single store setup, then just return the default store. + * If it is a multi store setup, the expect a store id to passed in the + * request params and return that store as the current one. + * + * @param RequestInterface $request + * @return Store the store or null if not found. + * @throws NotFoundException + */ + public function getSelectedStore(RequestInterface $request) + { + $store = null; + if ($this->isSingleStoreMode()) { + $store = $this->getStore(true); + } elseif ($storeId = $request->getParam('store')) { + $store = $this->getStore($storeId); + } elseif ($this->getStore()) { + $store = $this->getStore(); + } else { + throw new NotFoundException(new Phrase('Store not found.')); + } + + return $store; + } +} diff --git a/Helper/Url.php b/Helper/Url.php index 31cb96687..49de19cf4 100755 --- a/Helper/Url.php +++ b/Helper/Url.php @@ -1,93 +1,172 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Helper; -use Magento\Catalog\Model\Product\Visibility; -use /** @noinspection PhpUndefinedClassInspection */ - Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; -use /** @noinspection PhpUndefinedClassInspection */ - Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Exception; +use Magento\Backend\Helper\Data as BackendDataHelper; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Url as UrlBuilder; +use Magento\Framework\UrlInterface; use Magento\Store\Model\Store; +use Nosto\Helper\UrlHelper; +use Nosto\Request\Http\HttpRequest; +use Nosto\Tagging\Helper\Data as NostoDataHelper; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Product\Repository as ProductRepository; +use Nosto\Tagging\Model\Product\Url\Builder as NostoUrlBuilder; +use Zend_Uri_Exception; +use Zend_Uri_Http; /** * Url helper class for common URL related tasks. */ class Url extends AbstractHelper { - /** @noinspection PhpUndefinedClassInspection */ + public const URL_PATH_NOSTO_CONFIG = 'adminhtml/system_config/edit/section/nosto/'; + public const MAGENTO_URL_OPTION_STORE_ID = 'store'; + + public const MAGENTO_PATH_SEARCH_RESULT = 'catalogsearch/result'; + /** + * Path to Magento's cart controller + */ + public const MAGENTO_PATH_CART = 'checkout/cart'; + + /** + * The ___store parameter in Magento URLs + */ + public const MAGENTO_URL_PARAMETER_STORE = '___store'; + + /** + * The array option key for scope in Magento's URLs + */ + public const MAGENTO_URL_OPTION_SCOPE = '_scope'; + /** - * @var ProductCollectionFactory auto generated product collection factory. + * The array option key for store to url in Magento's URLs */ - protected $_productCollectionFactory; + public const MAGENTO_URL_OPTION_SCOPE_TO_URL = '_scope_to_url'; - /** @noinspection PhpUndefinedClassInspection */ /** - * @var CategoryCollectionFactory auto generated category collection factory. + * The array option key for URL type in Magento's URLs */ - protected $_categoryCollectionFactory; + public const MAGENTO_URL_OPTION_LINK_TYPE = '_type'; /** - * @var Visibility product visibility. + * Path to Nosto's restore cart controller */ - protected $_productVisibility; + public const NOSTO_PATH_RESTORE_CART = 'nosto/frontend/cart'; /** - * @var \Magento\Framework\Url frontend URL builder. + * The array option key for no session id in Magento's URLs. + * The session id should be included into the URLs which are potentially + * used during the same session, e.g. Oauth redirect URL. For example for + * product URLs we cannot include the session id as the product URL should + * be the same for all visitors and it will be saved to Nosto. */ - protected $_urlBuilder; + public const MAGENTO_URL_OPTION_NOSID = '_nosid'; + + /** + * The url type to be used for links. + * + * This is the only URL type that works correctly the URls when + * "Add Store Code to Urls" setting is set to "Yes" + * + * UrlInterface::URL_TYPE_WEB + * - returns an URL without rewrites and without store codes + * + * UrlInterface::URL_TYPE_LINK + * - returns an URL with rewrites and with store codes in URL (if + * setting "Add Store Code to Urls" set to yes) + * + * UrlInterface::URL_TYPE_DIRECT_LINK + * - returns an URL with rewrites but without store codes + * + * @see UrlInterface::URL_TYPE_LINK + * + * @var string + */ + public static string $urlType = UrlInterface::URL_TYPE_LINK; + + private CategoryCollectionFactory $categoryCollectionFactory; + private UrlBuilder $urlBuilder; + private Data $nostoDataHelper; + private BackendDataHelper $backendDataHelper; + private ProductRepository $productRepository; + private NostoUrlBuilder $nostoUrlBuilder; + private NostoLogger $logger; - /** @noinspection PhpUndefinedClassInspection */ /** * Constructor. * * @param Context $context the context. - * @param ProductCollectionFactory $productCollectionFactory auto generated product collection factory. + * @param ProductRepository $productRepository * @param CategoryCollectionFactory $categoryCollectionFactory auto generated category collection factory. - * @param Visibility $productVisibility product visibility. - * @param \Magento\Framework\Url $urlBuilder frontend URL builder. + * @param Data $nostoDataHelper + * @param UrlBuilder $urlBuilder frontend URL builder. + * @param BackendDataHelper $backendDataHelper + * @param NostoUrlBuilder $nostoUrlBuilder + * @param NostoLogger $nostoLogger */ public function __construct( Context $context, - /** @noinspection PhpUndefinedClassInspection */ - ProductCollectionFactory $productCollectionFactory, - /** @noinspection PhpUndefinedClassInspection */ + ProductRepository $productRepository, CategoryCollectionFactory $categoryCollectionFactory, - Visibility $productVisibility, - \Magento\Framework\Url $urlBuilder + NostoDataHelper $nostoDataHelper, + UrlBuilder $urlBuilder, + /** @noinspection PhpDeprecationInspection */ + BackendDataHelper $backendDataHelper, + NostoUrlBuilder $nostoUrlBuilder, + NostoLogger $nostoLogger ) { parent::__construct($context); - $this->_productCollectionFactory = $productCollectionFactory; - $this->_categoryCollectionFactory = $categoryCollectionFactory; - $this->_productVisibility = $productVisibility; - $this->_urlBuilder = $urlBuilder; + $this->productRepository = $productRepository; + $this->categoryCollectionFactory = $categoryCollectionFactory; + $this->urlBuilder = $urlBuilder; + $this->nostoDataHelper = $nostoDataHelper; + $this->backendDataHelper = $backendDataHelper; + $this->nostoUrlBuilder = $nostoUrlBuilder; + $this->logger = $nostoLogger; } /** @@ -96,112 +175,137 @@ public function __construct( * The preview url includes "nostodebug=true" parameter. * * @param Store $store the store to get the url for. - * - * @return string the url. + * @return string|null the url. + * @suppress PhanTypeMismatchReturn */ public function getPreviewUrlProduct(Store $store) { - /** @noinspection PhpUndefinedNamespaceInspection */ - /** @var \Magento\Catalog\Model\Resource\Product\Collection $collection */ - $collection = $this->_productCollectionFactory->create(); - $collection->addStoreFilter($store->getId()); - $collection->setVisibility($this->_productVisibility->getVisibleInSiteIds()); - $collection->addAttributeToFilter('status', ['eq' => '1']); - $collection->setCurPage(1); - $collection->setPageSize(1); - $collection->load(); - - $url = ''; - foreach ($collection->getItems() as $product) { - /** @var \Magento\Catalog\Model\Product $product */ - $url = $product->getUrlInStore( - [ - '_nosid' => true, - '_scope_to_url' => true, - '_scope' => $store->getCode(), - ] - ); + $product = $this->productRepository->getRandomSingleActiveProduct(); + $url = null; + if ($product instanceof Product) { + $url = $this->nostoUrlBuilder->getUrlInStore($product, $store); $url = $this->addNostoDebugParamToUrl($url); } return $url; } + /** + * Adds the `nostodebug` parameter to a url. + * + * @param string $url the url. + * @return string the updated url. + */ + public function addNostoDebugParamToUrl(string $url) + { + return HttpRequest::replaceQueryParamInUrl( + 'nostodebug', + 'true', + $url + ); + } + /** * Gets the absolute preview URL to a given store's category page. * The category is the first one found in the database for the store. * The preview url includes "nostodebug=true" parameter. * * @param Store $store the store to get the url for. - * * @return string the url. + * + * @throws LocalizedException */ public function getPreviewUrlCategory(Store $store) { $rootCatId = (int)$store->getRootCategoryId(); - /** @noinspection PhpUndefinedNamespaceInspection */ - /** @noinspection PhpUndefinedClassInspection */ - /** @var \Magento\Catalog\Model\Resource\Category\Collection $collection */ - $collection = $this->_categoryCollectionFactory->create(); + $collection = $this->categoryCollectionFactory->create(); $collection->addAttributeToFilter('is_active', ['eq' => 1]); $collection->addAttributeToFilter('path', ['like' => "1/$rootCatId/%"]); $collection->setCurPage(1); $collection->setPageSize(1); $collection->load(); - foreach ($collection->getItems() as $category) { - /** @var \Magento\Catalog\Model\Category $category */ + /** @var Category $category */ $url = $category->getUrl(); - $url = $this->replaceQueryParamsInUrl( - array('___store' => $store->getCode()), - $url - ); + if ($this->nostoDataHelper->getStoreCodeToUrl($store)) { + $url = $this->replaceQueryParamsInUrl( + ['___store' => $store->getCode()], + $url + ); + } + return $this->addNostoDebugParamToUrl($url); } return ''; } + /** + * Replaces or adds a query parameters to a url. + * + * @param array $params the query params to replace. + * @param string $url the url. + * @return string the updated url. + */ + public function replaceQueryParamsInUrl(array $params, string $url) + { + return HttpRequest::replaceQueryParamsInUrl($params, $url); + } + /** * Gets the absolute preview URL to the given store's search page. * The search query in the URL is "q=nosto". * The preview url includes "nostodebug=true" parameter. * * @param Store $store the store to get the url for. - * * @return string the url. */ public function getPreviewUrlSearch(Store $store) { - $url = $this->_urlBuilder->getUrl( - 'catalogsearch/result', - array( - '_nosid' => true, - '_scope_to_url' => true, - '_scope' => $store->getCode(), - ) + $url = $this->urlBuilder->getUrl( + self::MAGENTO_PATH_SEARCH_RESULT, + [ + self::MAGENTO_URL_OPTION_NOSID => true, + self::MAGENTO_URL_OPTION_SCOPE_TO_URL => $this->nostoDataHelper->getStoreCodeToUrl($store), + self::MAGENTO_URL_OPTION_SCOPE => $store->getCode(), + ] ); - $url = $this->replaceQueryParamsInUrl(array('q' => 'nosto'), $url); + $url = $this->replaceQueryParamsInUrl(['q' => 'nosto'], $url); return $this->addNostoDebugParamToUrl($url); } + /** + * Returns the store domain + * + * @param Store $store + * @return string + */ + public function getActiveDomain(Store $store) + { + try { + return UrlHelper::parseDomain($store->getBaseUrl()); + } catch (Exception $e) { + $this->logger->exception($e); + return ''; + } + } + /** * Gets the absolute preview URL to the given store's cart page. * The preview url includes "nostodebug=true" parameter. * * @param Store $store the store to get the url for. - * * @return string the url. */ public function getPreviewUrlCart(Store $store) { - $url = $this->_urlBuilder->getUrl( - 'checkout/cart', - array( - '_nosid' => true, - '_scope_to_url' => true, - '_scope' => $store->getCode(), - ) + $url = $this->urlBuilder->getUrl( + self::MAGENTO_PATH_CART, + [ + self::MAGENTO_URL_OPTION_NOSID => true, + self::MAGENTO_URL_OPTION_SCOPE_TO_URL => $this->nostoDataHelper->getStoreCodeToUrl($store), + self::MAGENTO_URL_OPTION_SCOPE => $store->getCode(), + ] ); return $this->addNostoDebugParamToUrl($url); } @@ -211,46 +315,80 @@ public function getPreviewUrlCart(Store $store) * The preview url includes "nostodebug=true" parameter. * * @param Store $store the store to get the url for. - * * @return string the url. */ public function getPreviewUrlFront(Store $store) { - $url = $this->_urlBuilder->getUrl( + $url = $this->urlBuilder->getUrl( '', - array( - '_nosid' => true, - '_scope_to_url' => true, - '_scope' => $store->getCode(), - ) + [ + self::MAGENTO_URL_OPTION_NOSID => true, + self::MAGENTO_URL_OPTION_SCOPE_TO_URL => $this->nostoDataHelper->getStoreCodeToUrl($store), + self::MAGENTO_URL_OPTION_SCOPE => $store->getCode(), + ] ); return $this->addNostoDebugParamToUrl($url); } /** - * Replaces or adds a query parameters to a url. + * Gets the absolute URL to the current store view cart page. * - * @param array $params the query params to replace. - * @param string $url the url. - * @return string the updated url. + * @param Store $store the store to get the url for. + * @param string $currentUrl restore cart url + * @return string cart url. + * @throws Zend_Uri_Exception + * @throws NoSuchEntityException */ - public function replaceQueryParamsInUrl(array $params, $url) + public function getUrlCart(Store $store, string $currentUrl) { - return \NostoHttpRequest::replaceQueryParamsInUrl($params, $url); + $zendHttp = Zend_Uri_Http::fromString($currentUrl); + $urlParameters = $zendHttp->getQueryAsArray(); + + $defaultParams = $this->getUrlOptionsWithNoSid($store); + $url = $store->getUrl( + self::MAGENTO_PATH_CART, + $defaultParams + ); + + if (!empty($urlParameters)) { + foreach ($urlParameters as $key => $val) { + $url = HttpRequest::replaceQueryParamInUrl( + $key, + $val, + $url + ); + } + } + + return $url; } /** - * Adds the `nostodebug` parameter to a url. + * Returns the default options for fetching Magento urls with no session id * - * @param string $url the url. - * @return string the updated url. + * @param Store $store + * @return array */ - public function addNostoDebugParamToUrl($url) + public function getUrlOptionsWithNoSid(Store $store) { - return \NostoHttpRequest::replaceQueryParamInUrl( - 'nostodebug', - 'true', - $url - ); + return [ + self::MAGENTO_URL_OPTION_SCOPE_TO_URL => $this->nostoDataHelper->getStoreCodeToUrl($store), + self::MAGENTO_URL_OPTION_NOSID => true, + self::MAGENTO_URL_OPTION_LINK_TYPE => self::$urlType, + self::MAGENTO_URL_OPTION_SCOPE => $store->getCode(), + ]; + } + + /** + * Gets the absolute URL to the Nosto configuration page + * + * @param Store $store the store to get the url for. + * + * @return string the url. + */ + public function getAdminNostoConfigurationUrl(Store $store) + { + $params = [self::MAGENTO_URL_OPTION_STORE_ID => $store->getStoreId()]; + return $this->backendDataHelper->getUrl(self::URL_PATH_NOSTO_CONFIG, $params); } } diff --git a/Helper/Variation.php b/Helper/Variation.php new file mode 100644 index 000000000..1a3a070fa --- /dev/null +++ b/Helper/Variation.php @@ -0,0 +1,132 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Helper; + +use Exception; +use Magento\Customer\Api\Data\GroupInterface; +use Magento\Customer\Api\GroupRepositoryInterface as GroupRepository; +use Magento\Customer\Model\Data\Group; +use Magento\Customer\Model\GroupManagement; +use Magento\Framework\App\Helper\AbstractHelper; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Nosto\Tagging\Logger\Logger; + +/** + * Variation helper + */ +class Variation extends AbstractHelper +{ + public const DEFAULT_CUSTOMER_GROUP_ID = GroupManagement::NOT_LOGGED_IN_ID; + + /** @var GroupRepository */ + private GroupRepository $groupRepository; + + /** @var Logger */ + private Logger $logger; + + /** + * Variation constructor. + * @param Context $context + * @param GroupRepository $groupRepository + * @param Logger $logger + */ + public function __construct( + Context $context, + GroupRepository $groupRepository, + Logger $logger + ) { + parent::__construct($context); + $this->groupRepository = $groupRepository; + $this->logger = $logger; + } + + /** + * @return GroupInterface|null + * @throws LocalizedException + * @throws NoSuchEntityException + */ + private function getDefaultGroupVariation() + { + $defaultGroup = $this->groupRepository->getById(self::DEFAULT_CUSTOMER_GROUP_ID); + if ($defaultGroup instanceof Group) { + return $defaultGroup; + } + return null; + } + + /** + * @return int|null + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function getDefaultVariationId() + { + return $this->getDefaultGroupVariation() ? $this->getDefaultGroupVariation()->getId() : null; + } + + /** + * @return string|null + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function getDefaultVariationCode() + { + return $this->getDefaultGroupVariation() ? $this->getDefaultGroupVariation()->getCode() : null; + } + + /** + * Checks if the code is the default variation + * + * @param string $code + * @return bool + */ + public function isDefaultVariationCode(string $code) + { + try { + if ($code === $this->getDefaultVariationCode()) { + return true; + } + } catch (Exception $e) { + $this->logger->exception($e); + return false; + } + + return false; + } +} diff --git a/LICENSE_AFL.txt b/LICENSE_AFL.txt deleted file mode 100644 index 7232b53a6..000000000 --- a/LICENSE_AFL.txt +++ /dev/null @@ -1,48 +0,0 @@ - -Academic Free License ("AFL") v. 3.0 - -This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: - -Licensed under the Academic Free License version 3.0 - - 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: - - 1. to reproduce the Original Work in copies, either alone or as part of a collective work; - - 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; - - 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; - - 4. to perform the Original Work publicly; and - - 5. to display the Original Work publicly. - - 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. - - 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. - - 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. - - 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). - - 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. - - 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. - - 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. - - 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). - - 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. - - 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. - - 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. - - 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. - - 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. - - 16. Modification of This License. This License is Copyright � 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/Logger/Logger.php b/Logger/Logger.php new file mode 100644 index 000000000..984936084 --- /dev/null +++ b/Logger/Logger.php @@ -0,0 +1,127 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Logger; + +use Magento\Store\Model\Store; +use Monolog\Logger as MonologLogger; +use Nosto\Tagging\Helper\NewRelic; +use Nosto\Util\Memory; +use Throwable; + +class Logger extends MonologLogger +{ + /** + * Logs an exception and sends it to New relic if available + * @param Throwable $exception + * @return bool + */ + public function exception(Throwable $exception) + { + $newRelic = new NewRelic(); + $newRelic->reportException($exception); + return $this->error($exception->__toString()); + } + + /** + * Logs a message along with the memory consumption + * + * @param $message + * @param Store|null $store + * @param null $sourceClass + * @return bool + */ + public function logWithMemoryConsumption($message, Store $store = null, $sourceClass = null) + { + $msg = sprintf( + '%s [mem usage: %sM / %s] [realmem: %sM]', + $message, + Memory::getConsumption(), + Memory::getTotalMemoryLimit(), + Memory::getRealConsumption() + ); + $context = []; + if ($store) { + $context['storeId'] = $store->getId(); + } + if (is_object($sourceClass)) { + return $this->debugWithSource($message, $context, $sourceClass); + } + return $this->debug($msg, $context); + } + + /** + * Logs a debug level message with given source class info + * + * @param $message + * @param array $context + * @param object $sourceClass + * @return bool + */ + public function debugWithSource($message, array $context, object $sourceClass) + { + return $this->logWithSource($message, $context, $sourceClass, 'debug'); + } + + /** + * Logs an info level message with given source class info + * + * @param $message + * @param array $context + * @param object $sourceClass + * @return bool + */ + public function infoWithSource($message, array $context, object $sourceClass) + { + return $this->logWithSource($message, $context, $sourceClass, 'info'); + } + + /** + * @param $message + * @param $context + * @param $sourceClass + * @param $level + * @return bool + */ + private function logWithSource($message, $context, $sourceClass, $level) + { + $context['sourceClass'] = get_class($sourceClass); + if ($level === 'info') { + return $this->info($message, $context); + } + return $this->debug($message, $context); + } +} diff --git a/Magento2.iml b/Magento2.iml new file mode 100644 index 000000000..58bd1c9aa --- /dev/null +++ b/Magento2.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Model/Cache/Type/ProductData.php b/Model/Cache/Type/ProductData.php new file mode 100644 index 000000000..660df6d9f --- /dev/null +++ b/Model/Cache/Type/ProductData.php @@ -0,0 +1,64 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Cache\Type; + +use Magento\Framework\App\Cache\Type\FrontendPool; +use Magento\Framework\Cache\Frontend\Decorator\TagScope; + +class ProductData extends TagScope implements ProductDataInterface +{ + /** + * Cache type code unique among all cache types + */ + public const TYPE_IDENTIFIER = 'nosto_product_cache'; + + /** + * The tag name that limits the cache cleaning scope within a particular tag + */ + public const CACHE_TAG = 'NOSTO_PRODUCT'; + + /** + * @param FrontendPool $cacheFrontendPool + */ + public function __construct(FrontendPool $cacheFrontendPool) + { + parent::__construct( + $cacheFrontendPool->get(self::TYPE_IDENTIFIER), + self::CACHE_TAG + ); + } +} diff --git a/Model/Cache/Type/ProductDataInterface.php b/Model/Cache/Type/ProductDataInterface.php new file mode 100644 index 000000000..7986a94a1 --- /dev/null +++ b/Model/Cache/Type/ProductDataInterface.php @@ -0,0 +1,44 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Cache\Type; + +use Magento\Framework\Cache\FrontendInterface; + +interface ProductDataInterface extends FrontendInterface +{ + public function getTag(); +} diff --git a/Model/Cart/Builder.php b/Model/Cart/Builder.php index 34b9d1707..4ce9861b4 100644 --- a/Model/Cart/Builder.php +++ b/Model/Cart/Builder.php @@ -1,163 +1,98 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Model\Cart; -use Magento\Quote\Model\Quote\Item; +use Exception; +use Magento\Catalog\Model\Product; +use Magento\Framework\Event\ManagerInterface; +use Magento\Quote\Model\Quote; use Magento\Store\Model\Store; -use Nosto\Tagging\Helper\Data as DataHelper; -use Nosto\Tagging\Helper\Price as PriceHelper; -use Nosto\Tagging\Model\Cart\Item\Factory as CartItemFactory; -use Psr\Log\LoggerInterface; -use Nosto\Tagging\Helper\Item as NostoItemHelper; +use Nosto\Model\Cart\Cart; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Cart\Item\Builder as NostoCartItemBuilder; class Builder { - /** - * @var Factory - */ - protected $_cartFactory; - - /** - * @var CartItemFactory - */ - protected $_cartItemFactory; - - /** - * @var DataHelper - */ - protected $_dataHelper; - - /** - * @var PriceHelper - */ - protected $_priceHelper; - - /** - * @var NostoItemHelper - */ - protected $_nostoItemHelper; - - /** - * @var LoggerInterface - */ - protected $_logger; + private NostoCartItemBuilder $nostoCartItemBuilder; + private NostoLogger $logger; + private ManagerInterface $eventManager; /** * Constructor. * - * @param Factory $cartFactory - * @param CartItemFactory $cartItemFactory - * @param DataHelper $dataHelper - * @param PriceHelper $priceHelper - * @param NostoItemHelper $nostoItemHelper - * @param LoggerInterface $logger + * @param NostoCartItemBuilder $nostoCartItemBuilder + * @param NostoLogger $logger + * @param ManagerInterface $eventManager */ public function __construct( - Factory $cartFactory, - CartItemFactory $cartItemFactory, - DataHelper $dataHelper, - PriceHelper $priceHelper, - NostoItemHelper $nostoItemHelper, - LoggerInterface $logger + NostoCartItemBuilder $nostoCartItemBuilder, + NostoLogger $logger, + ManagerInterface $eventManager ) { - $this->_cartFactory = $cartFactory; - $this->_cartItemFactory = $cartItemFactory; - $this->_dataHelper = $dataHelper; - $this->_priceHelper = $priceHelper; - $this->_logger = $logger; - $this->_nostoItemHelper = $nostoItemHelper; + $this->nostoCartItemBuilder = $nostoCartItemBuilder; + $this->logger = $logger; + $this->eventManager = $eventManager; } /** - * @param array $items + * @param Quote $quote * @param Store $store - * @return \NostoCart + * @return Cart */ - public function build(array $items, Store $store) + public function build(Quote $quote, Store $store) { - $nostoCart = $this->_cartFactory->create(); - - try { - $nostoCart->setItems($this->buildItems($items, $store)); - } catch (\NostoException $e) { - $this->_logger->error($e, ['exception' => $e]); - } + $nostoCart = new Cart(); - return $nostoCart; - } - - /** - * @param Item[] $items - * @param Store $store - * @return \NostoCartItemInterface[] - */ - protected function buildItems(array $items, Store $store) - { - $cartItems = array(); - - foreach ($items as $item) { + foreach ($quote->getAllVisibleItems() as $item) { try { - $cartItem = $this->_cartItemFactory->create(); - $cartItem->setItemId($this->buildItemId($item)); - $cartItem->setQuantity((int)$item->getQty()); - $cartItem->setName($this->buildItemName($item)); - $cartItem->setUnitPrice( - new \NostoPrice($item->getBasePriceInclTax()) - ); - $cartItem->setCurrency( - new \NostoCurrencyCode($store->getBaseCurrencyCode()) - ); - $cartItems[] = $cartItem; - } catch (\NostoException $e) { - + if ($item->getProduct() instanceof Product) { + $cartItem = $this->nostoCartItemBuilder->build( + $item, + $store->getCurrentCurrencyCode() ?: $store->getDefaultCurrencyCode() + ); + $nostoCart->addItem($cartItem); + } + } catch (Exception $e) { + $this->logger->exception($e); } } - return $cartItems; - } - - /** - * @param Item $item - * @return string - */ - protected function buildItemId(Item $item) - { - - return $this->_nostoItemHelper->buildProductId($item); - } + $this->eventManager->dispatch('nosto_cart_load_after', ['cart' => $nostoCart, 'magentoQuote' => $quote]); - /** - * @param Item $item - * @return string - */ - protected function buildItemName(Item $item) - { - // todo: the name must include the variant properties just like in Magento 1 - return $item->getName(); + return $nostoCart; } } diff --git a/Model/Cart/Factory.php b/Model/Cart/Factory.php deleted file mode 100644 index d972ed653..000000000 --- a/Model/Cart/Factory.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) - */ - -namespace Nosto\Tagging\Model\Cart; - -use Magento\Framework\ObjectManagerInterface; - -class Factory -{ - /** - * @var ObjectManagerInterface - */ - protected $_objectManager; - - /** - * @param ObjectManagerInterface $objectManager - */ - public function __construct(ObjectManagerInterface $objectManager) - { - $this->_objectManager = $objectManager; - } - - /** - * Create new cart object. - * - * @param array $data - * @return \NostoCart - */ - public function create(array $data = []) - { - return $this->_objectManager->create('NostoCart', $data); - } -} diff --git a/Model/Cart/Item/Builder.php b/Model/Cart/Item/Builder.php new file mode 100644 index 000000000..f480aaf30 --- /dev/null +++ b/Model/Cart/Item/Builder.php @@ -0,0 +1,197 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Cart\Item; + +use Exception; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\Event\ManagerInterface; +use Magento\Quote\Model\Quote\Item; +use Nosto\Model\Cart\LineItem; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Item\Downloadable; +use Nosto\Tagging\Model\Item\Giftcard; +use Nosto\Tagging\Model\Item\Virtual; +use Throwable; +use Nosto\Tagging\Model\Item\Grouped as GroupedItem; +use Nosto\Tagging\Model\Cart\Item\Grouped as CartGroupedItem; + +class Builder +{ + /** + * @var ManagerInterface $eventManager + */ + private ManagerInterface $eventManager; + + /** + * @var ProductRepository $productRepository + */ + private ProductRepository $productRepository; + + /** + * @var NostoLogger $logger + */ + private NostoLogger $logger; + + /** + * Builder constructor. + * + * @param ManagerInterface $eventManager + * @param ProductRepository $productRepository + * @param NostoLogger $logger + */ + public function __construct( + ManagerInterface $eventManager, + ProductRepository $productRepository, + NostoLogger $logger + ) { + $this->eventManager = $eventManager; + $this->productRepository = $productRepository; + $this->logger = $logger; + } + + /** + * @param Item $item + * @param $currencyCode + * @return LineItem + */ + public function build(Item $item, $currencyCode) + { + $cartItem = new LineItem(); + $cartItem->setPriceCurrencyCode($currencyCode); + $cartItem->setProductId($this->buildItemId($item)); + $cartItem->setQuantity((int)$item->getQty()); + $cartItem->setSkuId($this->buildSkuId($item)); + $productType = $item->getProductType(); + // Set default name - this will be overwritten below if matching + // product type is defined + $cartItem->setName(sprintf( + 'Not defined - unknown product type: %s', + $productType + )); + switch ($productType) { + case Simple::TYPE: + case Virtual::TYPE: + case Downloadable::TYPE: + case Giftcard::TYPE: + $simple = new Simple(); + $cartItem->setName($simple->buildItemName($item)); + break; + case Configurable::TYPE: + $configurable = new Configurable(); + $cartItem->setName($configurable->buildItemName($item)); + break; + case Bundle::TYPE: + $bundle = new Bundle(); + $cartItem->setName($bundle->buildItemName($item)); + break; + case GroupedItem::TYPE: + $cartItem->setName((new CartGroupedItem($this->productRepository))->buildItemName($item)); + break; + } + try { + $cartItem->setPrice($item->getPriceInclTax()); + } catch (Exception $e) { + $cartItem->setPrice(0); + } + + $this->eventManager->dispatch('nosto_cart_item_load_after', ['item' => $cartItem, 'magentoItem' => $item]); + + return $cartItem; + } + + /** + * @param Item $item + * @return string + */ + public function buildItemId(Item $item) + { + /** @var Item $parentItem */ + $parentItem = $item->getOptionByCode('product_type'); + if ($parentItem !== null) { + return (string) $parentItem->getProduct()->getId(); + } + if ($item->getProductType() === Type::TYPE_SIMPLE) { + try { + $type = $item->getProduct()->getTypeInstance(); + $parentIds = $type->getParentIdsByChild($item->getItemId()); + $attributes = $item->getBuyRequest()->getData('super_attribute'); + // If the product has a configurable parent, we assume we should tag + // the parent. If there are many parent IDs, we are safer to tag the + // products own ID. + if (!empty($attributes) && count($parentIds) === 1) { + return $parentIds[0]; + } + } catch (Throwable $e) { + $this->logger->exception($e); + } + } + $product = $item->getProduct(); + if ($product instanceof Product) { + return (string)$product->getId(); + } + return LineItem::PSEUDO_PRODUCT_ID; + } + + /** + * Returns the sku id. If it is a configurable product, + * try to get the child item because the child item is the simple product + * + * @param Item $item the sales item model. + * @return string|null sku id + */ + public function buildSkuId(Item $item) + { + if ($item->getProductType() === Configurable::TYPE) { + $children = $item->getChildren(); + //An item with bundle product and group product may have more than 1 child. + //But configurable product item should have max 1 child item. + //Here we check the size of children, return only if the size is 1 + if (array_key_exists(0, $children) + && count($children) === 1 + && $children[0] instanceof Item + && $children[0]->getProduct() instanceof Product + ) { + return (string)$children[0]->getProduct()->getId(); + } + } + + return null; + } +} diff --git a/Model/Cart/Item/Bundle.php b/Model/Cart/Item/Bundle.php new file mode 100644 index 000000000..e51f968a5 --- /dev/null +++ b/Model/Cart/Item/Bundle.php @@ -0,0 +1,78 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Cart\Item; + +use Magento\Quote\Model\Quote\Item; +use Nosto\Tagging\Model\Item\Bundle as BundledItem; + +class Bundle extends BundledItem +{ + /** + * Returns the name of the product. Bundle products will have their chosen child product names + * added. + * + * @param Item $item the ordered item + * @return string the name of the product + */ + public function buildItemName(Item $item): string + { + $name = $item->getName() ?: ''; + $optNames = []; + $type = $item->getProduct()->getTypeInstance(); + $opts = $type->getOrderOptions($item->getProduct()); + if (is_array($opts)) { + foreach ($opts as $opt) { + if (isset($opt['value']) && is_array($opt['value'])) { + foreach ($opt['value'] as $val) { + $qty = ''; + if (isset($val['qty']) && is_int($val['qty'])) { + $qty .= $val['qty'] . ' x '; + } + if (isset($val['title']) && is_string($val['title'])) { + $optNames[] = $qty . $val['title']; + } + } + } + } + } + + if (!empty($optNames)) { + $name .= ' (' . implode(', ', $optNames) . ')'; + } + return $name; + } +} diff --git a/Model/Cart/Item/Configurable.php b/Model/Cart/Item/Configurable.php new file mode 100644 index 000000000..45ede723d --- /dev/null +++ b/Model/Cart/Item/Configurable.php @@ -0,0 +1,71 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Cart\Item; + +use Magento\Quote\Model\Quote\Item; +use Nosto\Tagging\Model\Item\Configurable as ConfigurableItem; + +class Configurable extends ConfigurableItem +{ + /** + * Returns the name of the product. Configurable products will have their chosen options + * added to their name. + * + * @param Item $item the ordered item + * @return string the name of the product + */ + public function buildItemName(Item $item): string + { + $name = $item->getName() ?: ''; + $optNames = []; + $opts = $item->getOptionByCode('attributes_info'); + if (is_array($opts)) { + foreach ($opts as $opt) { + if (isset($opt['value']) && is_string($opt['value'])) { + $optNames[] = $opt['value']; + } + } + } + + if (!empty($optNames)) { + $name .= ' (' . implode(', ', $optNames) . ')'; + } + + return $name; + } +} diff --git a/Model/Cart/Item/Factory.php b/Model/Cart/Item/Factory.php deleted file mode 100644 index e68da4289..000000000 --- a/Model/Cart/Item/Factory.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) - */ - -namespace Nosto\Tagging\Model\Cart\Item; - -use Magento\Framework\ObjectManagerInterface; - -class Factory -{ - /** - * @var ObjectManagerInterface - */ - protected $_objectManager; - - /** - * @param ObjectManagerInterface $objectManager - */ - public function __construct(ObjectManagerInterface $objectManager) - { - $this->_objectManager = $objectManager; - } - - /** - * Create new cart item object. - * - * @param array $data - * @return \NostoCartItem - */ - public function create(array $data = []) - { - return $this->_objectManager->create('NostoCartItem', $data); - } -} diff --git a/Model/Cart/Item/Grouped.php b/Model/Cart/Item/Grouped.php new file mode 100644 index 000000000..5eb4b7831 --- /dev/null +++ b/Model/Cart/Item/Grouped.php @@ -0,0 +1,102 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Cart\Item; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Model\Quote\Item; +use Nosto\Tagging\Model\Item\Grouped as GroupedItem; +use Throwable; + +class Grouped extends GroupedItem +{ + /** + * @var ProductRepository + */ + private ProductRepository $productRepository; + + /** + * Grouped constructor. + * @param ProductRepository $productRepository + */ + public function __construct(ProductRepository $productRepository) + { + $this->productRepository = $productRepository; + } + + /** + * Returns the name of the product. Grouped products will have their parent's name prepended to + * their name. + * + * @param Item $item the ordered item + * @return string|null the name of the product + */ + public function buildItemName(Item $item) + { + $name = $item->getName(); + try { + $config = $item->getBuyRequest()->getData('super_product_config'); + $itemParent = $this->getGroupedItemParent($config['product_id']); + if ($itemParent instanceof Product) { + $itemParentName = $itemParent->getName(); + if ($itemParentName !== null) { + return $itemParentName . ' - ' . $name; + } + } + } catch (Throwable $e) { + // If the item name building fails, it's not crucial + // No need to handle the exception in any specific way + unset($e); + } + + return $name; + } + + /** + * Query the product id and returns the Product Object + * + * @param $productId + * @return ProductInterface|mixed + * @throws NoSuchEntityException + */ + private function getGroupedItemParent($productId) + { + return $this->productRepository->getById($productId); + } +} diff --git a/Model/Cart/Item/Simple.php b/Model/Cart/Item/Simple.php new file mode 100644 index 000000000..a07ac3e36 --- /dev/null +++ b/Model/Cart/Item/Simple.php @@ -0,0 +1,56 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Cart\Item; + +use Magento\Quote\Model\Quote\Item; +use Nosto\Tagging\Model\Item\Simple as SimpleItem; + +class Simple extends SimpleItem +{ + /** + * Returns the name of the product. Simple products will have their own name + * + * @param Item $item the ordered item + * @return string the name of the product + */ + public function buildItemName(Item $item): string + { + $type = $item->getProduct()->getTypeInstance(); + $parentIds = $type->getParentIdsByChild($item->getItemId()); + return $this->buildName($item, $parentIds); + } +} diff --git a/Model/Cart/Restore/Builder.php b/Model/Cart/Restore/Builder.php new file mode 100644 index 000000000..f2f21c3bc --- /dev/null +++ b/Model/Cart/Restore/Builder.php @@ -0,0 +1,191 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Cart\Restore; + +use Exception; +use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Quote\Model\Quote; +use Magento\Store\Model\Store; +use Nosto\Tagging\Api\Data\CustomerInterface; +use Nosto\Tagging\Helper\Url as NostoHelperUrl; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Customer\Customer as NostoCustomer; +use Nosto\Tagging\Model\Customer\CustomerFactory as NostoCustomerFactory; +use Nosto\Tagging\Model\Customer\Repository as NostoCustomerRepository; + +class Builder +{ + private NostoLogger $logger; + private CookieManagerInterface $cookieManager; + private DateTime $date; + private EncryptorInterface $encryptor; + private NostoCustomerFactory $nostoCustomerFactory; + private NostoHelperUrl $urlHelper; + private NostoCustomerRepository $nostoCustomerRepository; + + /** + * Builder constructor. + * @param NostoLogger $logger + * @param CookieManagerInterface $cookieManager + * @param EncryptorInterface $encryptor + * @param NostoCustomerFactory $nostoCustomerFactory + * @param NostoCustomerRepository $nostoCustomerRepository + * @param NostoHelperUrl $urlHelper + * @param DateTime $date + */ + public function __construct( + NostoLogger $logger, + CookieManagerInterface $cookieManager, + EncryptorInterface $encryptor, + NostoCustomerFactory $nostoCustomerFactory, + NostoCustomerRepository $nostoCustomerRepository, + NostoHelperUrl $urlHelper, + DateTime $date + ) { + $this->logger = $logger; + $this->cookieManager = $cookieManager; + $this->encryptor = $encryptor; + $this->date = $date; + $this->nostoCustomerFactory = $nostoCustomerFactory; + $this->urlHelper = $urlHelper; + $this->nostoCustomerRepository = $nostoCustomerRepository; + } + + /** + * @param Quote $quote + * @param Store $store + * @return string|null + * @throws NoSuchEntityException + */ + public function build(Quote $quote, Store $store) + { + $nostoCustomer = $this->updateNostoId($quote); + if ($nostoCustomer && $nostoCustomer->getRestoreCartHash()) { + return $this->generateRestoreCartUrl($nostoCustomer->getRestoreCartHash(), $store); + } + + return null; + } + + /** + * @param Quote $quote + * + * @return CustomerInterface|null + */ + private function updateNostoId(Quote $quote) + { + // Handle the Nosto customer & quote mapping + $nostoCustomerId = $this->cookieManager->getCookie(NostoCustomer::COOKIE_NAME); + + if ($nostoCustomerId === null || $quote->getId() === null) { + return null; + } + $nostoCustomer = $this->nostoCustomerRepository->getOneByNostoIdAndQuoteId( + $nostoCustomerId, + $quote->getId() + ); + + if ($nostoCustomer instanceof CustomerInterface + && $nostoCustomer->getCustomerId() + ) { + if ($nostoCustomer->getRestoreCartHash() === null) { + $nostoCustomer->setRestoreCartHash($this->generateRestoreCartHash()); + } + $nostoCustomer->setUpdatedAt($this->getNow()); + } else { + $nostoCustomer = $this->nostoCustomerFactory->create(); + $nostoCustomer->setQuoteId($quote->getId()); + $nostoCustomer->setNostoId($nostoCustomerId); + $nostoCustomer->setCreatedAt($this->getNow()); + $nostoCustomer->setRestoreCartHash($this->generateRestoreCartHash()); + } + try { + $nostoCustomer = $this->nostoCustomerRepository->save($nostoCustomer); + + return $nostoCustomer; + } catch (Exception $e) { + $this->logger->exception($e); + } + + return null; + } + + /** + * Generate unique hash for restore cart + * Size of it equals to or less than restore_cart_hash column length + * + * @return string + */ + private function generateRestoreCartHash() + { + $hash = $this->encryptor->getHash(uniqid('nostocartrestore', true)); + if (strlen($hash) > NostoCustomer::NOSTO_TAGGING_RESTORE_CART_ATTRIBUTE_LENGTH) { + $hash = substr($hash, 0, NostoCustomer::NOSTO_TAGGING_RESTORE_CART_ATTRIBUTE_LENGTH); + } + + return $hash; + } + + /** + * Returns the current datetime object + * + * @return \DateTime the current datetime + */ + private function getNow() + { + return \DateTime::createFromFormat('Y-m-d H:i:s', $this->date->date()); + } + + /** + * Returns restore cart url + * + * @param string $hash + * @param Store $store + * @return string the restore cart URL + * @throws NoSuchEntityException + */ + private function generateRestoreCartUrl(string $hash, Store $store) + { + $params = $this->urlHelper->getUrlOptionsWithNoSid($store); + $params['h'] = $hash; + return $store->getUrl(NostoHelperUrl::NOSTO_PATH_RESTORE_CART, $params); + } +} diff --git a/Model/Category/Builder.php b/Model/Category/Builder.php index 187607ba3..435166884 100644 --- a/Model/Category/Builder.php +++ b/Model/Category/Builder.php @@ -1,76 +1,100 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Model\Category; -use Magento\Catalog\Api\CategoryRepositoryInterface; +use Exception; use Magento\Catalog\Model\Category; -use Nosto\Tagging\Model\Category\Factory as CategoryFactory; -use Psr\Log\LoggerInterface; +use Magento\Framework\Event\ManagerInterface; +use Magento\Store\Model\Store; +use Nosto\Model\Category as NostoCategory; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Service\Product\Category\CategoryServiceInterface as NostoCategoryService; class Builder { - /** - * @var CategoryFactory - */ - protected $_categoryFactory; - - /** - * @var LoggerInterface - */ - protected $_logger; + private NostoLogger $logger; + private ManagerInterface $eventManager; + private NostoCategoryService $nostoCategoryService; /** - * @param CategoryFactory $productFactory - * @param CategoryRepositoryInterface $categoryRepository - * @param LoggerInterface $logger + * Builder constructor. + * @param NostoLogger $logger + * @param ManagerInterface $eventManager + * @param NostoCategoryService $nostoCategoryService */ public function __construct( - CategoryFactory $productFactory, - CategoryRepositoryInterface $categoryRepository, - LoggerInterface $logger + NostoLogger $logger, + ManagerInterface $eventManager, + NostoCategoryService $nostoCategoryService ) { - $this->_categoryFactory = $productFactory; - $this->_categoryRepository = $categoryRepository; - $this->_logger = $logger; + $this->logger = $logger; + $this->eventManager = $eventManager; + $this->nostoCategoryService = $nostoCategoryService; } /** * @param Category $category - * @return \NostoCategory + * @param Store $store + * @return NostoCategory|null */ - public function build(Category $category) + public function build(Category $category, Store $store) { - $nostoCategory = $this->_categoryFactory->create(); - + $nostoCategory = new NostoCategory(); try { - $nostoCategory->setPath($this->buildPath($category)); - } catch (\NostoException $e) { - $this->_logger->error($e, ['exception' => $e]); + $nostoCategory->setId($category->getId()); + $nostoCategory->setParentId($category->getParentId()); + $nostoCategory->setImageUrl($category->getImageUrl()); + $nostoCategory->setLevel($category->getLevel()); + $nostoCategory->setUrl($category->getUrl()); + $nostoCategory->setVisibleInMenu($this->getCategoryVisibleInMenu($category)); + $nostoCategory->setCategoryString( + $this->nostoCategoryService->getCategory($category, $store) + ); + $nostoCategory->setName($category->getName()); + } catch (Exception $e) { + $this->logger->exception($e); + } + if (empty($nostoCategory)) { + $nostoCategory = null; + } else { + $this->eventManager->dispatch( + 'nosto_category_load_after', + ['category' => $nostoCategory, 'magentoCategory' => $category] + ); } return $nostoCategory; @@ -78,18 +102,11 @@ public function build(Category $category) /** * @param Category $category - * @return string + * @return bool */ - protected function buildPath(Category $category) + private function getCategoryVisibleInMenu(Category $category) { - $data = []; - $path = $category->getPath(); - foreach (explode('/', $path) as $categoryId) { - $category = $this->_categoryRepository->get($categoryId); - if ($category && $category->getLevel() > 1) { - $data[] = $category->getName(); - } - } - return count($data) ? '/' . implode('/', $data) : ''; + $visibleInMenu = $category->getIncludeInMenu(); + return $visibleInMenu === "1"; } } diff --git a/Model/Category/Factory.php b/Model/Category/Factory.php deleted file mode 100644 index b3a074e3f..000000000 --- a/Model/Category/Factory.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) - */ - -namespace Nosto\Tagging\Model\Category; - -use Magento\Framework\ObjectManagerInterface; - -class Factory -{ - /** - * @var ObjectManagerInterface - */ - protected $_objectManager; - - /** - * @param ObjectManagerInterface $objectManager - */ - public function __construct(ObjectManagerInterface $objectManager) - { - $this->_objectManager = $objectManager; - } - - /** - * Create new product object. - * - * @param array $data - * @return \NostoCategory - */ - public function create(array $data = []) - { - return $this->_objectManager->create('NostoCategory', $data); - } -} diff --git a/Model/Config/Backend/MultiCurrency.php b/Model/Config/Backend/MultiCurrency.php new file mode 100644 index 000000000..2933d9107 --- /dev/null +++ b/Model/Config/Backend/MultiCurrency.php @@ -0,0 +1,102 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Config\Backend; + +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\App\Config\Value; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Registry; +use Nosto\Tagging\Helper\Data as NostoHelperData; + +class MultiCurrency extends Value +{ + private WriterInterface $configWriter; + + /** + * MultiCurrency constructor. + * @param Context $context + * @param Registry $registry + * @param ScopeConfigInterface $config + * @param TypeListInterface $cacheTypeList + * @param WriterInterface $configWriter + * @param AbstractResource|null $resource + * @param AbstractDb|null $resourceCollection + * @param array $data + */ + public function __construct( + Context $context, + Registry $registry, + ScopeConfigInterface $config, + TypeListInterface $cacheTypeList, + WriterInterface $configWriter, + AbstractResource $resource = null, + AbstractDb $resourceCollection = null, + array $data = [] + ) { + $this->configWriter = $configWriter; + parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); + } + + /** + * Set Price_Variation to No if MultiCurrency is not disabled + * + * @return Value + */ + public function beforeSave() //@codingStandardsIgnoreLine + { + $value = $this->getValue(); + $scopeType = $this->getScope(); + $scopeId = $this->getScopeId(); + + if ($value == NostoHelperData::SETTING_VALUE_MC_EXCHANGE_RATE + || $value == NostoHelperData::SETTING_VALUE_MC_SINGLE + ) { + $this->configWriter->save( + NostoHelperData::XML_PATH_PRICING_VARIATION, + '0', + $scopeType, + $scopeId + ); + } + + return parent::beforeSave(); + } +} diff --git a/Model/Config/Source/Brand.php b/Model/Config/Source/Brand.php new file mode 100644 index 000000000..21ad46969 --- /dev/null +++ b/Model/Config/Source/Brand.php @@ -0,0 +1,66 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Config\Source; + +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; + +/** + * Option array class to generate a list of selectable options that allows the merchant to choose + * any attribute for his brand tag. + */ +class Brand extends Selector +{ + + /** + * @param Collection $collection + */ + public function filterCollection(Collection $collection) + { + /** + * Argument is of type array but string expected + */ + /** @phan-suppress-next-next-line PhanTypeMismatchArgumentProbablyReal */ + /** @noinspection PhpParamsInspection */ + $collection->setFrontendInputTypeFilter(['text', 'select', 'textarea']); + $collection->addFieldToFilter('attribute_code', ['nin' => ['sku']]); + } + + public function isNullable() + { + return true; + } +} diff --git a/Model/Config/Source/GoogleCategory.php b/Model/Config/Source/GoogleCategory.php new file mode 100644 index 000000000..6fd6baab5 --- /dev/null +++ b/Model/Config/Source/GoogleCategory.php @@ -0,0 +1,64 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Config\Source; + +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; + +/** + * Option array class to generate a list of selectable options that allows the merchant to choose + * any attribute for his google category tag. + */ +class GoogleCategory extends Selector +{ + /** + * @param Collection $collection + */ + public function filterCollection(Collection $collection) + { + /** + * Argument is of type array but string is expected + */ + /** @phan-suppress-next-next-line PhanTypeMismatchArgumentProbablyReal */ + /** @noinspection PhpParamsInspection */ + $collection->setFrontendInputTypeFilter(['text', 'textarea']); + } + + public function isNullable() + { + return true; + } +} diff --git a/Model/Config/Source/Gtin.php b/Model/Config/Source/Gtin.php new file mode 100644 index 000000000..e0887ef7d --- /dev/null +++ b/Model/Config/Source/Gtin.php @@ -0,0 +1,64 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Config\Source; + +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; + +/** + * Option array class to generate a list of selectable options that allows the merchant to choose + * any attribute for his GTIN tag. + */ +class Gtin extends Selector +{ + /** + * @param Collection $collection + */ + public function filterCollection(Collection $collection) + { + /** + * Argument is of type array but string is expected + */ + /** @phan-suppress-next-next-line PhanTypeMismatchArgumentProbablyReal */ + /** @noinspection PhpParamsInspection */ + $collection->setFrontendInputTypeFilter(['text', 'textarea']); + } + + public function isNullable() + { + return true; + } +} diff --git a/Model/Config/Source/Image.php b/Model/Config/Source/Image.php new file mode 100644 index 000000000..58cfb7fb5 --- /dev/null +++ b/Model/Config/Source/Image.php @@ -0,0 +1,56 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Config\Source; + +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; + +/** + * Option array class to generate a list of selectable options that allows the merchant to choose + * any image attribute for his image tag. + */ +class Image extends Selector +{ + public function filterCollection(Collection $collection) + { + $collection->setFrontendInputTypeFilter('media_image'); + } + + public function isNullable() + { + return false; + } +} diff --git a/Model/Config/Source/Margin.php b/Model/Config/Source/Margin.php new file mode 100644 index 000000000..559d6f365 --- /dev/null +++ b/Model/Config/Source/Margin.php @@ -0,0 +1,56 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Config\Source; + +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; + +/** + * Option array class to generate a list of selectable options that allows the merchant to choose + * any price attribute for his supplier-cost tag. + */ +class Margin extends Selector +{ + public function filterCollection(Collection $collection) + { + $collection->setFrontendInputTypeFilter('price'); + } + + public function isNullable() + { + return true; + } +} diff --git a/Model/Config/Source/Memory.php b/Model/Config/Source/Memory.php new file mode 100644 index 000000000..c511d6dd1 --- /dev/null +++ b/Model/Config/Source/Memory.php @@ -0,0 +1,57 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Config\Source; + +use Magento\Framework\Data\OptionSourceInterface; +use Magento\Framework\Phrase; + +class Memory implements OptionSourceInterface +{ + /** + * Options getter + * + * @return array + */ + public function toOptionArray() + { + $options = []; + for ($i = 10; $i <= 100; $i += 10) { + $options[] = ['value' => $i, 'label' => new Phrase($i . '%')]; + } + return $options; + } +} diff --git a/Model/Config/Source/MultiCurrency.php b/Model/Config/Source/MultiCurrency.php new file mode 100644 index 000000000..e1b27eef7 --- /dev/null +++ b/Model/Config/Source/MultiCurrency.php @@ -0,0 +1,62 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Config\Source; + +use Magento\Framework\Data\OptionSourceInterface; +use Magento\Framework\Phrase; +use Nosto\Tagging\Helper\Data; + +/** + * Option array class to generate a list of selectable options that allows the merchant to choose + * any image attribute for his image tag. + */ +class MultiCurrency implements OptionSourceInterface +{ + /** + * Options getter + * + * @return array + */ + public function toOptionArray() + { + return [ + ['value' => Data::SETTING_VALUE_MC_EXCHANGE_RATE, 'label' => new Phrase('Exchange rates')], + ['value' => Data::SETTING_VALUE_MC_SINGLE, 'label' => new Phrase('Single currency')], + ['value' => Data::SETTING_VALUE_MC_DISABLED, 'label' => new Phrase('Disabled')] + ]; + } +} diff --git a/Model/Config/Source/Ratings.php b/Model/Config/Source/Ratings.php new file mode 100644 index 000000000..ffd9495e4 --- /dev/null +++ b/Model/Config/Source/Ratings.php @@ -0,0 +1,84 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Config\Source; + +use Magento\Framework\Data\OptionSourceInterface; +use Magento\Framework\Phrase; +use Nosto\Tagging\Helper\Data; +use Nosto\Tagging\Helper\Ratings as NostoRatingsHelper; + +/** + * Option array class to generate a list of selectable options that allows the merchant to choose + * the provider for the ratings and reviews. + */ +class Ratings implements OptionSourceInterface +{ + private NostoRatingsHelper $nostoRatingsHelper; + + /** + * Ratings constructor. + * @param NostoRatingsHelper $nostoRatingsHelper + */ + public function __construct(NostoRatingsHelper $nostoRatingsHelper) + { + $this->nostoRatingsHelper = $nostoRatingsHelper; + } + + /** + * Options getter + * + * @return array + */ + public function toOptionArray() + { + $options = [ + ['value' => Data::SETTING_VALUE_NO_RATINGS, 'label' => new Phrase('No Ratings')] + ]; + + if ($this->nostoRatingsHelper->canUseYotpo()) { + $yotpo = ['value' => Data::SETTING_VALUE_YOTPO_RATINGS, 'label' => new Phrase('Yotpo Ratings')]; + $options[] = $yotpo; + } + + if ($this->nostoRatingsHelper->canUseMagentoRatingsAndReviews()) { + $mageRatings = ['value' => Data::SETTING_VALUE_MAGENTO_RATINGS, 'label' => new Phrase('Magento Ratings')]; + $options[] = $mageRatings; + } + + return $options; + } +} diff --git a/Model/Config/Source/Selector.php b/Model/Config/Source/Selector.php new file mode 100644 index 000000000..78ef851fc --- /dev/null +++ b/Model/Config/Source/Selector.php @@ -0,0 +1,92 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Config\Source; + +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; +use Magento\Framework\Data\OptionSourceInterface; +use Nosto\Tagging\Model\Service\Product\Attribute\AttributeProviderInterface; + +/** + * Abstract option array class to generate a list of selectable options that allows the merchant to + * choose an attribute for for the specified tagging fields requirements. + */ +abstract class Selector implements OptionSourceInterface +{ + /** @var AttributeProviderInterface */ + private AttributeProviderInterface $attributeProvider; + + /** + * Selector constructor. + * @param AttributeProviderInterface $attributeProvider + */ + public function __construct( + AttributeProviderInterface $attributeProvider + ) { + $this->attributeProvider = $attributeProvider; + } + + /** + * Returns all available product attributes + * + * @return array + */ + public function toOptionArray() + { + $collection = $this->attributeProvider->getSelectableAttributesForNosto(); + if ($collection === null) { + return []; + } + $this->filterCollection($collection); + + $options = $this->isNullable() ? [['value' => 0, 'label' => 'None']] : []; + + /** @var Attribute $attribute */ + foreach ($collection->load() as $attribute) { + $options[] = [ + 'value' => $attribute->getAttributeCode(), + 'label' => $attribute->getFrontend()->getLabel(), + ]; + } + + return $options; + } + + abstract public function filterCollection(Collection $collection); + + abstract public function isNullable(); +} diff --git a/Model/Config/Source/Tags.php b/Model/Config/Source/Tags.php new file mode 100644 index 000000000..c930e7341 --- /dev/null +++ b/Model/Config/Source/Tags.php @@ -0,0 +1,56 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Config\Source; + +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; + +/** + * Option array class to generate a list of selectable options that allows the merchant to choose + * any attribute for his brand tag. + */ +class Tags extends Selector +{ + // @codingStandardsIgnoreLine + public function filterCollection(Collection $collection) + { + } + + public function isNullable() + { + return true; + } +} diff --git a/Model/Customer.php b/Model/Customer.php deleted file mode 100644 index 1fe4fe7c5..000000000 --- a/Model/Customer.php +++ /dev/null @@ -1,129 +0,0 @@ - - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) - */ - -namespace Nosto\Tagging\Model; - -use Magento\Framework\Model\AbstractModel; -use Nosto\Tagging\Api\Data\CustomerInterface; - -class Customer extends AbstractModel implements CustomerInterface -{ - /** - * Name of cookie that holds Nosto visitor id - */ - const COOKIE_NAME = '2c_cId'; - - /** - * Initialize resource model - * - * @return void - */ - protected function _construct() - { - $this->_init('Nosto\Tagging\Model\ResourceModel\Customer'); - } - - /** - * @inheritdoc - */ - public function getCustomerId() - { - return $this->getData(self::CUSTOMER_ID); - } - - /** - * @inheritdoc - */ - public function getQuoteId() - { - return $this->getData(self::QUOTE_ID); - } - - /** - * @inheritdoc - */ - public function getNostoId() - { - return $this->getData(self::NOSTO_ID); - } - - /** - * @inheritdoc - */ - public function getCreatedAt() - { - return $this->getData(self::CREATED_AT); - } - - /** - * @inheritdoc - */ - public function getUpdatedAt() - { - return $this->getData(self::UPDATED_AT); - } - - /** - * @inheritdoc - */ - public function setCustomerId($customerId) - { - return $this->setData(self::CUSTOMER_ID, $customerId); - } - - /** - * @inheritdoc - */ - public function setQuoteId($quoteId) - { - return $this->setData(self::QUOTE_ID, $quoteId); - } - - /** - * @inheritdoc - */ - public function setNostoId($nostoId) - { - return $this->setData(self::NOSTO_ID, $nostoId); - } - - /** - * @inheritdoc - */ - public function setCreatedAt(\DateTime $createdAt) - { - return $this->setData(self::CREATED_AT, $createdAt); - } - - /** - * @inheritdoc - */ - public function setUpdatedAt(\DateTime $updatedAt) - { - return $this->setData(self::UPDATED_AT, $updatedAt); - } -} diff --git a/Model/Customer/Customer.php b/Model/Customer/Customer.php new file mode 100644 index 000000000..635f7cded --- /dev/null +++ b/Model/Customer/Customer.php @@ -0,0 +1,156 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Customer; + +use DateTime; +use Magento\Framework\Model\AbstractModel; +use Nosto\Tagging\Api\Data\CustomerInterface; +use Nosto\Tagging\Model\ResourceModel\Customer as NostoCustomer; + +class Customer extends AbstractModel implements CustomerInterface +{ + /** + * Name of cookie that holds Nosto visitor id + */ + public const COOKIE_NAME = '2c_cId'; + + /** + * @inheritDoc + */ + public function getCustomerId() + { + return $this->getData(self::CUSTOMER_ID); + } + + /** + * @inheritDoc + */ + public function getQuoteId() + { + return $this->getData(self::QUOTE_ID); + } + + /** + * @inheritDoc + */ + public function getNostoId() + { + return $this->getData(self::NOSTO_ID); + } + + /** + * @inheritDoc + */ + public function getCreatedAt() + { + return $this->getData(self::CREATED_AT); + } + + /** + * @inheritDoc + */ + public function getUpdatedAt() + { + return $this->getData(self::UPDATED_AT); + } + + /** + * @inheritDoc + */ + public function setCustomerId(int $customerId) + { + return $this->setData(self::CUSTOMER_ID, $customerId); + } + + /** + * @inheritDoc + */ + public function setQuoteId(int $quoteId) + { + return $this->setData(self::QUOTE_ID, $quoteId); + } + + /** + * @inheritDoc + */ + public function setNostoId(string $nostoId) + { + return $this->setData(self::NOSTO_ID, $nostoId); + } + + /** + * @inheritDoc + */ + public function setCreatedAt(DateTime $createdAt) + { + return $this->setData(self::CREATED_AT, $createdAt); + } + + /** + * @inheritDoc + */ + public function setUpdatedAt(DateTime $updatedAt) + { + return $this->setData(self::UPDATED_AT, $updatedAt); + } + + /** + * Initialize resource model + * + * @return void + */ + public function _construct() + { + $this->_init(NostoCustomer::class); + } + + /** + * @inheritDoc + */ + public function getRestoreCartHash() + { + return $this->getData(self::RESTORE_CART_HASH); + } + + /** + * @inheritDoc + */ + public function setRestoreCartHash(string $restoreCartHash) + { + return $this->setData(self::RESTORE_CART_HASH, $restoreCartHash); + } +} diff --git a/Model/Customer/CustomerSearchResults.php b/Model/Customer/CustomerSearchResults.php new file mode 100644 index 000000000..4227a082e --- /dev/null +++ b/Model/Customer/CustomerSearchResults.php @@ -0,0 +1,44 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Customer; + +use Magento\Framework\Api\Search\SearchResult; +use Nosto\Tagging\Api\Data\CustomerSearchResultInterface; + +class CustomerSearchResults extends SearchResult implements CustomerSearchResultInterface // @codingStandardsIgnoreLine +{ +} diff --git a/Model/Customer/Repository.php b/Model/Customer/Repository.php new file mode 100644 index 000000000..d2a2b432c --- /dev/null +++ b/Model/Customer/Repository.php @@ -0,0 +1,189 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Customer; + +use Exception; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\Exception\AlreadyExistsException; +use Nosto\Tagging\Api\CustomerRepositoryInterface; +use Nosto\Tagging\Api\Data\CustomerInterface; +use Nosto\Tagging\Model\Customer\Customer as NostoCustomer; +use Nosto\Tagging\Model\ResourceModel\Customer as CustomerResource; +use Nosto\Tagging\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory; +use Nosto\Tagging\Util\Repository as RepositoryUtil; + +class Repository implements CustomerRepositoryInterface +{ + private SearchCriteriaBuilder $searchCriteriaBuilder; + private CustomerCollectionFactory $customerCollectionFactory; + private CustomerSearchResultsFactory $customerSearchResultsFactory; + private CustomerResource $customerResource; + + /** + * Customer repository constructor + * + * @param CustomerResource $customerResource + * @param CustomerCollectionFactory $customerCollectionFactory + * @param CustomerSearchResultsFactory $customerSearchResultsFactory + * @param SearchCriteriaBuilder $searchCriteriaBuilder + */ + public function __construct( + CustomerResource $customerResource, + CustomerCollectionFactory $customerCollectionFactory, + CustomerSearchResultsFactory $customerSearchResultsFactory, + SearchCriteriaBuilder $searchCriteriaBuilder + ) { + $this->customerSearchResultsFactory = $customerSearchResultsFactory; + $this->customerCollectionFactory = $customerCollectionFactory; + $this->customerResource = $customerResource; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * Save Queue entry + * + * @param CustomerInterface $customer + * + * @return CustomerInterface + * @throws Exception + * @throws AlreadyExistsException + * + * @suppress PhanTypeMismatchArgument + */ + public function save(CustomerInterface $customer) + { + /** @noinspection PhpParamsInspection */ + $this->customerResource->save($customer); + + return $customer; + } + + /** + * Get customer entry by nosto id and quote id. If multiple entries + * are found first one will be returned. + * + * @param string $nostoId + * @param int $quoteId + * + * @return CustomerInterface|null + */ + public function getOneByNostoIdAndQuoteId(string $nostoId, int $quoteId) + { + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(CustomerInterface::NOSTO_ID, $nostoId) + ->addFilter(CustomerInterface::QUOTE_ID, $quoteId) + ->setPageSize(1) + ->setCurrentPage(1) + ->create(); + + /** @var CustomerInterface[]|null $items */ + $items = $this->search($searchCriteria)->getItems(); + /** @var CustomerInterface|null $item */ + $item = $items ? reset($items) : null; + return $item; + } + + /** + * Get customer entry by field name and quote id. If multiple entries + * are found first one will be returned. + * + * @param int $quoteId + * + * @return CustomerInterface|null + */ + public function getOneByQuoteId(int $quoteId) + { + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(NostoCustomer::QUOTE_ID, $quoteId) + ->setPageSize(1) + ->setCurrentPage(1) + ->create(); + + /** @var CustomerInterface[]|null $items */ + $items = $this->search($searchCriteria)->getItems(); + /** @var CustomerInterface|null $item */ + $item = $items ? reset($items) : null; + return $item; + } + + /** + * Get customer entry by restore cart hash. If multiple entries + * are found first one will be returned. + * + * @param string $hash + * + * @return CustomerInterface|null + */ + public function getOneByRestoreCartHash(string $hash) + { + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(CustomerInterface::RESTORE_CART_HASH, $hash) + ->setPageSize(1) + ->setCurrentPage(1) + ->create(); + + /** @var CustomerInterface[]|null $items */ + $items = $this->search($searchCriteria)->getItems(); + /** @var CustomerInterface|null $item */ + $item = $items ? reset($items) : null; + return $item; + } + + /** + * @param SearchCriteriaInterface $searchCriteria + * + * @return CustomerSearchResults + */ + public function search(SearchCriteriaInterface $searchCriteria) + { + $collection = $this->customerCollectionFactory->create(); + $searchResults = $this->customerSearchResultsFactory->create(); + + /** + * Returning \Magento\Framework\Api\Search\SearchResult + * but declared to return CustomerSearchResults + */ + /** @phan-suppress-next-next-line PhanTypeMismatchReturnSuperType */ + /** @noinspection PhpIncompatibleReturnTypeInspection */ + return (new RepositoryUtil())->search( + $collection, + $searchCriteria, + $searchResults + ); + } +} diff --git a/Model/Email/Repository.php b/Model/Email/Repository.php new file mode 100644 index 000000000..5c0765286 --- /dev/null +++ b/Model/Email/Repository.php @@ -0,0 +1,94 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Email; + +use Magento\Newsletter\Model\ResourceModel\Subscriber as SubscriberResource; +use Magento\Newsletter\Model\Subscriber; + +/** + * Repository wrapper / helper class for fetching marketing permission related items + */ +class Repository +{ + /** + * @var SubscriberResource + */ + private SubscriberResource $subscriber; + + /** + * Repository constructor. + * @param SubscriberResource $subscriber + */ + public function __construct( + SubscriberResource $subscriber + ) { + $this->subscriber = $subscriber; + } + + /** + * Gets newsletter subscription by email + * @param $email + * @return array + */ + public function getNewsletterOptInForEmail($email) + { + // @phan-suppress-next-line PhanDeprecatedFunction + return $this->subscriber->loadByEmail($email); + } + + /** + * Checks if email is opted in / marketing permission has been given + * + * @param $email + * @return bool + */ + public function isOptedIn($email) + { + $subscriber = $this->getNewsletterOptInForEmail($email); + if (!$subscriber || empty($subscriber)) { + return false; + } + + if (isset($subscriber['subscriber_status']) + && (int)$subscriber['subscriber_status'] === Subscriber::STATUS_SUBSCRIBED + ) { + return true; + } + + return false; + } +} diff --git a/Model/Indexer/AbstractIndexer.php b/Model/Indexer/AbstractIndexer.php new file mode 100644 index 000000000..5ede39662 --- /dev/null +++ b/Model/Indexer/AbstractIndexer.php @@ -0,0 +1,378 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Indexer; + +use ArrayIterator; +use Exception; +use InvalidArgumentException; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Indexer\ActionInterface as IndexerActionInterface; +use Magento\Framework\Indexer\Dimension; +use Magento\Framework\Indexer\DimensionalIndexerInterface; +use Magento\Framework\Indexer\DimensionProviderInterface; +use Magento\Framework\Mview\ActionInterface as MviewActionInterface; +use Magento\Indexer\Model\ProcessManager; +use Magento\Store\Model\App\Emulation; +use Magento\Store\Model\Store; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Indexer\Dimensions\AbstractDimensionModeConfiguration as DimensionModeConfiguration; +use Nosto\Tagging\Model\Indexer\Dimensions\ModeSwitcherInterface; +use Nosto\Tagging\Model\Indexer\Dimensions\StoreDimensionProvider; +use Nosto\Tagging\Model\Service\Indexer\IndexerStatusServiceInterface; +use Nosto\Tagging\Util\Benchmark; +use Symfony\Component\Console\Input\InputInterface; +use Traversable; +use UnexpectedValueException; + +/** + * Class AbstractIndexer + */ +abstract class AbstractIndexer implements DimensionalIndexerInterface, IndexerActionInterface, MviewActionInterface +{ + /** @var NostoHelperScope */ + private NostoHelperScope $nostoHelperScope; + + /** @var NostoLogger */ + public NostoLogger $nostoLogger; + + /** @var ProcessManager */ + private ?ProcessManager $processManager; + + /** @var DimensionProviderInterface */ + private $dimensionProvider; + + /** @var Emulation */ + private Emulation $storeEmulator; + + /** @var InputInterface */ + private InputInterface $input; + + /** @var IndexerStatusServiceInterface */ + private IndexerStatusServiceInterface $indexerStatusService; + + /** + * AbstractIndexer constructor. + * @param NostoHelperScope $nostoHelperScope + * @param NostoLogger $nostoLogger + * @param StoreDimensionProvider $dimensionProvider + * @param Emulation $storeEmulator + * @param InputInterface $input + * @param IndexerStatusServiceInterface $indexerStatusService + * @param ProcessManager|null $processManager + */ + public function __construct( + NostoHelperScope $nostoHelperScope, + NostoLogger $nostoLogger, + StoreDimensionProvider $dimensionProvider, + Emulation $storeEmulator, + InputInterface $input, + IndexerStatusServiceInterface $indexerStatusService, + ProcessManager $processManager = null + ) { + $this->nostoHelperScope = $nostoHelperScope; + $this->nostoLogger = $nostoLogger; + $this->dimensionProvider = $dimensionProvider; + $this->processManager = $processManager; + $this->input = $input; + $this->storeEmulator = $storeEmulator; + $this->indexerStatusService = $indexerStatusService; + } + + /** + * Get ModeSwitcher class to later get the indexer mode + * + * @return ModeSwitcherInterface + */ + abstract public function getModeSwitcher(): ModeSwitcherInterface; + + /** + * Implement logic of single store indexing + * + * @param Store $store + * @param array $ids + * @throws Exception + */ + abstract public function doIndex(Store $store, array $ids = []); + + /** + * @return string + */ + abstract public function getIndexerId(): string; + + /** + * @inheritDoc + * @throws Exception + */ + public function executeFull() + { + if ($this->allowFullExecution() === true) { + $this->logInfo('Begin a full reindex'); + $this->doWork(); + $this->logInfo('Finished full reindex'); + } else { + $this->logInfo( + 'Full reindex is disabled in Nosto module settings' + . ' or indexer is being called from setup:upgrade' + ); + } + } + + /** + * @inheritDoc + * @throws Exception + */ + public function executeList(array $ids) + { + $this->execute($ids); + } + + /** + * @inheritDoc + * @throws Exception + */ + public function executeRow($id) + { + $this->logInfo('Begin a row reindex'); + $this->execute([$id]); + $this->logInfo('Finished row reindex'); + } + + /** + * @inheritDoc + * @throws Exception + */ + public function execute($ids) + { + $idCount = count($ids); + $totalEntries = $this->getTotalCLRows(); + $message = sprintf( + 'Begin a partial reindex for total of %d ids. ' . + 'Total number of entries in CL table: %d', + $idCount, + $totalEntries + ); + $this->logInfo($message); + $this->doWork($ids); + $this->logInfo('Finished partial reindex'); + } + + /** + * @param array $ids + * @throws Exception + * @suppress PhanTypeMismatchArgument + */ + public function doWork(array $ids = []) + { + $userFunctions = []; + $mode = $this->getModeSwitcher()->getMode(); + $this->logDebug(sprintf('Indexing by mode "%s"', $mode)); + switch ($mode) { + case DimensionModeConfiguration::DIMENSION_NONE: + /** @var Dimension[] $dimension */ + foreach ($this->dimensionProvider->getIterator() as $dimension) { + if (is_array($dimension)) { + (function () use ($dimension, $ids) { + $this->executeByDimensions($dimension, new ArrayIterator($ids)); + })(); + } + } + break; + case DimensionModeConfiguration::DIMENSION_STORE: + /** @var Dimension[] $dimension */ + foreach ($this->dimensionProvider->getIterator() as $dimension) { + /** @suppress PhanTypeMismatchArgument */ + $userFunctions[] = function () use ($dimension, $ids) { + $this->executeByDimensions($dimension, new ArrayIterator($ids)); + }; + } + /** + * Argument is of type array but Traversable is expected + */ + /** @phan-suppress-next-next-line PhanTypeMismatchArgumentProbablyReal */ + /** @var Traversable $userFunctions */ + $this->getProcessManager()->execute($userFunctions); + break; + default: + throw new UnexpectedValueException('Undefined dimension mode.'); + } + $this->clearProcessedChangelog(); + } + + /** + * In case processManager has value null, pass a new instance of ProcessManager + * This operation is not done in the constructor in order for the constr to have only + * value assignments + * + * @return ProcessManager + */ + private function getProcessManager() + { + if ($this->processManager === null) { + $this->processManager = ObjectManager::getInstance()->get( + ProcessManager::class + ); + } + return $this->processManager; + } + + /** + * @param Dimension[] $dimensions + * @param Traversable|null $entityIds + */ + public function executeByDimensions(array $dimensions, Traversable $entityIds = null) + { + if (count($dimensions) > 1 || !isset($dimensions[StoreDimensionProvider::DIMENSION_NAME])) { + throw new InvalidArgumentException('Indexer "' . $this->getIndexerId() . '" support only Store dimension'); + } + $storeId = $dimensions[StoreDimensionProvider::DIMENSION_NAME]->getValue(); + $store = $this->nostoHelperScope->getStore($storeId); + $benchmarkName = sprintf('STORE-DIMENSION-%s', $store->getCode()); + Benchmark::getInstance()->startInstrumentation($benchmarkName, 0); + $this->logDebug( + sprintf( + '[START] Processing dimension: "%s"', + $store->getCode() + ), + $storeId + ); + $ids = []; + if ($entityIds !== null) { + $ids = iterator_to_array($entityIds); + } + $this->storeEmulator->startEnvironmentEmulation((int)$storeId); + try { + $this->doIndex($store, $ids); + } catch (Exception $e) { + $this->nostoLogger->error($e->getMessage()); + } finally { + $this->storeEmulator->stopEnvironmentEmulation(); + } + Benchmark::getInstance()->stopInstrumentation($benchmarkName); + $duration = Benchmark::getInstance()->getElapsed($benchmarkName); + $this->logDebug( + sprintf( + '[END] Finished processing dimension: "%s", (%f)', + $store->getCode(), + round($duration, 2) + ), + $storeId + ); + } + + /** + * @return bool + */ + public function allowFullExecution(): bool + { + $indexerUtil = new IndexerUtil(); + return $indexerUtil->isCalledFromSetupUpgrade($this->input) === false; + } + + /** + * Clears the CL tables + * @throws Exception + */ + private function clearProcessedChangelog() + { + $benchmarkName = sprintf('CHANGELOG-CLEANUP-%s', $this->getIndexerId()); + Benchmark::getInstance()->startInstrumentation($benchmarkName, 0); + $this->logDebug('Cleaning up the CL tables'); + $this->indexerStatusService->clearProcessedChangelog($this->getIndexerId()); + Benchmark::getInstance()->stopInstrumentation($benchmarkName); + $duration = round(Benchmark::getInstance()->getElapsed($benchmarkName), 4); + $this->logDebug( + sprintf( + 'Cleanup took %f secs. Rows left in changelog table %d. Cleanup up until version #%d', + $duration, + $this->getTotalCLRows(), + $this->indexerStatusService->getCurrentWatermark($this->getIndexerId()) + ) + ); + } + + /** + * @return int + * @throws Exception + */ + private function getTotalCLRows() + { + return $this->indexerStatusService->getTotalChangelogCount($this->getIndexerId()); + } + + /** + * Shortcut method for logging debug + * + * @param string $message + * @param int|string|null $storeId + */ + private function logDebug(string $message, $storeId = null) + { + $this->log($message, 'debug', $storeId); + } + + /** + * Shortcut method for logging info + * + * @param string $message + * @param int|string|null $storeId + */ + private function logInfo(string $message, $storeId = null) + { + $this->log($message, 'info', $storeId); + } + + /** + * Shortcut method for logging indexer related messages + * + * @param string $message + * @param string $level + * @param int|string|null $storeId + * @return bool + */ + private function log(string $message, string $level, $storeId = null) + { + $logContext = ['indexerId' => $this->getIndexerId()]; + if ($storeId !== null) { + $logContext['storeId'] = $storeId; + } + if ($level === 'info') { + return $this->nostoLogger->info($message, $logContext); + } + return $this->nostoLogger->debugWithSource($message, $logContext, $this); + } +} diff --git a/Model/Indexer/Dimensions/AbstractDimensionModeConfiguration.php b/Model/Indexer/Dimensions/AbstractDimensionModeConfiguration.php new file mode 100644 index 000000000..57181615a --- /dev/null +++ b/Model/Indexer/Dimensions/AbstractDimensionModeConfiguration.php @@ -0,0 +1,89 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Indexer\Dimensions; + +use Magento\Framework\App\Config\ScopeConfigInterface; + +abstract class AbstractDimensionModeConfiguration +{ + /** + * Available modes of dimensions for nosto product data indexer + */ + public const DIMENSION_NONE = 'none'; + public const DIMENSION_STORE = 'store'; + + /** + * Mapping between dimension mode and dimension provider name + * + * @var array + */ + public array $modesMapping = [ + self::DIMENSION_NONE => [ + ], + self::DIMENSION_STORE => [ + StoreDimensionProvider::DIMENSION_NAME + ] + ]; + + /** + * @var ScopeConfigInterface + */ + public ScopeConfigInterface $scopeConfig; + + /** + * @return string + */ + abstract public function getCurrentMode(): string; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Return dimension modes configuration. + * + * @return array + */ + public function getDimensionModes(): array + { + return $this->modesMapping; + } +} diff --git a/Model/Indexer/Dimensions/ModeSwitcherInterface.php b/Model/Indexer/Dimensions/ModeSwitcherInterface.php new file mode 100644 index 000000000..1b65029ed --- /dev/null +++ b/Model/Indexer/Dimensions/ModeSwitcherInterface.php @@ -0,0 +1,47 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Indexer\Dimensions; + +use Magento\Indexer\Model\ModeSwitcherInterface as MagentoModeSwitcherInterface; + +interface ModeSwitcherInterface extends MagentoModeSwitcherInterface +{ + /** + * @return string + */ + public function getMode(): string; +} diff --git a/Model/Indexer/Dimensions/Queue/DimensionModeConfiguration.php b/Model/Indexer/Dimensions/Queue/DimensionModeConfiguration.php new file mode 100644 index 000000000..be0a4c1a7 --- /dev/null +++ b/Model/Indexer/Dimensions/Queue/DimensionModeConfiguration.php @@ -0,0 +1,66 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Indexer\Dimensions\Queue; + +use Nosto\Tagging\Model\Indexer\Dimensions\AbstractDimensionModeConfiguration; + +class DimensionModeConfiguration extends AbstractDimensionModeConfiguration +{ + /** + * @var string + */ + private string $currentMode = ''; + + /** + * @return string + */ + public function getCurrentMode(): string + { + if ($this->currentMode === '') { + $mode = $this->scopeConfig->getValue( + ModeSwitcherConfiguration::XML_PATH_PRODUCT_QUEUE_DIMENSIONS_MODE + ); + if ($mode) { + $this->currentMode = $mode; + } else { + $this->currentMode = self::DIMENSION_NONE; + } + } + + return $this->currentMode; + } +} diff --git a/Model/Indexer/Dimensions/Queue/ModeSwitcher.php b/Model/Indexer/Dimensions/Queue/ModeSwitcher.php new file mode 100644 index 000000000..08b7d1b51 --- /dev/null +++ b/Model/Indexer/Dimensions/Queue/ModeSwitcher.php @@ -0,0 +1,97 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Indexer\Dimensions\Queue; + +use Magento\Indexer\Model\DimensionMode; +use Magento\Indexer\Model\DimensionModes; +use Nosto\Tagging\Model\Indexer\Dimensions\ModeSwitcherInterface; + +class ModeSwitcher implements ModeSwitcherInterface +{ + /** + * @var DimensionModeConfiguration + */ + private DimensionModeConfiguration $dimensionModeConfiguration; + + /** + * @var ModeSwitcherConfiguration + */ + private ModeSwitcherConfiguration $modeSwitcherConfiguration; + + /** + * ModeSwitcher constructor. + * @param DimensionModeConfiguration $dimensionModeConfiguration + * @param ModeSwitcherConfiguration $modeSwitcherConfiguration + */ + public function __construct( + DimensionModeConfiguration $dimensionModeConfiguration, + ModeSwitcherConfiguration $modeSwitcherConfiguration + ) { + $this->dimensionModeConfiguration = $dimensionModeConfiguration; + $this->modeSwitcherConfiguration = $modeSwitcherConfiguration; + } + + /** + * @inheritDoc + */ + public function getDimensionModes(): DimensionModes + { + $dimensionsList = []; + foreach ($this->dimensionModeConfiguration->getDimensionModes() as $dimension => $modes) { + $dimensionsList[] = new DimensionMode($dimension, $modes); + } + + return new DimensionModes($dimensionsList); + } + + /** + * @inheritDoc + */ + public function switchMode(string $currentMode, string $previousMode) // @codingStandardsIgnoreLine + { + $this->modeSwitcherConfiguration->saveMode($currentMode); + } + + /** + * @return string + */ + public function getMode(): string + { + return $this->dimensionModeConfiguration->getCurrentMode(); + } +} diff --git a/Model/Indexer/Dimensions/Queue/ModeSwitcherConfiguration.php b/Model/Indexer/Dimensions/Queue/ModeSwitcherConfiguration.php new file mode 100644 index 000000000..9ae46c674 --- /dev/null +++ b/Model/Indexer/Dimensions/Queue/ModeSwitcherConfiguration.php @@ -0,0 +1,86 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Indexer\Dimensions\Queue; + +use InvalidArgumentException; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ConfigResource\ConfigInterface; + +class ModeSwitcherConfiguration +{ + public const XML_PATH_PRODUCT_QUEUE_DIMENSIONS_MODE = 'indexer/nosto_index_product_queue/dimensions_mode'; + + /** + * ConfigInterface + * + * @var ConfigInterface + */ + private ConfigInterface $configWriter; + + /** + * TypeListInterface + * + * @var TypeListInterface + */ + private TypeListInterface $cacheTypeList; + + /** + * ModeSwitcherConfiguration constructor. + * @param ConfigInterface $configWriter + * @param TypeListInterface $cacheTypeList + */ + public function __construct( + ConfigInterface $configWriter, + TypeListInterface $cacheTypeList + ) { + $this->configWriter = $configWriter; + $this->cacheTypeList = $cacheTypeList; + } + + /** + * Save switcher mode and invalidate reindex. + * + * @param string $mode + * @return void + * @throws InvalidArgumentException + */ + public function saveMode(string $mode) + { + $this->configWriter->saveConfig(self::XML_PATH_PRODUCT_QUEUE_DIMENSIONS_MODE, $mode); + $this->cacheTypeList->cleanType('config'); + } +} diff --git a/Model/Indexer/Dimensions/QueueProcessor/DimensionModeConfiguration.php b/Model/Indexer/Dimensions/QueueProcessor/DimensionModeConfiguration.php new file mode 100644 index 000000000..3484f5b12 --- /dev/null +++ b/Model/Indexer/Dimensions/QueueProcessor/DimensionModeConfiguration.php @@ -0,0 +1,66 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Indexer\Dimensions\QueueProcessor; + +use Nosto\Tagging\Model\Indexer\Dimensions\AbstractDimensionModeConfiguration; + +class DimensionModeConfiguration extends AbstractDimensionModeConfiguration +{ + /** + * @var string + */ + private string $currentMode = ''; + + /** + * @return string + */ + public function getCurrentMode(): string + { + if ($this->currentMode === '') { + $mode = $this->scopeConfig->getValue( + ModeSwitcherConfiguration::XML_PATH_PRODUCT_QUEUE_PROCESSOR_DIMENSIONS_MODE + ); + if ($mode) { + $this->currentMode = $mode; + } else { + $this->currentMode = self::DIMENSION_NONE; + } + } + + return $this->currentMode; + } +} diff --git a/Model/Indexer/Dimensions/QueueProcessor/ModeSwitcher.php b/Model/Indexer/Dimensions/QueueProcessor/ModeSwitcher.php new file mode 100644 index 000000000..cf6fa57e2 --- /dev/null +++ b/Model/Indexer/Dimensions/QueueProcessor/ModeSwitcher.php @@ -0,0 +1,97 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Indexer\Dimensions\QueueProcessor; + +use Magento\Indexer\Model\DimensionMode; +use Magento\Indexer\Model\DimensionModes; +use Nosto\Tagging\Model\Indexer\Dimensions\ModeSwitcherInterface; + +class ModeSwitcher implements ModeSwitcherInterface +{ + /** + * @var DimensionModeConfiguration + */ + private DimensionModeConfiguration $dimensionModeConfiguration; + + /** + * @var ModeSwitcherConfiguration + */ + private ModeSwitcherConfiguration $modeSwitcherConfiguration; + + /** + * ModeSwitcher constructor. + * @param DimensionModeConfiguration $dimensionModeConfiguration + * @param ModeSwitcherConfiguration $modeSwitcherConfiguration + */ + public function __construct( + DimensionModeConfiguration $dimensionModeConfiguration, + ModeSwitcherConfiguration $modeSwitcherConfiguration + ) { + $this->dimensionModeConfiguration = $dimensionModeConfiguration; + $this->modeSwitcherConfiguration = $modeSwitcherConfiguration; + } + + /** + * @inheritDoc + */ + public function getDimensionModes(): DimensionModes + { + $dimensionsList = []; + foreach ($this->dimensionModeConfiguration->getDimensionModes() as $dimension => $modes) { + $dimensionsList[] = new DimensionMode($dimension, $modes); + } + + return new DimensionModes($dimensionsList); + } + + /** + * @inheritDoc + */ + public function switchMode(string $currentMode, string $previousMode) // @codingStandardsIgnoreLine + { + $this->modeSwitcherConfiguration->saveMode($currentMode); + } + + /** + * @return string + */ + public function getMode(): string + { + return $this->dimensionModeConfiguration->getCurrentMode(); + } +} diff --git a/Model/Indexer/Dimensions/QueueProcessor/ModeSwitcherConfiguration.php b/Model/Indexer/Dimensions/QueueProcessor/ModeSwitcherConfiguration.php new file mode 100644 index 000000000..9b02ad847 --- /dev/null +++ b/Model/Indexer/Dimensions/QueueProcessor/ModeSwitcherConfiguration.php @@ -0,0 +1,87 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Indexer\Dimensions\QueueProcessor; + +use InvalidArgumentException; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ConfigResource\ConfigInterface; + +class ModeSwitcherConfiguration +{ + // phpcs:ignore Generic.Files.LineLength + public const XML_PATH_PRODUCT_QUEUE_PROCESSOR_DIMENSIONS_MODE = 'indexer/nosto_index_product_queue_processor/dimensions_mode'; + + /** + * ConfigInterface + * + * @var ConfigInterface + */ + private ConfigInterface $configWriter; + + /** + * TypeListInterface + * + * @var TypeListInterface + */ + private TypeListInterface $cacheTypeList; + + /** + * ModeSwitcherConfiguration constructor. + * @param ConfigInterface $configWriter + * @param TypeListInterface $cacheTypeList + */ + public function __construct( + ConfigInterface $configWriter, + TypeListInterface $cacheTypeList + ) { + $this->configWriter = $configWriter; + $this->cacheTypeList = $cacheTypeList; + } + + /** + * Save switcher mode and invalidate reindex. + * + * @param string $mode + * @return void + * @throws InvalidArgumentException + */ + public function saveMode(string $mode) + { + $this->configWriter->saveConfig(self::XML_PATH_PRODUCT_QUEUE_PROCESSOR_DIMENSIONS_MODE, $mode); + $this->cacheTypeList->cleanType('config'); + } +} diff --git a/Model/Indexer/Dimensions/StoreDimensionProvider.php b/Model/Indexer/Dimensions/StoreDimensionProvider.php new file mode 100644 index 000000000..0ac6c3180 --- /dev/null +++ b/Model/Indexer/Dimensions/StoreDimensionProvider.php @@ -0,0 +1,95 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Indexer\Dimensions; + +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Indexer\DimensionProviderInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Nosto\Tagging\Helper\Account; +use Traversable; + +class StoreDimensionProvider implements DimensionProviderInterface +{ + /** + * Hold the name of Store dimension. Uses for retrieve dimension value. + * Used "scope" name for support current indexer implementation + */ + public const DIMENSION_NAME = 'scope'; + + /** @var StoreManagerInterface */ + private StoreManagerInterface $storeManager; + + /** @var DimensionFactory */ + private DimensionFactory $dimensionFactory; + + /** @var Account */ + private Account $account; + + /** + * @param StoreManagerInterface $storeManager + * @param DimensionFactory $dimensionFactory + * @param Account $account + */ + public function __construct( + StoreManagerInterface $storeManager, + DimensionFactory $dimensionFactory, + Account $account + ) { + $this->storeManager = $storeManager; + $this->dimensionFactory = $dimensionFactory; + $this->account = $account; + } + + /** + * @inheritDoc + */ + public function getIterator(): Traversable + { + foreach ($this->storeManager->getStores() as $store) { + // instanceof check for Phan + if ($store instanceof Store && $this->account->nostoInstalledAndEnabled($store)) { + yield [ + self::DIMENSION_NAME => $this->dimensionFactory->create( + self::DIMENSION_NAME, + (string)$store->getId() + ) + ]; + } + } + } +} diff --git a/Model/Indexer/IndexerUtil.php b/Model/Indexer/IndexerUtil.php new file mode 100644 index 000000000..dd27c633f --- /dev/null +++ b/Model/Indexer/IndexerUtil.php @@ -0,0 +1,75 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Indexer; + +use Exception; +use Symfony\Component\Console\Input\InputInterface; + +class IndexerUtil +{ + /** Non-ambiguous scope for settings commands */ + public const SETUP_UPGRADE_SCOPE = 'se'; + + /** Non-ambiguous action argument for settings command */ + public const SETUP_UPGRADE_ACTION = 'up'; + + /** + * Checks if the execution scope is from Magento's setup:upgrade + * + * @param InputInterface $input + * @return bool + */ + public function isCalledFromSetupUpgrade(InputInterface $input): bool + { + try { + $parts = explode(':', $input->getFirstArgument()); + if (count($parts) !== 2) { + return false; + } + list($commandScope, $commandAction) = $parts; + $currentCommandScope = substr($commandScope, 0, strlen(self::SETUP_UPGRADE_SCOPE)); + $currentCommandAction = substr($commandAction, 0, strlen(self::SETUP_UPGRADE_ACTION)); + return ( + $currentCommandScope === self::SETUP_UPGRADE_SCOPE + && $currentCommandAction === self::SETUP_UPGRADE_ACTION + ); + // Exception will be thrown if InputInterface\Proxy is instantiated in non-cli context + } catch (Exception $e) { + return false; + } + } +} diff --git a/Model/Indexer/QueueIndexer.php b/Model/Indexer/QueueIndexer.php new file mode 100644 index 000000000..a3b6b2250 --- /dev/null +++ b/Model/Indexer/QueueIndexer.php @@ -0,0 +1,191 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Indexer; + +use Exception; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Indexer\Model\ProcessManager; +use Magento\Store\Model\App\Emulation; +use Magento\Store\Model\Store; +use Nosto\NostoException; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Indexer\Dimensions\Queue\ModeSwitcher as QueueModeSwitcher; +use Nosto\Tagging\Model\Indexer\Dimensions\ModeSwitcherInterface; +use Nosto\Tagging\Model\Indexer\Dimensions\StoreDimensionProvider; +use Nosto\Tagging\Model\ResourceModel\Magento\Product\Collection as ProductCollection; +use Nosto\Tagging\Model\ResourceModel\Magento\Product\CollectionBuilder; +use Nosto\Tagging\Model\Service\Indexer\IndexerStatusServiceInterface; +use Nosto\Tagging\Model\Service\Update\QueueService; +use Nosto\Tagging\Util\PagingIterator; +use Symfony\Component\Console\Input\InputInterface; + +/** + * Class Invalidate + * This class is responsible for listening to product changes + * and setting the `is_dirty` value in `nosto_product_index` table + */ +class QueueIndexer extends AbstractIndexer +{ + public const INDEXER_ID = 'nosto_index_product_queue'; + + /** @var QueueService */ + private QueueService $queueService; + + /** @var CollectionBuilder */ + private CollectionBuilder $productCollectionBuilder; + + /** @var QueueModeSwitcher */ + private QueueModeSwitcher $modeSwitcher; + + /** + * Invalidate constructor. + * @param NostoHelperScope $nostoHelperScope + * @param QueueService $queueService + * @param NostoLogger $logger + * @param CollectionBuilder $productCollectionBuilder + * @param QueueModeSwitcher $modeSwitcher + * @param StoreDimensionProvider $dimensionProvider + * @param Emulation $storeEmulation + * @param ProcessManager $processManager + * @param InputInterface $input + * @param IndexerStatusServiceInterface $indexerStatusService + */ + public function __construct( + NostoHelperScope $nostoHelperScope, + QueueService $queueService, + NostoLogger $logger, + CollectionBuilder $productCollectionBuilder, + QueueModeSwitcher $modeSwitcher, + StoreDimensionProvider $dimensionProvider, + Emulation $storeEmulation, + ProcessManager $processManager, + InputInterface $input, + IndexerStatusServiceInterface $indexerStatusService + ) { + $this->queueService = $queueService; + $this->productCollectionBuilder = $productCollectionBuilder; + $this->modeSwitcher = $modeSwitcher; + parent::__construct( + $nostoHelperScope, + $logger, + $dimensionProvider, + $storeEmulation, + $input, + $indexerStatusService, + $processManager + ); + } + + /** + * @inheritDoc + */ + public function getModeSwitcher(): ModeSwitcherInterface + { + return $this->modeSwitcher; + } + + /** + * @inheritDoc + * @throws NostoException + * @throws Exception + */ + public function doIndex(Store $store, array $ids = []) + { + $collection = $this->getCollection($store, $ids); + $this->queueService->addCollectionToUpsertQueue( + $collection, + $store + ); + $this->handleDeletedProducts($collection, $store, $ids); + } + + /** + * @param ProductCollection $existingCollection + * @param Store $store + * @param array $givenIds + * @throws NostoException + * @throws AlreadyExistsException + */ + private function handleDeletedProducts(ProductCollection $existingCollection, Store $store, array $givenIds) + { + if (!empty($givenIds)) { + $existingCollection->setPageSize(1000); + $iterator = new PagingIterator($existingCollection); + $present = []; + foreach ($iterator as $page) { + foreach ($page->getItems() as $item) { + /** @noinspection PhpPossiblePolymorphicInvocationInspection */ + $id = $item->getId(); + $present[$id] = $id; + } + } + $removed = []; + foreach ($givenIds as $productId) { + if (!isset($present[$productId])) { + $removed[] = $productId; + } + } + if (count($removed) > 0) { + $this->queueService->addIdsToDeleteQueue($removed, $store); + } + } + } + /** + * @inheritDoc + */ + public function getIndexerId(): string + { + return self::INDEXER_ID; + } + + /** + * @param Store $store + * @param array $ids + * @return ProductCollection + */ + public function getCollection(Store $store, array $ids = []): ProductCollection + { + $this->productCollectionBuilder->initDefault($store); + if (!empty($ids)) { + $this->productCollectionBuilder->withIds($ids); + } else { + $this->productCollectionBuilder->withDefaultVisibility($store); + } + return $this->productCollectionBuilder->build(); + } +} diff --git a/Model/Indexer/QueueProcessorIndexer.php b/Model/Indexer/QueueProcessorIndexer.php new file mode 100644 index 000000000..5da350a77 --- /dev/null +++ b/Model/Indexer/QueueProcessorIndexer.php @@ -0,0 +1,151 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Indexer; + +use Exception; +use Magento\Indexer\Model\ProcessManager; +use Magento\Store\Model\App\Emulation; +use Magento\Store\Model\Store; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Indexer\Dimensions\QueueProcessor\ModeSwitcher as QueueProcessorModeSwitcher; +use Nosto\Tagging\Model\Indexer\Dimensions\ModeSwitcherInterface; +use Nosto\Tagging\Model\Indexer\Dimensions\StoreDimensionProvider; +use Nosto\Tagging\Model\ResourceModel\Product\Update\Queue\QueueCollection; +use Nosto\Tagging\Model\ResourceModel\Product\Update\Queue\QueueCollectionBuilder; +use Nosto\Tagging\Model\Service\Indexer\IndexerStatusServiceInterface; +use Nosto\Tagging\Model\Service\Update\QueueProcessorService; +use Symfony\Component\Console\Input\InputInterface; + +/** + * Class Invalidate + * This class is responsible for listening to product changes + * and setting the `is_dirty` value in `nosto_product_index` table + */ +class QueueProcessorIndexer extends AbstractIndexer +{ + public const INDEXER_ID = 'nosto_index_product_queue_processor'; + + /** @var QueueProcessorService */ + private QueueProcessorService $queueProcessorService; + + /** @var QueueCollectionBuilder */ + private QueueCollectionBuilder $queueCollectionBuilder; + + /** @var QueueProcessorModeSwitcher */ + private QueueProcessorModeSwitcher $modeSwitcher; + + /** + * QueueProcessorIndexer constructor. + * @param NostoHelperScope $nostoHelperScope + * @param QueueProcessorService $queueProcessorService + * @param NostoLogger $logger + * @param QueueCollectionBuilder $queueCollectionBuilder + * @param QueueProcessorModeSwitcher $modeSwitcher + * @param StoreDimensionProvider $dimensionProvider + * @param Emulation $storeEmulation + * @param ProcessManager $processManager + * @param InputInterface $input + * @param IndexerStatusServiceInterface $indexerStatusService + */ + public function __construct( + NostoHelperScope $nostoHelperScope, + QueueProcessorService $queueProcessorService, + NostoLogger $logger, + QueueCollectionBuilder $queueCollectionBuilder, + QueueProcessorModeSwitcher $modeSwitcher, + StoreDimensionProvider $dimensionProvider, + Emulation $storeEmulation, + ProcessManager $processManager, + InputInterface $input, + IndexerStatusServiceInterface $indexerStatusService + ) { + $this->queueProcessorService = $queueProcessorService; + $this->queueCollectionBuilder = $queueCollectionBuilder; + $this->modeSwitcher = $modeSwitcher; + parent::__construct( + $nostoHelperScope, + $logger, + $dimensionProvider, + $storeEmulation, + $input, + $indexerStatusService, + $processManager + ); + } + + /** + * @inheritDoc + */ + public function getModeSwitcher(): ModeSwitcherInterface + { + return $this->modeSwitcher; + } + + /** + * @inheritDoc + * @throws Exception + */ + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter + public function doIndex(Store $store, array $ids = []) + { + $collection = $this->getCollection($store); + $this->queueProcessorService->processQueueCollection($collection, $store); + } + + /** + * @inheritDoc + */ + public function getIndexerId(): string + { + return self::INDEXER_ID; + } + + /** + * @param Store $store + * @return QueueCollection + */ + public function getCollection(Store $store) + { + // Fetch always all queue entries having status new. + // It makes the merging of queues more efficient. + return $this->queueCollectionBuilder + ->initDefault($store) + ->withStatusNew() + ->build(); + } +} diff --git a/Model/Item/Bundle.php b/Model/Item/Bundle.php new file mode 100644 index 000000000..86412ffb0 --- /dev/null +++ b/Model/Item/Bundle.php @@ -0,0 +1,45 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Item; + +use Magento\Catalog\Model\Product\Type; + +class Bundle +{ + /** Product type for bundled item */ + public const TYPE = Type::TYPE_BUNDLE; +} diff --git a/Model/Item/Configurable.php b/Model/Item/Configurable.php new file mode 100644 index 000000000..18ad48566 --- /dev/null +++ b/Model/Item/Configurable.php @@ -0,0 +1,45 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Item; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as Type; + +class Configurable +{ + /** Product type for configurable item */ + public const TYPE = Type::TYPE_CODE; +} diff --git a/Model/Item/Downloadable.php b/Model/Item/Downloadable.php new file mode 100644 index 000000000..7b215e69d --- /dev/null +++ b/Model/Item/Downloadable.php @@ -0,0 +1,45 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Item; + +use Magento\Downloadable\Model\Product\Type; + +class Downloadable +{ + /** Product type for downloadable item */ + public const TYPE = Type::TYPE_DOWNLOADABLE; +} diff --git a/Model/Item/Giftcard.php b/Model/Item/Giftcard.php new file mode 100644 index 000000000..6f0bc43a3 --- /dev/null +++ b/Model/Item/Giftcard.php @@ -0,0 +1,43 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Item; + +class Giftcard +{ + /** Product type for gift card item */ + public const TYPE = 'giftcard'; +} diff --git a/Model/Item/Grouped.php b/Model/Item/Grouped.php new file mode 100644 index 000000000..c651c2d5d --- /dev/null +++ b/Model/Item/Grouped.php @@ -0,0 +1,45 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Item; + +use Magento\GroupedProduct\Model\Product\Type\Grouped as Type; + +class Grouped +{ + /** Product type for grouped item */ + public const TYPE = Type::TYPE_CODE; +} diff --git a/Model/Item/Simple.php b/Model/Item/Simple.php new file mode 100644 index 000000000..14ad4542c --- /dev/null +++ b/Model/Item/Simple.php @@ -0,0 +1,95 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Item; + +use InvalidArgumentException; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Framework\App\ObjectManager; +use Magento\Quote\Model\Quote\Item; +use Magento\Sales\Model\Order\Item as SalesItem; +use Throwable; + +class Simple +{ + /** Product type for simple item */ + public const TYPE = Type::TYPE_SIMPLE; + + /** + * @param SalesItem|Item $item + * @param $parentIds + * @return string + */ + public function buildName($item, $parentIds): string + { + if (!$item instanceof Item && !$item instanceof SalesItem) { + throw new InvalidArgumentException( + 'item should be instance of Magento\Quote\Model\Quote\Item or Magento\Sales\Model\Order\Item' + ); + } + $name = $item->getName(); + $optNames = []; + $objectManager = ObjectManager::getInstance(); + // If the product has a configurable parent, we assume we should tag + // the parent. If there are many parent IDs, we are safer to tag the + // products own name alone. + if (count($parentIds) === 1) { + try { + $attributes = $item->getBuyRequest()->getData('super_attribute'); + if (is_array($attributes)) { + foreach ($attributes as $id => $value) { + /** @var Attribute $attribute */ + $attribute = $objectManager->get(Attribute::class)->load($id); // @codingStandardsIgnoreLine + $label = $attribute->getSource()->getOptionText($value); + if (!empty($label)) { + $optNames[] = $label; + } + } + } + } catch (Throwable $e) { + // If the item name building fails, it's not crucial + // No need to handle the exception in any specific way + unset($e); + } + } + + if (!empty($optNames)) { + $name .= ' (' . implode(', ', $optNames) . ')'; + } + return (string)$name; + } +} diff --git a/Model/Item/Virtual.php b/Model/Item/Virtual.php new file mode 100644 index 000000000..3db5fc0db --- /dev/null +++ b/Model/Item/Virtual.php @@ -0,0 +1,45 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Item; + +use Magento\Catalog\Model\Product\Type; + +class Virtual +{ + /** Product type for configurable item */ + public const TYPE = Type::TYPE_VIRTUAL; +} diff --git a/Model/Meta/Account/Billing/Builder.php b/Model/Meta/Account/Billing/Builder.php index d627cf055..f4cb79896 100644 --- a/Model/Meta/Account/Billing/Builder.php +++ b/Model/Meta/Account/Billing/Builder.php @@ -1,64 +1,81 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Model\Meta\Account\Billing; +use Exception; +use Magento\Framework\Event\ManagerInterface; use Magento\Store\Model\Store; -use Psr\Log\LoggerInterface; +use Nosto\Model\Signup\Billing; +use Nosto\Tagging\Logger\Logger as NostoLogger; class Builder { - protected $_logger; + private NostoLogger $logger; + private ManagerInterface $eventManager; /** - * @param LoggerInterface $logger + * @param NostoLogger $logger + * @param ManagerInterface $eventManager */ - public function __construct(LoggerInterface $logger) + public function __construct(NostoLogger $logger, ManagerInterface $eventManager) { - $this->_logger = $logger; + $this->logger = $logger; + $this->eventManager = $eventManager; } /** * @param Store $store - * @return \NostoBilling + * @return Billing */ public function build(Store $store) { - $metaData = new \NostoBilling(); + $metaData = new Billing(); try { $country = $store->getConfig('general/country/default'); - if (!empty($country)) { - $metaData->setCountry(new \NostoCountryCode($country)); + if ($country !== null) { + $metaData->setCountry($country); } - } catch (\NostoException $e) { - $this->_logger->error($e, ['exception' => $e]); + } catch (Exception $e) { + $this->logger->exception($e); } + $this->eventManager->dispatch('nosto_account_billing_load_after', ['billing' => $metaData]); + return $metaData; } } diff --git a/Model/Meta/Account/Builder.php b/Model/Meta/Account/Builder.php index 428fa71a1..89ef55cd4 100644 --- a/Model/Meta/Account/Builder.php +++ b/Model/Meta/Account/Builder.php @@ -1,74 +1,103 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Model\Meta\Account; +use Exception; +use Magento\Framework\Event\ManagerInterface; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\UrlInterface; use Magento\Store\Model\Store; -use Nosto\Tagging\Helper\Currency; -use Nosto\Tagging\Helper\Data; -use Nosto\Tagging\Model\Meta\Account\Billing\Builder as BillingBuilder; -use Nosto\Tagging\Model\Meta\Account\Owner\Builder as OwnerBuilder; -use Psr\Log\LoggerInterface; +use Nosto\Model\Signup\Signup; +use Nosto\Request\Http\HttpRequest; +use Nosto\Tagging\Helper\Currency as NostoHelperCurrency; +use Nosto\Tagging\Helper\Data as NostoDataHelper; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Meta\Account\Billing\Builder as NostoBillingBuilder; +use Nosto\Tagging\Model\Meta\Account\Settings\Currencies\Builder as NostoCurrenciesBuilder; +use stdClass; class Builder { + public const API_TOKEN = 'YBDKYwSqTCzSsU8Bwbg4im2pkHMcgTy9cCX7vevjJwON1UISJIwXOLMM0a8nZY7h'; + public const PLATFORM_NAME = 'magento'; + private NostoBillingBuilder $accountBillingMetaBuilder; + private ResolverInterface $localeResolver; + private NostoLogger $logger; + private ManagerInterface $eventManager; + private NostoHelperCurrency $nostoHelperCurrency; + private NostoCurrenciesBuilder $nostoCurrenciesBuilder; + private NostoDataHelper $nostoDataHelper; + /** - * @param Data $dataHelper - * @param Currency $currencyHelper - * @param OwnerBuilder $accountOwnerMetaBuilder - * @param BillingBuilder $accountBillingMetaBuilder + * @param NostoHelperCurrency $nostoHelperCurrency + * @param NostoBillingBuilder $nostoAccountBillingMetaBuilder + * @param NostoCurrenciesBuilder $nostoCurrenciesBuilder * @param ResolverInterface $localeResolver - * @param LoggerInterface $logger + * @param NostoLogger $logger + * @param ManagerInterface $eventManager + * @param NostoDataHelper $nostoDataHelper */ public function __construct( - Data $dataHelper, - Currency $currencyHelper, - OwnerBuilder $accountOwnerMetaBuilder, - BillingBuilder $accountBillingMetaBuilder, + NostoHelperCurrency $nostoHelperCurrency, + NostoBillingBuilder $nostoAccountBillingMetaBuilder, + NostoCurrenciesBuilder $nostoCurrenciesBuilder, ResolverInterface $localeResolver, - LoggerInterface $logger + NostoLogger $logger, + ManagerInterface $eventManager, + NostoDataHelper $nostoDataHelper ) { - $this->_dataHelper = $dataHelper; - $this->_currencyHelper = $currencyHelper; - $this->_accountOwnerMetaBuilder = $accountOwnerMetaBuilder; - $this->_accountBillingMetaBuilder = $accountBillingMetaBuilder; - $this->_localeResolver = $localeResolver; - $this->_logger = $logger; + $this->accountBillingMetaBuilder = $nostoAccountBillingMetaBuilder; + $this->localeResolver = $localeResolver; + $this->logger = $logger; + $this->eventManager = $eventManager; + $this->nostoHelperCurrency = $nostoHelperCurrency; + $this->nostoCurrenciesBuilder = $nostoCurrenciesBuilder; + $this->nostoDataHelper = $nostoDataHelper; } /** * @param Store $store - * @return \NostoAccount + * @param $accountOwner + * @param stdClass|array $signupDetails + * @return Signup */ - public function build(Store $store) + public function build(Store $store, $accountOwner, $signupDetails) { - $metaData = new \NostoAccount(); + $metaData = new Signup(self::PLATFORM_NAME, self::API_TOKEN, null); try { $metaData->setTitle( @@ -81,32 +110,40 @@ public function build(Store $store) ] ) ); - $metaData->setName(substr(sha1(rand()), 0, 8)); - $metaData->setFrontPageUrl( - \NostoHttpRequest::replaceQueryParamInUrl( + $metaData->setName(substr(sha1((string)rand()), 0, 8)); + $url = $store->getBaseUrl(UrlInterface::URL_TYPE_WEB); + if ($this->nostoDataHelper->getStoreCodeToUrl($store)) { + $url = HttpRequest::replaceQueryParamInUrl( '___store', $store->getCode(), - $store->getBaseUrl(UrlInterface::URL_TYPE_WEB) - ) - ); - - $metaData->setCurrency( - new \NostoCurrencyCode($store->getBaseCurrencyCode()) - ); + $url + ); + } + $metaData->setFrontPageUrl($url); + $metaData->setCurrencies($this->nostoCurrenciesBuilder->build($store)); + $metaData->setCurrencyCode($this->nostoHelperCurrency->getTaggingCurrency($store)->getCode()); $lang = substr($store->getConfig('general/locale/code'), 0, 2); - $metaData->setLanguage(new \NostoLanguageCode($lang)); - $lang = substr($this->_localeResolver->getLocale(), 0, 2); - $metaData->setOwnerLanguage(new \NostoLanguageCode($lang)); + $metaData->setLanguageCode($lang); + $lang = substr($this->localeResolver->getLocale(), 0, 2); + $metaData->setOwnerLanguageCode($lang); + $metaData->setOwner($accountOwner); + if ($this->nostoHelperCurrency->exchangeRatesInUse($store)) { + $metaData->setDefaultVariantId( + $this->nostoHelperCurrency->getTaggingCurrency($store) + ->getCode() + ); + } - $owner = $this->_accountOwnerMetaBuilder->build(); - $metaData->setOwner($owner); + $billing = $this->accountBillingMetaBuilder->build($store); + $metaData->setBillingDetails($billing); - $billing = $this->_accountBillingMetaBuilder->build($store); - $metaData->setBilling($billing); - } catch (\NostoException $e) { - $this->_logger->error($e, ['exception' => $e]); + $metaData->setDetails($signupDetails); + } catch (Exception $e) { + $this->logger->exception($e); } + $this->eventManager->dispatch('nosto_account_load_after', ['account' => $metaData]); + return $metaData; } } diff --git a/Model/Meta/Account/Iframe/Builder.php b/Model/Meta/Account/Iframe/Builder.php index 45202ac9b..45e8717da 100644 --- a/Model/Meta/Account/Iframe/Builder.php +++ b/Model/Meta/Account/Iframe/Builder.php @@ -1,91 +1,115 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Model\Meta\Account\Iframe; +use Magento\Backend\Model\Auth\Session; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Locale\ResolverInterface; use Magento\Store\Model\Store; -use Nosto\Tagging\Helper\Data; -use Nosto\Tagging\Helper\Url; -use Psr\Log\LoggerInterface; +use Nosto\NostoException; +use Nosto\Model\Iframe; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Helper\Url as NostoHelperUrl; +use Nosto\Tagging\Logger\Logger as NostoLogger; class Builder { - private $_urlHelper; - private $_dataHelper; - private $_localeResolver; - private $_logger; + private NostoHelperUrl $nostoHelperUrl; + private NostoHelperData $nostoHelperData; + private ResolverInterface $localeResolver; + private Session $backendAuthSession; + private NostoLogger $logger; + private ManagerInterface $eventManager; /** - * @param Url $urlHelper - * @param Data $dataHelper + * @param NostoHelperUrl $nostoHelperUrl + * @param NostoHelperData $nostoHelperData + * @param Session $backendAuthSession * @param ResolverInterface $localeResolver - * @param LoggerInterface $logger + * @param NostoLogger $logger + * @param ManagerInterface $eventManager */ public function __construct( - Url $urlHelper, - Data $dataHelper, + NostoHelperUrl $nostoHelperUrl, + NostoHelperData $nostoHelperData, + Session $backendAuthSession, ResolverInterface $localeResolver, - LoggerInterface $logger + NostoLogger $logger, + ManagerInterface $eventManager ) { - $this->_urlHelper = $urlHelper; - $this->_dataHelper = $dataHelper; - $this->_localeResolver = $localeResolver; - $this->_logger = $logger; + $this->nostoHelperUrl = $nostoHelperUrl; + $this->nostoHelperData = $nostoHelperData; + $this->backendAuthSession = $backendAuthSession; + $this->localeResolver = $localeResolver; + $this->logger = $logger; + $this->eventManager = $eventManager; } /** * @param Store $store - * @return \NostoIframe + * @return Iframe + * @throws LocalizedException */ public function build(Store $store) { - $metaData = new \NostoIframe(); - - try { - $metaData->setUniqueId($this->_dataHelper->getInstallationId()); - - $lang = substr($this->_localeResolver->getLocale(), 0, 2); - $metaData->setLanguage(new \NostoLanguageCode($lang)); - $lang = substr($store->getConfig('general/locale/code'), 0, 2); - $metaData->setShopLanguage(new \NostoLanguageCode($lang)); - - $metaData->setShopName($store->getName()); - $metaData->setUniqueId($this->_dataHelper->getInstallationId()); - $metaData->setVersionPlatform($this->_dataHelper->getPlatformVersion()); - $metaData->setVersionModule($this->_dataHelper->getModuleVersion()); - $metaData->setPreviewUrlProduct($this->_urlHelper->getPreviewUrlProduct($store)); - $metaData->setPreviewUrlCategory($this->_urlHelper->getPreviewUrlCategory($store)); - $metaData->setPreviewUrlSearch($this->_urlHelper->getPreviewUrlSearch($store)); - $metaData->setPreviewUrlCart($this->_urlHelper->getPreviewUrlCart($store)); - $metaData->setPreviewUrlFront($this->_urlHelper->getPreviewUrlFront($store)); - } catch (\NostoException $e) { - $this->_logger->error($e, ['exception' => $e]); + $metaData = new Iframe(); + $metaData->setUniqueId($this->nostoHelperData->getInstallationId()); + $lang = substr($this->localeResolver->getLocale(), 0, 2); + $metaData->setLanguageIsoCode($lang); + $lang = substr($store->getConfig('general/locale/code'), 0, 2); + $metaData->setLanguageIsoCodeShop($lang); + if ($this->backendAuthSession->getUser()) { + $metaData->setEmail($this->backendAuthSession->getUser()->getEmail()); + } else { + $this->logger->exception(new NostoException('Could not get user from Backend Auth Session')); } + $metaData->setPlatform('magento'); + $metaData->setShopName($store->getName()); + $metaData->setUniqueId($this->nostoHelperData->getInstallationId()); + $metaData->setVersionPlatform($this->nostoHelperData->getPlatformVersion()); + $metaData->setVersionModule($this->nostoHelperData->getModuleVersion()); + $metaData->setPreviewUrlProduct($this->nostoHelperUrl->getPreviewUrlProduct($store)); + $metaData->setPreviewUrlCategory($this->nostoHelperUrl->getPreviewUrlCategory($store)); + $metaData->setPreviewUrlSearch($this->nostoHelperUrl->getPreviewUrlSearch($store)); + $metaData->setPreviewUrlCart($this->nostoHelperUrl->getPreviewUrlCart($store)); + $metaData->setPreviewUrlFront($this->nostoHelperUrl->getPreviewUrlFront($store)); + + $this->eventManager->dispatch('nosto_iframe_load_after', ['iframe' => $metaData]); return $metaData; } diff --git a/Model/Meta/Account/Owner/Builder.php b/Model/Meta/Account/Owner/Builder.php index 9f5511529..ba21ed9dc 100644 --- a/Model/Meta/Account/Owner/Builder.php +++ b/Model/Meta/Account/Owner/Builder.php @@ -1,69 +1,88 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Model\Meta\Account\Owner; +use Exception; use Magento\Backend\Model\Auth\Session; -use Psr\Log\LoggerInterface; +use Magento\Framework\Event\ManagerInterface; +use Nosto\Model\Signup\Owner; +use Nosto\Tagging\Logger\Logger as NostoLogger; class Builder { - protected $_logger; + private NostoLogger $logger; + private Session $backendAuthSession; + private ManagerInterface $eventManager; /** * @param Session $backendAuthSession - * @param LoggerInterface $logger + * @param NostoLogger $logger + * @param ManagerInterface $eventManager */ public function __construct( Session $backendAuthSession, - LoggerInterface $logger + NostoLogger $logger, + ManagerInterface $eventManager ) { - $this->_backendAuthSession = $backendAuthSession; - $this->_logger = $logger; + $this->backendAuthSession = $backendAuthSession; + $this->logger = $logger; + $this->eventManager = $eventManager; } /** - * @return \NostoOwner + * @return Owner */ public function build() { - $metaData = new \NostoOwner(); + $owner = new Owner(); try { - $user = $this->_backendAuthSession->getUser(); - if (!is_null($user)) { - $metaData->setFirstName($user->getFirstname()); - $metaData->setLastName($user->getLastname()); - $metaData->setEmail($user->getEmail()); + $user = $this->backendAuthSession->getUser(); + if ($user !== null) { + $owner->setFirstName($user->getFirstName()); + $owner->setLastName($user->getLastName()); + $owner->setEmail($user->getEmail()); } - } catch (\NostoException $e) { - $this->_logger->error($e, ['exception' => $e]); + } catch (Exception $e) { + $this->logger->exception($e); } - return $metaData; + $this->eventManager->dispatch('nosto_account_owner_load_after', ['owner' => $owner]); + + return $owner; } } diff --git a/Model/Meta/Account/Settings/Builder.php b/Model/Meta/Account/Settings/Builder.php new file mode 100644 index 000000000..01c8a2300 --- /dev/null +++ b/Model/Meta/Account/Settings/Builder.php @@ -0,0 +1,151 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Meta\Account\Settings; + +use Exception; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\UrlInterface; +use Magento\Store\Model\Store; +use Nosto\Model\Settings; +use Nosto\Request\Http\HttpRequest; +use Nosto\Tagging\Helper\Currency as NostoHelperCurrency; +use Nosto\Tagging\Helper\Data as NostoDataHelper; +use Nosto\Tagging\Helper\Variation as NostoVariationHelper; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Meta\Account\Settings\Currencies\Builder as NostoCurrenciesBuilder; + +class Builder +{ + private NostoLogger $logger; + private ManagerInterface $eventManager; + private NostoCurrenciesBuilder $nostoCurrenciesBuilder; + private NostoHelperCurrency $nostoHelperCurrency; + private NostoDataHelper $nostoDataHelper; + private NostoVariationHelper $nostoVariationHelper; + + /** + * Builder constructor. + * @param NostoLogger $logger + * @param ManagerInterface $eventManager + * @param NostoHelperCurrency $nostoHelperCurrency + * @param NostoCurrenciesBuilder $nostoCurrenciesBuilder + * @param NostoDataHelper $nostoDataHelper + * @param NostoVariationHelper $nostoVariationHelper + */ + public function __construct( + NostoLogger $logger, + ManagerInterface $eventManager, + NostoHelperCurrency $nostoHelperCurrency, + NostoCurrenciesBuilder $nostoCurrenciesBuilder, + NostoDataHelper $nostoDataHelper, + NostoVariationHelper $nostoVariationHelper + ) { + $this->logger = $logger; + $this->eventManager = $eventManager; + $this->nostoCurrenciesBuilder = $nostoCurrenciesBuilder; + $this->nostoHelperCurrency = $nostoHelperCurrency; + $this->nostoDataHelper = $nostoDataHelper; + $this->nostoVariationHelper = $nostoVariationHelper; + } + + /** + * @param Store $store + * @return Settings + */ + public function build(Store $store) + { + $settings = new Settings(); + + try { + $settings->setTitle($this->buildTitle($store)); + $settings->setFrontPageUrl($this->buildURL($store)); + $settings->setCurrencyCode($this->nostoHelperCurrency->getTaggingCurrency($store)->getCode()); + $settings->setLanguageCode(substr($store->getConfig('general/locale/code'), 0, 2)); + $settings->setUseCurrencyExchangeRates($this->nostoHelperCurrency->exchangeRatesInUse($store)); + if ($this->nostoHelperCurrency->exchangeRatesInUse($store)) { + $settings->setDefaultVariantId($this->nostoHelperCurrency->getTaggingCurrency($store)->getCode()); + } elseif ($this->nostoDataHelper->isPricingVariationEnabled($store)) { + $settings->setDefaultVariantId($this->nostoVariationHelper->getDefaultVariationCode()); + } + $settings->setCurrencies($this->nostoCurrenciesBuilder->build($store)); + } catch (Exception $e) { + $this->logger->exception($e); + } + + $this->eventManager->dispatch('nosto_settings_load_after', ['settings' => $settings]); + + return $settings; + } + + /** + * Helper method to correctly build the store's front page URL by using the store's base URL and + * explicitly adding the ___store parameter + * + * @param Store $store the store for which to build the front-page URL + * @return string the absolute front-page URL of the store + */ + private function buildURL(Store $store) + { + $url = $store->getBaseUrl(UrlInterface::URL_TYPE_WEB); + if ($this->nostoDataHelper->getStoreCodeToUrl($store)) { + $url = HttpRequest::replaceQueryParamInUrl( + '___store', + $store->getCode(), + $url + ); + } + + return $url; + } + + /** + * Helper method to correctly build the store's name for readability by concatenating the name + * of the website, group and store + * + * @param Store $store the store for which to build the common name + * @return string the complete common name of the store + * @throws NoSuchEntityException + */ + private function buildTitle(Store $store): string + { + return implode( + ' - ', + [$store->getWebsite()->getName(), $store->getGroup()->getName(), $store->getName()] + ); + } +} diff --git a/Model/Meta/Account/Settings/Currencies/Builder.php b/Model/Meta/Account/Settings/Currencies/Builder.php new file mode 100644 index 000000000..5153e1dc2 --- /dev/null +++ b/Model/Meta/Account/Settings/Currencies/Builder.php @@ -0,0 +1,281 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Meta\Account\Settings\Currencies; + +use Exception; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Locale\Bundle\DataBundle; +use Magento\Framework\Locale\ResolverInterface as LocaleResolver; +use Magento\Store\Model\Store; +use Nosto\Model\Format; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Helper\Currency as NostoHelperCurrency; + +class Builder +{ + /** @var NostoLogger */ + private NostoLogger $logger; + + /** @var ManagerInterface */ + private ManagerInterface $eventManager; + + /** @var CurrencyFactory */ + private CurrencyFactory $currencyFactory; + + /** @var LocaleResolver */ + private LocaleResolver $localeResolver; + + /** @var NostoHelperCurrency */ + private NostoHelperCurrency $nostoCurrencyHelper; + + /* List of zero decimal currencies in compliance with ISO-4217 */ + public const ZERO_DECIMAL_CURRENCIES = [ + 'XOF', + 'BIF', + 'XAF', + 'CLP', + 'KMF', + 'DJF', + 'GNF', + 'ISK', + 'JPY', + 'KRW', + 'PYG', + 'RWF', + 'UGX', + 'UYI', + 'VUV', + 'VND', + 'XPF' + ]; + + /** + * @param NostoLogger $logger + * @param ManagerInterface $eventManager + * @param CurrencyFactory $currencyFactory + * @param NostoHelperCurrency $nostoCurrencyHelper + * @param LocaleResolver $localeResolver + */ + public function __construct( + NostoLogger $logger, + ManagerInterface $eventManager, + CurrencyFactory $currencyFactory, + NostoHelperCurrency $nostoCurrencyHelper, + LocaleResolver $localeResolver + ) { + $this->logger = $logger; + $this->eventManager = $eventManager; + $this->currencyFactory = $currencyFactory; + $this->nostoCurrencyHelper = $nostoCurrencyHelper; + $this->localeResolver = $localeResolver; + } + + /** + * @param Store $store + * @return array + * @suppress PhanTypeArraySuspicious + */ + public function build(Store $store) + { + $currencies = []; + try { + $storeLocale = $store->getConfig('general/locale/code'); + $localeCode = $storeLocale ?: $this->localeResolver->getLocale(); + $localeData = (new DataBundle())->get($localeCode); + $defaultSet = $localeData['NumberElements']['default'] ?: 'latn'; + + $priceFormat = $this->getPriceFormat($localeData, $defaultSet); + $decimalSymbol = $this->buildDecimalSymbol($localeData, $defaultSet); + $groupSymbol = $this->buildGroupSymbol($localeData, $defaultSet); + $precision = $this->getDecimalPrecision($priceFormat); + + // Get other active currencies when multicurrency is enabled + if ($this->nostoCurrencyHelper->exchangeRatesInUse($store)) { + $currencyCodes = $store->getAvailableCurrencyCodes(true); + } else { + $currencyCodes = [$store->getBaseCurrencyCode()]; + } + if (is_array($currencyCodes) && !empty($currencyCodes)) { + foreach ($currencyCodes as $currencyCode) { + $finalPrecision = $this->isZeroDecimalCurrency($currencyCode) ? 0 : $precision; + $currency = $this->currencyFactory->create()->load($currencyCode); // @codingStandardsIgnoreLine + $currencies[$currency->getCode()] = new Format( + $this->isSymbolBeforeAmount($localeData, $defaultSet), + $currency->getCurrencySymbol(), + $decimalSymbol, + $groupSymbol, + $finalPrecision + ); + } + } + } catch (Exception $e) { + $this->logger->exception($e); + } + + $this->eventManager->dispatch('nosto_currencies_load_after', ['currencies' => $currencies]); + + return $currencies; + } + + /** + * Returns the decimal precision used by the currency + * + * @param $priceFormat + * @return bool|int + */ + private function getDecimalPrecision($priceFormat) + { + $precision = 0; + if (($decimalPos = strpos($priceFormat, '.')) !== false) { + $precision = (strlen($priceFormat) - (strrpos($priceFormat, '.') + 1)); + } else { + $decimalPos = strlen($priceFormat); + } + $decimalFormat = substr($priceFormat, $decimalPos); + if (($pos = strpos($decimalFormat, '#')) !== false) { + $precision = strlen($decimalFormat) - $pos - $precision; + } + return $precision; + } + + /** + * Returns true if currency is defined to have no decimal part + * according to ISO-4217 + * + * @param $currencyCode + * @return bool + */ + private function isZeroDecimalCurrency($currencyCode) + { + return in_array($currencyCode, self::ZERO_DECIMAL_CURRENCIES); + } + + /** + * Returns true if symbol position is before the amount, false otherwise. + * + * @param $localeData + * @param $defaultSet + * @return bool + */ + private function isSymbolBeforeAmount($localeData, $defaultSet) + { + // Check if the currency symbol is before or after the amount. + $priceFormat = $this->getPriceFormat($localeData, $defaultSet); + return strpos(trim($priceFormat), '¤') !== 0; + } + + /** + * Returns the price format from the locale only with + * the following characters: ["0", "#", ".", ",",] + * + * @param $localeData + * @param $defaultSet + * @return null|string|string[] + */ + private function getPriceFormat($localeData, $defaultSet) + { + $priceFormat = $this->buildPriceFormatWithSymbol($localeData, $defaultSet); + return $this->clearPriceFormat($priceFormat); + } + + /** + * Removes currency symbol from the price format. + * Returns in a format like '#,##0.00' + * + * @param $priceFormat + * @return null|string|string[] + */ + private function clearPriceFormat($priceFormat) + { + // Remove extra part, e.g. "¤ #,##0.00; (¤ #,##0.00)" => "¤ #,##0.00". + if (($pos = strpos($priceFormat, ';')) !== false) { + $priceFormat = substr($priceFormat, 0, $pos); + } + // Remove all other characters than "0", "#", "." and ",", + return preg_replace('/[^0#.]/', '', $priceFormat); + } + + /** + * Returns the price format with symbol position, thousands and decimal digits + * + * @param $localeData + * @param $defaultSet + * @return mixed + * @suppress PhanTypeArraySuspicious + */ + private function buildPriceFormatWithSymbol($localeData, $defaultSet) + { + if ($localeData['NumberElements'][$defaultSet]['patterns']['currencyFormat']) { + return $localeData['NumberElements']['latn']['patterns']['currencyFormat']; + } + return explode(';', $localeData['NumberPatterns'][1])[0]; + } + + /** + * Returns the symbol used to separate decimal digits + * + * @param $localeData + * @param $defaultSet + * @return mixed + * @suppress PhanTypeArraySuspicious + */ + private function buildDecimalSymbol($localeData, $defaultSet) + { + if ($localeData['NumberElements'][$defaultSet]['symbols']['decimal']) { + return $localeData['NumberElements']['latn']['symbols']['decimal']; + } + return $localeData['NumberElements'][0]; + } + + /** + * Returns the symbol used to separate thousands + * + * @param $localeData + * @param $defaultSet + * @return mixed + * @suppress PhanTypeArraySuspicious + */ + private function buildGroupSymbol($localeData, $defaultSet) + { + if ($localeData['NumberElements'][$defaultSet]['symbols']['group']) { + return $localeData['NumberElements']['latn']['symbols']['group']; + } + return $localeData['NumberElements'][1]; + } +} diff --git a/Model/Meta/Account/Settings/Service.php b/Model/Meta/Account/Settings/Service.php new file mode 100644 index 000000000..28267828c --- /dev/null +++ b/Model/Meta/Account/Settings/Service.php @@ -0,0 +1,93 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Meta\Account\Settings; + +use Exception; +use Magento\Store\Model\Store; +use Nosto\Operation\UpdateSettings; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Meta\Account\Settings\Builder as NostoSettingsBuilder; + +class Service +{ + private NostoLogger $logger; + private NostoHelperAccount $nostoHelperAccount; + private Builder $nostoSettingsBuilder; + + /** + * @param NostoLogger $logger + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoSettingsBuilder $nostoSettingsBuilder + */ + public function __construct( + NostoLogger $logger, + NostoHelperAccount $nostoHelperAccount, + NostoSettingsBuilder $nostoSettingsBuilder + ) { + $this->logger = $logger; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->nostoSettingsBuilder = $nostoSettingsBuilder; + } + + /** + * Sends a account settings update request to Nosto via the API. + * + * @param Store $store the store for which the settings are to be updated. + * @return bool a boolean value indicating whether the operation was successful + */ + public function update(Store $store) + { + if ($account = $this->nostoHelperAccount->findAccount($store)) { + $settings = $this->nostoSettingsBuilder->build($store); + + try { + $service = new UpdateSettings($account); + return $service->update($settings); + } catch (Exception $e) { + $this->logger->exception($e); + } + } else { + $this->logger->info( + 'Skipping update; an account doesn\'t exist for ' . + $store->getName() + ); + } + + return false; + } +} diff --git a/Model/Meta/Account/Sso/Builder.php b/Model/Meta/Account/Sso/Builder.php index a3be2c788..ddea7d538 100644 --- a/Model/Meta/Account/Sso/Builder.php +++ b/Model/Meta/Account/Sso/Builder.php @@ -1,72 +1,88 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Model\Meta\Account\Sso; +use Exception; use Magento\Backend\Model\Auth\Session; -use NostoSso; -use Psr\Log\LoggerInterface; +use Magento\Framework\Event\ManagerInterface; +use Nosto\Model\Signup\Owner; +use Nosto\Tagging\Logger\Logger as NostoLogger; class Builder { - protected $_factory; - protected $_logger; - private $_backendAuthSession; + private NostoLogger $logger; + private Session $backendAuthSession; + private ManagerInterface $eventManager; /** * @param Session $backendAuthSession - * @param LoggerInterface $logger + * @param NostoLogger $logger + * @param ManagerInterface $eventManager */ public function __construct( Session $backendAuthSession, - LoggerInterface $logger + NostoLogger $logger, + ManagerInterface $eventManager ) { - $this->_backendAuthSession = $backendAuthSession; - $this->_logger = $logger; + $this->backendAuthSession = $backendAuthSession; + $this->logger = $logger; + $this->eventManager = $eventManager; } /** - * @return NostoSso + * @return Owner */ public function build() { - $metaData = new NostoSso(); + $owner = new Owner(); try { - $user = $this->_backendAuthSession->getUser(); - if (!is_null($user)) { - $metaData->setFirstName($user->getFirstname()); - $metaData->setLastName($user->getLastname()); - $metaData->setEmail($user->getEmail()); + $user = $this->backendAuthSession->getUser(); + if ($user !== null) { + $owner->setFirstName($user->getFirstName()); + $owner->setLastName($user->getLastName()); + $owner->setEmail($user->getEmail()); } - } catch (\NostoException $e) { - $this->_logger->error($e, ['exception' => $e]); + } catch (Exception $e) { + $this->logger->exception($e); } - return $metaData; + $this->eventManager->dispatch('nosto_sso_load_after', ['sso' => $owner]); + + return $owner; } } diff --git a/Model/Meta/Oauth/Builder.php b/Model/Meta/Oauth/Builder.php index 3442bd8c5..30635c803 100644 --- a/Model/Meta/Oauth/Builder.php +++ b/Model/Meta/Oauth/Builder.php @@ -1,83 +1,115 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Model\Meta\Oauth; +use Exception; +use Magento\Framework\Event\ManagerInterface; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Url; use Magento\Store\Model\Store; -use Psr\Log\LoggerInterface; +use Nosto\OAuth; +use Nosto\Model\Signup\Account; +use Nosto\Request\Api\Token; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Logger\Logger as NostoLogger; class Builder { + private ResolverInterface $localeResolver; + private Url $urlBuilder; + private NostoLogger $logger; + private ManagerInterface $eventManager; + private NostoHelperData $nostoHelperData; + /** * @param ResolverInterface $localeResolver * @param Url $urlBuilder - * @param LoggerInterface $logger + * @param NostoLogger $logger + * @param ManagerInterface $eventManager + * @param NostoHelperData $nostoHelperData */ public function __construct( ResolverInterface $localeResolver, Url $urlBuilder, - LoggerInterface $logger + NostoLogger $logger, + ManagerInterface $eventManager, + NostoHelperData $nostoHelperData ) { - $this->_localeResolver = $localeResolver; - $this->_urlBuilder = $urlBuilder; - $this->_logger = $logger; + $this->localeResolver = $localeResolver; + $this->urlBuilder = $urlBuilder; + $this->logger = $logger; + $this->eventManager = $eventManager; + $this->nostoHelperData = $nostoHelperData; } /** * @param Store $store - * @param \NostoAccount $account - * @return \NostoOauth + * @param Account|null $account + * @return OAuth */ - public function build(Store $store, \NostoAccount $account = null) + public function build(Store $store, Account $account = null) { - $metaData = new \NostoOauth(); + $metaData = new OAuth(); try { - $metaData->setScopes(\NostoApiToken::getApiTokenNames()); - $redirectUrl = $this->_urlBuilder->getUrl( + $metaData->setScopes(Token::getApiTokenNames()); + $redirectUrl = $this->urlBuilder->getUrl( 'nosto/oauth', [ '_nosid' => true, - '_scope_to_url' => true, + '_scope_to_url' => $this->nostoHelperData->getStoreCodeToUrl($store), '_scope' => $store->getCode(), + '_query' => ['___store' => $store->getCode()] ] ); + $metaData->setClientId('magento'); + $metaData->setClientSecret('magento'); $metaData->setRedirectUrl($redirectUrl); - $lang = substr($this->_localeResolver->getLocale(), 0, 2); - $metaData->setLanguage(new \NostoLanguageCode($lang)); - if (!is_null($account)) { + $lang = substr($this->localeResolver->getLocale(), 0, 2); + $metaData->setLanguageIsoCode($lang); + if ($account !== null) { $metaData->setAccount($account); } - } catch (\NostoException $e) { - $this->_logger->error($e, ['exception' => $e]); + } catch (Exception $e) { + $this->logger->exception($e); } + $this->eventManager->dispatch('nosto_oauth_load_after', ['oauth' => $metaData]); + return $metaData; } } diff --git a/Model/Mview/ChangeLog.php b/Model/Mview/ChangeLog.php new file mode 100644 index 000000000..a68a7ec28 --- /dev/null +++ b/Model/Mview/ChangeLog.php @@ -0,0 +1,62 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Mview; + +use Exception; +use Magento\Framework\Mview\View\Changelog as MagentoChangelog; + +class ChangeLog extends MagentoChangelog implements ChangeLogInterface +{ + /** + * @inheritDoc + * @throws Exception + */ + public function getTotalRows() + { + $changelogTableName = $this->resource->getTableName($this->getName()); + if ($this->connection->isTableExists($changelogTableName)) { + $select = $this->connection->select() // @codingStandardsIgnoreLine + ->from( // @codingStandardsIgnoreLine + $changelogTableName, + 'COUNT(*)' + ); + + return (int)$this->connection->fetchOne($select); + } + return 0; + } +} diff --git a/Model/Mview/ChangeLogInterface.php b/Model/Mview/ChangeLogInterface.php new file mode 100644 index 000000000..e0ed609d3 --- /dev/null +++ b/Model/Mview/ChangeLogInterface.php @@ -0,0 +1,48 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Mview; + +use Magento\Framework\Mview\View\ChangelogInterface as MagentoChangelogInterface; + +interface ChangeLogInterface extends MagentoChangelogInterface +{ + /** + * Get total rows in changelog table + * @return int + */ + public function getTotalRows(); +} diff --git a/Model/Mview/Mview.php b/Model/Mview/Mview.php new file mode 100644 index 000000000..f447eb3fb --- /dev/null +++ b/Model/Mview/Mview.php @@ -0,0 +1,49 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Mview; + +use Magento\Framework\Mview\View; + +/** + * Class Mview + * This dummy class is needed because Magento's ViewInterface is missing setId method + * but it exists in the implementation class + */ +// @codingStandardsIgnoreFile +class Mview extends View implements MviewInterface +{ +} diff --git a/Model/Mview/MviewInterface.php b/Model/Mview/MviewInterface.php new file mode 100644 index 000000000..fac2d444a --- /dev/null +++ b/Model/Mview/MviewInterface.php @@ -0,0 +1,49 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Mview; + +use Magento\Framework\Mview\ViewInterface; + +interface MviewInterface extends ViewInterface +{ + /** + * Set view id + * @param string|int $id + * @return $this + */ + public function setId($id); +} diff --git a/Model/Order/Builder.php b/Model/Order/Builder.php index 7446bf380..2e233c94a 100644 --- a/Model/Order/Builder.php +++ b/Model/Order/Builder.php @@ -1,179 +1,173 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Model\Order; +use DateTime; +use DateTimeInterface; use Exception; use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Type; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute; -use Magento\SalesRule\Model\RuleFactory as SalesRuleFactory; -use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\GroupedProduct\Model\Product\Type\Grouped; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Phrase; +use Magento\Sales\Api\Data\OrderPaymentInterface; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Item; -use NostoCurrencyCode; -use NostoDate; -use NostoOrderBuyer; -use NostoOrderItem; -use NostoOrderPaymentProvider; -use NostoOrderStatus; -use Nosto\Tagging\Helper\Price as PriceHelper; -use NostoPrice; -use Psr\Log\LoggerInterface; -use Nosto\Tagging\Helper\Item as NostoItemHelper; +use Magento\SalesRule\Model\RuleFactory as SalesRuleFactory; +use Nosto\Model\Cart\LineItem; +use Nosto\Model\Order\Buyer; +use Nosto\Model\Order\Order as NostoOrder; +use Nosto\Model\Order\OrderStatus; +use Nosto\NostoException; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Order\Buyer\Builder as NostoBuyerBuilder; +use Nosto\Tagging\Model\Order\Item\Builder as NostoOrderItemBuilder; class Builder { - /** - * @var LoggerInterface - */ - protected $_logger; + public const ORDER_NUMBER_PREFIX = 'M2_'; - /** - * @var SalesRuleFactory - */ - protected $_salesRuleFactory; - - /** - * @var PriceHelper - */ - protected $_priceHelper; + private NostoLogger $logger; + private SalesRuleFactory $salesRuleFactory; + private NostoOrderItemBuilder $nostoOrderItemBuilder; + private ManagerInterface $eventManager; + private NostoBuyerBuilder $buyerBuilder; /** - * @var NostoItemHelper - */ - protected $_nostoItemHelper; - - /** - * @param LoggerInterface $logger + * @param NostoLogger $logger * @param SalesRuleFactory $salesRuleFactory - * @param PriceHelper $priceHelper - * @param NostoItemHelper $nostoItemHelper - * @internal param ObjectManager $objectManager + * @param NostoOrderItemBuilder $nostoOrderItemBuilder + * @param ManagerInterface $eventManager + * @param NostoBuyerBuilder $buyerBuilder */ public function __construct( - LoggerInterface $logger, + NostoLogger $logger, SalesRuleFactory $salesRuleFactory, - PriceHelper $priceHelper, - NostoItemHelper $nostoItemHelper + NostoOrderItemBuilder $nostoOrderItemBuilder, + ManagerInterface $eventManager, + NostoBuyerBuilder $buyerBuilder ) { - $this->_logger = $logger; - $this->_salesRuleFactory= $salesRuleFactory; - $this->_priceHelper = $priceHelper; - $this->_nostoItemHelper = $nostoItemHelper; + $this->logger = $logger; + $this->salesRuleFactory = $salesRuleFactory; + $this->nostoOrderItemBuilder = $nostoOrderItemBuilder; + $this->eventManager = $eventManager; + $this->buyerBuilder = $buyerBuilder; } /** * Loads the order info from a Magento order model. * * @param Order $order the order model. - * @return \NostoOrder + * @return NostoOrder */ public function build(Order $order) { - $nostoOrder = new \NostoOrder(); - + $nostoOrder = new NostoOrder(); try { - $nostoCurrency = new NostoCurrencyCode($order->getOrderCurrencyCode()); - $nostoOrder->setOrderNumber($order->getId()); - $nostoOrder->setExternalRef($order->getRealOrderId()); - $nostoOrder->setCreatedDate(new NostoDate(strtotime($order->getCreatedAt()))); - $nostoOrder->setPaymentProvider(new NostoOrderPaymentProvider($order->getPayment()->getMethod())); + $nostoOrder->setOrderNumber(self::ORDER_NUMBER_PREFIX . $order->getId()); + $nostoOrder->setExternalOrderRef($order->getRealOrderId()); + $orderCreated = $order->getCreatedAt(); + if (is_string($orderCreated)) { + $orderCreatedDate = DateTime::createFromFormat('Y-m-d H:i:s', $orderCreated); + if ($orderCreatedDate instanceof DateTimeInterface) { + $nostoOrder->setCreatedAt($orderCreatedDate); + } + } + if ($order->getPayment() instanceof OrderPaymentInterface) { + $nostoOrder->setPaymentProvider($order->getPayment()->getMethod()); + } else { + throw new NostoException('Order has no payment associated'); + } if ($order->getStatus()) { - $nostoStatus = new NostoOrderStatus(); + $nostoStatus = new OrderStatus(); $nostoStatus->setCode($order->getStatus()); - $nostoStatus->setLabel($order->getStatusLabel()); - $nostoOrder->setStatus($nostoStatus); - } - foreach ($order->getAllStatusHistory() as $item) { - if ($item->getStatus()) { - $nostoStatus = new NostoOrderStatus(); - $nostoStatus->setCode($item->getStatus()); - $nostoStatus->setLabel($item->getStatusLabel()); - $nostoStatus->setCreatedAt(new NostoDate(strtotime($item->getCreatedAt()))); - $nostoOrder->addHistoryStatus($nostoStatus); + $nostoStatus->setDate($order->getUpdatedAt()); + $label = $order->getStatusLabel(); + if ($label instanceof Phrase) { + $nostoStatus->setLabel($label->getText()); } + $nostoOrder->setOrderStatus($nostoStatus); + } + $nostoBuyer = $this->buyerBuilder->fromOrder($order); + if ($nostoBuyer instanceof Buyer) { + $nostoOrder->setCustomer($nostoBuyer); } - - // Set the buyer information - $nostoBuyer = new NostoOrderBuyer(); - $nostoBuyer->setFirstName($order->getCustomerFirstname()); - $nostoBuyer->setLastName($order->getCustomerLastname()); - $nostoBuyer->setEmail($order->getCustomerEmail()); - $nostoOrder->setBuyer($nostoBuyer); // Add each ordered item as a line item /** @var Item $item */ foreach ($order->getAllVisibleItems() as $item) { - $nostoItem = new NostoOrderItem(); - $nostoItem->setItemId((int)$this->buildItemProductId($item)); - $nostoItem->setQuantity((int)$item->getQtyOrdered()); - $nostoItem->setName($this->buildItemName($item)); - try { - $nostoItem->setUnitPrice( - new NostoPrice( - $this->_priceHelper->getItemFinalPriceInclTax($item) - ) - ); - } catch (\NostoInvalidArgumentException $E) { - $nostoItem->setUnitPrice( - new NostoPrice(0) - ); + if ($item->getProduct() instanceof Product) { + $nostoItem = $this->nostoOrderItemBuilder->build($item); + $nostoOrder->addPurchasedItems($nostoItem); } - $nostoItem->setCurrency($nostoCurrency); - $nostoOrder->addItem($nostoItem); } // Add discounts as a pseudo line item if (($discount = $order->getDiscountAmount()) < 0) { - $nostoItem = new NostoOrderItem(); - $nostoItem->setItemId(-1); - $nostoItem->setQuantity(1); - $nostoItem->setName($this->buildDiscountRuleDescription($order)); - $nostoItem->setUnitPrice(new NostoPrice($discount)); - $nostoItem->setCurrency($nostoCurrency); - $nostoOrder->addItem($nostoItem); + $nostoItem = new LineItem(); + if ($order->getBaseCurrencyCode() !== $order->getOrderCurrencyCode()) { + $baseCurrency = $order->getBaseCurrency(); + $discount = $baseCurrency->convert($discount, $order->getOrderCurrencyCode()); + } + $nostoItem->loadSpecialItemData( + $this->buildDiscountRuleDescription($order), + $discount === null ? 0 : $discount, + $order->getOrderCurrencyCode() + ); + $nostoOrder->addPurchasedItems($nostoItem); } - // Add shipping and handling as a pseudo line item if (($shippingInclTax = $order->getShippingInclTax()) > 0) { - $nostoItem = new NostoOrderItem(); - $nostoItem->setItemId(-1); - $nostoItem->setQuantity(1); - $nostoItem->setName('Shipping and handling'); - $nostoItem->setUnitPrice(new NostoPrice($shippingInclTax)); - $nostoItem->setCurrency($nostoCurrency); - $nostoOrder->addItem($nostoItem); + $nostoItem = new LineItem(); + if ($order->getBaseCurrencyCode() !== $order->getOrderCurrencyCode()) { + $baseCurrency = $order->getBaseCurrency(); + $shippingInclTax = $baseCurrency->convert($shippingInclTax, $order->getOrderCurrencyCode()); + } + $nostoItem->loadSpecialItemData( + 'Shipping and handling', + $shippingInclTax === null ? 0 : $shippingInclTax, + $order->getOrderCurrencyCode() + ); + $nostoOrder->addPurchasedItems($nostoItem); } } catch (Exception $e) { - $this->_logger->error($e, ['exception' => $e]); + $this->logger->exception($e); } + $this->eventManager->dispatch('nosto_order_load_after', ['order' => $nostoOrder, 'magentoOrder' => $order]); + return $nostoOrder; } @@ -182,130 +176,36 @@ public function build(Order $order) * * @param Order $order * @return string discount description + * @suppress PhanDeprecatedFunction */ - protected function buildDiscountRuleDescription(Order $order) + public function buildDiscountRuleDescription(Order $order) { try { - $appliedRules = array(); + $appliedRules = []; foreach ($order->getAllVisibleItems() as $item) { /* @var Item $item */ $itemAppliedRules = $item->getAppliedRuleIds(); - if (empty($itemAppliedRules)) { + if ($itemAppliedRules === null) { continue; } $ruleIds = explode(',', $item->getAppliedRuleIds()); foreach ($ruleIds as $ruleId) { - $rule = $this->_salesRuleFactory->create()->load($ruleId); + /** @noinspection PhpDeprecationInspection */ + $rule = $this->salesRuleFactory->create()->load((int)$ruleId); // @codingStandardsIgnoreLine $appliedRules[$ruleId] = $rule->getName(); } } - if (count($appliedRules) == 0) { + if (count($appliedRules) === 0) { $appliedRules[] = 'unknown rule'; } $discountTxt = sprintf( 'Discount (%s)', implode(', ', $appliedRules) ); - } catch (\Exception $e) { + } catch (Exception $e) { $discountTxt = 'Discount (error)'; } return $discountTxt; } - - /** - * Returns the product id for a quote item. - * Always try to find the "parent" product ID if the product is a child of - * another product type. We do this because it is the parent product that - * we tag on the product page, and the child does not always have it's own - * product page. This is important because it is the tagged info on the - * product page that is used to generate recommendations and email content. - * - * @param Item $item the sales item model. - * - * @return int - */ - protected function buildItemProductId(Item $item) - { - return $this->_nostoItemHelper->buildProductId($item); - } - - /** - * Returns the name for a sales item. - * Configurable products will have their chosen options added to their name. - * Bundle products will have their chosen child product names added. - * Grouped products will have their parents name prepended. - * All others will have their own name only. - * - * @param Item $item the sales item model. - * - * @return string - */ - protected function buildItemName(Item $item) - { - $name = $item->getName(); - $optNames = array(); - if ($item->getProductType() === Type::TYPE_SIMPLE) { - $type = $item->getProduct()->getTypeInstance(); - $parentIds = $type->getParentIdsByChild($item->getProductId()); - // If the product has a configurable parent, we assume we should tag - // the parent. If there are many parent IDs, we are safer to tag the - // products own name alone. - if (count($parentIds) === 1) { - $attributes = $item->getBuyRequest()->getData('super_attribute'); - if (is_array($attributes)) { - foreach ($attributes as $id => $value) { - /** @var Attribute $attribute */ - $attribute = $this->_objectManager->get('Magento\Catalog\Model\ResourceModel\Eav\Attribute') - ->load($id); - $label = $attribute->getSource()->getOptionText($value); - if (!empty($label)) { - $optNames[] = $label; - } - } - } - } - } elseif ($item->getProductType() === Configurable::TYPE_CODE) { - $opts = $item->getProductOptionByCode('attributes_info'); - if (is_array($opts)) { - foreach ($opts as $opt) { - if (isset($opt['value']) && is_string($opt['value'])) { - $optNames[] = $opt['value']; - } - } - } - } elseif ($item->getProductType() === Type::TYPE_BUNDLE) { - $opts = $item->getProductOptionByCode('bundle_options'); - if (is_array($opts)) { - foreach ($opts as $opt) { - if (isset($opt['value']) && is_array($opt['value'])) { - foreach ($opt['value'] as $val) { - $qty = ''; - if (isset($val['qty']) && is_int($val['qty'])) { - $qty .= $val['qty'] . ' x '; - } - if (isset($val['title']) && is_string($val['title'])) { - $optNames[] = $qty . $val['title']; - } - } - } - } - } - } elseif ($item->getProductType() === Grouped::TYPE_CODE) { - $config = $item->getProductOptionByCode('super_product_config'); - if (isset($config['product_id'])) { - /** @var Product $parent */ - $parent = $this->_objectManager->get('Magento\Catalog\Model\Product') - ->load($config['product_id']); - $parentName = $parent->getName(); - if (!empty($parentName)) { - $name = $parentName . ' - ' . $name; - } - } - } - if (!empty($optNames)) { - $name .= ' (' . implode(', ', $optNames) . ')'; - } - return $name; - } } diff --git a/Model/Order/Buyer/Builder.php b/Model/Order/Buyer/Builder.php new file mode 100644 index 000000000..071508ea2 --- /dev/null +++ b/Model/Order/Buyer/Builder.php @@ -0,0 +1,107 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Order\Buyer; + +use Magento\Sales\Api\Data\OrderAddressInterface; +use Magento\Sales\Model\Order; +use Nosto\Model\Order\Buyer; +use Nosto\Tagging\Model\Person\Builder as PersonBuilder; + +/** + * Builder class for buyer + */ +class Builder extends PersonBuilder +{ + /** + * @inheritDoc + * @return Buyer + */ + public function buildObject( // @codingStandardsIgnoreLine + string $firstName, + string $lastName, + string $email, + string $phone = null, + string $postCode = null, + string $country = null, + string $customerGroup = null, + string $dateOfBirth = null, + string $gender = null, + string $customerReference = null + ) { + $buyer = new Buyer(); + $buyer->setFirstName($firstName); + $buyer->setLastName($lastName); + $buyer->setEmail($email); + $buyer->setPhone($phone); + $buyer->setPostCode($postCode); + $buyer->setCountry($country); + + return $buyer; + } + + /** + * Builds buyer from the order + * + * @param Order $order + * @return \Nosto\Model\AbstractPerson|null + * @suppress PhanTypeMismatchArgument + * @noinspection PhpFullyQualifiedNameUsageInspection + */ + public function fromOrder(Order $order) + { + $address = $order->getBillingAddress(); + $telephone = null; + $postcode = null; + $countryId = null; + if ($address instanceof OrderAddressInterface) { + $telephone = $address->getTelephone() ? (string)$address->getTelephone() : null; + $postcode = $address->getPostcode() ? (string)$address->getPostcode() : null; + $countryId = $address->getCountryId() ? (string)$address->getCountryId() : null; + } + $customerFirstname = $order->getCustomerFirstname() ? (string)$order->getCustomerFirstname() : ''; + $customerLastname = $order->getCustomerLastname() ? (string)$order->getCustomerLastname() : ''; + $customerEmail = $order->getCustomerEmail() ? (string)$order->getCustomerEmail() : ''; + return $this->build( + $customerFirstname, + $customerLastname, + $customerEmail, + $telephone, + $postcode, + $countryId + ); + } +} diff --git a/Model/Order/Collection.php b/Model/Order/Collection.php new file mode 100644 index 000000000..fe59e98f8 --- /dev/null +++ b/Model/Order/Collection.php @@ -0,0 +1,119 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Order; + +use Magento\Sales\Api\Data\EntityInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory as OrderCollectionFactory; +use Magento\Store\Model\Store; +use Nosto\NostoException; +use Nosto\Model\Order\OrderCollection; +use Nosto\Tagging\Model\Order\Builder as NostoOrderBuilder; +use Traversable; + +class Collection +{ + private OrderCollectionFactory $orderCollectionFactory; + private Builder $nostoOrderBuilder; + + public function __construct( + OrderCollectionFactory $orderCollectionFactory, + NostoOrderBuilder $nostoOrderBuilder + ) { + $this->orderCollectionFactory = $orderCollectionFactory; + $this->nostoOrderBuilder = $nostoOrderBuilder; + } + + public function getCollection(Store $store) + { + $collection = $this->orderCollectionFactory->create(); + $collection->addAttributeToFilter('store_id', ['eq' => $store->getId()]); + $collection->addAttributeToSelect('*'); + return $collection; + } + + /** + * @param Store $store + * @param $id + * @return OrderCollection + * @throws NostoException + */ + public function buildSingle(Store $store, $id) + { + $collection = $this->getCollection($store); + $collection->addFieldToFilter(EntityInterface::ENTITY_ID, $id); + return $this->build($collection); + } + + /** + * @param Store $store + * @param int $limit + * @param int $offset + * @return OrderCollection + * @throws NostoException + */ + public function buildMany(Store $store, int $limit = 100, int $offset = 0) + { + $collection = $this->getCollection($store); + $currentPage = ($offset / $limit) + 1; + $collection->getSelect()->limitPage($currentPage, $limit); + $collection->setOrder(EntityInterface::CREATED_AT, $collection::SORT_ORDER_DESC); + return $this->build($collection); + } + + /** + * @param $collection + * @return OrderCollection + * @throws NostoException + */ + private function build($collection) + { + /** @var \Magento\Sales\Model\ResourceModel\Order\Collection $collection */ + $orders = new OrderCollection(); + $items = $collection->loadData(); + if ($items instanceof Traversable === false && !is_array($items)) { + throw new NostoException( + sprintf('Invalid collection type %s for product export', get_class($collection)) + ); + } + foreach ($items as $order) { + /** @var Order $order */ + $orders->append($this->nostoOrderBuilder->build($order)); + } + return $orders; + } +} diff --git a/Model/Order/Item/Builder.php b/Model/Order/Item/Builder.php new file mode 100644 index 000000000..cb1e303f5 --- /dev/null +++ b/Model/Order/Item/Builder.php @@ -0,0 +1,221 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Order\Item; + +use Exception; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Sales\Model\Order\Item; +use Nosto\Model\Cart\LineItem; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Item\Downloadable; +use Nosto\Tagging\Model\Item\Giftcard; +use Nosto\Tagging\Model\Item\Virtual; +use Throwable; +use Nosto\Tagging\Model\Item\Grouped as GroupedItem; +use Nosto\Tagging\Model\Order\Item\Grouped as OrderGroupedItem; + +class Builder +{ + /** + * @var ManagerInterface $eventManager + */ + private ManagerInterface $eventManager; + + /** + * @var ProductRepository $productRepository + */ + private ProductRepository $productRepository; + + /** + * @var NostoLogger $logger + */ + private NostoLogger $logger; + + /** + * Builder constructor. + * + * @param ManagerInterface $eventManager + * @param ProductRepository $productRepository + * @param NostoLogger $logger + */ + public function __construct( + ManagerInterface $eventManager, + ProductRepository $productRepository, + NostoLogger $logger + ) { + $this->eventManager = $eventManager; + $this->productRepository = $productRepository; + $this->logger = $logger; + } + + /** + * @param Item $item + * @return LineItem + * @throws LocalizedException + */ + public function build(Item $item) + { + $order = $item->getOrder(); + $nostoItem = new LineItem(); + $nostoItem->setPriceCurrencyCode($order->getOrderCurrencyCode()); + $nostoItem->setProductId($this->buildItemProductId($item)); + $nostoItem->setQuantity((int)$item->getQtyOrdered()); + $nostoItem->setSkuId($this->buildSkuId($item)); + $productType = $item->getProductType(); + // Set default name - this will be overwritten below if matching + // product type is defined + $nostoItem->setName(sprintf( + 'Not defined - unknown product type: %s', + $productType + )); + switch ($productType) { + case Simple::TYPE: + case Virtual::TYPE: + case Downloadable::TYPE: + case Giftcard::TYPE: + $simple = new Simple(); + $nostoItem->setName($simple->buildItemName($item)); + break; + case Configurable::TYPE: + $configurable = new Configurable(); + $nostoItem->setName($configurable->buildItemName($item)); + break; + case Bundle::TYPE: + $bundle = new Bundle(); + $nostoItem->setName($bundle->buildItemName($item)); + break; + case GroupedItem::TYPE: + $nostoItem->setName((new OrderGroupedItem($this->productRepository))->buildItemName($item)); + break; + } + try { + $lineDiscount = 0; + if ($item->getBaseDiscountAmount() > 0) { + // baseDiscountAmount contains the discount for the whole row + $lineDiscount = $item->getBaseDiscountAmount() / $item->getQtyOrdered(); + } + $taxPerUnit = $item->getBaseTaxAmount() / $item->getQtyOrdered(); + $price = $item->getBasePrice() + $taxPerUnit - $lineDiscount; + // The item prices are always in base currency, convert to order currency if non base currency + // is used for the order + if ($order->getBaseCurrencyCode() !== $order->getOrderCurrencyCode()) { + $baseCurrency = $order->getBaseCurrency(); + $price = $baseCurrency->convert($price, $order->getOrderCurrencyCode()); + } + $nostoItem->setPrice($price); + } catch (Exception $e) { + $nostoItem->setPrice(0); + } + + $this->eventManager->dispatch( + 'nosto_order_item_load_after', + ['item' => $nostoItem, 'magentoItem' => $item] + ); + + return $nostoItem; + } + + /** + * Returns the product id for a quote item. + * Always try to find the "parent" product ID if the product is a child of + * another product type. We do this because it is the parent product that + * we tag on the product page, and the child does not always have it's own + * product page. This is important because it is the tagged info on the + * product page that is used to generate recommendations and email content. + * + * @param Item $item the sales item model. + * @return string + */ + public function buildItemProductId(Item $item) + { + $parent = $item->getProductOptionByCode('super_product_config'); + if (isset($parent['product_id'])) { + return $parent['product_id']; + } + if ($item->getProductType() === Type::TYPE_SIMPLE && $item->getProduct() !== null) { + try { + $type = $item->getProduct()->getTypeInstance(); + $parentIds = $type->getParentIdsByChild($item->getProductId()); + $attributes = $item->getBuyRequest()->getData('super_attribute'); + // If the product has a configurable parent, we assume we should tag + // the parent. If there are many parent IDs, we are safer to tag the + // products own ID. + if (!empty($attributes) && count($parentIds) === 1) { + return $parentIds[0]; + } + } catch (Throwable $e) { + $this->logger->exception($e); + } + } + $productId = $item->getProductId(); + if (!$productId) { + return LineItem::PSEUDO_PRODUCT_ID; + } + return (string)$productId; + } + + /** + * Returns the sku id. If it is a configurable product, + * try to get the child item because the child item is the simple product + * + * @param Item $item the sales item model. + * @return string|null sku id + */ + public function buildSkuId(Item $item) + { + if ($item->getProductType() === Configurable::TYPE) { + $children = $item->getChildrenItems(); + //An item with bundle product and group product may have more than 1 child. + //But configurable product item should have max 1 child item. + //Here we check the size of children, return only if the size is 1 + /** @var Item[] $children */ + if (array_key_exists(0, $children) + && $children[0] instanceof Item + && count($children) === 1 + && $children[0]->getProductId() + ) { + return (string)$children[0]->getProductId(); + } + } + + return null; + } +} diff --git a/Model/Order/Item/Bundle.php b/Model/Order/Item/Bundle.php new file mode 100644 index 000000000..bb89dccab --- /dev/null +++ b/Model/Order/Item/Bundle.php @@ -0,0 +1,77 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Order\Item; + +use Magento\Sales\Model\Order\Item; +use Nosto\Tagging\Model\Item\Bundle as BundleItem; + +class Bundle extends BundleItem +{ + /** + * Returns the name of the product. Bundle products will have their chosen child product names + * added. + * + * @param Item $item the ordered item + * @return string the name of the product + */ + public function buildItemName(Item $item): string + { + $name = $item->getName() ?: ''; + $optNames = []; + $opts = $item->getProductOptionByCode('bundle_options'); + if (is_array($opts)) { + foreach ($opts as $opt) { + if (isset($opt['value']) && is_array($opt['value'])) { + foreach ($opt['value'] as $val) { + $qty = ''; + if (isset($val['qty']) && is_int($val['qty'])) { + $qty .= $val['qty'] . ' x '; + } + if (isset($val['title']) && is_string($val['title'])) { + $optNames[] = $qty . $val['title']; + } + } + } + } + } + + if (!empty($optNames)) { + $name .= ' (' . implode(', ', $optNames) . ')'; + } + return $name; + } +} diff --git a/Model/Order/Item/Configurable.php b/Model/Order/Item/Configurable.php new file mode 100644 index 000000000..c688d403a --- /dev/null +++ b/Model/Order/Item/Configurable.php @@ -0,0 +1,70 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Order\Item; + +use Magento\Sales\Model\Order\Item; +use Nosto\Tagging\Model\Item\Configurable as ConfigurableItem; + +class Configurable extends ConfigurableItem +{ + /** + * Returns the name of the product. Configurable products will have their chosen options + * added to their name. + * + * @param Item $item the ordered item + * @return string the name of the product + */ + public function buildItemName(Item $item): string + { + $name = $item->getName() ?: ''; + $optNames = []; + $opts = $item->getProductOptionByCode('attributes_info'); + if (is_array($opts)) { + foreach ($opts as $opt) { + if (isset($opt['value']) && is_string($opt['value'])) { + $optNames[] = $opt['value']; + } + } + } + + if (!empty($optNames)) { + $name .= ' (' . implode(', ', $optNames) . ')'; + } + return $name; + } +} diff --git a/Model/Order/Item/Grouped.php b/Model/Order/Item/Grouped.php new file mode 100644 index 000000000..bc2a0cc80 --- /dev/null +++ b/Model/Order/Item/Grouped.php @@ -0,0 +1,96 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Order\Item; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Sales\Model\Order\Item; +use Nosto\Tagging\Model\Item\Grouped as GroupedItem; + +class Grouped extends GroupedItem +{ + /** + * @var ProductRepository + */ + private ProductRepository $productRepository; + + /** + * Grouped constructor. + * @param ProductRepository $productRepository + */ + public function __construct(ProductRepository $productRepository) + { + $this->productRepository = $productRepository; + } + + /** + * Returns the name of the product. Grouped products will have their parent's name prepended to + * their name. + * + * @param Item $item the ordered item + * @return string|null the name of the product + * @throws NoSuchEntityException + * @suppress PhanTypeMismatchReturn + */ + public function buildItemName(Item $item) + { + $name = $item->getName(); + $config = $item->getProductOptionByCode('super_product_config'); + $itemParent = $this->getGroupedItemParent($config['product_id']); + if ($itemParent instanceof Product) { + $itemParentName = $itemParent->getName(); + if ($itemParentName !== null) { + $name = $itemParentName . ' - ' . $name; + } + } + return $name; + } + + /** + * Query the product id and returns the Product Object + * + * @param $productId + * @return ProductInterface|mixed + * @throws NoSuchEntityException + */ + private function getGroupedItemParent($productId) + { + return $this->productRepository->getById($productId); + } +} diff --git a/Model/Order/Item/Simple.php b/Model/Order/Item/Simple.php new file mode 100644 index 000000000..da91d4d1d --- /dev/null +++ b/Model/Order/Item/Simple.php @@ -0,0 +1,60 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Order\Item; + +use Magento\Sales\Model\Order\Item; +use Nosto\Tagging\Model\Item\Simple as SimpleItem; + +class Simple extends SimpleItem +{ + /** + * Returns the name of the product. Simple products will have their own name + * + * @param Item $item the ordered item + * @return string the name of the product + */ + public function buildItemName(Item $item): string + { + if ($item->getProduct()) { + $type = $item->getProduct()->getTypeInstance(); + $parentIds = $type->getParentIdsByChild($item->getProductId()); + } else { + $parentIds = 0; + } + return $this->buildName($item, $parentIds); + } +} diff --git a/Model/Order/Status/Builder.php b/Model/Order/Status/Builder.php new file mode 100644 index 000000000..2559f2164 --- /dev/null +++ b/Model/Order/Status/Builder.php @@ -0,0 +1,104 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Order\Status; + +use Exception; +use Magento\Framework\Event\ManagerInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use Nosto\NostoException; +use Nosto\Model\Order\GraphQL\OrderStatus as NostoOrderStatus; +use Nosto\Tagging\Logger\Logger as NostoLogger; + +class Builder +{ + public const ORDER_NUMBER_PREFIX = 'M2_'; + + /** @var NostoLogger */ + private NostoLogger $logger; + + /** @var ManagerInterface */ + private ManagerInterface $eventManager; + + /** + * Builder constructor. + * @param ManagerInterface $eventManager + * @param NostoLogger $logger + */ + public function __construct( + ManagerInterface $eventManager, + NostoLogger $logger + ) { + $this->logger = $logger; + $this->eventManager = $eventManager; + } + + /** + * @param Order $order + * @return NostoOrderStatus|null + */ + public function build(Order $order) + { + $orderNumber = self::ORDER_NUMBER_PREFIX . '' . $order->getId(); + $orderStatus = $order->getStatus(); + $updatedAt = $order->getUpdatedAt(); + try { + if ($order->getPayment() instanceof Payment) { + $paymentProvider = $order->getPayment()->getMethod(); + } else { + throw new NostoException('Order has no payment associated'); + } + + $nostoOrderStatus = new NostoOrderStatus( + $orderNumber, + $orderStatus, + $paymentProvider, + $updatedAt + ); + + $this->eventManager->dispatch( + 'nosto_order_status_load_after', + ['order' => $nostoOrderStatus, 'magentoOrder' => $order] + ); + + return $nostoOrderStatus; + } catch (Exception $e) { + $this->logger->exception($e); + } + return null; + } +} diff --git a/Model/Person/Builder.php b/Model/Person/Builder.php new file mode 100644 index 000000000..3affdd8e0 --- /dev/null +++ b/Model/Person/Builder.php @@ -0,0 +1,175 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Person; + +use Magento\Framework\Event\ManagerInterface as EventManager; +use Nosto\Model\AbstractPerson; +use Nosto\Model\ModelFilter; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Model\Email\Repository as NostoEmailRepository; + +abstract class Builder +{ + /** + * @var NostoEmailRepository + */ + private NostoEmailRepository $emailRepository; + /** + * @var EventManager + */ + private EventManager $eventManager; + /** + * @var NostoHelperData + */ + private NostoHelperData $nostoHelperData; + + /** + * Builder constructor. + * @param NostoEmailRepository $emailRepository + * @param EventManager $eventManager + * @param NostoHelperData $nostoHelperData + */ + public function __construct( + NostoEmailRepository $emailRepository, + EventManager $eventManager, + NostoHelperData $nostoHelperData + ) { + $this->emailRepository = $emailRepository; + $this->eventManager = $eventManager; + $this->nostoHelperData = $nostoHelperData; + } + + /** + * @param string $firstName + * @param string $lastName + * @param string $email + * @param string|null $phone + * @param string|null $postCode + * @param string|null $country + * @param string|null $customerGroup + * @param string|null $dateOfBirth + * @param string|null $gender + * @param string|null $customerReference + * + * @return AbstractPerson|null + */ + public function build( + string $firstName, + string $lastName, + string $email, + ?string $phone = null, + ?string $postCode = null, + ?string $country = null, + ?string $customerGroup = null, + ?string $dateOfBirth = null, + ?string $gender = null, + ?string $customerReference = null + ) { + if (!$this->nostoHelperData->isSendCustomerDataToNostoEnabled()) { + return null; + } + $modelFilter = new ModelFilter(); + $this->eventManager->dispatch( + 'nosto_person_load_before', + [ + 'modelFilter' => $modelFilter, + 'fields' => [ + 'firstName' => $firstName, + 'lastLane' => $lastName, + 'email' => $email, + 'phone' => $phone, + 'postCode' => $postCode, + 'country' => $country + ] + ] + ); + if (!$modelFilter->isValid()) { + return null; + } + $person = $this->buildObject( + $firstName, + $lastName, + $email, + $phone, + $postCode, + $country, + $customerGroup, + $dateOfBirth, + $gender, + $customerReference + ); + $person->setMarketingPermission( + $this->emailRepository->isOptedIn($person->getEmail()) + ); + $this->eventManager->dispatch('nosto_person_load_after', [ + 'modelFilter' => $modelFilter, + 'person' => $person + ]); + if (!$modelFilter->isValid()) { + return null; + } + + return $person; + } + + /** + * @param string $firstName + * @param string $lastName + * @param string $email + * @param string|null $phone + * @param string|null $postCode + * @param string|null $country + * @param string|null $customerGroup + * @param string|null $dateOfBirth + * @param string|null $gender + * @param string|null $customerReference + * + * @return AbstractPerson + */ + abstract public function buildObject( + string $firstName, + string $lastName, + string $email, + string $phone = null, + string $postCode = null, + string $country = null, + string $customerGroup = null, + string $dateOfBirth = null, + string $gender = null, + string $customerReference = null + ); +} diff --git a/Model/Person/Tagging/Builder.php b/Model/Person/Tagging/Builder.php new file mode 100644 index 000000000..ae3248eba --- /dev/null +++ b/Model/Person/Tagging/Builder.php @@ -0,0 +1,219 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Person\Tagging; + +use DateTime; +use Exception; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\GroupRepositoryInterface as GroupRepository; +use Magento\Customer\Helper\Session\CurrentCustomer; +use Magento\Framework\Event\ManagerInterface as EventManager; +use Nosto\Model\Customer; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Email\Repository as NostoEmailRepository; +use Nosto\Tagging\Model\Person\Builder as PersonBuilder; +use Nosto\Tagging\Util\Customer as CustomerUtil; + +/** + * Builder class for buyer + */ +class Builder extends PersonBuilder +{ + + public const GENDER_MALE = 'Male'; + public const GENDER_FEMALE = 'Female'; + public const GENDER_MALE_ID = '1'; + public const GENDER_FEMALE_ID = '2'; + + private GroupRepository $groupRepository; + private CustomerRepositoryInterface $customerRepository; + private NostoLogger $logger; + + /** + * Builder constructor. + * @param GroupRepository $groupRepository + * @param CustomerRepositoryInterface $customerRepository + * @param NostoEmailRepository $emailRepository + * @param NostoLogger $logger + * @param EventManager $eventManager + * @param NostoHelperData $nostoHelperData + */ + public function __construct( + GroupRepository $groupRepository, + CustomerRepositoryInterface $customerRepository, + NostoEmailRepository $emailRepository, + NostoLogger $logger, + EventManager $eventManager, + NostoHelperData $nostoHelperData + ) { + $this->groupRepository = $groupRepository; + $this->customerRepository = $customerRepository; + $this->logger = $logger; + parent::__construct($emailRepository, $eventManager, $nostoHelperData); + } + + /** + * @inheritDoc + * @return Customer + */ + public function buildObject( + string $firstName, + string $lastName, + string $email, + string $phone = null, + string $postCode = null, + string $country = null, + string $customerGroup = null, + string $dateOfBirth = null, + string $gender = null, + string $customerReference = null + ) { + $customer = new Customer(); + $customer->setFirstName($firstName); + $customer->setLastName($lastName); + $customer->setEmail($email); + $customer->setPhone($phone); + $customer->setPostCode($postCode); + $customer->setCountry($country); + $customer->setCustomerGroup($customerGroup); + $customer->setCustomerReference($customerReference); + $customer->setGender($gender); + if ($dateOfBirth !== null) { + $customer->setDateOfBirth(DateTime::createFromFormat('Y-m-d', $dateOfBirth)); + } + + return $customer; + } + + /** + * Builds person from the current session / logged in user + * + * @param CurrentCustomer $currentCustomer + * @return Customer|null + */ + public function fromSession(CurrentCustomer $currentCustomer) + { + try { + $customer = $currentCustomer->getCustomer(); + $customerGroup = $this->getCustomerGroupName($customer); + $gender = $this->getGenderName($customer); + $customerReference = $this->getCustomerReference($currentCustomer); + + /** @noinspection PhpIncompatibleReturnTypeInspection */ + return $this->build( + $customer->getFirstname(), + $customer->getLastname(), + $customer->getEmail(), + null, + null, + null, + $customerGroup, + $customer->getDob(), + $gender, + $customerReference + ); + } catch (Exception $e) { + $this->logger->exception($e); + return null; + } + } + + /** + * @param CustomerInterface $customer + * @return string|null + */ + private function getCustomerGroupName(CustomerInterface $customer) + { + $groupId = (int)$customer->getGroupId(); + try { + return $this->groupRepository->getById($groupId)->getCode(); + } catch (Exception $e) { + return null; + } + } + + /** + * @param CustomerInterface $customer + * @return null|string + */ + private function getGenderName(CustomerInterface $customer) + { + $gender = $customer->getGender(); + switch ($gender) { + case self::GENDER_MALE_ID: + return self::GENDER_MALE; + case self::GENDER_FEMALE_ID: + return self::GENDER_FEMALE; + default: + return null; + } + } + + /** + * @param CurrentCustomer $currentCustomer + * @return string + */ + private function getCustomerReference(CurrentCustomer $currentCustomer) + { + $customerReference = ''; + + try { + $customer = $currentCustomer->getCustomer(); + $customerReference = $customer->getCustomAttribute( + NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME + ); + + if ($customerReference === null) { + $customerUtil = new CustomerUtil(); + $customerReference = $customerUtil->generateCustomerReference($customer); + $customer->setCustomAttribute( + NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME, + $customerReference + ); + $this->customerRepository->save($customer); + return $customerReference; + } + return $customerReference->getValue(); + } catch (Exception $e) { + $this->logger->exception($e); + } + + return $customerReference; + } +} diff --git a/Model/Product/Builder.php b/Model/Product/Builder.php index 97959a346..14c0fb2bf 100644 --- a/Model/Product/Builder.php +++ b/Model/Product/Builder.php @@ -1,243 +1,461 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Model\Product; -use Magento\Catalog\Api\CategoryRepositoryInterface; +use Exception; use Magento\Catalog\Model\Product; -use Magento\Store\Model\Store; -use Nosto\Tagging\Helper\Data as DataHelper; -use Nosto\Tagging\Helper\Price as PriceHelper; -use Nosto\Tagging\Model\Category\Builder as CategoryBuilder; -use Psr\Log\LoggerInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; +use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; use Magento\Framework\Event\ManagerInterface; +use Magento\Store\Model\Store; +use Nosto\Exception\FilteredProductException; +use Nosto\Exception\NonBuildableProductException; +use Nosto\NostoException; +use Nosto\Model\ModelFilter; +use Nosto\Model\Product\Product as NostoProduct; +use Nosto\Tagging\Helper\Currency as CurrencyHelper; +use Nosto\Tagging\Helper\Data as NostoDataHelper; +use Nosto\Tagging\Helper\Price as NostoPriceHelper; +use Nosto\Tagging\Helper\Ratings as NostoRating; +use Nosto\Tagging\Helper\Variation as NostoVariationHelper; +use Nosto\Tagging\Model\Product\Sku\Collection as NostoSkuCollection; +use Nosto\Tagging\Model\Product\Tags\LowStock as LowStockHelper; +use Nosto\Tagging\Model\Product\Url\Builder as NostoUrlBuilder; +use Nosto\Tagging\Model\Product\Variation\Collection as PriceVariationCollection; +use Nosto\Tagging\Model\Service\Product\Attribute\AttributeServiceInterface; +use Nosto\Tagging\Model\Service\Product\AvailabilityService; +use Nosto\Tagging\Model\Service\Product\Category\CategoryServiceInterface; +use Nosto\Tagging\Model\Service\Product\ImageService; +use Nosto\Tagging\Model\Service\Stock\StockService; +use Nosto\Types\Product\ProductInterface; class Builder { - /** - * @var DataHelper - */ - protected $_dataHelper; + public const CUSTOMIZED_TAGS = ['tag1', 'tag2', 'tag3']; - /** - * @var PriceHelper - */ - protected $_priceHelper; + /** @var NostoDataHelper */ + private NostoDataHelper $nostoDataHelper; - /** - * @var CategoryBuilder - */ - protected $_categoryBuilder; + /** @var NostoPriceHelper */ + private NostoPriceHelper $nostoPriceHelper; - /** - * @var CategoryRepositoryInterface - */ - protected $_categoryRepository; + /** @var GalleryReadHandler */ + private GalleryReadHandler $galleryReadHandler; - /** - * Event manager - * - * @var ManagerInterface - */ - protected $_eventManager; + /** @var ManagerInterface */ + private ManagerInterface $eventManager; - /** - * @var LoggerInterface - */ - protected $_logger; + /** @var NostoUrlBuilder */ + private NostoUrlBuilder $urlBuilder; + + /** @var NostoSkuCollection */ + private NostoSkuCollection $skuCollection; + + /** @var CurrencyHelper */ + private CurrencyHelper $nostoCurrencyHelper; + + /** @var LowStockHelper */ + private LowStockHelper $lowStockHelper; + + /** @var PriceVariationCollection */ + private PriceVariationCollection $priceVariationCollection; + + /** @var NostoVariationHelper */ + private NostoVariationHelper $nostoVariationHelper; + + /** @var NostoRating */ + private NostoRating $nostoRatingHelper; + + /** @var CategoryServiceInterface */ + private CategoryServiceInterface $nostoCategoryService; + + /** @var AttributeServiceInterface */ + private AttributeServiceInterface $attributeService; + + /** @var AvailabilityService */ + private AvailabilityService $availabilityService; + + /** @var ImageService */ + private ImageService $imageService; + + /** @var StockService */ + private StockService $stockService; /** - * @param DataHelper $dataHelper - * @param PriceHelper $priceHelper - * @param CategoryBuilder $categoryBuilder - * @param CategoryRepositoryInterface $categoryRepository - * @param LoggerInterface $logger + * Builder constructor. + * @param NostoDataHelper $nostoDataHelper + * @param NostoPriceHelper $priceHelper + * @param CategoryServiceInterface $nostoCategoryService + * @param NostoSkuCollection $skuCollection * @param ManagerInterface $eventManager + * @param GalleryReadHandler $galleryReadHandler + * @param NostoUrlBuilder $urlBuilder + * @param CurrencyHelper $nostoCurrencyHelper + * @param LowStockHelper $lowStockHelper + * @param PriceVariationCollection $priceVariationCollection + * @param NostoVariationHelper $nostoVariationHelper + * @param NostoRating $nostoRatingHelper + * @param AttributeServiceInterface $attributeService + * @param AvailabilityService $availabilityService + * @param ImageService $imageService + * @param StockService $stockService */ public function __construct( - DataHelper $dataHelper, - PriceHelper $priceHelper, - CategoryBuilder $categoryBuilder, - CategoryRepositoryInterface $categoryRepository, - LoggerInterface $logger, - ManagerInterface $eventManager + NostoDataHelper $nostoDataHelper, + NostoPriceHelper $priceHelper, + CategoryServiceInterface $nostoCategoryService, + NostoSkuCollection $skuCollection, + ManagerInterface $eventManager, + GalleryReadHandler $galleryReadHandler, + NostoUrlBuilder $urlBuilder, + CurrencyHelper $nostoCurrencyHelper, + LowStockHelper $lowStockHelper, + PriceVariationCollection $priceVariationCollection, + NostoVariationHelper $nostoVariationHelper, + NostoRating $nostoRatingHelper, + AttributeServiceInterface $attributeService, + AvailabilityService $availabilityService, + ImageService $imageService, + StockService $stockService ) { - $this->_dataHelper = $dataHelper; - $this->_priceHelper = $priceHelper; - $this->_categoryBuilder = $categoryBuilder; - $this->_categoryRepository = $categoryRepository; - $this->_logger = $logger; - $this->_eventManager = $eventManager; + $this->nostoDataHelper = $nostoDataHelper; + $this->nostoPriceHelper = $priceHelper; + $this->eventManager = $eventManager; + $this->galleryReadHandler = $galleryReadHandler; + $this->urlBuilder = $urlBuilder; + $this->skuCollection = $skuCollection; + $this->nostoCurrencyHelper = $nostoCurrencyHelper; + $this->lowStockHelper = $lowStockHelper; + $this->priceVariationCollection = $priceVariationCollection; + $this->nostoVariationHelper = $nostoVariationHelper; + $this->nostoRatingHelper = $nostoRatingHelper; + $this->nostoCategoryService = $nostoCategoryService; + $this->attributeService = $attributeService; + $this->availabilityService = $availabilityService; + $this->imageService = $imageService; + $this->stockService = $stockService; } /** * @param Product $product * @param Store $store - * @return \NostoProduct + * @return NostoProduct + * @throws FilteredProductException + * @throws NonBuildableProductException */ - public function build(Product $product, Store $store) - { - $nostoProduct = new \NostoProduct(); - + public function build( + Product $product, + Store $store + ) { + $nostoProduct = new NostoProduct(); + $modelFilter = new ModelFilter(); + $this->eventManager->dispatch( + 'nosto_product_load_before', + ['product' => $nostoProduct, 'magentoProduct' => $product, 'modelFilter' => $modelFilter] + ); + if (!$modelFilter->isValid()) { + throw new FilteredProductException( + sprintf( + 'Product id %d did not pass pre-build model filter for store %s', + $product->getId(), + $store->getCode() + ) + ); + } try { - $nostoProduct->setUrl($this->buildUrl($product, $store)); - $nostoProduct->setProductId($product->getId()); + $nostoProduct->setUrl($this->urlBuilder->getUrlInStore($product, $store)); + $nostoProduct->setProductId((string)$product->getId()); $nostoProduct->setName($product->getName()); - $nostoProduct->setImageUrl($this->buildImageUrl($product, $store)); - $price = $this->_priceHelper->getProductFinalPriceInclTax($product); - $nostoProduct->setPrice(new \NostoPrice($price)); - $listPrice = $this->_priceHelper->getProductPriceInclTax($product); - $nostoProduct->setListPrice(new \NostoPrice($listPrice)); - $nostoProduct->setCurrency( - new \NostoCurrencyCode($store->getBaseCurrencyCode()) + $nostoProduct->setImageUrl( + $this->imageService->buildImageUrl($product, $store) ); - $nostoProduct->setAvailability( - new \NostoProductAvailability( - $product->isAvailable() - ? \NostoProductAvailability::IN_STOCK - : \NostoProductAvailability::OUT_OF_STOCK - ) + $price = $this->nostoCurrencyHelper->convertToTaggingPrice( + $this->nostoPriceHelper->getProductFinalDisplayPrice( + $product, + $store + ), + $store ); - $nostoProduct->setCategories($this->buildCategories($product)); - - // Optional properties. - - $descriptions = array(); + if ($this->nostoCurrencyHelper->exchangeRatesInUse($store)) { + $nostoProduct->setVariationId( + $this->nostoCurrencyHelper->getTaggingCurrency( + $store + )->getCode() + ); + } elseif ($this->nostoDataHelper->isPricingVariationEnabled($store)) { + $nostoProduct->setVariationId( + $this->nostoVariationHelper->getDefaultVariationCode() + ); + } + $nostoProduct->setAvailability($this->buildAvailability($product, $store)); + $nostoProduct->setCategories($this->nostoCategoryService->getCategories($product, $store)); + if ($this->nostoDataHelper->isInventoryTaggingEnabled($store)) { + $inventoryLevel = $this->stockService->getQuantity($product, $store); + $nostoProduct->setInventoryLevel($inventoryLevel); + } + $rating = $this->nostoRatingHelper->getRatings($product, $store); + if ($rating !== null) { + $nostoProduct->setRatingValue($rating->getRating()); + $nostoProduct->setReviewCount($rating->getReviewCount()); + } + $nostoProduct->setCustomFields($this->getCustomFieldsWithAttributes($product, $store)); + // Update customised Tag1, Tag2 and Tag3 + if ($this->nostoDataHelper->isAltimgTaggingEnabled($store)) { + $nostoProduct->setAlternateImageUrls($this->buildAlternativeImages($product, $store)); + } + if ($this->nostoDataHelper->isVariationTaggingEnabled($store)) { + $nostoProduct->setSkus($this->skuCollection->build($product, $store)); + } + $descriptions = []; if ($product->hasData('short_description')) { $descriptions[] = $product->getData('short_description'); } if ($product->hasData('description')) { $descriptions[] = $product->getData('description'); } - if (count($descriptions) > 0) { + if (!empty($descriptions)) { $nostoProduct->setDescription(implode(' ', $descriptions)); } - - if ($product->hasData('manufacturer')) { + if (($tags = $this->buildDefaultTags($product, $store)) !== []) { + $nostoProduct->setTag1($tags); + } + $this->amendAttributeTags($product, $nostoProduct, $store); + $brandAttribute = $this->nostoDataHelper->getBrandAttribute($store); + if (is_string($brandAttribute) && $product->hasData($brandAttribute)) { $nostoProduct->setBrand( - $product->getAttributeText('manufacturer') + $this->attributeService->getAttributeValueByAttributeCode( + $product, + $brandAttribute + ) ); } - if (($tags = $this->buildTags($product)) !== []) { - $nostoProduct->setTag1($tags); + $marginAttribute = $this->nostoDataHelper->getMarginAttribute($store); + if (is_string($marginAttribute) && $product->hasData($marginAttribute)) { + $nostoProduct->setSupplierCost( + $this->attributeService->getAttributeValueByAttributeCode( + $product, + $marginAttribute + ) + ); } - if ($product->hasData('created_at')) { - if (($timestamp = strtotime($product->getData('created_at')))) { - $nostoProduct->setDatePublished(new \NostoDate($timestamp)); - } + $gtinAttribute = $this->nostoDataHelper->getGtinAttribute($store); + if (is_string($gtinAttribute) && $product->hasData($gtinAttribute)) { + $nostoProduct->setGtin( + $this->attributeService->getAttributeValueByAttributeCode( + $product, + $gtinAttribute + ) + ); + } + $googleCategoryAttr = $this->nostoDataHelper->getGoogleCategoryAttribute($store); + if (is_string($googleCategoryAttr) && $product->hasData($googleCategoryAttr)) { + $nostoProduct->setGoogleCategory( + $this->attributeService->getAttributeValueByAttributeCode( + $product, + $googleCategoryAttr + ) + ); + } + // When using customer group price variations, set the variations + if ($this->nostoDataHelper->isPricingVariationEnabled($store) + && $this->nostoDataHelper->isMultiCurrencyDisabled($store) + ) { + $nostoProduct->setVariations( + $this->priceVariationCollection->build($product, $nostoProduct, $store) + ); + } + if ($this->nostoDataHelper->isTagDatePublishedEnabled($store)) { + $nostoProduct->setDatePublished($product->getCreatedAt()); } - } catch (\NostoException $e) { - $this->_logger->error($e, ['exception' => $e]); - } - $this->_eventManager->dispatch( + // These will be always fetched from price index tables + $nostoProduct->setPrice($price); + $listPrice = $this->nostoCurrencyHelper->convertToTaggingPrice( + $this->nostoPriceHelper->getProductDisplayPrice( + $product, + $store + ), + $store + ); + $nostoProduct->setListPrice($listPrice); + $nostoProduct->setPriceCurrencyCode( + $this->nostoCurrencyHelper->getTaggingCurrency( + $store + )->getCode() + ); + } catch (Exception $e) { + $message = sprintf("Could not build product with id: %s", $product->getId()); + throw new NonBuildableProductException($message, $e); + } + $this->eventManager->dispatch( 'nosto_product_load_after', - ['product' => $nostoProduct] + ['product' => $nostoProduct, 'magentoProduct' => $product, 'modelFilter' => $modelFilter] ); + if (!$modelFilter->isValid()) { + throw new FilteredProductException( + sprintf( + 'Product id %d did not pass post-build model filter for store %s', + $product->getId(), + $store->getCode() + ) + ); + } return $nostoProduct; } /** + * Adds selected attributes to all tags also in the custom fields section + * * @param Product $product * @param Store $store - * @return string + * @return array */ - protected function buildUrl(Product $product, Store $store) + private function getCustomFieldsWithAttributes(Product $product, Store $store) { - return $product->getUrlInStore( - [ - '_ignore_category' => true, - '_nosid' => true, - '_scope_to_url' => true, - '_scope' => $store->getCode(), - ] - ); + if (!$this->nostoDataHelper->isCustomFieldsEnabled($store)) { + return []; + } + // Note that for main product the attributes are the same for custom fields & tags + return $this->attributeService->getAttributesForCustomFields($product, $store); + } + + /** + * Amends the product attributes to tags array if attributes are defined + * and are present in product + * + * @param Product $product the magento product model. + * @param NostoProduct $nostoProduct nosto product object + * @param Store $store the store model. + * @throws NostoException + */ + private function amendAttributeTags(Product $product, NostoProduct $nostoProduct, Store $store) + { + $attributeValues = $this->attributeService->getAttributesForTags($product, $store); + foreach (self::CUSTOMIZED_TAGS as $tag) { + $configuredTagAttributes = $this->nostoDataHelper->getTagAttributes($tag, $store); + if (empty($configuredTagAttributes)) { + continue; + } + foreach ($configuredTagAttributes as $configuredTagAttribute) { + if (!isset($attributeValues[$configuredTagAttribute])) { + continue; + } + $value = $attributeValues[$configuredTagAttribute]; + switch ($tag) { + case 'tag1': + $nostoProduct->addTag1(sprintf('%s:%s', $configuredTagAttribute, $value)); + break; + case 'tag2': + $nostoProduct->addTag2(sprintf('%s:%s', $configuredTagAttribute, $value)); + break; + case 'tag3': + $nostoProduct->addTag3(sprintf('%s:%s', $configuredTagAttribute, $value)); + break; + default: + throw new NostoException('Method add' . $tag . ' is not defined.'); + } + } + } } /** + * Generates the availability for the product + * * @param Product $product * @param Store $store - * @return string|null + * @return string */ - protected function buildImageUrl(Product $product, Store $store) + private function buildAvailability(Product $product, Store $store) { - $primary = $this->_dataHelper->getProductImageVersion($store); - $secondary = 'image'; // The "base" image. - $media = $product->getMediaAttributeValues(); - $image = (isset($media[$primary]) - ? $media[$primary] - : (isset($media[$secondary]) ? $media[$secondary] : null) - ); - - if (empty($image)) { - return null; + $availability = ProductInterface::OUT_OF_STOCK; + $isInStock = $this->availabilityService->isInStock($product, $store); + if (!$product->isVisibleInSiteVisibility() + || (!$this->availabilityService->isAvailableInStore($product, $store) && $isInStock) + || ($product->getStatus() == ProductStatus::STATUS_DISABLED) + ) { + $availability = ProductInterface::INVISIBLE; + } elseif ($isInStock + && $product->isAvailable() + ) { + $availability = ProductInterface::IN_STOCK; } - return $product->getMediaConfig()->getMediaUrl($image); + return $availability; } /** - * @param Product $product + * Adds the alternative image urls + * + * @param Product $product the product model. + * @param Store $store * @return array */ - protected function buildCategories(Product $product) + public function buildAlternativeImages(Product $product, Store $store) { - $categories = []; - foreach ($product->getCategoryCollection() as $category) { - $categories[] = $this->_categoryBuilder->build($category); + $images = []; + $this->galleryReadHandler->execute($product); + foreach ($product->getMediaGalleryImages() as $image) { + if (isset($image['url']) && (isset($image['disabled']) && $image['disabled'] !== '1')) { + $images[] = $this->imageService + ->finalizeImageUrl($image['url'], $store); + } } - return $categories; + + return $images; } /** * @param Product $product + * @param Store $store * @return array */ - protected function buildTags(Product $product) + public function buildDefaultTags(Product $product, Store $store) { $tags = []; - foreach ($product->getAttributes() as $attr) { - if ($attr->getIsVisibleOnFront() - && $product->hasData($attr->getAttributeCode()) - ) { - $label = $attr->getStoreLabel(); - $value = $attr->getFrontend()->getValue($product); - if (is_string($label) && strlen($label) - && is_string($value) && strlen($value) - ) { - $tags[] = "{$label}: {$value}"; - } - } + if (!$product->canConfigure()) { + $tags[] = ProductInterface::ADD_TO_CART; } - if (!$product->canConfigure()) { - $tags[] = \NostoProduct::PRODUCT_ADD_TO_CART; + if ($this->nostoDataHelper->isLowStockIndicationEnabled($store) + && $this->lowStockHelper->build($product) + ) { + $tags[] = ProductInterface::LOW_STOCK; } return $tags; diff --git a/Model/Product/CollectionBuilder.php b/Model/Product/CollectionBuilder.php new file mode 100644 index 000000000..41bc49cef --- /dev/null +++ b/Model/Product/CollectionBuilder.php @@ -0,0 +1,141 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Product; + +use Exception; +use Magento\Catalog\Model\Product; +use Magento\Store\Model\Store; +use Nosto\NostoException; +use Nosto\Model\Product\ProductCollection as NostoProductCollection; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\ResourceModel\Magento\Product\Collection as ProductCollection; +use Nosto\Tagging\Model\ResourceModel\Magento\Product\CollectionBuilder as ProductCollectionBuilder; +use Nosto\Tagging\Model\Service\Product\ProductServiceInterface; +use Traversable; + +/** + * A builder class for building collection containing Nosto products + */ +class CollectionBuilder +{ + /** @var ProductCollectionBuilder */ + private ProductCollectionBuilder $productCollectionBuilder; + + /** @var ProductServiceInterface */ + private ProductServiceInterface $productService; + + /** @var NostoLogger */ + private NostoLogger $logger; + + /** + * Collection constructor. + * @param ProductCollectionBuilder $productCollectionBuilder + * @param ProductServiceInterface $productService + * @param NostoLogger $logger + */ + public function __construct( + ProductCollectionBuilder $productCollectionBuilder, + ProductServiceInterface $productService, + NostoLogger $logger + ) { + $this->productCollectionBuilder = $productCollectionBuilder; + $this->productService = $productService; + $this->logger = $logger; + } + + /** + * @param Store $store + * @param $id + * @return NostoProductCollection + * @throws NostoException + */ + public function buildSingle(Store $store, $id) + { + return $this->load( + $store, + $this->productCollectionBuilder->buildSingle($store, $id) + ); + } + + /** + * @param Store $store + * @param int $limit + * @param int $offset + * @return NostoProductCollection + * @throws NostoException + */ + public function buildMany(Store $store, int $limit = 100, int $offset = 0) + { + return $this->load( + $store, + $this->productCollectionBuilder->buildMany($store, $limit, $offset) + ); + } + + /** + * @param Store $store + * @param $collection + * @return NostoProductCollection + * @throws NostoException + */ + private function load(Store $store, $collection) + { + /** @var ProductCollection $collection */ + $products = new NostoProductCollection(); + $items = $collection->load(); + if ($items instanceof Traversable === false && !is_array($items)) { + throw new NostoException( + sprintf('Invalid collection type %s for product export', get_class($collection)) + ); + } + foreach ($items as $product) { + /** @var Product $product */ + try { + $nostoProduct = $this->productService->getProduct( + $product, + $store + ); + if ($nostoProduct !== null) { + $products->append($nostoProduct); + } + } catch (Exception $e) { + $this->logger->exception($e); + } + } + return $products; + } +} diff --git a/Model/Product/Queue/QueueBuilder.php b/Model/Product/Queue/QueueBuilder.php new file mode 100644 index 000000000..7b41d0bd1 --- /dev/null +++ b/Model/Product/Queue/QueueBuilder.php @@ -0,0 +1,111 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Product\Queue; + +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Api\Data\StoreInterface; +use Nosto\Tagging\Api\Data\ProductUpdateQueueInterface; +use Nosto\Tagging\Model\Product\Update\Queue as QueueModel; +use Nosto\Tagging\Model\Product\Update\QueueFactory; + +class QueueBuilder +{ + /** @var QueueFactory */ + private QueueFactory $queueFactory; + + /** @var TimezoneInterface */ + private TimezoneInterface $magentoTimeZone; + + /** + * Builder constructor. + * @param QueueFactory $queueFactory + * @param TimezoneInterface $magentoTimeZone + */ + public function __construct( + QueueFactory $queueFactory, + TimezoneInterface $magentoTimeZone + ) { + $this->queueFactory = $queueFactory; + $this->magentoTimeZone = $magentoTimeZone; + } + + /** + * @param StoreInterface $store + * @param array $productIds + * @return QueueModel + */ + public function build( + StoreInterface $store, + array $productIds + ) { + $queueModel = $this->queueFactory->create(); + $queueModel->setProductIds(array_values($productIds)); + $queueModel->setCreatedAt($this->magentoTimeZone->date()); + $queueModel->setStore($store); + $queueModel->setStatus(ProductUpdateQueueInterface::STATUS_VALUE_NEW); + $queueModel->setProductIdCount(count($productIds)); + return $queueModel; + } + + /** + * @param StoreInterface $store + * @param array $productIds + * @return QueueModel + */ + public function buildForUpsert( + StoreInterface $store, + array $productIds + ) { + $queueModel = $this->build($store, $productIds); + $queueModel->setAction(ProductUpdateQueueInterface::ACTION_VALUE_UPSERT); + return $queueModel; + } + + /** + * @param StoreInterface $store + * @param array $productIds + * @return QueueModel + */ + public function buildForDeletion( + StoreInterface $store, + array $productIds + ) { + $queueModel = $this->build($store, $productIds); + $queueModel->setAction(ProductUpdateQueueInterface::ACTION_VALUE_DELETE); + return $queueModel; + } +} diff --git a/Model/Product/Queue/QueueRepository.php b/Model/Product/Queue/QueueRepository.php new file mode 100644 index 000000000..ec56112c8 --- /dev/null +++ b/Model/Product/Queue/QueueRepository.php @@ -0,0 +1,113 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Product\Queue; + +use Exception; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\Store; +use Nosto\Tagging\Api\Data\ProductUpdateQueueInterface; +use Nosto\Tagging\Api\ProductUpdateQueueRepositoryInterface; +use Nosto\Tagging\Model\ResourceModel\Product\Update\Queue as QueueResource; +use Nosto\Tagging\Model\ResourceModel\Product\Update\Queue\QueueCollection; +use Nosto\Tagging\Model\ResourceModel\Product\Update\Queue\QueueCollectionFactory; + +class QueueRepository implements ProductUpdateQueueRepositoryInterface +{ + /** @var QueueCollectionFactory */ + private QueueCollectionFactory $queueCollectionFactory; + + /** @var QueueResource */ + private QueueResource $queueResource; + + /** + * IndexRepository constructor. + * + * @param QueueResource $queueResource + * @param QueueCollectionFactory $queueCollectionFactory + */ + public function __construct( + QueueResource $queueResource, + QueueCollectionFactory $queueCollectionFactory + ) { + $this->queueResource = $queueResource; + $this->queueCollectionFactory = $queueCollectionFactory; + } + + public function getTotalCount(Store $store) + { + $collection = $this->queueCollectionFactory->create(); + if ((int)$store->getId() !== 0) { + $collection->addStoreFilter($store); + } + return $collection->getSize(); + } + + /** + * @param StoreInterface $store + * @return QueueCollection + */ + public function getByStore(StoreInterface $store) + { + /* @var QueueCollection $collection */ + return $this->queueCollectionFactory->create() + ->addStoreFilter($store); + } + + /** + * @param ProductUpdateQueueInterface $entry + * @return ProductUpdateQueueInterface|QueueResource + * @throws AlreadyExistsException + * @noinspection PhpParamsInspection + */ + public function save(ProductUpdateQueueInterface $entry) + { + /** @phan-suppress-next-line PhanTypeMismatchArgument */ + return $this->queueResource->save($entry); + } + + /** + * @param ProductUpdateQueueInterface $entry + * @throws Exception + * @noinspection PhpParamsInspection + */ + public function delete(ProductUpdateQueueInterface $entry) + { + /** @phan-suppress-next-line PhanTypeMismatchArgument */ + $this->queueResource->delete($entry); + } +} diff --git a/Model/Product/Ratings.php b/Model/Product/Ratings.php new file mode 100644 index 000000000..0df4265d5 --- /dev/null +++ b/Model/Product/Ratings.php @@ -0,0 +1,75 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Product; + +class Ratings +{ + /** @var int */ + private int $reviewCount; + + /** @var float */ + private float $rating; + + /** + * @return int $reviewCount + */ + public function getReviewCount() + { + return $this->reviewCount; + } + + /** + * @param int $reviewCount + */ + public function setReviewCount(int $reviewCount) + { + $this->reviewCount = (int)$reviewCount; + } + + public function getRating() + { + return $this->rating; + } + + /** + * @param float $rating + */ + public function setRating(float $rating) + { + $this->rating = (float)$rating; + } +} diff --git a/Model/Product/Repository.php b/Model/Product/Repository.php new file mode 100644 index 000000000..b67535cd5 --- /dev/null +++ b/Model/Product/Repository.php @@ -0,0 +1,346 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Product; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductSearchResultsInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\Product\Visibility as ProductVisibility; +use Magento\Catalog\Model\ProductRepository; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableType; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable as ConfigurableProduct; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\FilterGroupBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\Store; +use Nosto\Tagging\Exception\ParentProductDisabledException; +use Nosto\Tagging\Model\ResourceModel\Sku; +use Nosto\Tagging\Model\Service\Stock\Provider\StockProviderInterface; + +/** + * Repository wrapper class for fetching products + */ +class Repository +{ + public const MAX_SKUS = 5000; + + private array $parentProductIdCache = []; + + /** @var ProductRepository $productRepository */ + private ProductRepository $productRepository; + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + private SearchCriteriaBuilder $searchCriteriaBuilder; + + /** @var ConfigurableProduct $configurableProduct */ + private ConfigurableProduct $configurableProduct; + + /** @var FilterGroupBuilder $filterGroupBuilder */ + private FilterGroupBuilder $filterGroupBuilder; + + /** @var FilterBuilder $filterBuilder */ + private FilterBuilder $filterBuilder; + + /** @var ConfigurableType $configurableType */ + private ConfigurableType $configurableType; + + /** @var ProductVisibility $productVisibility */ + private ProductVisibility $productVisibility; + + /** @var StockProviderInterface $stockProvider */ + private StockProviderInterface $stockProvider; + + /** @var Sku $skuResource */ + private Sku $skuResource; + + /** + * Constructor to instantiating the reindex command. This constructor uses proxy classes for + * two of the Nosto objects to prevent introspection of constructor parameters when the DI + * compile command is run. + * Not using the proxy classes will lead to a "Area code not set" exception being thrown in the + * compile phase. + * + * @param ProductRepository $productRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param ConfigurableProduct $configurableProduct + * @param FilterBuilder $filterBuilder + * @param FilterGroupBuilder $filterGroupBuilder + * @param ConfigurableType $configurableType + * @param ProductVisibility $productVisibility + * @param StockProviderInterface $stockProvider + * @param Sku $skuResource + */ + public function __construct( + ProductRepository $productRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + ConfigurableProduct $configurableProduct, + FilterBuilder $filterBuilder, + FilterGroupBuilder $filterGroupBuilder, + ConfigurableType $configurableType, + ProductVisibility $productVisibility, + StockProviderInterface $stockProvider, + Sku $skuResource + ) { + $this->productRepository = $productRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->configurableProduct = $configurableProduct; + $this->filterGroupBuilder = $filterGroupBuilder; + $this->filterBuilder = $filterBuilder; + $this->configurableType = $configurableType; + $this->productVisibility = $productVisibility; + $this->stockProvider = $stockProvider; + $this->skuResource = $skuResource; + } + + /** + * Gets products by product ids + * + * @param array $ids + * @return ProductSearchResultsInterface + */ + public function getByIds(array $ids) + { + $this->productRepository->cleanCache(); + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('entity_id', $ids, 'in') + ->create(); + return $this->productRepository->getList($searchCriteria); + } + + /** + * Gets a product that is active in a given Store + * + * @return Product|null + * @suppress PhanTypeMismatchArgument + * + */ + public function getRandomSingleActiveProduct() + { + $filterStatus = $this->filterBuilder + ->setField('status') + ->setValue(1) + ->setConditionType('eq') + ->create(); + + $filterVisible = $this->filterBuilder + ->setField('visibility') + ->setValue($this->productVisibility->getVisibleInSiteIds()) + ->setConditionType('in') + ->create(); + + $filterGroup = $this->filterGroupBuilder->setFilters([$filterStatus, $filterVisible])->create(); + $searchCriteria = $this->searchCriteriaBuilder + ->setFilterGroups([$filterGroup]) + ->setCurrentPage(1) + ->setPageSize(1) + ->create(); + + $product = $this->productRepository->getList($searchCriteria)->setTotalCount(1); + + foreach ($product->getItems() as $item) { + /** + * Returning ProductInterface but declared to return Product|null + */ + /** @phan-suppress-next-next-line PhanTypeMismatchReturnSuperType */ + /** @var Product $item */ + return $item; + } + return null; + } + + /** + * Gets the parent products for simple product + * + * @param ProductInterface $product + * @return string[]|null + * @throws ParentProductDisabledException + * @suppress PhanTypeMismatchReturn + */ + public function resolveParentProductIds(ProductInterface $product) + { + if ($this->getParentIdsFromCache($product)) { + return $this->getParentIdsFromCache($product); + } + $parentProductIds = null; + if ($product->getTypeId() === Type::TYPE_SIMPLE) { + $parentProductIds = $this->configurableProduct->getParentIdsByChild( + $product->getId() + ); + + //If product has parent ids, sanitize and check if they are not disabled + if (count($parentProductIds) != 0) { + $parentProductIds = $this->filterWithDefaultVisibility($parentProductIds); + if (count($parentProductIds) == 0) { + throw new ParentProductDisabledException($product->getId()); + } + } + + $this->saveParentIdsToCache($product, $parentProductIds); + } + return $parentProductIds; + } + + /** + * + * @param array $ids + * @return array + */ + private function filterWithDefaultVisibility(array $ids) + { + $this->productRepository->cleanCache(); + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('entity_id', $ids, 'in') + ->addFilter('status', Status::STATUS_ENABLED, 'eq') + ->addFilter('visibility', Visibility::VISIBILITY_NOT_VISIBLE, 'neq') + ->create(); + $items = $this->productRepository->getList($searchCriteria) + ->getItems(); + + $filteredParentIds = []; + foreach ($items as $item) { + $filteredParentIds[] = $item->getId(); + } + return $filteredParentIds; + } + + /** + * Gets the variations / SKUs of configurable product + * + * @param Product $product + * @return array + */ + public function getSkus(Product $product) + { + $skuIds = $this->getSkuIds($product); + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('entity_id', $skuIds, 'in') + ->create(); + $products = $this->productRepository->getList($searchCriteria)->setTotalCount(self::MAX_SKUS); + + return $products->getItems(); + } + + /** + * Returns the sku ids for a specific product + * + * @param Product $product + * @return array + */ + public function getSkuIds(Product $product) + { + $batched = $this->configurableType->getChildrenIds($product->getId()); + $flat = []; + foreach ($batched as $batch => $ids) { + if (is_array($ids)) { + foreach ($ids as $id) { + $flat[$id] = $id; + } + } + } + + return $flat; + } + + /** + * Get parent ids from cache. Return null if the cache is not available + * + * @param ProductInterface $product + * @return string[]|null + */ + private function getParentIdsFromCache(ProductInterface $product) + { + if (isset($this->parentProductIdCache[$product->getId()])) { + return $this->parentProductIdCache[$product->getId()]; + } + + return null; + } + + /** + * Saves the parents product ids to internal cache to avoid redundant + * database queries + * + * @param ProductInterface $product + * @param string[] $parentProductIds + */ + private function saveParentIdsToCache(ProductInterface $product, array $parentProductIds) + { + $this->parentProductIdCache[$product->getId()] = $parentProductIds; + } + + /** + * Gets the variations / SKUs of configurable product as an associative array. + * + * @param Product $product + * @param Store $store + * @return array + * @throws NoSuchEntityException + */ + public function getSkusAsArray(Product $product, Store $store) + { + $skuIds = $this->getSkuIds($product); + if (empty($skuIds)) { + return []; + } + $inStockProductsByIds = $this->stockProvider->getInStockProductIds( + $skuIds, + $store->getWebsite() + ); + return $this->skuResource->getSkuPricesByIds($store->getWebsite(), $inStockProductsByIds); + } + + /** + * Loads (or reloads) Product object + * @param int $productId + * @param int $storeId + * @return ProductInterface|Product + * @throws NoSuchEntityException + */ + public function reloadProduct(int $productId, int $storeId) + { + return $this->productRepository->getById( + $productId, + false, + $storeId, + true + ); + } +} diff --git a/Model/Product/Sku/Builder.php b/Model/Product/Sku/Builder.php new file mode 100644 index 000000000..6f2f62c66 --- /dev/null +++ b/Model/Product/Sku/Builder.php @@ -0,0 +1,206 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Product\Sku; + +use Exception; +use Magento\Catalog\Model\Product; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection + as ConfigurableAttributeCollection; +use Magento\Framework\Event\ManagerInterface; +use Magento\Store\Model\Store; +use Nosto\Model\Product\Sku as NostoSku; +use Nosto\Tagging\Helper\Currency as CurrencyHelper; +use Nosto\Tagging\Helper\Data as NostoDataHelper; +use Nosto\Tagging\Helper\Price as NostoPriceHelper; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Service\Product\Attribute\AttributeServiceInterface; +use Nosto\Tagging\Model\Service\Product\AvailabilityService; +use Nosto\Tagging\Model\Service\Product\ImageService; +use Nosto\Tagging\Model\Service\Stock\StockService; +use Nosto\Types\Product\ProductInterface; + +// @codingStandardsIgnoreLine + +class Builder +{ + /** @var NostoDataHelper */ + private NostoDataHelper $nostoDataHelper; + + /** @var NostoPriceHelper */ + private NostoPriceHelper $nostoPriceHelper; + + /** @var NostoLogger */ + private NostoLogger $nostoLogger; + + /** @var ManagerInterface */ + private ManagerInterface $eventManager; + + /** @var CurrencyHelper */ + private CurrencyHelper $nostoCurrencyHelper; + + /** @var AttributeServiceInterface */ + private AttributeServiceInterface $attributeService; + + /** @var AvailabilityService */ + private AvailabilityService $availabilityService; + + /** @var ImageService */ + private ImageService $imageService; + + /** @var StockService */ + private StockService $stockService; + + /** + * Builder constructor. + * @param NostoDataHelper $nostoDataHelper + * @param NostoPriceHelper $priceHelper + * @param NostoLogger $nostoLogger + * @param ManagerInterface $eventManager + * @param CurrencyHelper $nostoCurrencyHelper + * @param AttributeServiceInterface $attributeService + * @param AvailabilityService $availabilityService + * @param ImageService $imageService + * @param StockService $stockService + */ + public function __construct( + NostoDataHelper $nostoDataHelper, + NostoPriceHelper $priceHelper, + NostoLogger $nostoLogger, + ManagerInterface $eventManager, + CurrencyHelper $nostoCurrencyHelper, + AttributeServiceInterface $attributeService, + AvailabilityService $availabilityService, + ImageService $imageService, + StockService $stockService + ) { + $this->nostoDataHelper = $nostoDataHelper; + $this->nostoPriceHelper = $priceHelper; + $this->nostoLogger = $nostoLogger; + $this->eventManager = $eventManager; + $this->nostoCurrencyHelper = $nostoCurrencyHelper; + $this->attributeService = $attributeService; + $this->availabilityService = $availabilityService; + $this->imageService = $imageService; + $this->stockService = $stockService; + } + + /** + * @param Product $product + * @param Store $store + * @param ConfigurableAttributeCollection $attributes + * @return NostoSku|null + * @throws Exception + */ + public function build( + Product $product, + Store $store, + ConfigurableAttributeCollection $attributes + ) { + if (!$this->availabilityService->isAvailableInStore($product, $store)) { + return null; + } + + $nostoSku = new NostoSku(); + try { + $nostoSku->setId($product->getId()); + $nostoSku->setName($product->getName()); + $nostoSku->setAvailability($this->buildSkuAvailability($product, $store)); + $nostoSku->setImageUrl($this->imageService->buildImageUrl($product, $store)); + $price = $this->nostoCurrencyHelper->convertToTaggingPrice( + $this->nostoPriceHelper->getProductFinalDisplayPrice( + $product, + $store + ), + $store + ); + $nostoSku->setPrice($price); + $listPrice = $this->nostoCurrencyHelper->convertToTaggingPrice( + $this->nostoPriceHelper->getProductDisplayPrice( + $product, + $store + ), + $store + ); + $nostoSku->setListPrice($listPrice); + $gtinAttribute = $this->nostoDataHelper->getGtinAttribute($store); + if (is_string($gtinAttribute) && $product->hasData($gtinAttribute)) { + $nostoSku->setGtin($product->getData($gtinAttribute)); + } + + if ($this->nostoDataHelper->isCustomFieldsEnabled($store)) { + foreach ($attributes as $attribute) { + try { + $code = $attribute->getProductAttribute()->getAttributeCode(); + $nostoSku->addCustomField( + $code, + $this->attributeService->getAttributeValueByAttributeCode($product, $code) + ); + } catch (Exception $e) { + $this->nostoLogger->exception($e); + } + } + } + if ($this->nostoDataHelper->isInventoryTaggingEnabled($store)) { + $nostoSku->setInventoryLevel($this->stockService->getQuantity($product, $store)); + } + } catch (Exception $e) { + $this->nostoLogger->exception($e); + } + + $this->eventManager->dispatch('nosto_sku_load_after', ['sku' => $nostoSku, 'magentoProduct' => $product]); + + return $nostoSku; + } + + /** + * Generates the availability for the SKU + * + * @param Product $product + * @param Store $store + * @return string + */ + private function buildSkuAvailability(Product $product, Store $store) + { + if ($product->isAvailable() + && $this->availabilityService->isInStock($product, $store) + ) { + return ProductInterface::IN_STOCK; + } + + return ProductInterface::OUT_OF_STOCK; + } +} diff --git a/Model/Product/Sku/Collection.php b/Model/Product/Sku/Collection.php new file mode 100644 index 000000000..ba929095d --- /dev/null +++ b/Model/Product/Sku/Collection.php @@ -0,0 +1,121 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Product\Sku; + +use Exception; +use Magento\Catalog\Model\Product; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableType; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection + as ConfigurableAttributeCollection; +use Magento\Store\Model\Store; +use Nosto\Model\Product\SkuCollection; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Product\Repository as NostoProductRepository; +use Nosto\Tagging\Model\Product\Sku\Builder as NostoSkuBuilder; +use Nosto\Types\Product\SkuInterface; + +class Collection +{ + private ConfigurableType $configurableType; + private NostoLogger $logger; + private Builder $nostoSkuBuilder; + private NostoProductRepository $nostoProductRepository; + + /** + * Builder constructor. + * @param NostoLogger $logger + * @param ConfigurableType $configurableType + * @param Builder $nostoSkuBuilder + * @param NostoProductRepository $nostoProductRepository + */ + public function __construct( + NostoLogger $logger, + ConfigurableType $configurableType, + NostoSkuBuilder $nostoSkuBuilder, + NostoProductRepository $nostoProductRepository + ) { + $this->configurableType = $configurableType; + $this->logger = $logger; + $this->nostoSkuBuilder = $nostoSkuBuilder; + $this->nostoProductRepository = $nostoProductRepository; + } + + /** + * @param Product $product + * @param Store $store + * @return SkuCollection + * @throws Exception + * @suppress PhanUndeclaredMethod + */ + public function build(Product $product, Store $store) + { + $skuCollection = new SkuCollection(); + if ($product->getTypeId() === ConfigurableType::TYPE_CODE) { + $configurableAttributes = $this->getConfigurableAttributes($product); + /** @var ConfigurableType $productTypeInstance */ + $productTypeInstance = $product->getTypeInstance(); + $usedProducts = $productTypeInstance->getUsedProducts($product); + /** @var Product $product */ + foreach ($usedProducts as $usedProduct) { + /** @var Product $usedProduct */ + if (!$usedProduct->isDisabled()) { + $sku = $this->nostoSkuBuilder->build($usedProduct, $store, $configurableAttributes); + if ($sku instanceof SkuInterface) { + $skuCollection->append($sku); + } + } + } + } + return $skuCollection; + } + + /** + * @param Product $product + * @return ConfigurableAttributeCollection + */ + public function getConfigurableAttributes(Product $product) + { + /* @var ConfigurableAttributeCollection $attributes */ + $attributes = $this->configurableType->getConfigurableAttributes($product); + /** + * Returning \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute[] + * but declared to return ConfigurableAttributeCollection + */ + /** @phan-suppress-next-line PhanTypeMismatchReturn */ + return $attributes; + } +} diff --git a/Model/Product/Tags/LowStock.php b/Model/Product/Tags/LowStock.php new file mode 100644 index 000000000..80e9931db --- /dev/null +++ b/Model/Product/Tags/LowStock.php @@ -0,0 +1,69 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Product\Tags; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type; +use Magento\CatalogInventory\Api\StockStateInterface; + +class LowStock +{ + private StockStateInterface $stockItem; + + /** + * LowStock constructor. + * @param StockStateInterface $stockItem + */ + public function __construct(StockStateInterface $stockItem) + { + $this->stockItem = $stockItem; + } + + /** + * Builds a custom tag to denote a low-stock for simple product + * + * @param Product $product + * @return bool + */ + public function build(Product $product) + { + if ($product->getTypeId() === Type::TYPE_SIMPLE) { + return $this->stockItem->verifyNotification($product->getId()); + } + return false; + } +} diff --git a/Model/Product/Update/Queue.php b/Model/Product/Update/Queue.php new file mode 100644 index 000000000..55a83928a --- /dev/null +++ b/Model/Product/Update/Queue.php @@ -0,0 +1,208 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Product\Update; + +use DateTime; +use Magento\Framework\Model\AbstractModel; +use Magento\Store\Api\Data\StoreInterface; +use Nosto\Tagging\Api\Data\ProductUpdateQueueInterface; +use Nosto\Tagging\Model\ResourceModel\Product\Update\Queue as QueueResource; + +class Queue extends AbstractModel implements ProductUpdateQueueInterface +{ + /** + * @inheritDoc + */ + public function getProductIdCount() + { + return $this->getData(self::PRODUCT_ID_COUNT); + } + + /** + * @inheritDoc + */ + public function setProductIdCount(int $count) + { + return $this->setData(self::PRODUCT_ID_COUNT, $count); + } + + /** + * @inheritDoc + */ + public function getAction() + { + return $this->getData(self::ACTION); + } + + /** + * @inheritDoc + */ + public function setAction(string $action) + { + return $this->setData(self::ACTION, $action); + } + + /** + * @inheritDoc + */ + public function getStatus() + { + return $this->getData(self::STATUS); + } + + /** + * @inheritDoc + */ + public function setStatus(string $status) + { + return $this->setData(self::STATUS, $status); + } + + /** + * @inheritDoc + */ + public function getProductIds() + { + return $this->getData(self::PRODUCT_IDS); + } + + /** + * @inheritDoc + */ + public function setProductIds(array $productIds) + { + return $this->setData(self::PRODUCT_IDS, $productIds); + } + + /** + * @inheritDoc + */ + public function getId() + { + return $this->getData(self::ID); + } + + /** + * @inheritDoc + */ + public function getStoreId() + { + return $this->getData(self::STORE_ID); + } + + /** + * @inheritDoc + */ + public function getCreatedAt() + { + return $this->getData(self::CREATED_AT); + } + + /** + * @inheritDoc + */ + public function getStartedAt() + { + return $this->getData(self::STARTED_AT); + } + + /** + * @inheritDoc + */ + public function getCompletedAt() + { + return $this->getData(self::COMPLETED_AT); + } + + /** + * @inheritDoc + */ + public function setId($id) + { + return $this->setData(self::ID, $id); + } + + /** + * @inheritDoc + */ + public function setCreatedAt(DateTime $createdAt) + { + return $this->setData(self::CREATED_AT, $createdAt); + } + + /** + * @inheritDoc + */ + public function setStartedAt(DateTime $startedAt) + { + return $this->setData(self::STARTED_AT, $startedAt); + } + + /** + * @inheritDoc + */ + public function setCompletedAt(DateTime $completedAt) + { + return $this->setData(self::COMPLETED_AT, $completedAt); + } + + /** + * @inheritDoc + */ + public function setStoreId(int $storeId) + { + return $this->setData(self::STORE_ID, $storeId); + } + + /** + * @inheritDoc + */ + public function setStore(StoreInterface $store) + { + return $this->setStoreId($store->getId()); + } + + /** + * Initialize resource model + * + * @return void + */ + public function _construct() + { + $this->_init(QueueResource::class); + } +} diff --git a/Model/Product/Url/Builder.php b/Model/Product/Url/Builder.php new file mode 100644 index 000000000..7071ad31d --- /dev/null +++ b/Model/Product/Url/Builder.php @@ -0,0 +1,113 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Product\Url; + +use Magento\Catalog\Model\Product; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\DataObject; +use Magento\Framework\Url; +use Magento\Store\Model\Store; +use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Nosto\Tagging\Helper\Data as NostoDataHelper; + +/** + * Url builder class cannibalised from the Magento core. When trying to get the URL of a product + * from the Magento backend, the DI rules inject inject a reference to \Magento\Backend\Model\Url + * instead of \Magento\Framework\Url. Both these classes implement the Magento\Framework\UrlInterface + * and are responsible for URL building. The building of the route parameters is the same whether + * it occurs on the backend or the frontend but the actual building of the URL differs from the + * backend to the frontend. + *
+ * There's no clean way of changing this behaviour without modifying the core so this class contains + * code from \Magento\Catalog\Model\Product\Url which now always uses the frontend version of the + * Magento\Framework\UrlInterface class. + */ +class Builder extends DataObject +{ + private UrlFinderInterface $urlFinder; + private Url $urlFactory; + private NostoDataHelper $nostoDataHelper; + + /** + * @param Url $urlFactory + * @param UrlFinderInterface $urlFinder + * @param NostoDataHelper $nostoDataHelper + * @param array $data + */ + public function __construct( + Url $urlFactory, + UrlFinderInterface $urlFinder, + NostoDataHelper $nostoDataHelper, + array $data = [] + ) { + parent::__construct($data); + $this->urlFinder = $urlFinder; + $this->urlFactory = $urlFactory; + $this->nostoDataHelper = $nostoDataHelper; + } + + public function getUrlInStore(Product $product, Store $store) + { + $routeParams = []; + $routePath = ''; + $filterData = [ + UrlRewrite::ENTITY_ID => $product->getId(), + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => $store->getId(), + ]; + $productRequestPath = $product->getData('request_path'); + if (!empty($productRequestPath)) { + $filterData[UrlRewrite::REQUEST_PATH] = $productRequestPath; + } + $rewrite = $this->urlFinder->findOneByData($filterData); + if ($rewrite) { + $routeParams['_direct'] = $rewrite->getRequestPath(); + } else { // If the rewrite is not found fallback to the "ugly version" of the URL + $routePath = 'catalog/product/view'; + $routeParams['id'] = $product->getId(); + $routeParams['s'] = $product->getUrlKey(); + } + $routeParams['_nosid'] = true; // Remove the session identifier from the URL + $routeParams['_scope'] = $store->getCode(); // Specify the store identifier for the URL + $routeParams['_scope_to_url'] = $this->nostoDataHelper->getStoreCodeToUrl($store); + $routeParams['_query'] = []; // Reset the cached URL instance GET query params + + return $this->urlFactory->setScope($store->getId()) + ->getUrl($routePath, $routeParams); + } +} diff --git a/Model/Product/Variation/Builder.php b/Model/Product/Variation/Builder.php new file mode 100644 index 000000000..759249c5e --- /dev/null +++ b/Model/Product/Variation/Builder.php @@ -0,0 +1,251 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Product\Variation; + +use Exception; +use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory as PriceFactory; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product as MageProduct; +use Magento\CatalogRule\Model\ResourceModel\Rule as RuleResourceModel; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableType; +use Magento\Customer\Model\Data\Group; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Model\Store; +use Nosto\Model\Product\Product as NostoProduct; +use Nosto\Model\Product\Variation; +use Nosto\NostoException; +use Nosto\Tagging\Helper\Currency as CurrencyHelper; +use Nosto\Tagging\Helper\Price as NostoPriceHelper; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Product\Repository as NostoProductRepository; + +class Builder +{ + private NostoPriceHelper $nostoPriceHelper; + private ManagerInterface $eventManager; + private NostoLogger $logger; + private CurrencyHelper $nostoCurrencyHelper; + private PriceFactory $priceFactory; + private RuleResourceModel $ruleResourceModel; + private NostoProductRepository $nostoProductRepository; + private TimezoneInterface $localeDate; + + /** + * Builder constructor. + * @param NostoPriceHelper $priceHelper + * @param NostoLogger $logger + * @param ManagerInterface $eventManager + * @param CurrencyHelper $nostoCurrencyHelper + * @param PriceFactory $priceFactory + * @param RuleResourceModel $ruleResourceModel + * @param NostoProductRepository $nostoProductRepository + * @param TimezoneInterface $localeDate + */ + public function __construct( + NostoPriceHelper $priceHelper, + NostoLogger $logger, + ManagerInterface $eventManager, + CurrencyHelper $nostoCurrencyHelper, + PriceFactory $priceFactory, + RuleResourceModel $ruleResourceModel, + NostoProductRepository $nostoProductRepository, + TimezoneInterface $localeDate + ) { + $this->nostoPriceHelper = $priceHelper; + $this->logger = $logger; + $this->eventManager = $eventManager; + $this->nostoCurrencyHelper = $nostoCurrencyHelper; + $this->priceFactory = $priceFactory; + $this->ruleResourceModel = $ruleResourceModel; + $this->nostoProductRepository = $nostoProductRepository; + $this->localeDate = $localeDate; + } + + /** + * @param Product $product + * @param NostoProduct $nostoProduct + * @param Store $store + * @param Group $group + * @return Variation + */ + public function build( + Product $product, + NostoProduct $nostoProduct, + Store $store, + Group $group + ) { + $variation = new Variation(); + try { + $variation->setVariationId($group->getCode()); + $variation->setAvailability($nostoProduct->getAvailability()); + $variation->setPrice($this->getLowestVariationPrice($product, $group, $store)); + $listPrice = $this->nostoCurrencyHelper->convertToTaggingPrice( + $this->nostoPriceHelper->getProductDisplayPrice( + $product, + $store + ), + $store + ); + $variation->setListPrice($listPrice); + $variation->setPriceCurrencyCode($nostoProduct->getPriceCurrencyCode()); + } catch (Exception $e) { + $this->logger->exception($e); + } + + $this->eventManager->dispatch( + 'nosto_variation_load_after', + [ + 'variation' => $variation, + 'magentoProduct' => $product + ] + ); + return $variation; + } + + /** + * @param Product $product + * @param Group $group + * @param Store $store + * @return float + * @throws LocalizedException + * @throws NoSuchEntityException|NostoException + */ + private function getLowestVariationPrice(Product $product, Group $group, Store $store) + { + // If product is configurable, the parent has no customer group price. Get SKU with lowest price + if ($product->getTypeInstance() instanceof ConfigurableType) { + $product = $this->getMinPriceSku($product, $group, $store); + } + + // Only returns the SKU price if it's lower than final price + // Merchant can have a fixed customer group price that is higher than the product + // price with a catalog price discount rule applied. + // This is normal Magento 2 behaviour + $productTierPriceInterfaces = $product->getTierPrices(); + foreach ($productTierPriceInterfaces as $price) { + if ($price->getCustomerGroupId() === $group->getId() + && $price->getValue() < $product->getFinalPrice() + ) { + return $this->nostoPriceHelper->addTaxDisplayPriceIfApplicable( + $product, + $store, + $price->getValue() + ); + } + } + + $rulePrice = $this->ruleResourceModel->getRulePrice( + $this->localeDate->scopeDate(), + $store->getWebsiteId(), + $group->getId(), + $product->getId() + ); + + if ($rulePrice) { + return $this->nostoPriceHelper->addTaxDisplayPriceIfApplicable( + $product, + $store, + $rulePrice + ); + } + + // If no tier prices, there's no customer group pricing for this product + // or it's higher than final price with catalog price rule discount + return $this->nostoPriceHelper->getProductPrice($product, $store); + } + + /** + * Returns the SKU|Product object with the lowest price. + * + * @param MageProduct $product + * @param Group $group + * @param Store $store + * @return MageProduct + */ + public function getMinPriceSku(Product $product, Group $group, Store $store) + { + $minPriceSku = []; + if (!$product->getTypeInstance() instanceof ConfigurableType) { + return $product; + } + $skus = $this->nostoProductRepository->getSkus($product); + if (empty($skus)) { + return $product; + } + foreach ($skus as $sku) { + if (!$sku instanceof MageProduct) { + continue; + } + $skuPrice = $sku->getPrice(); + $skuRulePrice = $this->ruleResourceModel->getRulePrice( + $this->localeDate->scopeDate(), + $store->getWebsiteId(), + $group->getId(), + $sku->getId() + ); + foreach ($sku->getTierPrices() as $tierPrice) { + if ((int)$tierPrice->getCustomerGroupId() === (int)$group->getId()) { + $skuTierPrice = $tierPrice->getValue(); + } + break; + } + // If has a customer group pricing for current group, + // check if it's lower than regular SKU price + /* @suppress UndeclaredVariable */ + if (isset($skuTierPrice) && $skuRulePrice !== false) { + $skuPrice = min($skuPrice, $skuTierPrice, $skuRulePrice); + } elseif (!isset($skuTierPrice) && $skuRulePrice !== false) { + $skuPrice = min($skuPrice, $skuRulePrice); + } elseif (isset($skuTierPrice) && $skuRulePrice === false) { + $skuPrice = min($skuPrice, $skuTierPrice); + } + + if (empty($minPriceSku)) { // First loop run + $minPriceSku['sku'] = $sku; + $minPriceSku['price'] = $skuPrice; + } elseif ($skuPrice < $minPriceSku['price']) { + $minPriceSku['sku'] = $sku; + $minPriceSku['price'] = $skuPrice; + } + } + /** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */ + return $minPriceSku['sku']; + } +} diff --git a/Model/Product/Variation/Collection.php b/Model/Product/Variation/Collection.php new file mode 100644 index 000000000..aba541743 --- /dev/null +++ b/Model/Product/Variation/Collection.php @@ -0,0 +1,105 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Product\Variation; + +use Magento\Catalog\Model\Product; +use Magento\Customer\Api\GroupRepositoryInterface as GroupRepository; +use Magento\Customer\Model\Data\Group; +use Magento\Customer\Model\GroupManagement; +use Magento\Framework\Exception\LocalizedException; +use Magento\Store\Model\Store; +use Nosto\Model\Product\Product as NostoProduct; +use Nosto\Model\Product\VariationCollection; +use Nosto\Tagging\Model\Product\Variation\Builder as VariationBuilder; + +class Collection +{ + private Builder $nostoVariationBuilder; + private GroupManagement $customerGroupManager; + private GroupRepository $groupRepository; + private Builder $variationBuilder; + + /** + * Collection constructor. + * @param Builder $nostoVariationBuilder + * @param GroupManagement $customerGroupManager + * @param GroupRepository $groupRepository + * @param Builder $variationBuilder + */ + public function __construct( + Builder $nostoVariationBuilder, + GroupManagement $customerGroupManager, + GroupRepository $groupRepository, + VariationBuilder $variationBuilder + ) { + $this->nostoVariationBuilder = $nostoVariationBuilder; + $this->customerGroupManager = $customerGroupManager; + $this->groupRepository = $groupRepository; + $this->variationBuilder = $variationBuilder; + } + + /** + * @param Product $product + * @param NostoProduct $nostoProduct + * @param Store $store + * @return VariationCollection + * @throws LocalizedException + * @suppress PhanTypeMismatchArgument + */ + public function build(Product $product, NostoProduct $nostoProduct, Store $store) + { + $collection = new VariationCollection(); + $groups = $this->customerGroupManager->getLoggedInGroups(); + foreach ($groups as $group) { + // For some (broken?) Magento setups the default group / default + // variation is also part of the customer groups + if ($group->getCode() === (string)$nostoProduct->getVariationId()) { + continue; + } + /** @var Group $group */ + $collection->append( + $this->variationBuilder->build( + $product, + $nostoProduct, + $store, + $group + ) + ); + } + return $collection; + } +} diff --git a/Model/Rates/Builder.php b/Model/Rates/Builder.php new file mode 100644 index 000000000..b05232111 --- /dev/null +++ b/Model/Rates/Builder.php @@ -0,0 +1,109 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Rates; + +use Exception; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Framework\Event\ManagerInterface; +use Magento\Store\Model\Store; +use Nosto\Model\ExchangeRate; +use Nosto\Model\ExchangeRateCollection; +use Nosto\Tagging\Logger\Logger as NostoLogger; + +class Builder +{ + private NostoLogger $logger; + private ManagerInterface $eventManager; + private CurrencyFactory $currencyFactory; + + /** + * @param NostoLogger $logger + * @param ManagerInterface $eventManager + * @param CurrencyFactory $currencyFactory + */ + public function __construct( + NostoLogger $logger, + ManagerInterface $eventManager, + CurrencyFactory $currencyFactory + ) { + $this->logger = $logger; + $this->eventManager = $eventManager; + $this->currencyFactory = $currencyFactory; + } + + /** + * Builds the collection of exchange-rates for the specified store view. The collection of rates + * contains rates from the store's base currency to each of the other currencies. + * + * @param Store $store the store view for which to build the exchange rates + * @return ExchangeRateCollection the collection of exchange rates for the store + */ + public function build(Store $store) + { + $exchangeRates = new ExchangeRateCollection(); + + try { + $currencyCodes = $store->getAvailableCurrencyCodes(true); + $baseCurrencyCode = $store->getBaseCurrencyCode(); + + $currencyModel = $this->currencyFactory->create(); + $rates = $currencyModel->getCurrencyRates($baseCurrencyCode, $currencyCodes); + foreach ($rates as $code => $rate) { + if ($baseCurrencyCode === $code) { + continue; // Skip base currency. + } + + $this->logger->info(sprintf( + 'The rate from %s to %s is %f', + $baseCurrencyCode, + $code, + $rate + )); + $exchangeRates->addRate($code, new ExchangeRate($code, $rate)); + } + } catch (Exception $e) { + $this->logger->exception($e); + } + + $this->eventManager->dispatch( + 'nosto_exchange_rates_load_after', + ['rates' => $exchangeRates] + ); + + return $exchangeRates; + } +} diff --git a/Model/Rates/Service.php b/Model/Rates/Service.php new file mode 100644 index 000000000..3e7c6bc24 --- /dev/null +++ b/Model/Rates/Service.php @@ -0,0 +1,123 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Rates; + +use Exception; +use Magento\Store\Model\Store; +use Nosto\Operation\SyncRates; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Currency as NostoHelperCurrency; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Rates\Builder as NostoExchangeRatesBuilder; + +class Service +{ + private NostoLogger $logger; + private Builder $nostoExchangeRatesBuilder; + private NostoHelperAccount $nostoHelperAccount; + private NostoHelperCurrency $nostoHelperCurrency; + + /** + * @param NostoLogger $logger + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoExchangeRatesBuilder $nostoExchangeRatesBuilder + * @param NostoHelperCurrency $nostoHelperCurrency + */ + public function __construct( + NostoLogger $logger, + NostoHelperAccount $nostoHelperAccount, + NostoExchangeRatesBuilder $nostoExchangeRatesBuilder, + NostoHelperCurrency $nostoHelperCurrency + ) { + $this->logger = $logger; + $this->nostoExchangeRatesBuilder = $nostoExchangeRatesBuilder; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->nostoHelperCurrency = $nostoHelperCurrency; + } + + /** + * Sends a currency exchange rate update request to Nosto via the API. Checks if multi currency + * is enabled for the store before attempting to send the exchange rates. + * + * @param Store $store the store for which the rates are to be updated. + * @return bool a boolean value indicating whether the operation was successful + */ + public function update(Store $store) + { + if ($account = $this->nostoHelperAccount->findAccount($store)) { + if (!$this->nostoHelperCurrency->exchangeRatesInUse($store)) { + $this->logger->debug( + sprintf( + 'Skipping update; multi-currency is disabled for %s', + $store->getName() + ) + ); + + return true; + } + $rates = $this->nostoExchangeRatesBuilder->build($store); + if (empty($rates->getRates())) { + $this->logger->debug( + sprintf( + 'Skipping update; no rates found for %s', + $store->getName() + ) + ); + + return false; + } + + try { + $this->logger->info( + sprintf('Found %d currencies for store ', count($rates->getRates())) + ); + $service = new SyncRates($account); + + return $service->update($rates); + } catch (Exception $e) { + $this->logger->exception($e); + } + } else { + $this->logger->debug( + 'Skipping update; an account doesn\'t exist for ' . + $store->getName() + ); + } + + return true; + } +} diff --git a/Model/ResourceModel/Customer.php b/Model/ResourceModel/Customer.php index 28c23f6a8..aa322e04b 100644 --- a/Model/ResourceModel/Customer.php +++ b/Model/ResourceModel/Customer.php @@ -1,57 +1,55 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Model\ResourceModel; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; -use Magento\Framework\Model\ResourceModel\Db\Context; +use Nosto\Tagging\Api\Data\CustomerInterface; class Customer extends AbstractDb { - /** - * Construct - * - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param string|null $resourcePrefix - */ - public function __construct( - Context $context, - $resourcePrefix = null - ) { - parent::__construct($context, $resourcePrefix); - } + public const TABLE_NAME = 'nosto_tagging_customer'; /** * Initialize resource model * * @return void */ - protected function _construct() + public function _construct() { - $this->_init('nosto_tagging_customer', 'customer_id'); + $this->_init(self::TABLE_NAME, CustomerInterface::CUSTOMER_ID); } } diff --git a/Model/ResourceModel/Customer/Collection.php b/Model/ResourceModel/Customer/Collection.php index 155d68747..af2596925 100644 --- a/Model/ResourceModel/Customer/Collection.php +++ b/Model/ResourceModel/Customer/Collection.php @@ -1,33 +1,44 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Model\ResourceModel\Customer; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; +use Nosto\Tagging\Model\Customer\Customer; +use Nosto\Tagging\Model\ResourceModel\Customer as ResourceModelCustomer; class Collection extends AbstractCollection { @@ -36,11 +47,11 @@ class Collection extends AbstractCollection * * @return void */ - protected function _construct() + public function _construct() { $this->_init( - 'Nosto\Tagging\Model\Customer', - 'Nosto\Tagging\Model\ResourceModel\Customer' + Customer::class, + ResourceModelCustomer::class ); } } diff --git a/Model/ResourceModel/Magento/Product/Collection.php b/Model/ResourceModel/Magento/Product/Collection.php new file mode 100644 index 000000000..c0f6f1d24 --- /dev/null +++ b/Model/ResourceModel/Magento/Product/Collection.php @@ -0,0 +1,69 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\ResourceModel\Magento\Product; + +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\ResourceModel\Product\Collection as MagentoProductCollection; + +class Collection extends MagentoProductCollection +{ + /** + * @return Collection + */ + public function addActiveFilter() + { + return $this->addAttributeToFilter('status', ['eq' => Status::STATUS_ENABLED]); + } + + /** + * @param array $ids + * @return Collection + */ + public function addIdsToFilter(array $ids) + { + return $this->addAttributeToFilter($this->getIdFieldName(), ['in', $ids]); + } + + /** + * @param array $skus + * @return Collection + */ + public function addSkuFilter(array $skus) + { + return $this->addAttributeToFilter('sku', ['in', $skus]); + } +} diff --git a/Model/ResourceModel/Magento/Product/CollectionBuilder.php b/Model/ResourceModel/Magento/Product/CollectionBuilder.php new file mode 100644 index 000000000..405e6bf32 --- /dev/null +++ b/Model/ResourceModel/Magento/Product/CollectionBuilder.php @@ -0,0 +1,267 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\ResourceModel\Magento\Product; + +use Magento\Catalog\Model\Product\Visibility as ProductVisibility; +use Magento\Sales\Api\Data\EntityInterface; +use Magento\Store\Model\Store; +use Nosto\Tagging\Model\ResourceModel\Magento\Product\Collection as ProductCollection; +use Nosto\Tagging\Model\ResourceModel\Magento\Product\CollectionFactory as ProductCollectionFactory; +use Nosto\Tagging\Helper\Data as NostoHelperData; + +/** + * A builder class for building product collection with the most common filters + */ +class CollectionBuilder +{ + /** @var ProductVisibility */ + private ProductVisibility $productVisibility; + + /** @var Collection */ + private Collection $collection; + + /** @var CollectionFactory */ + private CollectionFactory $productCollectionFactory; + + /** @var NostoHelperData */ + private NostoHelperData $nostoHelperData; + + /** + * Collection constructor. + * @param ProductCollectionFactory $productCollectionFactory + * @param ProductVisibility $productVisibility + * @param NostoHelperData $nostoHelperData + */ + public function __construct( + ProductCollectionFactory $productCollectionFactory, + ProductVisibility $productVisibility, + NostoHelperData $nostoHelperData + ) { + $this->productCollectionFactory = $productCollectionFactory; + $this->productVisibility = $productVisibility; + $this->nostoHelperData = $nostoHelperData; + } + + /** + * @return Collection + */ + public function build() + { + return $this->collection; + } + + /** + * Sets filter for only products that are visible in active sites defined + * by store + * @return $this + */ + public function withOnlyVisibleInSites() + { + $this->collection->addAttributeToFilter('visibility', ['neq' => ProductVisibility::VISIBILITY_NOT_VISIBLE]); + return $this; + } + + /** + * Sets filter for product status based on configuration + * + * @param Store $store + * @return $this + */ + public function withConfiguredProductStatus(Store $store) + { + if (!$this->nostoHelperData->canIndexDisabledProducts($store)) { + $this->collection->addActiveFilter(); + } + return $this; + } + + /** + * Sets the store filter + * + * @param Store $store + * @return $this + */ + public function withStore(Store $store) + { + $this->collection->addStoreFilter($store); + $this->collection->setStore($store); + return $this; + } + + /** + * Defines all attributes to be included into the collection items + * + * @return $this + */ + public function withAllAttributes() + { + $this->collection->addAttributeToSelect('*'); + return $this; + } + + /** + * Sets filter for only given product ids + * + * @param array $ids + * @return $this + */ + public function withIds(array $ids) + { + $this->collection->addIdsToFilter($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->collection->setOrder($field, $sortOrder); + return $this; + } + + /** + * Sets the page size + * + * @param $pageSize + * @return $this + */ + public function setPageSize($pageSize) + { + $this->collection->setPageSize($pageSize); + return $this; + } + + /** + * Sets the current page + * + * @param $currentPage + * @return $this + */ + public function setCurrentPage($currentPage) + { + $this->collection->setCurPage($currentPage); + return $this; + } + + /** + * Sets the default visibility and set active products filter based on configuration + * + * @param Store $store + * @return CollectionBuilder + */ + public function withDefaultVisibility(Store $store) + { + return $this->withOnlyVisibleInSites()->withConfiguredProductStatus($store); + } + + /** + * Resets the data and filters in collection + * @return $this + */ + public function reset() + { + return $this->init(); + } + + /** + * Initializes the collection + * + * @return $this + */ + public function init() + { + $this->collection = $this->productCollectionFactory->create(); + return $this; + } + + /** + * Initializes the collection with store filter and defaults + * + * @param Store $store + * @return CollectionBuilder + */ + public function initDefault(Store $store) + { + /** @var ProductCollection $collection */ + return $this + ->reset() + ->withStore($store) + ->withAllAttributes() + ->setSort(EntityInterface::CREATED_AT, $this->collection::SORT_ORDER_DESC); + } + + /** + * Builds and returns the collection with single item (if found) + * + * @param Store $store + * @param $id + * @return Collection + */ + public function buildSingle(Store $store, $id) + { + return $this + ->initDefault($store) + ->withIds([$id]) + ->withDefaultVisibility($store) + ->build(); + } + + /** + * Builds collection with default visibility filter and given limit + * and offset. + * + * @param Store $store + * @param int $limit + * @param int $offset + * @return Collection + */ + public function buildMany(Store $store, int $limit = 100, int $offset = 0) + { + $currentPage = ($offset / $limit) + 1; + return $this + ->initDefault($store) + ->withDefaultVisibility($store) + ->setPageSize($limit) + ->setCurrentPage($currentPage) + ->build(); + } +} diff --git a/Model/ResourceModel/Product/Update/Queue.php b/Model/ResourceModel/Product/Update/Queue.php new file mode 100644 index 000000000..a1e79fdbe --- /dev/null +++ b/Model/ResourceModel/Product/Update/Queue.php @@ -0,0 +1,55 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\ResourceModel\Product\Update; + +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Nosto\Tagging\Api\Data\ProductUpdateQueueInterface; + +class Queue extends AbstractDb +{ + public const TABLE_NAME = 'nosto_tagging_product_update_queue'; + /** + * Initialize resource model + * + * @return void + */ + public function _construct() + { + $this->_serializableFields = [ProductUpdateQueueInterface::PRODUCT_IDS => [[], []]]; + $this->_init(self::TABLE_NAME, ProductUpdateQueueInterface::ID); + } +} diff --git a/Model/ResourceModel/Product/Update/Queue/QueueCollection.php b/Model/ResourceModel/Product/Update/Queue/QueueCollection.php new file mode 100644 index 000000000..c6e0db904 --- /dev/null +++ b/Model/ResourceModel/Product/Update/Queue/QueueCollection.php @@ -0,0 +1,184 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\ResourceModel\Product\Update\Queue; + +use DateTimeInterface; +use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; +use Magento\Store\Api\Data\StoreInterface; +use Nosto\Tagging\Api\Data\ProductUpdateQueueInterface; +use Nosto\Tagging\Model\Product\Update\Queue; +use Nosto\Tagging\Model\ResourceModel\Product\Update\Queue as QueueResource; + +class QueueCollection extends AbstractCollection +{ + /** + * Define resource model + * + * @return void + */ + public function _construct() + { + $this->_init( + Queue::class, + QueueResource::class + ); + } + + /** + * @param StoreInterface $store + * @return QueueCollection + */ + public function addStoreFilter(StoreInterface $store) + { + return $this->addStoreIdFilter($store->getId()); + } + + /** + * @param array $ids + * @return QueueCollection + */ + public function addIdsFilter(array $ids) + { + return $this->addFieldToFilter( + ProductUpdateQueueInterface::ID, + ['in' => $ids] + ); + } + + /** + * Filters collection by store id + * + * @param int $storeId + * @return QueueCollection + */ + public function addStoreIdFilter(int $storeId) + { + return $this->addFieldToFilter( + ProductUpdateQueueInterface::STORE_ID, + ['eq' => $storeId] + ); + } + + /** + * Filters collection by status + * + * @param string $status + * @return QueueCollection + */ + public function addStatusFilter(string $status) + { + return $this->addFieldToFilter( + ProductUpdateQueueInterface::STATUS, + ['eq' => $status] + ); + } + + /** + * Filters collection by completed by + * + * @param DateTimeInterface $dateTime + * @return QueueCollection + */ + public function addCompletedBeforeFilter(DateTimeInterface $dateTime) + { + return $this->addFieldToFilter( + ProductUpdateQueueInterface::COMPLETED_AT, + ['lteq' => $dateTime->format('Y-m-d H:i:s')] + ); + } + + /** + * Filters collection by id (primary key) + * + * @param int $indexId + * @return QueueCollection + */ + public function addIdFilter(int $indexId) + { + return $this->addFieldToFilter( + ProductUpdateQueueInterface::ID, + ['eq' => $indexId] + ); + } + + /** + * Sets a limit to this query + * + * @param int $limit + * @return QueueCollection + */ + public function limitResults(int $limit) + { + $this->getSelect()->limit($limit); + return $this; + } + + /** + * Add sortby to query + * + * @param string $field + * @param string $sort + * @return QueueCollection + */ + public function orderBy(string $field, string $sort) + { + $this->getSelect()->order($field . ' ' . $sort); + return $this; + } + + /** + * Deserialize fields + * + * @return QueueCollection + */ + protected function _afterLoad() + { + parent::_afterLoad(); + foreach ($this->getItems() as $item) { + /** + * Argument is of type Magento\Framework\DataObject + * but \Magento\Framework\Model\AbstractModel is expected + */ + /** @phan-suppress-next-next-line PhanTypeMismatchArgumentSuperType */ + /** @noinspection PhpParamsInspection */ + $this->getResource()->unserializeFields($item); + /** @noinspection PhpPossiblePolymorphicInvocationInspection */ + $item->setDataChanges(false); + } + return $this; + } +} diff --git a/Model/ResourceModel/Product/Update/Queue/QueueCollectionBuilder.php b/Model/ResourceModel/Product/Update/Queue/QueueCollectionBuilder.php new file mode 100644 index 000000000..1ce9c90ca --- /dev/null +++ b/Model/ResourceModel/Product/Update/Queue/QueueCollectionBuilder.php @@ -0,0 +1,208 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\ResourceModel\Product\Update\Queue; + +use DateInterval; +use DateTime; +use Exception; +use Magento\Sales\Api\Data\EntityInterface; +use Magento\Store\Model\Store; +use Nosto\Tagging\Api\Data\ProductUpdateQueueInterface; + +/** + * A builder class for building update queue collection with the most common filters + */ +class QueueCollectionBuilder +{ + /** @var QueueCollection */ + private QueueCollection $collection; + + /** @var QueueCollectionFactory */ + private QueueCollectionFactory $queueCollectionFactory; + + /** + * Collection constructor. + * @param QueueCollectionFactory $productCollectionFactory + */ + public function __construct( + QueueCollectionFactory $productCollectionFactory + ) { + $this->queueCollectionFactory = $productCollectionFactory; + } + + /** + * @return QueueCollection + */ + public function build(): QueueCollection + { + return $this->collection; + } + + /** + * Sets the store filter + * + * @param Store $store + * @return $this + */ + public function withStore(Store $store) + { + $this->collection->addStoreFilter($store); + return $this; + } + + /** + * Sets the filter to only done (completed) queue entries + * + * @return $this + */ + public function withStatusNew() + { + $this->collection->addStatusFilter(ProductUpdateQueueInterface::STATUS_VALUE_NEW); + return $this; + } + + /** + * Sets the filter to only entries completed before given date time + * + * @param int $hrs + * @return $this + * @throws Exception + */ + public function withCompletedHrsAgo(int $hrs) + { + $date = new DateTime('now'); + $interval = new DateInterval('PT' . $hrs . 'H'); + $date->sub($interval); + $this->collection->addCompletedBeforeFilter($date); + return $this->withStatusCompleted(); + } + + /** + * Sets the filter to only new (unprocessed) + * + * @return $this + */ + public function withStatusCompleted() + { + $this->collection->addStatusFilter(ProductUpdateQueueInterface::STATUS_VALUE_DONE); + return $this; + } + + /** + * Sets the filter to only for given ids + * + * @param array $ids + * @return $this + */ + public function withIds(array $ids) + { + $this->collection->addIdsFilter($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->collection->setOrder($field, $sortOrder); + return $this; + } + + /** + * Sets the page size + * + * @param $pageSize + * @return $this + */ + public function setPageSize($pageSize) + { + $this->collection->setPageSize($pageSize); + return $this; + } + + /** + * Sets the current page + * + * @param $currentPage + * @return $this + */ + public function setCurrentPage($currentPage) + { + $this->collection->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->collection = $this->queueCollectionFactory->create(); + return $this; + } + + /** + * Initializes the collection with store filter and defaults + * + * @param Store $store + * @return QueueCollectionBuilder + */ + public function initDefault(Store $store) + { + /** @var QueueCollection $collection */ + return $this + ->reset() + ->withStore($store) + ->setSort(EntityInterface::CREATED_AT, $this->collection::SORT_ORDER_ASC); + } +} diff --git a/Model/ResourceModel/Sku.php b/Model/ResourceModel/Sku.php new file mode 100644 index 000000000..333ac2cc9 --- /dev/null +++ b/Model/ResourceModel/Sku.php @@ -0,0 +1,84 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\ResourceModel; + +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Customer\Model\GroupManagement; +use Magento\Framework\DB\Select; +use Magento\Store\Model\Website; + +class Sku extends ProductResource +{ + public const CATALOG_PRODUCT_PRICE_INDEX_TABLE = "catalog_product_index_price"; + + /** + * Fetches prices for the SKUs regardless if they are in stock or not + * + * @param Website $website + * @param array $skuIds + * @return array + * @suppress PhanTypeMismatchArgument + */ + public function getSkuPricesByIds( + Website $website, + array $skuIds + ): array { + if (empty($skuIds)) { + return []; + } + $select = $this->buildSelect($website, $skuIds); + return $this->_resource->getConnection()->fetchAll($select); // @codingStandardsIgnoreLine + } + + /** + * Builder for the select statement + * + * @param Website $website + * @param array $skuIds + * @return Select + */ + public function buildSelect(Website $website, array $skuIds): Select + { + $gid = (string)GroupManagement::NOT_LOGGED_IN_ID; + return $this->_resource->getConnection()->select() + ->from(["cpip" => $this->_resource->getTableName(self::CATALOG_PRODUCT_PRICE_INDEX_TABLE)]) + ->where("cpip.website_id = ?", $website->getId()) + /** @phan-suppress-next-line PhanTypeMismatchArgument */ + ->where("cpip.entity_id IN(?)", $skuIds) + ->where("cpip.customer_group_id = ?", $gid); + } +} diff --git a/Model/Service/AbstractService.php b/Model/Service/AbstractService.php new file mode 100644 index 000000000..6351e5b3a --- /dev/null +++ b/Model/Service/AbstractService.php @@ -0,0 +1,234 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service; + +use Exception; +use Magento\Store\Model\Store; +use Nosto\Exception\MemoryOutOfBoundsException; +use Nosto\NostoException; +use Nosto\Tagging\Helper\Data as NostoDataHelper; +use Nosto\Tagging\Helper\Account as NostoAccountHelper; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Util\Benchmark; +use Nosto\Util\Memory as NostoMemUtil; + +abstract class AbstractService +{ + /** @var NostoDataHelper */ + private NostoDataHelper $nostoDataHelper; + + /** @var NostoLogger */ + private NostoLogger $nostoLogger; + + /** @var NostoAccountHelper */ + private NostoAccountHelper $nostoAccountHelper; + + /** + * AbstractService constructor. + * @param NostoDataHelper $nostoDataHelper + * @param NostoAccountHelper $nostoAccountHelper + * @param NostoLogger $nostoLogger + */ + public function __construct( + NostoDataHelper $nostoDataHelper, + NostoAccountHelper $nostoAccountHelper, + NostoLogger $nostoLogger + ) { + $this->nostoDataHelper = $nostoDataHelper; + $this->nostoLogger = $nostoLogger; + $this->nostoAccountHelper = $nostoAccountHelper; + } + + /** + * Throws new memory out of bounds exception if the memory + * consumption is higher than configured amount + * + * @param string $serviceName + * @throws MemoryOutOfBoundsException + */ + public function checkMemoryConsumption(string $serviceName) + { + $maxMemPercentage = $this->nostoDataHelper->getIndexerMemory(); + if (NostoMemUtil::getPercentageUsedMem() >= $maxMemPercentage) { + throw new MemoryOutOfBoundsException( + sprintf( + 'Memory Out Of Bounds Error: Memory used by %s is over %d%% allowed', + $serviceName, + $maxMemPercentage + ) + ); + } + } + + /** + * @param string $name + * @param int $breakpoint + */ + public function startBenchmark(string $name, int $breakpoint) + { + Benchmark::getInstance()->startInstrumentation($name, $breakpoint); + } + + /** + * Records calls this function and writes log if breakpoint is reached + * + * @param string $name + * @param bool $writeLog if set to true debug log will be written + * @return float|null + * @throws Exception + */ + public function tickBenchmark(string $name, bool $writeLog = false) + { + $elapsed = Benchmark::getInstance()->tick($name); + if ($elapsed !== null) { + if ($writeLog === true) { + $reachedBreakpoints = count(Benchmark::getInstance()->getCheckpointTimes($name)); + $this->nostoLogger->logWithMemoryConsumption( + sprintf( + 'Execution for %s took %f seconds - checkpoints reached %d', + $name, + $elapsed, + $reachedBreakpoints + ) + ); + } + return $elapsed; + } + return null; + } + + /** + * Logs the recorded benchmark summary + * + * @param string $name + * @param Store $store + * @param object|null $sourceClass + */ + public function logBenchmarkSummary(string $name, Store $store, ?object $sourceClass = null) + { + try { + Benchmark::getInstance()->stopInstrumentation($name); + $this->nostoLogger->logWithMemoryConsumption( + sprintf( + 'Summary of processing %s for store %s. Total amount of iterations %d' + . ', single iteration took on avg %f sec, total time was %f sec', + $name, + $store->getName(), + Benchmark::getInstance()->getTickCount($name), + Benchmark::getInstance()->getAvgTickTime($name), + Benchmark::getInstance()->getTotalTime($name) + ), + $store, + $sourceClass + ); + } catch (NostoException $e) { + $this->nostoLogger->exception($e); + } + } + + /** + * @return NostoLogger + */ + public function getLogger() + { + return $this->nostoLogger; + } + + /** + * @return NostoDataHelper + */ + public function getDataHelper() + { + return $this->nostoDataHelper; + } + + /** + * @return NostoAccountHelper + */ + public function getAccountHelper() + { + return $this->nostoAccountHelper; + } + + /** + * Shortcut for logging debug messages + * + * @param string $message + * @param array $context + */ + public function logDebug(string $message, array $context = []) + { + $this->getLogger()->debugWithSource($message, $context, $this); + } + + /** + * Shortcut for logging info messages + * + * @param string $message + * @param array $context + */ + public function logInfo(string $message, array $context = []) + { + $this->getLogger()->infoWithSource($message, $context, $this); + } + + /** + * Shortcut for logging debug messages with store id + * + * @param string $message + * @param Store $store + * @param array $context + */ + public function logDebugWithStore(string $message, Store $store, array $context = []) + { + $context['storeId'] = $store->getId(); + $this->logDebug($message, $context); + } + + /** + * Shortcut for logging info messages with store id + * + * @param string $message + * @param Store $store + * @param array $context + */ + public function logInfoWithStore(string $message, Store $store, array $context = []) + { + $context['storeId'] = $store->getId(); + $this->logInfo($message, $context); + } +} diff --git a/Model/Service/Cache/CacheService.php b/Model/Service/Cache/CacheService.php new file mode 100644 index 000000000..5322bc029 --- /dev/null +++ b/Model/Service/Cache/CacheService.php @@ -0,0 +1,157 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Cache; + +use Exception; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Store\Api\Data\StoreInterface; +use Nosto\Model\Product\Product; +use Nosto\Tagging\Helper\Account as NostoAccountHelper; +use Nosto\Tagging\Helper\Data as NostoDataHelper; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Cache\Type\ProductDataInterface; +use Nosto\Tagging\Model\Service\AbstractService; +use Nosto\Tagging\Model\Service\Product\ProductSerializerInterface; +use Nosto\Types\Product\ProductInterface as NostoProductInterface; + +class CacheService extends AbstractService +{ + + /** @var ProductSerializerInterface */ + private ProductSerializerInterface $productSerializer; + + /** @var ProductDataInterface */ + private ProductDataInterface $productDataCache; + + /** @var int|null */ + private ?int $lifeTime; + + /** + * CacheService constructor. + * @param NostoLogger $logger + * @param NostoDataHelper $nostoDataHelper + * @param NostoAccountHelper $nostoAccountHelper + * @param ProductSerializerInterface $productSerializer + * @param ProductDataInterface $productDataCache + * @param int $lifeTime + */ + public function __construct( + NostoLogger $logger, + NostoDataHelper $nostoDataHelper, + NostoAccountHelper $nostoAccountHelper, + ProductSerializerInterface $productSerializer, + ProductDataInterface $productDataCache, + int $lifeTime + ) { + parent::__construct($nostoDataHelper, $nostoAccountHelper, $logger); + $this->productSerializer = $productSerializer; + $this->productDataCache = $productDataCache; + $this->lifeTime = $lifeTime; + } + + /** + * @param NostoProductInterface $nostoProduct + * @param StoreInterface $store + */ + public function save(NostoProductInterface $nostoProduct, StoreInterface $store) + { + try { + $serializedNostoProduct = $this->productSerializer->toString($nostoProduct); + if ($this->lifeTime < 0) { + $this->lifeTime = null; + } + $this->productDataCache->save( + $serializedNostoProduct, + $this->generateCacheKey($nostoProduct->getProductId(), $store->getId()), + [], + $this->lifeTime + ); + } catch (Exception $e) { + $this->getLogger()->exception($e); + } + } + + /** + * @param ProductInterface $product + * @param StoreInterface $store + * @return Product|null + */ + public function get(ProductInterface $product, StoreInterface $store) + { + return $this->getById($product->getId(), $store->getId()); + } + + /** + * @param StoreInterface $store + * @param array $productIds + */ + public function removeByProductIds(StoreInterface $store, array $productIds) + { + $storeId = $store->getId(); + foreach ($productIds as $productId) { + $this->productDataCache->remove( + $this->generateCacheKey($productId, $storeId) + ); + } + } + + /** + * @param int $productId + * @param int $storeId + * @return Product|null + */ + private function getById(int $productId, int $storeId) + { + $cachedProduct = $this->productDataCache->load($this->generateCacheKey($productId, $storeId)); + return $cachedProduct ? $this->productSerializer->fromString($cachedProduct) : null; + } + + /** + * @param int $productId + * @param int $storeId + * @return string + */ + private function generateCacheKey(int $productId, int $storeId) + { + return sprintf( + '%s-%d-%d', + $this->productDataCache->getTag(), + $productId, + $storeId + ); + } +} diff --git a/Model/Service/Indexer/IndexerStatusService.php b/Model/Service/Indexer/IndexerStatusService.php new file mode 100644 index 000000000..82e23dbff --- /dev/null +++ b/Model/Service/Indexer/IndexerStatusService.php @@ -0,0 +1,112 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Indexer; + +use Magento\Framework\Indexer\IndexerRegistry; +use Nosto\Tagging\Model\Mview\ChangeLogInterface; +use Nosto\Tagging\Model\Mview\MviewInterface; + +class IndexerStatusService implements IndexerStatusServiceInterface +{ + /** @var IndexerRegistry */ + private IndexerRegistry $indexerRegistry; + + /** @var ChangeLogInterface */ + private ChangeLogInterface $changeLog; + + /** @var MviewInterface */ + private MviewInterface $mview; + + /** + * @param ChangelogInterface $changeLog + * @param MviewInterface $mview + * @param IndexerRegistry $indexerRegistry + */ + public function __construct( + ChangeLogInterface $changeLog, + MviewInterface $mview, + IndexerRegistry $indexerRegistry + ) { + $this->changeLog = $changeLog; + $this->mview = $mview; + $this->indexerRegistry = $indexerRegistry; + } + + /** + * @inheritDoc + */ + public function clearProcessedChangelog($indexerId) + { + if (!$this->isScheduled($indexerId)) { + return; + } + $this->mview->setId($indexerId); + $this->mview->clearChangelog(); + } + + /** + * @inheritDoc + */ + public function getTotalChangelogCount($indexerId) + { + if (!$this->isScheduled($indexerId)) { + return 0; + } + $this->changeLog->setViewId($indexerId); + return $this->changeLog->getTotalRows(); + } + + /** + * @inheritDoc + */ + public function getCurrentWatermark($indexerId) + { + if (!$this->isScheduled($indexerId)) { + return 0; + } + return (int)$this->mview->getState()->getVersionId(); + } + + /** + * @param $indexerId + * @return bool + */ + private function isScheduled($indexerId) + { + return $this->indexerRegistry->get($indexerId)->isScheduled(); + } +} diff --git a/Model/Service/Indexer/IndexerStatusServiceInterface.php b/Model/Service/Indexer/IndexerStatusServiceInterface.php new file mode 100644 index 000000000..d18bcc2be --- /dev/null +++ b/Model/Service/Indexer/IndexerStatusServiceInterface.php @@ -0,0 +1,58 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Indexer; + +interface IndexerStatusServiceInterface +{ + /** + * @param $indexerId + * @return void + */ + public function clearProcessedChangelog($indexerId); + + /** + * @param $indexerId + * @return int + */ + public function getTotalChangelogCount($indexerId); + + /** + * @param $indexerId + * @return int + */ + public function getCurrentWatermark($indexerId); +} diff --git a/Model/Service/Product/Attribute/AbstractAttributeService.php b/Model/Service/Product/Attribute/AbstractAttributeService.php new file mode 100644 index 000000000..c2c7d337d --- /dev/null +++ b/Model/Service/Product/Attribute/AbstractAttributeService.php @@ -0,0 +1,195 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product\Attribute; + +use Exception; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Store\Api\Data\StoreInterface; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Product\Builder; + +abstract class AbstractAttributeService implements AttributeServiceInterface +{ + /** @var NostoHelperData */ + private NostoHelperData $nostoHelperData; + + /** @var NostoLogger */ + private NostoLogger $logger; + + /** @var AttributeProviderInterface */ + private AttributeProviderInterface $attributeProvider; + + /** + * AbstractAttributeService constructor. + * @param NostoHelperData $nostoHelperData + * @param NostoLogger $logger + * @param AttributeProviderInterface $attributeProvider + */ + public function __construct( + NostoHelperData $nostoHelperData, + NostoLogger $logger, + AttributeProviderInterface $attributeProvider + ) { + $this->nostoHelperData = $nostoHelperData; + $this->logger = $logger; + $this->attributeProvider = $attributeProvider; + } + + private function getAttributesByArray(Product $product, array $attributes): array + { + $attributesAndValues = []; + foreach ($attributes as $attribute) { + try { + $attributeValue = $this->getAttributeValue($product, $attribute); + if ($attributeValue !== null && $attributeValue !== false) { + $attributesAndValues[$attribute->getAttributeCode()] = $attributeValue; + } + } catch (Exception $e) { + $this->logger->exception($e); + } + } + return $attributesAndValues; + } + + /** + * @inheritDoc + */ + public function getAttributesForTags(Product $product, StoreInterface $store): array + { + // Attributes that should be used in tagging + $attributes = array_merge( + $this->getConfiguredAttributesForTags($store), + $this->getDefaultAttributesForProduct($product) + ); + + return $this->getAttributesByArray($product, $attributes); + } + + /** + * Note that this returns the same attributes than getAttributesForTags + * + * @inheritDoc + */ + public function getAttributesForCustomFields(Product $product, StoreInterface $store): array + { + return $this->getAttributesForTags($product, $store); + } + + /** + * Returns the default (user defined & visible in frontend) attributes for the given product. + * + * @param Product $product + * @return AbstractAttribute[] + */ + private function getDefaultAttributesForProduct(Product $product): array + { + $configuredAttributes = $product->getAttributes(); + $attributes = []; + /** @var Attribute $attribute */ + foreach ($configuredAttributes as $attributeCode => $attribute) { + try { + if ($attribute->getIsUserDefined() + && ($attribute->getIsVisibleOnFront() || $attribute->getIsFilterable()) + ) { + $attributes[$attribute->getAttributeCode()] = $attribute; + } + } catch (Exception $e) { + $this->logger->exception($e); + } + } + return $attributes; + } + + /** + * Returns unique selected attributes from all tags + * + * @param StoreInterface $store + * @return AbstractAttribute[] + */ + private function getConfiguredAttributesForTags(StoreInterface $store): array + { + $configuredAttributes = []; + $attributes = []; + foreach (Builder::CUSTOMIZED_TAGS as $tag) { + $tagAttributes = $this->nostoHelperData->getTagAttributes($tag, $store); + if (!$tagAttributes) { + continue; + } + foreach ($tagAttributes as $productAttribute) { + $configuredAttributes[] = $productAttribute; + } + } + $configuredAttributes = array_unique($configuredAttributes); + $attributeCollection = $this->attributeProvider->getAttributesByAttributeCodes($configuredAttributes); + if ($attributeCollection === null) { + return []; + } + foreach ($attributeCollection as $code => $productAttribute) { + if (!in_array($code, $configuredAttributes, true)) { + $attributes[$code] = $productAttribute; + } + } + return $attributes; + } + + /** + * @return NostoLogger + */ + public function getLogger(): NostoLogger + { + return $this->logger; + } + + /** + * Resolves "textual" product attribute value. + * If value is an array containing scalar values the array will be imploded + * using comma as glue. + * + * @param Product $product + * @param AbstractAttribute $attribute + * @return bool|float|int|string|null + */ + abstract public function getAttributeValue(Product $product, AbstractAttribute $attribute); + + /** + * @inheritDoc + */ + abstract public function getAttributeValueByAttributeCode(Product $product, string $attributeCode); +} diff --git a/Model/Service/Product/Attribute/AttributeProviderInterface.php b/Model/Service/Product/Attribute/AttributeProviderInterface.php new file mode 100644 index 000000000..65cdcd064 --- /dev/null +++ b/Model/Service/Product/Attribute/AttributeProviderInterface.php @@ -0,0 +1,58 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product\Attribute; + +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributeCollection; + +interface AttributeProviderInterface +{ + /** + * Returns a collection of attributes that are possible to be added either + * into the custom fields or tags in Nosto product. + * + * @return AttributeCollection|null + */ + public function getSelectableAttributesForNosto(); + + /** + * Returns a collection of attributes filtered by the give attribute codes. + * + * @param array $attributeCodes a list of attribute codes + * @return AttributeCollection|null + */ + public function getAttributesByAttributeCodes(array $attributeCodes); +} diff --git a/Model/Service/Product/Attribute/AttributeServiceInterface.php b/Model/Service/Product/Attribute/AttributeServiceInterface.php new file mode 100644 index 000000000..69ec65d99 --- /dev/null +++ b/Model/Service/Product/Attribute/AttributeServiceInterface.php @@ -0,0 +1,70 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product\Attribute; + +use Magento\Catalog\Model\Product; +use Magento\Store\Api\Data\StoreInterface; + +interface AttributeServiceInterface +{ + /** + * Returns that attributes to be used in Nosto product tags. + * + * @param Product $product + * @param StoreInterface $store + * @return array ['attributeCode1' => 'value1', 'attributeCode2' => 'value2', ...] + */ + public function getAttributesForTags(Product $product, StoreInterface $store): array; + + /** + * Returns the attributes to be used in custom fields. + * + * @param Product $product + * @param StoreInterface $store + * @return array ['attributeCode1' => 'value1', 'attributeCode2' => 'value2', ...] + */ + public function getAttributesForCustomFields(Product $product, StoreInterface $store): array; + + /** + * Resolves "textual" product attribute value by attribute code. + * + * @param Product $product + * @param string $attributeCode + * @return bool|float|int|null|string + */ + public function getAttributeValueByAttributeCode(Product $product, string $attributeCode); +} diff --git a/Model/Service/Product/Attribute/CachingAttributeService.php b/Model/Service/Product/Attribute/CachingAttributeService.php new file mode 100644 index 000000000..c038e7e27 --- /dev/null +++ b/Model/Service/Product/Attribute/CachingAttributeService.php @@ -0,0 +1,168 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product\Attribute; + +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Store\Api\Data\StoreInterface; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Logger\Logger as NostoLogger; + +class CachingAttributeService extends AbstractAttributeService +{ + /** @var array */ + private array $productAttributeCache = []; + + /** @var AttributeServiceInterface */ + private AttributeServiceInterface $attributeService; + + /** @var int */ + private int $maxCachedProducts; + + /** + * CachingAttributeService constructor. + * @param NostoHelperData $nostoHelperData + * @param NostoLogger $logger + * @param AttributeServiceInterface $attributeService + * @param AttributeProviderInterface $attributeProvider + * @param $maxCachedProducts + */ + public function __construct( + NostoHelperData $nostoHelperData, + NostoLogger $logger, + AttributeServiceInterface $attributeService, + AttributeProviderInterface $attributeProvider, + $maxCachedProducts + ) { + parent::__construct($nostoHelperData, $logger, $attributeProvider); + $this->attributeService = $attributeService; + $this->maxCachedProducts = $maxCachedProducts; + } + + /** + * Saves a single attribute and value into the cache. + * + * @param Product $product + * @param StoreInterface $store + * @param string $attributeCode + * @param bool|float|int|null|string $value + */ + private function saveAttributeToCache(Product $product, StoreInterface $store, string $attributeCode, $value) + { + $storeId = $store->getId(); + $productId = $product->getId(); + if (!isset($this->productAttributeCache[$storeId])) { + $this->productAttributeCache[$storeId] = []; + } + if (!isset($this->productAttributeCache[$storeId][$productId])) { + $this->productAttributeCache[$storeId][$productId] = []; + } + $this->productAttributeCache[$storeId][$productId][$attributeCode] = $value; + $this->sliceCache($store); + } + + /** + * Returns the value of given product, store and attribute code from the cache + * if it exists. + * + * @param Product $product + * @param StoreInterface $store + * @param $attributeCode + * @return bool|float|int|null|string + */ + private function getAttributeFromCacheByAttributeCode(Product $product, StoreInterface $store, $attributeCode) + { + $storeId = $store->getId(); + $productId = $product->getId(); + return $this->productAttributeCache[$storeId][$productId][$attributeCode] ?? null; + } + + /** + * Returns if the value has been cached + * + * @param Product $product + * @param StoreInterface $store + * @param string $attributeCode + * @return bool + */ + private function isAttributeCached(Product $product, StoreInterface $store, string $attributeCode): bool + { + $storeId = $store->getId(); + $productId = $product->getId(); + return isset($this->productAttributeCache[$storeId][$productId][$attributeCode]); + } + + /** + * Keeps the cache sizes within the given limits + * + * @param StoreInterface $store + */ + private function sliceCache(StoreInterface $store) + { + $storeId = $store->getId(); + /* Product attribute cache slicing */ + $productCacheOffset = count($this->productAttributeCache[$storeId]) - $this->maxCachedProducts; + if ($productCacheOffset > 0) { + $this->productAttributeCache[$storeId] = array_slice( + $this->productAttributeCache[$storeId], + $productCacheOffset, + $this->maxCachedProducts, + true + ); + } + } + + /** + * @inheritDoc + */ + public function getAttributeValueByAttributeCode(Product $product, string $attributeCode) + { + if ($this->isAttributeCached($product, $product->getStore(), $attributeCode) === false) { + $value = $this->attributeService->getAttributeValueByAttributeCode($product, $attributeCode); + $this->saveAttributeToCache($product, $product->getStore(), $attributeCode, $value); + } + return $this->getAttributeFromCacheByAttributeCode($product, $product->getStore(), $attributeCode); + } + + /** + * @inheritDoc + */ + public function getAttributeValue(Product $product, AbstractAttribute $attribute) + { + return $this->getAttributeValueByAttributeCode($product, $attribute->getAttributeCode()); + } +} diff --git a/Model/Service/Product/Attribute/DefaultAttributeProvider.php b/Model/Service/Product/Attribute/DefaultAttributeProvider.php new file mode 100644 index 000000000..4097fae15 --- /dev/null +++ b/Model/Service/Product/Attribute/DefaultAttributeProvider.php @@ -0,0 +1,120 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product\Attribute; + +use Exception; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributeCollectionFactory; +use Magento\Eav\Model\Config; +use Nosto\Tagging\Logger\Logger; + +class DefaultAttributeProvider implements AttributeProviderInterface +{ + /** @var AttributeCollectionFactory */ + private AttributeCollectionFactory $attributeCollectionFactory; + + /** @var Config */ + private Config $eavConfig; + + /** @var Logger */ + private Logger $logger; + + /** + * AttributeProvider constructor. + * @param AttributeCollectionFactory $attributeCollectionFactory + * @param Config $eavConfig + * @param Logger $logger + */ + public function __construct( + AttributeCollectionFactory $attributeCollectionFactory, + Config $eavConfig, + Logger $logger + ) { + $this->attributeCollectionFactory = $attributeCollectionFactory; + $this->eavConfig = $eavConfig; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function getSelectableAttributesForNosto() + { + try { + $entity = $this->eavConfig->getEntityType(Product::ENTITY); + $collection = $this->attributeCollectionFactory->create(); + $collection->setEntityTypeFilter($entity->getId()); + $collection->addFieldToFilter('attribute_code', [ + 'nin' => [ + 'name', + 'category_ids', + 'has_options', + 'image_label', + 'old_id', + 'url_key', + 'url_path', + 'small_image_label', + 'thumbnail_label', + 'required_options', + 'tier_price', + 'meta_title', + 'media_gallery', + 'gallery' + ] + ]); + return $collection; + } catch (Exception $e) { + $this->logger->exception($e); + return null; + } + } + + /** + * @inheritDoc + */ + public function getAttributesByAttributeCodes(array $attributeCodes) + { + if (empty($attributeCodes)) { + return null; + } + $collection = $this->getSelectableAttributesForNosto(); + if ($collection !== null) { + $collection->addFieldToFilter('attribute_code', $attributeCodes); + } + return $collection; + } +} diff --git a/Model/Service/Product/Attribute/DefaultAttributeService.php b/Model/Service/Product/Attribute/DefaultAttributeService.php new file mode 100644 index 000000000..5ffa48d05 --- /dev/null +++ b/Model/Service/Product/Attribute/DefaultAttributeService.php @@ -0,0 +1,83 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product\Attribute; + +use Exception; +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Framework\Phrase; +use Nosto\Helper\ArrayHelper; + +class DefaultAttributeService extends AbstractAttributeService +{ + /** + * @inheritDoc + */ + public function getAttributeValue(Product $product, AbstractAttribute $attribute) + { + $value = null; + try { + $abstractFrontend = $attribute->getFrontend(); + $frontendValue = $abstractFrontend->getValue($product); + if (is_array($frontendValue) && !empty($frontendValue) + && ArrayHelper::onlyScalarValues($frontendValue) + ) { + $value = implode(',', $frontendValue); + } elseif (is_scalar($frontendValue)) { + $value = $frontendValue; + } elseif ($frontendValue instanceof Phrase) { + $value = (string)$frontendValue; + } + } catch (Exception $e) { + $this->getLogger()->exception($e); + } + return $value; + } + + /** + * @inheritDoc + */ + public function getAttributeValueByAttributeCode(Product $product, string $attributeCode) + { + $attributes = $product->getAttributes(); // This result is cached by Magento + if (isset($attributes[$attributeCode]) && $attributes[$attributeCode] instanceof AbstractAttribute) { + /** @var AbstractAttribute $attributes [$attributeCode] */ + return $this->getAttributeValue($product, $attributes[$attributeCode]); + } + return null; + } +} diff --git a/Model/Service/Product/AvailabilityService.php b/Model/Service/Product/AvailabilityService.php new file mode 100644 index 000000000..bf043ecc9 --- /dev/null +++ b/Model/Service/Product/AvailabilityService.php @@ -0,0 +1,89 @@ + + * @copyright 2021 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product; + +use Magento\Catalog\Model\Product; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Nosto\Tagging\Model\Service\Stock\StockService; + +class AvailabilityService +{ + /** @var StoreManagerInterface */ + private StoreManagerInterface $storeManager; + + /** @var StockService */ + private StockService $stockService; + + /** + * AvailabiltyService constructor. + * @param StoreManagerInterface $storeManager + * @param StockService $stockService + */ + public function __construct( + StoreManagerInterface $storeManager, + StockService $stockService + ) { + $this->storeManager = $storeManager; + $this->stockService = $stockService; + } + + /** + * @param Product $product + * @param Store $store + * @return bool + */ + public function isAvailableInStore(Product $product, Store $store) + { + if ($this->storeManager->isSingleStoreMode()) { + return $product->isAvailable(); + } + return in_array($store->getId(), $product->getStoreIds(), false); + } + + /** + * Checks if the product is in stock + * + * @param Product $product + * @param Store $store + * @return bool + */ + public function isInStock(Product $product, Store $store) + { + return $this->stockService->isInStock($product, $store); + } +} diff --git a/Model/Service/Product/CachingProductService.php b/Model/Service/Product/CachingProductService.php new file mode 100644 index 000000000..bd6441bef --- /dev/null +++ b/Model/Service/Product/CachingProductService.php @@ -0,0 +1,98 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product; + +use Exception; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Store\Api\Data\StoreInterface; +use Nosto\Tagging\Logger\Logger; +use Nosto\Tagging\Model\Service\Cache\CacheService; +use Nosto\Types\Product\ProductInterface as NostoProductInterface; + +class CachingProductService implements ProductServiceInterface +{ + /** @var Logger */ + private Logger $nostoLogger; + + /** @var CacheService */ + private CacheService $nostoCacheService; + + /** @var ProductServiceInterface */ + private ProductServiceInterface $defaultProductService; + + /** + * CachingProductService constructor. + * @param Logger $nostoLogger + * @param CacheService $nostoCacheService + * @param ProductServiceInterface $defaultProductService + */ + public function __construct( + Logger $nostoLogger, + CacheService $nostoCacheService, + ProductServiceInterface $defaultProductService + ) { + $this->nostoLogger = $nostoLogger; + $this->nostoCacheService = $nostoCacheService; + $this->defaultProductService = $defaultProductService; + } + + /** + * Get Nosto Product + * If is not indexed or dirty, rebuilds, saves product to the indexed table + * and returns NostoProduct from indexed product + * + * @param ProductInterface $product + * @param StoreInterface $store + * @return NostoProductInterface|null + */ + public function getProduct(ProductInterface $product, StoreInterface $store) + { + try { + $nostoProduct = $this->nostoCacheService->get($product, $store); + if ($nostoProduct === null) { + $nostoProduct = $this->defaultProductService->getProduct($product, $store); + if ($nostoProduct !== null) { + $this->nostoCacheService->save($nostoProduct, $store); + } + } + return $nostoProduct; + } catch (Exception $e) { + $this->nostoLogger->exception($e); + return null; + } + } +} diff --git a/Model/Service/Product/Category/CachingCategoryService.php b/Model/Service/Product/Category/CachingCategoryService.php new file mode 100644 index 000000000..ec419439f --- /dev/null +++ b/Model/Service/Product/Category/CachingCategoryService.php @@ -0,0 +1,88 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product\Category; + +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\Store\Api\Data\StoreInterface; + +class CachingCategoryService implements CategoryServiceInterface +{ + /** + * @var CategoryServiceInterface + */ + private CategoryServiceInterface $categoryService; + + /** + * @var array + */ + private array $cache = []; + + /** + * CachingCategoryService constructor. + * @param CategoryServiceInterface $categoryService + */ + public function __construct( + CategoryServiceInterface $categoryService + ) { + $this->categoryService = $categoryService; + } + + /** + * @inheritDoc + */ + public function getCategory(Category $category, StoreInterface $store) + { + if (!isset($this->cache[$store->getId()])) { + $this->cache[$store->getId()] = []; + } + + if (!isset($this->cache[$store->getId()][$category->getId()])) { + $slug = $this->categoryService->getCategory($category, $store); + $this->cache[$store->getId()][$category->getId()] = $slug; + } + return $this->cache[$store->getId()][$category->getId()]; + } + + /** + * @inheritDoc + */ + public function getCategories(Product $product, StoreInterface $store) + { + return $this->categoryService->getCategories($product, $store); + } +} diff --git a/Model/Service/Product/Category/CategoryServiceInterface.php b/Model/Service/Product/Category/CategoryServiceInterface.php new file mode 100644 index 000000000..52f02ee70 --- /dev/null +++ b/Model/Service/Product/Category/CategoryServiceInterface.php @@ -0,0 +1,58 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product\Category; + +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\Store\Api\Data\StoreInterface; + +interface CategoryServiceInterface +{ + /** + * @param Category $category + * @param StoreInterface $store + * @return null|string + */ + public function getCategory(Category $category, StoreInterface $store); + + /** + * @param Product $product + * @param StoreInterface $store + * @return array + */ + public function getCategories(Product $product, StoreInterface $store); +} diff --git a/Model/Service/Product/Category/DefaultCategoryService.php b/Model/Service/Product/Category/DefaultCategoryService.php new file mode 100644 index 000000000..d1845f788 --- /dev/null +++ b/Model/Service/Product/Category/DefaultCategoryService.php @@ -0,0 +1,140 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product\Category; + +use Exception; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Framework\Event\ManagerInterface; +use Magento\Store\Api\Data\StoreInterface; +use Nosto\Tagging\Logger\Logger as NostoLogger; + +class DefaultCategoryService implements CategoryServiceInterface +{ + + private NostoLogger $logger; + private CategoryRepositoryInterface $categoryRepository; + private CollectionFactory $categoryCollectionFactory; + private ManagerInterface $eventManager; + + /** + * Builder constructor. + * @param CategoryRepositoryInterface $categoryRepository + * @param CollectionFactory $categoryCollectionFactory + * @param NostoLogger $logger + * @param ManagerInterface $eventManager + */ + public function __construct( + CategoryRepositoryInterface $categoryRepository, + CollectionFactory $categoryCollectionFactory, + NostoLogger $logger, + ManagerInterface $eventManager + ) { + $this->categoryRepository = $categoryRepository; + $this->categoryCollectionFactory = $categoryCollectionFactory; + $this->logger = $logger; + $this->eventManager = $eventManager; + } + + /** + * Get Nosto Product + * If is not indexed or dirty, rebuilds, saves product to the indexed table + * and returns NostoProduct from indexed product + * + * @param Product $product + * @param StoreInterface $store + * @return array + */ + public function getCategories(Product $product, StoreInterface $store) + { + $categories = []; + foreach ($product->getCategoryCollection() as $category) { + $categoryString = $this->getCategory($category, $store); + if (!empty($categoryString)) { + $categories[] = $categoryString; + } + } + + return $categories; + } + + /** + * @inheritDoc + */ + public function getCategory(Category $category, StoreInterface $store) + { + $nostoCategory = ''; + try { + $data = []; + $categoryIds = []; + $path = $category->getPath(); + foreach (explode('/', $path) as $categoryId) { + $categoryIds[] = $categoryId; + } + + $categories = $this->categoryCollectionFactory->create() + ->addAttributeToSelect('*') + ->addAttributeToFilter('entity_id', $categoryIds) + ->setStore($store->getId()) + ->addAttributeToSort('level', Collection::SORT_ORDER_ASC); + foreach ($categories as $cat) { + if ($cat instanceof Category + && $cat->getLevel() > 1 + && !empty($cat->getName()) + ) { + $data[] = $cat->getName(); + } + } + $nostoCategory = count($data) ? '/' . implode('/', $data) : ''; + } catch (Exception $e) { + $this->logger->exception($e); + } + if (empty($nostoCategory)) { + $nostoCategory = null; + } else { + $this->eventManager->dispatch( + 'nosto_category_string_load_after', + ['categoryString' => $nostoCategory, 'magentoCategory' => $category] + ); + } + + return $nostoCategory; + } +} diff --git a/Model/Service/Product/DefaultProductComparator.php b/Model/Service/Product/DefaultProductComparator.php new file mode 100644 index 000000000..735425bfb --- /dev/null +++ b/Model/Service/Product/DefaultProductComparator.php @@ -0,0 +1,56 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product; + +use Nosto\Helper\SerializationHelper; +use Nosto\Tagging\Util\StringUtil; +use Nosto\Types\Product\ProductInterface; + +class DefaultProductComparator implements ProductComparatorInterface +{ + /** + * @inheritDoc + */ + public function isEqual(ProductInterface $product1, ProductInterface $product2) + { + $stringUtil = new StringUtil(); + $product1string = $stringUtil->stripWhitespaceAndLinebreaks(SerializationHelper::serialize($product1)); + $product2string = $stringUtil->stripWhitespaceAndLinebreaks(SerializationHelper::serialize($product2)); + + return $product1string === $product2string; + } +} diff --git a/Model/Service/Product/DefaultProductSerializer.php b/Model/Service/Product/DefaultProductSerializer.php new file mode 100644 index 000000000..ee0db1c2a --- /dev/null +++ b/Model/Service/Product/DefaultProductSerializer.php @@ -0,0 +1,63 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product; + +use Nosto\Model\Product\Product; +use Nosto\Types\Product\ProductInterface; +use Nosto\Util\Base64Serialize; + +/** + * Default class for serializing and deserializing objects + */ +class DefaultProductSerializer implements ProductSerializerInterface +{ + /** + * @inheritDoc + */ + public function fromString(string $data) + { + return Base64Serialize::fromString($data, [Product::class]); + } + + /** + * @inheritDoc + */ + public function toString(ProductInterface $product) + { + return Base64Serialize::toString($product); + } +} diff --git a/Model/Service/Product/DefaultProductService.php b/Model/Service/Product/DefaultProductService.php new file mode 100644 index 000000000..d6861bc04 --- /dev/null +++ b/Model/Service/Product/DefaultProductService.php @@ -0,0 +1,111 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product; + +use Exception; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\Store; +use Nosto\Exception\FilteredProductException; +use Nosto\Exception\NonBuildableProductException; +use Nosto\Model\Product\Product as NostoProduct; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Product\Builder as NostoProductBuilder; +use Nosto\Tagging\Model\Product\Repository as NostoProductRepository; + +class DefaultProductService implements ProductServiceInterface +{ + + /** @var NostoProductBuilder */ + private NostoProductBuilder $nostoProductBuilder; + + /** @var NostoLogger */ + private NostoLogger $logger; + + /** @var NostoProductRepository */ + private NostoProductRepository $nostoProductRepository; + + /** + * DefaultProductService constructor. + * @param NostoProductBuilder $nostoProductBuilder + * @param NostoProductRepository $nostoProductRepository + * @param NostoLogger $logger + */ + public function __construct( + NostoProductBuilder $nostoProductBuilder, + NostoProductRepository $nostoProductRepository, + NostoLogger $logger + ) { + $this->nostoProductBuilder = $nostoProductBuilder; + $this->nostoProductRepository = $nostoProductRepository; + $this->logger = $logger; + } + + /** + * @param ProductInterface $product + * @param StoreInterface $store + * @return NostoProduct|null + * @suppress PhanTypeMismatchArgument + * @throws Exception + */ + public function getProduct(ProductInterface $product, StoreInterface $store) + { + /** @var Product $product */ + /** @var Store $store */ + try { + return $this->nostoProductBuilder->build( + $this->nostoProductRepository->reloadProduct( + $product->getId(), + $store->getId() + ), + $store + ); + } catch (NonBuildableProductException $e) { + $this->logger->exception($e); + return null; + } catch (FilteredProductException $e) { + $this->logger->debug( + sprintf( + 'Product filtered out with message: %s', + $e->getMessage() + ) + ); + return null; + } + } +} diff --git a/Model/Service/Product/ImageService.php b/Model/Service/Product/ImageService.php new file mode 100644 index 000000000..4b60cd268 --- /dev/null +++ b/Model/Service/Product/ImageService.php @@ -0,0 +1,110 @@ + + * @copyright 2021 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product; + +use Magento\Catalog\Helper\Image; +use Magento\Catalog\Model\Product; +use Magento\Store\Model\Store; +use Nosto\Tagging\Helper\Data as NostoDataHelper; +use Nosto\Tagging\Util\Url as UrlUtil; + +class ImageService +{ + /** @var Image */ + private Image $imageHelper; + + /** @var NostoDataHelper */ + private NostoDataHelper $nostoDataHelper; + + /** + * ImageService constructor. + * @param Image $imageHelper + * @param NostoDataHelper $nostoDataHelper + */ + public function __construct( + Image $imageHelper, + NostoDataHelper $nostoDataHelper + ) { + $this->imageHelper = $imageHelper; + $this->nostoDataHelper = $nostoDataHelper; + } + + /** + * @param Product $product + * @param Store $store + * @return string|null + */ + public function buildImageUrl(Product $product, Store $store) + { + $primary = $this->nostoDataHelper->getProductImageVersion($store); + $secondary = 'image'; // The "base" image. + $media = $product->getMediaAttributeValues(); + + if (isset($media[$primary])) { + $image = $media[$primary]; + } elseif (isset($media[$secondary])) { + $image = $media[$secondary]; + } else { + $image = $this->imageHelper->init($product, "product_base_image")->getUrl(); + } + + if (empty($image)) { + return null; + } + + return $this->finalizeImageUrl( + $product->getMediaConfig()->getMediaUrl($image), + $store + ); + } + + /** + * Finalizes product image urls, stips off "pub/" directory if applicable + * + * @param string $url + * @param Store $store + * @return string + */ + public function finalizeImageUrl(string $url, Store $store) + { + if ($this->nostoDataHelper->getRemovePubDirectoryFromProductImageUrl($store)) { + return (new UrlUtil())->removePubFromUrl($url); + } + + return $url; + } +} diff --git a/Model/Service/Product/ProductComparatorInterface.php b/Model/Service/Product/ProductComparatorInterface.php new file mode 100644 index 000000000..64cd0e0ac --- /dev/null +++ b/Model/Service/Product/ProductComparatorInterface.php @@ -0,0 +1,49 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product; + +use Nosto\Types\Product\ProductInterface; + +interface ProductComparatorInterface +{ + /** + * @param ProductInterface $product1 + * @param ProductInterface $product2 + * @return boolean + */ + public function isEqual(ProductInterface $product1, ProductInterface $product2); +} diff --git a/Model/Service/Product/ProductSerializerInterface.php b/Model/Service/Product/ProductSerializerInterface.php new file mode 100644 index 000000000..698bfdba0 --- /dev/null +++ b/Model/Service/Product/ProductSerializerInterface.php @@ -0,0 +1,55 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product; + +use Nosto\Model\Product\Product; +use Nosto\Types\Product\ProductInterface; + +interface ProductSerializerInterface +{ + /** + * @param ProductInterface $product + * @return string + */ + public function toString(ProductInterface $product); + + /** + * @param string $data + * @return Product|null + */ + public function fromString(string $data); +} diff --git a/Model/Service/Product/ProductServiceInterface.php b/Model/Service/Product/ProductServiceInterface.php new file mode 100644 index 000000000..9d572ff86 --- /dev/null +++ b/Model/Service/Product/ProductServiceInterface.php @@ -0,0 +1,51 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Store\Api\Data\StoreInterface; +use Nosto\Model\Product\Product as NostoProduct; + +interface ProductServiceInterface +{ + /** + * @param ProductInterface $product + * @param StoreInterface $store + * @return NostoProduct|null + */ + public function getProduct(ProductInterface $product, StoreInterface $store); +} diff --git a/Model/Service/Product/SanitizingProductService.php b/Model/Service/Product/SanitizingProductService.php new file mode 100644 index 000000000..aeed2a371 --- /dev/null +++ b/Model/Service/Product/SanitizingProductService.php @@ -0,0 +1,85 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Product; + +use Exception; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Store\Api\Data\StoreInterface; +use Nosto\Tagging\Logger\Logger; +use Nosto\Model\Product\Product; + +class SanitizingProductService implements ProductServiceInterface +{ + /** @var ProductServiceInterface */ + private ProductServiceInterface $nostoProductService; + + /** @var Logger */ + private Logger $logger; + + /** + * DefaultProductService constructor. + * @param ProductServiceInterface $nostoProductService + * @param Logger $logger + */ + public function __construct( + ProductServiceInterface $nostoProductService, + Logger $logger + ) { + $this->nostoProductService = $nostoProductService; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function getProduct(ProductInterface $product, StoreInterface $store) + { + /** @var Product $nostoProduct */ + $nostoProduct = $this->nostoProductService->getProduct( + $product, + $store + ); + if ($nostoProduct !== null) { + try { + return $nostoProduct->sanitize(); + } catch (Exception $e) { + $this->logger->exception($e); + } + } + return null; + } +} diff --git a/Model/Service/Stock/Provider/CachingStockProvider.php b/Model/Service/Stock/Provider/CachingStockProvider.php new file mode 100644 index 000000000..a246ec4e2 --- /dev/null +++ b/Model/Service/Stock/Provider/CachingStockProvider.php @@ -0,0 +1,282 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Stock\Provider; + +use Magento\Catalog\Model\Product; +use Magento\Store\Model\Website; + +class CachingStockProvider implements StockProviderInterface +{ + /** @var StockProviderInterface */ + private StockProviderInterface $stockProvider; + + /** @var array */ + private array $quantityCache = []; + + /** @var array */ + private array $inStockCache = []; + + /** @var array */ + private array $productIdsInStockCache = []; + + /** @var int */ + private int $maxCacheSize; + + /** + * CachingStockProvider constructor. + * @param StockProviderInterface $stockProvider + * @param int $maxCacheSize + */ + public function __construct( + StockProviderInterface $stockProvider, + int $maxCacheSize + ) { + $this->stockProvider = $stockProvider; + $this->maxCacheSize = $maxCacheSize; + } + + /** + * @inheritDoc + */ + public function getAvailableQuantity(Product $product, Website $website) + { + if ($this->existsInQuantityCache($product->getId(), $website)) { + return $this->getQuantityFromCache($product->getId(), $website); + } + $quantity = $this->stockProvider->getAvailableQuantity($product, $website); + $this->saveQuantityToCache($product->getId(), $website, $quantity); + return $quantity; + } + + /** + * @inheritDoc + */ + public function isInStock(Product $product, Website $website) + { + if ($this->existsInStockCache($product, $website)) { + return $this->getIsInStockFromCache($product, $website); + } + $inStock = $this->stockProvider->isInStock($product, $website); + $this->saveToInStockCache($product, $website, $inStock); + return $inStock; + } + + /** + * @inheritDoc + */ + public function getQuantitiesByIds(array $productIds, Website $website) + { + $quantities = []; + $nonCachedQuantities = []; + foreach ($productIds as $productId) { + if ($this->existsInQuantityCache($productId, $website)) { + $quantities[] = $this->getQuantityFromCache($productId, $website); + } else { + $nonCachedQuantities[] = $productId; + } + } + if (!empty($nonCachedQuantities)) { + $lookedUpQuantities = $this->stockProvider->getQuantitiesByIds($nonCachedQuantities, $website); + foreach ($lookedUpQuantities as $productId => $quantity) { + $quantities[$productId] = $quantity; + $this->saveQuantityToCache($productId, $website, $quantity); + } + } + return $quantities; + } + + /** + * @param Product $product + * @param Website $website + * @param bool $inStock + */ + private function saveToInStockCache(Product $product, Website $website, bool $inStock) + { + if (empty($this->inStockCache[$website->getId()])) { + $this->inStockCache[$website->getId()] = []; + } + $this->inStockCache[$website->getId()][$product->getId()] = $inStock; + $count = count($this->inStockCache[$website->getId()]); + $offset = $count-$this->maxCacheSize; + if ($offset > 0) { + $this->inStockCache[$website->getId()] = array_slice( + $this->inStockCache[$website->getId()], + $offset, + $this->maxCacheSize, + true + ); + } + } + + /** + * @param Product $product + * @param Website $website + * @return bool|null + */ + private function getIsInStockFromCache(Product $product, Website $website) + { + if (!isset($this->inStockCache[$website->getId()][$product->getId()])) { + return null; + } + return $this->inStockCache[$website->getId()][$product->getId()]; + } + + /** + * @param int $productId + * @param Website $website + * @param int $quantity + */ + private function saveQuantityToCache(int $productId, Website $website, int $quantity) + { + if (empty($this->quantityCache[$website->getId()])) { + $this->quantityCache[$website->getId()] = []; + } + $this->quantityCache[$website->getId()][$productId] = $quantity; + $count = count($this->quantityCache); + $offset = $count-$this->maxCacheSize; + if ($offset > 0) { + $this->quantityCache = array_slice($this->quantityCache, $offset, $this->maxCacheSize, true); + } + } + + /** + * @param int $productId + * @param Website $website + * @return int|null + */ + private function getQuantityFromCache(int $productId, Website $website) + { + if (!isset($this->quantityCache[$website->getId()][$productId])) { + return null; + } + return $this->quantityCache[$website->getId()][$productId]; + } + + /** + * @param Product $product + * @param Website $website + * @return bool + */ + private function existsInStockCache(Product $product, Website $website) + { + return isset($this->inStockCache[$website->getId()][$product->getId()]); + } + + /** + * @param int $productId + * @param Website $website + * @return bool + */ + private function existsInQuantityCache(int $productId, Website $website) + { + return isset($this->quantityCache[$website->getId()][$productId]); + } + + /** + * @param array $productIds + * @param Website $website + * @return bool + */ + private function existsInProductDataCache(array $productIds, Website $website) + { + $cacheKey = $this->generateCacheKeyProductIds($productIds); + return isset($this->productIdsInStockCache[$website->getId()][$cacheKey]); + } + + /** + * @inheritDoc + */ + public function getInStockProductIds(array $productIds, Website $website) + { + if ($this->existsInProductDataCache($productIds, $website)) { + return $this->getInStockProductsFromCache($productIds, $website); + } + $lookedUpProductIds = $this->stockProvider->getInStockProductIds($productIds, $website); + $this->saveToProductIdsInStockCache($productIds, $website, $lookedUpProductIds); + return $lookedUpProductIds; + } + + /** + * @param array $productIds + * @param Website $website + * @return array|null + */ + private function getInStockProductsFromCache(array $productIds, Website $website) + { + $cacheKey = $this->generateCacheKeyProductIds($productIds); + if (!isset($this->productIdsInStockCache[$website->getId()][$cacheKey])) { + return null; + } + return $this->productIdsInStockCache[$website->getId()][$cacheKey]; + } + + /** + * @param array $givenIds + * @param Website $website + * @param $inStockIds + */ + private function saveToProductIdsInStockCache(array $givenIds, Website $website, array $inStockIds) + { + if (empty($this->productIdsInStockCache[$website->getId()])) { + $this->productIdsInStockCache[$website->getId()] = []; + } + $cacheKey = $this->generateCacheKeyProductIds($givenIds); + $this->productIdsInStockCache[$website->getId()][$cacheKey] = $inStockIds; + $count = count($this->productIdsInStockCache); + $offset = $count-$this->maxCacheSize; + if ($offset > 0) { + $this->productIdsInStockCache = array_slice( + $this->productIdsInStockCache, + $offset, + $this->maxCacheSize, + true + ); + } + } + + /** + * Generates a cache key for product ids + * + * @param array $productIds + * @return string + */ + private function generateCacheKeyProductIds(array $productIds) + { + return (string)implode('-', $productIds); + } +} diff --git a/Model/Service/Stock/Provider/DefaultStockProvider.php b/Model/Service/Stock/Provider/DefaultStockProvider.php new file mode 100644 index 000000000..74fc048da --- /dev/null +++ b/Model/Service/Stock/Provider/DefaultStockProvider.php @@ -0,0 +1,140 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Stock\Provider; + +use Magento\Catalog\Model\Product; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\CatalogInventory\Model\Stock\Status; +use Magento\Store\Model\Website; + +class DefaultStockProvider implements StockProviderInterface +{ + /** @var StockRegistryProvider */ + private StockRegistryProvider $stockRegistryProvider; + + /** + * DefaultStockProvider constructor. + * @param StockRegistryProvider $stockRegistryProvider + */ + public function __construct( + StockRegistryProvider $stockRegistryProvider + ) { + $this->stockRegistryProvider = $stockRegistryProvider; + } + + /** + * Returns stock item from the default source + * + * @param Product $product + * @return StockItemInterface + */ + public function getStockItem(Product $product) + { + return $this->stockRegistryProvider->getStockItem( + $product->getId(), + StockRegistryProvider::DEFAULT_STOCK_SCOPE + ); + } + + /** + * @inheritDoc + */ + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter + public function getAvailableQuantity( + Product $product, + Website $website + ) { + return (int)$this->getStockItem($product)->getQty(); + } + + /** + * @inheritDoc + */ + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter + public function isInStock(// @codingStandardsIgnoreLine + Product $product, + Website $website + ) { + return (bool)$this->getStockItem($product)->getIsInStock(); + } + + /** + * @inheritDoc + */ + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter + public function getQuantitiesByIds(array $productIds, Website $website) + { + $quantities = []; + $stockItems = $this->getStockStatuses($productIds); + foreach ($stockItems as $stockItem) { + $quantities[$stockItem->getProductId()] = $stockItem->getQty(); + } + return $quantities; + } + + /** + * @param array $ids + * @return StockStatusInterface[] + */ + public function getStockStatuses(// @codingStandardsIgnoreLine + array $ids + ): array { + return $this->stockRegistryProvider->getStockStatuses( + $ids, + StockRegistryProvider::DEFAULT_STOCK_SCOPE + )->getItems(); + } + + /** + * @inheritDoc + */ + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter + public function getInStockProductIds(array $productIds, Website $website) + { + $stockItems = $this->getStockStatuses($productIds); + $inStockIds = []; + /** @var Status $stockItem */ + foreach ($stockItems as $stockItem) { + if ($stockItem->getStockStatus() === StockStatusInterface::STATUS_IN_STOCK) { + $inStockIds[] = $stockItem->getProductId(); + } + } + return $inStockIds; + } +} diff --git a/Model/Service/Stock/Provider/StockProviderInterface.php b/Model/Service/Stock/Provider/StockProviderInterface.php new file mode 100644 index 000000000..f0189e23a --- /dev/null +++ b/Model/Service/Stock/Provider/StockProviderInterface.php @@ -0,0 +1,115 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Stock\Provider; + +/** + * Copyright (c) 2019, Nosto Solutions Ltd + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @author Nosto Solutions Ltd + * @copyright 2019 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +use Magento\Catalog\Model\Product; +use Magento\Store\Model\Website; + +interface StockProviderInterface +{ + /** + * Gets the available quantity for a product in the given website + * + * @param Product $product + * @param Website $website + * @return int + */ + public function getAvailableQuantity(Product $product, Website $website); + + /** + * Checks the availability for a product in the given website + * + * @param Product $product + * @param Website $website + * @return bool + */ + public function isInStock(Product $product, Website $website); + + /** + * Returns an array of product ids and quantities in stock for the website + * for each product id + * + * @param array $productIds + * @param Website $website + * @return array ['productId1' => 4, 'productId2' => 2, ...] + */ + public function getQuantitiesByIds(array $productIds, Website $website); + + /** + * Checks and returns which of the given product ids are in stock + * + * @param array $productIds + * @param Website $website + * @return array an array of in stock productIds ['1','2', etc.] + */ + public function getInStockProductIds(array $productIds, Website $website); +} diff --git a/Model/Service/Stock/Provider/StockRegistryProvider.php b/Model/Service/Stock/Provider/StockRegistryProvider.php new file mode 100644 index 000000000..4c76fad09 --- /dev/null +++ b/Model/Service/Stock/Provider/StockRegistryProvider.php @@ -0,0 +1,100 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Stock\Provider; + +/** + * Copyright (c) 2019, Nosto Solutions Ltd + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @author Nosto Solutions Ltd + * @copyright 2019 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +use Magento\CatalogInventory\Api\Data\StockStatusCollectionInterface; +use Magento\CatalogInventory\Model\StockRegistryProvider as MagentoStockRegistryProvider; + +class StockRegistryProvider extends MagentoStockRegistryProvider +{ + public const DEFAULT_STOCK_SCOPE = 0; + + /** + * @param int[] $productIds + * @param int $scopeId + * @return StockStatusCollectionInterface + * @suppress PhanTypeMismatchArgument + */ + public function getStockStatuses(array $productIds, int $scopeId = self::DEFAULT_STOCK_SCOPE) + { + $criteria = $this->stockStatusCriteriaFactory->create(); + /** + * Argument is of type array but int is expected + */ + /** @phan-suppress-next-next-line PhanTypeMismatchArgumentProbablyReal */ + /** @noinspection PhpParamsInspection */ + $criteria->setProductsFilter($productIds); // @codingStandardsIgnoreLine + $criteria->setScopeFilter($scopeId); + + return $this->stockStatusRepository->getList($criteria); + } +} diff --git a/Model/Service/Stock/StockService.php b/Model/Service/Stock/StockService.php new file mode 100644 index 000000000..ef2e6b7e6 --- /dev/null +++ b/Model/Service/Stock/StockService.php @@ -0,0 +1,192 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Stock; + +use Magento\Bundle\Model\Product\Type as Bundled; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type as ProductType; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\GroupedProduct\Model\Product\Type\Grouped; +use Magento\Store\Model\Store; +use Magento\Store\Model\Website; +use Nosto\Tagging\Logger\Logger; +use Nosto\Tagging\Model\Service\Stock\Provider\StockProviderInterface; + +/** + * StockService helper used for product inventory level related tasks. + */ +class StockService +{ + /** @var StockProviderInterface */ + private StockProviderInterface $stockProvider; + + /** @var Logger */ + private Logger $logger; + + /** + * Constructor. + * + * @param StockProviderInterface $stockProvider + * @param Logger $nostoLogger + */ + public function __construct( + StockProviderInterface $stockProvider, + Logger $nostoLogger + ) { + $this->stockProvider = $stockProvider; + $this->logger = $nostoLogger; + } + + /** + * Calculates the total qty in stock. If the product is configurable the + * the sum of associated products will be calculated. + * + * @param Product $product + * @param Store $store + * @return int + * @suppress PhanUndeclaredMethod + * @suppress PhanDeprecatedFunction + */ + public function getQuantity(Product $product, Store $store) + { + $qty = 0; + try { + $website = $store->getWebsite(); + } catch (NoSuchEntityException $e) { + $this->logger->exception($e); + return 0; + } + if ($website instanceof Website === false) { + $this->logger->error('Could not resolve website when resolving quantity'); + return 0; + } + switch ($product->getTypeId()) { + case ProductType::TYPE_BUNDLE: + /** @var Bundled $productType */ + $productType = $product->getTypeInstance(); + $bundledItemIds = $productType->getChildrenIds($product->getId(), $required = true); + $productIds = []; + foreach ($bundledItemIds as $variants) { + if (is_array($variants) && count($variants) > 0) { // @codingStandardsIgnoreLine + foreach ($variants as $productId) { + $productIds[] = $productId; + } + } + } + $qty = $this->getMinQty($productIds, $website); + break; + case Grouped::TYPE_CODE: + $productType = $product->getTypeInstance(); + if ($productType instanceof Grouped) { + $productIds = $productType->getAssociatedProductIds($product); + $qty = $this->getMinQty($productIds, $website); + } + break; + case Configurable::TYPE_CODE: + $productType = $product->getTypeInstance(); + if ($productType instanceof Configurable) { + $productIds = $productType->getChildrenIds($product->getId()); + if (isset($productIds[0]) && is_array($productIds[0])) { + $productIds = $productIds[0]; + } + $qty = $this->getQtySum($productIds, $website); + } + break; + default: + $qty += $this->stockProvider->getAvailableQuantity($product, $website); + break; + } + + return $qty; + } + + /** + * Searches the minimum quantity from the products collection + * + * @param int[] $productIds + * @param Website $website + * @return int|mixed + */ + private function getMinQty(array $productIds, Website $website) + { + $quantities = $this->stockProvider->getQuantitiesByIds($productIds, $website); + $minQty = 0; + if (!empty($quantities)) { + rsort($quantities, SORT_NUMERIC); + $minQty = array_pop($quantities); + } + return $minQty; + } + + /** + * Sums quantities for all product ids in array + * + * @param int[] $productIds + * @param Website $website + * @return int + */ + private function getQtySum(array $productIds, Website $website) + { + $qty = 0; + $quantities = $this->stockProvider->getQuantitiesByIds($productIds, $website); + foreach ($quantities as $quantity) { + $qty += $quantity; + } + return $qty; + } + + /** + * Sums quantities for all product ids in array + * + * @param Product $product + * @param Store $store + * @return bool + */ + public function isInStock(Product $product, Store $store) + { + try { + return (bool)$this->stockProvider->isInStock( + $product, + $store->getWebsite() + ); + } catch (NoSuchEntityException $e) { + $this->logger->exception($e); + return false; + } + } +} diff --git a/Model/Service/Store/MissingStoreException.php b/Model/Service/Store/MissingStoreException.php new file mode 100644 index 000000000..980c128f1 --- /dev/null +++ b/Model/Service/Store/MissingStoreException.php @@ -0,0 +1,43 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Store; + +use RuntimeException; + +class MissingStoreException extends RuntimeException // @codingStandardsIgnoreLine +{ +} diff --git a/Model/Service/Sync/AbstractBulkConsumer.php b/Model/Service/Sync/AbstractBulkConsumer.php new file mode 100644 index 000000000..ce06719a9 --- /dev/null +++ b/Model/Service/Sync/AbstractBulkConsumer.php @@ -0,0 +1,134 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Sync; + +use Exception; +use InvalidArgumentException; +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Magento\Store\Model\App\Emulation; +use Nosto\Tagging\Logger\Logger; + +abstract class AbstractBulkConsumer implements BulkConsumerInterface +{ + /** @var Logger */ + private Logger $logger; + + /** @var JsonHelper */ + private JsonHelper $jsonHelper; + + /** @var EntityManager */ + private EntityManager $entityManager; + + /** @var Emulation */ + private Emulation $storeEmulation; + + /** + * AbstractBulkConsumer constructor. + * @param Logger $logger + * @param JsonHelper $jsonHelper + * @param EntityManager $entityManager + * @param Emulation $storeEmulation + */ + public function __construct( + Logger $logger, + JsonHelper $jsonHelper, + EntityManager $entityManager, + Emulation $storeEmulation + ) { + $this->logger = $logger; + $this->jsonHelper = $jsonHelper; + $this->entityManager = $entityManager; + $this->storeEmulation = $storeEmulation; + } + + /** + * Processing operation for product sync + * + * @param OperationInterface $operation + * @return void + * @throws Exception + * @suppress PhanUndeclaredClassConstant + */ + public function processOperation(OperationInterface $operation) + { + $errorCode = null; + if ($operation instanceof OperationInterface) { + $serializedData = $operation->getSerializedData(); + } else { + throw new InvalidArgumentException( + 'Wrong type passed to AsyncBulkConsumer::processOperation. + Expected \Magento\AsynchronousOperations\Api\Data\OperationInterface.' + ); + } + $unserializedData = $this->jsonHelper->jsonDecode($serializedData); + $productIds = $unserializedData['product_ids']; + $storeId = $unserializedData['store_id']; + try { + $this->storeEmulation->startEnvironmentEmulation((int)$storeId); + $this->doOperation($productIds, $storeId); + /** + * Argument is of type string but array is expected + */ + /** @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal */ + $message = __('Success.'); + $operation->setStatus(OperationInterface::STATUS_TYPE_COMPLETE) + ->setResultMessage($message); + } catch (Exception $e) { + $this->logger->critical(sprintf('Bulk uuid: %s. %s', $operation->getBulkUuid(), $e->getMessage())); + /** + * Argument is of type string but array is expected + */ + /** @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal */ + $message = __('Something went wrong when syncing products to Nosto. Check log for details.'); + $operation->setStatus(OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED) + ->setErrorCode($e->getCode()) + ->setResultMessage($message); + } finally { + $this->entityManager->save($operation); + $this->storeEmulation->stopEnvironmentEmulation(); + } + } + + /** + * @param array $productIds + * @param string $storeId + */ + abstract public function doOperation(array $productIds, string $storeId); +} diff --git a/Model/Service/Sync/AbstractBulkPublisher.php b/Model/Service/Sync/AbstractBulkPublisher.php new file mode 100644 index 000000000..1ec5ac337 --- /dev/null +++ b/Model/Service/Sync/AbstractBulkPublisher.php @@ -0,0 +1,221 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Sync; + +use Exception; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject\IdentityGeneratorInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Module\Manager; +use Magento\Framework\Serialize\SerializerInterface; +use Nosto\Tagging\Logger\Logger; +use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; + +abstract class AbstractBulkPublisher implements BulkPublisherInterface +{ + private const STATUS_TYPE_OPEN = \Magento\Framework\Bulk\OperationInterface::STATUS_TYPE_OPEN; + + /** @var \Magento\Framework\Bulk\BulkManagementInterface|null */ + private $bulkManagement; + + /** @var OperationInterfaceFactory|null */ + private ?OperationInterfaceFactory $operationFactory; + + /** @var IdentityGeneratorInterface */ + private IdentityGeneratorInterface $identityService; + + /** @var SerializerInterface */ + public SerializerInterface $serializer; + + /** @var Manager */ + private Manager $manager; + + /** @var Logger */ + private Logger $logger; + + /** + * AbstractBulkPublisher constructor. + * @param IdentityGeneratorInterface $identityService + * @param OperationInterfaceFactory $operationInterfaceFactory + * @param SerializerInterface $serializer + * @param Manager $manager + * @param Logger $logger + */ + public function __construct(// @codingStandardsIgnoreLine + IdentityGeneratorInterface $identityService, + OperationInterfaceFactory $operationInterfaceFactory, + SerializerInterface $serializer, + Manager $manager, + Logger $logger + ) { + $this->identityService = $identityService; + $this->operationFactory = $operationInterfaceFactory; + $this->serializer = $serializer; + $this->manager = $manager; + $this->logger = $logger; + try { + $this->bulkManagement = ObjectManager::getInstance() + ->get(\Magento\Framework\Bulk\BulkManagementInterface::class); + } catch (Exception $e) { + $logger->debug('Module Magento_AsynchronousOperations not available'); + } + } + + /** + * @inheritDoc + * @throws LocalizedException + */ + public function execute(int $storeId, array $productIds = []) + { + if (!empty($productIds)) { + $this->publishCollectionToQueue($storeId, $productIds); + } + } + + /** + * @param $storeId + * @param $productIds + * @throws LocalizedException + * @throws Exception + */ + private function publishCollectionToQueue( + $storeId, + $productIds + ) { + + if (!$this->canUseAsyncOperations()) { + $this->logger->critical( + "Module Magento_AsynchronousOperations not available. Aborting bulk publish operation" + ); + return; + } + $productIdsChunks = array_chunk($productIds, $this->getBulkSize()); + $bulkUuid = $this->identityService->generateId(); + /** + * Argument is of type string but array is expected + */ + /** @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal */ + $bulkDescription = __('Sync ' . count($productIds) . ' Nosto products'); + $operationsData = []; + foreach ($productIdsChunks as $productIdsChunk) { + $operationsData[] = $this->buildOperationData( + $storeId, + $productIdsChunk, + $bulkUuid + ); + } + + $operations = []; + foreach ($operationsData as $operationData) { + $operations[] = $this->operationFactory->create($operationData); + } + if (empty($operations)) { + return; + } + $result = $this->bulkManagement->scheduleBulk( + $bulkUuid, + $operations, + $bulkDescription + ); + if (!$result) { + /** + * Argument is of type string but array is expected + */ + /** @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal */ + throw new LocalizedException(__('Something went wrong while processing the request.')); + } + } + + /** + * @return bool + */ + private function canUseAsyncOperations(): bool + { + if ($this->manager->isEnabled('Magento_AsynchronousOperations')) { + return true; + } + return false; + } + + /** + * @return string + */ + abstract public function getTopicName(): string; + + /** + * @return int + */ + abstract public function getBulkSize(): int; + + /** + * @return string + */ + abstract public function getBulkDescription(): string; + + /** + * @return string + */ + abstract public function getMetaData(): string; + + /** + * Build asynchronous operation data + * @param int $storeId + * @param array $productIds + * @param string $bulkUuid + * @return array + */ + private function buildOperationData( + int $storeId, + array $productIds, + string $bulkUuid + ) { + $dataToEncode = [ + 'meta_information' => $this->getMetaData(), + 'product_ids' => $productIds, + 'store_id' => $storeId + ]; + return [ + 'data' => [ + 'bulk_uuid' => $bulkUuid, + 'topic_name' => $this->getTopicName(), + 'serialized_data' => $this->serializer->serialize($dataToEncode), + 'status' => self::STATUS_TYPE_OPEN + ] + ]; + } +} diff --git a/Model/Service/Sync/BulkConsumerInterface.php b/Model/Service/Sync/BulkConsumerInterface.php new file mode 100644 index 000000000..b879cbc07 --- /dev/null +++ b/Model/Service/Sync/BulkConsumerInterface.php @@ -0,0 +1,52 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Sync; + +use Exception; +use Magento\AsynchronousOperations\Api\Data\OperationInterface; + +interface BulkConsumerInterface +{ + /** + * Processing operation for product sync + * + * @param OperationInterface $operation + * @return void + * @throws Exception + */ + public function processOperation(OperationInterface $operation); +} diff --git a/Model/Service/Sync/BulkPublisherInterface.php b/Model/Service/Sync/BulkPublisherInterface.php new file mode 100644 index 000000000..b4e3107d2 --- /dev/null +++ b/Model/Service/Sync/BulkPublisherInterface.php @@ -0,0 +1,47 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Sync; + +interface BulkPublisherInterface +{ + /** + * @param int $storeId + * @param array $productIds + * @return void + */ + public function execute(int $storeId, array $productIds = []); +} diff --git a/Model/Service/Sync/Delete/AsyncBulkConsumer.php b/Model/Service/Sync/Delete/AsyncBulkConsumer.php new file mode 100644 index 000000000..0b067ebb5 --- /dev/null +++ b/Model/Service/Sync/Delete/AsyncBulkConsumer.php @@ -0,0 +1,93 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Sync\Delete; + +use Nosto\NostoException; +use Nosto\Tagging\Model\Service\Sync\AbstractBulkConsumer; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Nosto\Tagging\Logger\Logger; +use Magento\Store\Model\App\Emulation; + +class AsyncBulkConsumer extends AbstractBulkConsumer +{ + /** @var DeleteService */ + private DeleteService $deleteService; + + /** @var NostoHelperScope */ + private NostoHelperScope $nostoHelperScope; + + /** + * AsyncBulkConsumer constructor. + * @param DeleteService $deleteService + * @param NostoHelperScope $nostoHelperScope + * @param JsonHelper $jsonHelper + * @param EntityManager $entityManager + * @param Emulation $storeEmulation + * @param Logger $logger + */ + public function __construct( + DeleteService $deleteService, + NostoHelperScope $nostoHelperScope, + JsonHelper $jsonHelper, + EntityManager $entityManager, + Emulation $storeEmulation, + Logger $logger + ) { + $this->deleteService = $deleteService; + $this->nostoHelperScope = $nostoHelperScope; + parent::__construct( + $logger, + $jsonHelper, + $entityManager, + $storeEmulation + ); + } + + /** + * @inheritDoc + * @param array $productIds + * @param string $storeId + * @throws NostoException + */ + public function doOperation(array $productIds, string $storeId) + { + $store = $this->nostoHelperScope->getStore($storeId); + $this->deleteService->delete($productIds, $store); + } +} diff --git a/Model/Service/Sync/Delete/AsyncBulkPublisher.php b/Model/Service/Sync/Delete/AsyncBulkPublisher.php new file mode 100644 index 000000000..5198406c7 --- /dev/null +++ b/Model/Service/Sync/Delete/AsyncBulkPublisher.php @@ -0,0 +1,77 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Sync\Delete; + +use Nosto\Tagging\Model\Service\Sync\AbstractBulkPublisher; + +class AsyncBulkPublisher extends AbstractBulkPublisher +{ + public const NOSTO_DELETE_MESSAGE_QUEUE = 'nosto_product_sync.delete'; + public const BULK_SIZE = 100; + + /** + * @inheritDoc + */ + public function getTopicName(): string + { + return self::NOSTO_DELETE_MESSAGE_QUEUE; + } + + /** + * @inheritDoc + */ + public function getBulkSize(): int + { + return self::BULK_SIZE; + } + + /** + * @inheritDoc + */ + public function getBulkDescription(): string + { + return sprintf('Delete %d Nosto products', 2); + } + + /** + * @inheritDoc + */ + public function getMetaData(): string + { + return 'Delete Nosto products'; + } +} diff --git a/Model/Service/Sync/Delete/DeleteService.php b/Model/Service/Sync/Delete/DeleteService.php new file mode 100644 index 000000000..aafb1f39a --- /dev/null +++ b/Model/Service/Sync/Delete/DeleteService.php @@ -0,0 +1,134 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Sync\Delete; + +use Exception; +use Magento\Store\Model\Store; +use Nosto\Model\Signup\Account as NostoSignupAccount; +use Nosto\NostoException; +use Nosto\Operation\DeleteProduct; +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\Service\AbstractService; +use Nosto\Tagging\Model\Service\Cache\CacheService; + +class DeleteService extends AbstractService +{ + + public const BENCHMARK_DELETE_NAME = 'nosto_product_delete'; + public const BENCHMARK_DELETE_BREAKPOINT = 1; + public const PRODUCT_DELETION_BATCH_SIZE = 100; + + /** @var CacheService */ + private CacheService $cacheService; + + /** @var NostoHelperAccount */ + private NostoHelperAccount $nostoHelperAccount; + + /** @var NostoHelperUrl */ + private NostoHelperUrl $nostoHelperUrl; + + /** @var int */ + private int $deleteBatchSize; + + /** + * DeleteService constructor. + * @param CacheService $cacheService + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperData $nostoHelperData + * @param NostoHelperUrl $nostoHelperUrl + * @param NostoLogger $logger + * @param $deleteBatchSize + */ + public function __construct( + CacheService $cacheService, + NostoHelperAccount $nostoHelperAccount, + NostoHelperData $nostoHelperData, + NostoHelperUrl $nostoHelperUrl, + NostoLogger $logger, + $deleteBatchSize + ) { + $this->cacheService = $cacheService; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->nostoHelperUrl = $nostoHelperUrl; + $this->deleteBatchSize = $deleteBatchSize; + parent::__construct($nostoHelperData, $nostoHelperAccount, $logger); + } + + /** + * Discontinues products in Nosto and removes indexed products from Nosto product index + * + * @param array $productIds + * @param Store $store + * @throws NostoException + */ + public function delete(array $productIds, Store $store) + { + if (count($productIds) === 0) { + return; + } + $account = $this->nostoHelperAccount->findAccount($store); + if ($account instanceof NostoSignupAccount === false) { + throw new NostoException(sprintf('Store view %s does not have Nosto installed', $store->getName())); + } + $this->startBenchmark(self::BENCHMARK_DELETE_NAME, self::BENCHMARK_DELETE_BREAKPOINT); + $productIdBatches = array_chunk($productIds, $this->deleteBatchSize); + $this->logDebugWithStore( + sprintf( + 'Deleting total of %d products in batches of %d', + count($productIds), + count($productIdBatches) + ), + $store + ); + foreach ($productIdBatches as $ids) { + try { + $op = new DeleteProduct($account, $this->nostoHelperUrl->getActiveDomain($store)); + $op->setResponseTimeout(30); + $op->setProductIds($ids); + $op->delete(); // @codingStandardsIgnoreLine + $this->cacheService->removeByProductIds($store, $ids); + $this->tickBenchmark(self::BENCHMARK_DELETE_NAME); + } catch (Exception $e) { + $this->getLogger()->exception($e); + } + } + $this->logBenchmarkSummary(self::BENCHMARK_DELETE_NAME, $store); + } +} diff --git a/Model/Service/Sync/Upsert/AsyncBulkConsumer.php b/Model/Service/Sync/Upsert/AsyncBulkConsumer.php new file mode 100644 index 000000000..bff092ea7 --- /dev/null +++ b/Model/Service/Sync/Upsert/AsyncBulkConsumer.php @@ -0,0 +1,108 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Sync\Upsert; + +use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Magento\Store\Model\App\Emulation; +use Nosto\Exception\MemoryOutOfBoundsException; +use Nosto\NostoException; +use Nosto\Tagging\Helper\Scope as NostoScopeHelper; +use Nosto\Tagging\Logger\Logger; +use Nosto\Tagging\Model\ResourceModel\Magento\Product\CollectionFactory; +use Nosto\Tagging\Model\Service\Sync\AbstractBulkConsumer; + +/** + * Asynchronous Bulk Consumer + * + * Class AsyncBulkConsumer + */ +class AsyncBulkConsumer extends AbstractBulkConsumer +{ + /** @var SyncService */ + private SyncService $syncService; + + /** @var NostoScopeHelper */ + private NostoScopeHelper $nostoScopeHelper; + + /** @var CollectionFactory */ + private CollectionFactory $collectionFactory; + + /** + * AsyncBulkConsumer constructor. + * @param SyncService $syncService + * @param NostoScopeHelper $nostoScopeHelper + * @param CollectionFactory $collectionFactory + * @param JsonHelper $jsonHelper + * @param EntityManager $entityManager + * @param Emulation $storeEmulation + * @param Logger $logger + */ + public function __construct( + SyncService $syncService, + NostoScopeHelper $nostoScopeHelper, + CollectionFactory $collectionFactory, + JsonHelper $jsonHelper, + EntityManager $entityManager, + Emulation $storeEmulation, + Logger $logger + ) { + $this->syncService = $syncService; + $this->nostoScopeHelper = $nostoScopeHelper; + $this->collectionFactory = $collectionFactory; + parent::__construct( + $logger, + $jsonHelper, + $entityManager, + $storeEmulation + ); + } + + /** + * @inheritDoc + * @throws MemoryOutOfBoundsException + * @throws NostoException + */ + public function doOperation(array $productIds, string $storeId) + { + $store = $this->nostoScopeHelper->getStore($storeId); + $productCollection = $this->collectionFactory->create() + ->addIdsToFilter($productIds) + ->addStoreFilter($storeId); + $this->syncService->syncProducts($productCollection, $store); + } +} diff --git a/Model/Service/Sync/Upsert/AsyncBulkPublisher.php b/Model/Service/Sync/Upsert/AsyncBulkPublisher.php new file mode 100644 index 000000000..ecde254b9 --- /dev/null +++ b/Model/Service/Sync/Upsert/AsyncBulkPublisher.php @@ -0,0 +1,78 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Sync\Upsert; + +use Nosto\Tagging\Model\Service\Sync\AbstractBulkPublisher; + +// @codingStandardsIgnoreFile +class AsyncBulkPublisher extends AbstractBulkPublisher +{ + public const NOSTO_SYNC_MESSAGE_QUEUE = 'nosto_product_sync.update'; + public const BULK_SIZE = 100; + + /** + * @inheritDoc + */ + public function getTopicName(): string + { + return self::NOSTO_SYNC_MESSAGE_QUEUE; + } + + /** + * @inheritDoc + */ + public function getBulkSize(): int + { + return self::BULK_SIZE; + } + + /** + * @inheritDoc + */ + public function getBulkDescription(): string + { + return sprintf('Sync %d Nosto products', 2); + } + + /** + * @inheritDoc + */ + public function getMetaData(): string + { + return 'Sync Nosto products'; + } +} diff --git a/Model/Service/Sync/Upsert/SyncService.php b/Model/Service/Sync/Upsert/SyncService.php new file mode 100644 index 000000000..81fc58219 --- /dev/null +++ b/Model/Service/Sync/Upsert/SyncService.php @@ -0,0 +1,168 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Sync\Upsert; + +use Exception; +use Magento\Catalog\Model\Product; +use Magento\Store\Model\Store; +use Nosto\Exception\MemoryOutOfBoundsException; +use Nosto\NostoException; +use Nosto\Operation\UpsertProduct; +use Nosto\Request\Http\Exception\AbstractHttpException; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Data as NostoDataHelper; +use Nosto\Tagging\Helper\Url as NostoHelperUrl; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\ResourceModel\Magento\Product\Collection as ProductCollection; +use Nosto\Tagging\Model\Service\AbstractService; +use Nosto\Tagging\Model\Service\Cache\CacheService; +use Nosto\Tagging\Model\Service\Product\ProductServiceInterface; +use Nosto\Tagging\Util\PagingIterator; + +class SyncService extends AbstractService +{ + public const BENCHMARK_SYNC_NAME = 'nosto_product_upsert'; + public const BENCHMARK_SYNC_BREAKPOINT = 1; + + /** @var NostoHelperAccount */ + private NostoHelperAccount $nostoHelperAccount; + + /** @var NostoHelperUrl */ + private NostoHelperUrl $nostoHelperUrl; + + /** @var NostoDataHelper */ + private NostoDataHelper $nostoDataHelper; + + /** @var ProductServiceInterface */ + private ProductServiceInterface $productService; + + /** @var CacheService */ + private CacheService $cacheService; + + /** @var int */ + private int $apiBatchSize; + + /** @var int */ + private int $apiTimeout; + + /** + * Sync constructor. + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperUrl $nostoHelperUrl + * @param NostoLogger $logger + * @param NostoDataHelper $nostoDataHelper + * @param ProductServiceInterface $productService + * @param CacheService $cacheService + * @param $apiBatchSize + * @param $apiTimeout + */ + public function __construct( + NostoHelperAccount $nostoHelperAccount, + NostoHelperUrl $nostoHelperUrl, + NostoLogger $logger, + NostoDataHelper $nostoDataHelper, + ProductServiceInterface $productService, + CacheService $cacheService, + $apiBatchSize, + $apiTimeout + ) { + parent::__construct($nostoDataHelper, $nostoHelperAccount, $logger); + $this->productService = $productService; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->nostoHelperUrl = $nostoHelperUrl; + $this->nostoDataHelper = $nostoDataHelper; + $this->cacheService = $cacheService; + $this->apiBatchSize = $apiBatchSize; + $this->apiTimeout = $apiTimeout; + } + + /** + * @param ProductCollection $collection + * @param Store $store + * @throws MemoryOutOfBoundsException + * @throws NostoException + * @throws AbstractHttpException + * @throws Exception + */ + public function syncProducts(ProductCollection $collection, Store $store) + { + if (!$this->nostoDataHelper->isProductUpdatesEnabled($store)) { + $this->logDebugWithStore( + 'Nosto product sync is disabled - skipping upserting products to Nosto', + $store + ); + return; + } + $account = $this->nostoHelperAccount->findAccount($store); + $this->startBenchmark(self::BENCHMARK_SYNC_NAME, self::BENCHMARK_SYNC_BREAKPOINT); + + $collection->setPageSize($this->apiBatchSize); + $iterator = new PagingIterator($collection); + + /** @var ProductCollection $page */ + foreach ($iterator as $page) { + $productIdsInBatch = []; + $this->checkMemoryConsumption('product sync'); + $op = new UpsertProduct($account, $this->nostoHelperUrl->getActiveDomain($store)); + $op->setResponseTimeout($this->apiTimeout); + /** @var Product $product */ + foreach ($page as $product) { + $productIdsInBatch[] = $product->getId(); + $nostoProduct = $this->productService->getProduct($product, $store); + if ($nostoProduct === null) { + throw new NostoException('Could not get product from the product service.'); + } + $op->addProduct($nostoProduct); + // phpcs:ignore + $this->cacheService->save($nostoProduct, $store); + $this->tickBenchmark(self::BENCHMARK_SYNC_NAME); + } + + $this->logDebugWithStore( + sprintf( + 'Upserting batch of %d (%s) - API timeout is set to %d seconds', + $this->apiBatchSize, + implode(',', $productIdsInBatch), + $this->apiTimeout + ), + $store + ); + $op->upsert(); + } + $this->logBenchmarkSummary(self::BENCHMARK_SYNC_NAME, $store, $this); + } +} diff --git a/Model/Service/Update/QueueProcessorService.php b/Model/Service/Update/QueueProcessorService.php new file mode 100644 index 000000000..b3eda43b5 --- /dev/null +++ b/Model/Service/Update/QueueProcessorService.php @@ -0,0 +1,296 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Update; + +use Exception; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Model\Store; +use Nosto\Tagging\Api\Data\ProductUpdateQueueInterface; +use Nosto\Tagging\Helper\Account as NostoAccountHelper; +use Nosto\Tagging\Helper\Data as NostoDataHelper; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Product\Queue\QueueRepository; +use Nosto\Tagging\Model\Product\Update\Queue; +use Nosto\Tagging\Model\ResourceModel\Product\Update\Queue\QueueCollection; +use Nosto\Tagging\Model\ResourceModel\Product\Update\Queue\QueueCollectionBuilder; +use Nosto\Tagging\Model\Service\AbstractService; +use Nosto\Tagging\Model\Service\Sync\BulkPublisherInterface; + +/** + * Class QueueService + */ +class QueueProcessorService extends AbstractService +{ + /** @var BulkPublisherInterface */ + private BulkPublisherInterface $upsertBulkPublisher; + + /** @var QueueRepository */ + private QueueRepository $queueRepository; + + /** @var TimezoneInterface */ + private TimezoneInterface $magentoTimeZone; + + /** @var QueueCollectionBuilder */ + private QueueCollectionBuilder $queueCollectionBuilder; + + /** @var BulkPublisherInterface */ + private BulkPublisherInterface $deleteBulkPublisher; + + /** @var int */ + private int $maxProductsInBatch; + + /** @var int */ + private int $cleanupInterval; + + /** + * @param NostoLogger $logger + * @param NostoDataHelper $nostoDataHelper + * @param NostoAccountHelper $nostoAccountHelper + * @param BulkPublisherInterface $upsertBulkPublisher + * @param BulkPublisherInterface $deleteBulkPublisher + * @param QueueRepository $queueRepository + * @param TimezoneInterface $magentoTimeZone + * @param QueueCollectionBuilder $queueCollectionBuilder + * @param $maxProductsInBatch + * @param $cleanUpInterval + */ + public function __construct( + NostoLogger $logger, + NostoDataHelper $nostoDataHelper, + NostoAccountHelper $nostoAccountHelper, + BulkPublisherInterface $upsertBulkPublisher, + BulkPublisherInterface $deleteBulkPublisher, + QueueRepository $queueRepository, + TimezoneInterface $magentoTimeZone, + QueueCollectionBuilder $queueCollectionBuilder, + $maxProductsInBatch, + $cleanUpInterval + ) { + parent::__construct($nostoDataHelper, $nostoAccountHelper, $logger); + $this->upsertBulkPublisher = $upsertBulkPublisher; + $this->deleteBulkPublisher = $deleteBulkPublisher; + $this->queueRepository = $queueRepository; + $this->magentoTimeZone = $magentoTimeZone; + $this->queueCollectionBuilder = $queueCollectionBuilder; + $this->maxProductsInBatch = $maxProductsInBatch; + $this->cleanupInterval = $cleanUpInterval; + } + + /** + * Processes a collection of queue entries + * - merges product ids from queue entries within the same store + * @param QueueCollection $collection + * @param Store $store + */ + public function processQueueCollection(QueueCollection $collection, Store $store) + { + $initialCollectionSize = $collection->getSize(); + $this->logDebugWithStore( + sprintf( + 'Started processing %d of queue entries', + $initialCollectionSize + ), + $store + ); + if ($initialCollectionSize === 0) { + $this->logInfoWithStore('No unprocessed queue entries in the update queue for the store', $store); + return; + } + $this->capCollection($collection, $store); + $this->setStatusToProcessing($collection); + $merged = $this->mergeQueues($collection, $store); + foreach ($merged as $storeId => $actions) { + foreach ($actions as $action => $productIds) { + if ($action === ProductUpdateQueueInterface::ACTION_VALUE_UPSERT) { + $this->upsertBulkPublisher->execute($storeId, $productIds); + } else { + $this->deleteBulkPublisher->execute($storeId, $productIds); + } + } + } + $this->setStatusToDone($collection); + $this->cleanupUpdateQueue($store); + $this->logDebugWithStore( + sprintf( + 'Processed %d of queue entries', + // phpcs:ignore + $collection->count() + ), + $store + ); + } + + /** + * Caps the collection to the max amount of products in one batch + * + * @param QueueCollection $collection + * @param Store $store + */ + private function capCollection(QueueCollection $collection, Store $store) + { + // phpcs:ignore + $originalSize = $collection->count(); + $productIdCount = 0; + $leftIds = 0; + /** @var Queue $entry */ + foreach ($collection as $key => $entry) { + if ($productIdCount > $this->maxProductsInBatch) { + $leftIds += $entry->getProductIdCount(); + $collection->removeItemByKey($key); + } + $productIdCount += $entry->getProductIdCount(); + } + // phpcs:ignore + $sizeAfterCap = $collection->count(); + if ($sizeAfterCap < $originalSize) { + $this->logDebugWithStore( + sprintf( + 'QueueCollection capped from %d to %d - %d non-unique product ids remain in the queue', + $originalSize, + $sizeAfterCap, + $leftIds + ), + $store + ); + } + } + + /** + * Merges productIds from QueueCollection into an array containing only unique product ids per store + * + * @param QueueCollection $collection + * @param Store $store + * @return array + */ + private function mergeQueues(QueueCollection $collection, Store $store) + { + $merged = []; + $totalCount = 0; + /* @var ProductUpdateQueueInterface $queueEntry */ + foreach ($collection as $queueEntry) { + if (!isset($merged[$queueEntry->getStoreId()])) { + $merged[$queueEntry->getStoreId()] = [ + ProductUpdateQueueInterface::ACTION_VALUE_UPSERT => [], + ProductUpdateQueueInterface::ACTION_VALUE_DELETE => [] + ]; + } + $totalCount += $queueEntry->getProductIdCount(); + foreach ($queueEntry->getProductIds() as $productId) { + $merged[$queueEntry->getStoreId()][$queueEntry->getAction()][$productId] = $productId; + } + } + $mergedCount = 0; + foreach ($merged as $storeId => $arr) { + foreach ($arr as $method => $ids) { + // phpcs:ignore + $mergedCount += count($ids); + } + } + $this->logDebugWithStore( + sprintf( + 'Merged total of %d product ids into %d', + $totalCount, + $mergedCount + ), + $store + ); + return $merged; + } + + /** + * Sets the timestamp for started at & updates the status to be processing + * + * @param QueueCollection $collection + */ + private function setStatusToProcessing(QueueCollection $collection) + { + /* @var ProductUpdateQueueInterface $queueEntry */ + foreach ($collection as $queueEntry) { + $queueEntry->setStartedAt($this->magentoTimeZone->date()); + $queueEntry->setStatus(ProductUpdateQueueInterface::STATUS_VALUE_PROCESSING); + } + } + + /** + * Sets the timestamp for completed at & updates the status to be done + * + * @param QueueCollection $collection + */ + private function setStatusToDone(QueueCollection $collection) + { + /* @var ProductUpdateQueueInterface $queueEntry */ + foreach ($collection as $queueEntry) { + $queueEntry->setCompletedAt($this->magentoTimeZone->date()); + $queueEntry->setStatus(ProductUpdateQueueInterface::STATUS_VALUE_DONE); + try { + // phpcs:ignore + $this->queueRepository->save($queueEntry); + } catch (AlreadyExistsException $e) { + $this->getLogger()->exception($e); + } + } + } + + /** + * Cleans up completed entries from the queue table + * @param Store $store + */ + private function cleanupUpdateQueue(Store $store) + { + try { + $processed = $this->queueCollectionBuilder + ->init() + ->withCompletedHrsAgo($this->cleanupInterval) + ->build(); + $this->logDebugWithStore( + sprintf( + 'Cleaning up %d entries from update queue completed %d < hours ago', + $processed->count(), + $this->cleanupInterval + ), + $store + ); + foreach ($processed as $queueItem) { + // phpcs:ignore + $this->queueRepository->delete($queueItem); + } + } catch (Exception $e) { + $this->getLogger()->exception($e); + } + } +} diff --git a/Model/Service/Update/QueueService.php b/Model/Service/Update/QueueService.php new file mode 100644 index 000000000..6dcd6be0d --- /dev/null +++ b/Model/Service/Update/QueueService.php @@ -0,0 +1,186 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\Service\Update; + +use Exception; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Store\Model\Store; +use Nosto\NostoException; +use Nosto\Tagging\Exception\ParentProductDisabledException; +use Nosto\Tagging\Helper\Account as NostoAccountHelper; +use Nosto\Tagging\Helper\Data as NostoDataHelper; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Product\Queue\QueueBuilder; +use Nosto\Tagging\Model\Product\Queue\QueueRepository; +use Nosto\Tagging\Model\Product\Repository as NostoProductRepository; +use Nosto\Tagging\Model\ResourceModel\Magento\Product\Collection as ProductCollection; +use Nosto\Tagging\Model\Service\AbstractService; +use Nosto\Tagging\Util\PagingIterator; + +class QueueService extends AbstractService +{ + /** @var QueueRepository */ + private QueueRepository $queueRepository; + + /** @var QueueBuilder */ + private QueueBuilder $queueBuilder; + + /** @var NostoProductRepository $nostoProductRepository */ + private NostoProductRepository $nostoProductRepository; + + /** @var int $batchSize */ + private int $batchSize; + + /** + * QueueService constructor. + * @param QueueRepository $queueRepository + * @param QueueBuilder $queueBuilder + * @param NostoLogger $logger + * @param NostoDataHelper $nostoDataHelper + * @param NostoAccountHelper $nostoAccountHelper + * @param NostoProductRepository $nostoProductRepository + * @param int $batchSize + */ + public function __construct( + QueueRepository $queueRepository, + QueueBuilder $queueBuilder, + NostoLogger $logger, + NostoDataHelper $nostoDataHelper, + NostoAccountHelper $nostoAccountHelper, + NostoProductRepository $nostoProductRepository, + int $batchSize + ) { + parent::__construct($nostoDataHelper, $nostoAccountHelper, $logger); + $this->queueRepository = $queueRepository; + $this->queueBuilder = $queueBuilder; + $this->nostoProductRepository = $nostoProductRepository; + $this->batchSize = $batchSize; + } + + /** + * Sets the products into the update queue + * + * @param ProductCollection $collection + * @param Store $store + * @throws NostoException + * @throws Exception + */ + public function addCollectionToUpsertQueue(ProductCollection $collection, Store $store) + { + if ($this->getAccountHelper()->findAccount($store) === null) { + $this->logDebugWithStore('No nosto account found for the store', $store); + return; + } + $collection->setPageSize($this->batchSize); + $iterator = new PagingIterator($collection); + $this->getLogger()->debugWithSource( + sprintf( + 'Adding %d products to queue for store %s - batch size is %s, total amount of pages %d', + $collection->getSize(), + $store->getCode(), + $this->batchSize, + $iterator->getLastPageNumber() + ), + ['storeId' => $store->getId()], + $this + ); + /** @var ProductCollection $page */ + foreach ($iterator as $page) { + $queueEntry = $this->queueBuilder->buildForUpsert( + $store, + $this->toParentProductIds($page) + ); + if (!empty($queueEntry->getProductIds())) { + $this->queueRepository->save($queueEntry); // @codingStandardsIgnoreLine + } + } + } + + /** + * Sets the product ids into the delete queue + * + * @param $productIds + * @param Store $store + * @throws AlreadyExistsException + */ + public function addIdsToDeleteQueue($productIds, Store $store) + { + if ($this->getAccountHelper()->findAccount($store) === null) { + $this->logDebugWithStore('No nosto account found for the store', $store); + return; + } + $batchedIds = array_chunk($productIds, $this->batchSize); + /** @var ProductCollection $page */ + foreach ($batchedIds as $idBatch) { + $queueEntry = $this->queueBuilder->buildForDeletion( + $store, + $idBatch + ); + if (!empty($queueEntry->getProductIds())) { + $this->queueRepository->save($queueEntry); // @codingStandardsIgnoreLine + } + } + } + + /** + * @param ProductCollection $collection + * @return array + */ + private function toParentProductIds(ProductCollection $collection) + { + $productIds = []; + /** @var ProductInterface $product */ + foreach ($collection->getItems() as $product) { + try { + /** @phan-suppress-next-line PhanTypeMismatchArgument */ + $parents = $this->nostoProductRepository->resolveParentProductIds($product); + } catch (ParentProductDisabledException $e) { + $this->getLogger()->debug($e->getMessage()); + continue; + } + if (!empty($parents)) { + foreach ($parents as $id) { + $productIds[] = $id; + } + } else { + $productIds[] = $product->getId(); + } + } + return array_unique($productIds); + } +} diff --git a/Model/System/Message/Notification/InvalidAccount.php b/Model/System/Message/Notification/InvalidAccount.php new file mode 100644 index 000000000..699db4631 --- /dev/null +++ b/Model/System/Message/Notification/InvalidAccount.php @@ -0,0 +1,128 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\System\Message\Notification; + +use Magento\Framework\Notification\MessageInterface; +use Magento\Framework\Phrase; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; + +class InvalidAccount implements MessageInterface +{ + /** + * @var NostoHelperAccount + */ + private NostoHelperAccount $nostoHelperAccount; + + /** + * @var mixed + */ + private $message; + + /** + * Messages constructor. + * @param NostoHelperAccount $nostoHelperAccount + */ + public function __construct( + NostoHelperAccount $nostoHelperAccount + ) { + $this->nostoHelperAccount = $nostoHelperAccount; + } + + /** + * @return Phrase|string + */ + public function getText() + { + return __($this->message); + } + + /** + * @return string + */ + public function getIdentity() + { + return sha1('Nosto_Account_Notification'); + } + + /** + * @return bool + */ + public function isDisplayed() + { + $invalidAccounts = $this->nostoHelperAccount->getInvalidAccounts(); + + if (count($invalidAccounts) === 0) { + return false; + } + + $this->buildMessage($invalidAccounts); + return true; + } + + /** + * @return int + */ + public function getSeverity() + { + return MessageInterface::SEVERITY_CRITICAL; + } + + /** + * Set the value of the message + * + * @param array $invalidStores + */ + private function buildMessage(array $invalidStores) + { + $message = ''; + + foreach ($invalidStores as $store) { + $message .= 'It looks like Nosto account (' . $store['nostoAccount'] . ') ' + . 'is connected to the wrong store (' . $store['storeName'] . ') or ' + . 'the configured storefront domain is not matching with magento backend. ' + . 'It is not possible to share Nosto accounts across multiple domains. ' + . 'Please reconnect the nosto account or create a new one. ' + . 'Reset Nosto settings

'; + } + + /** + * Argument is of type string but array is expected + */ + /** @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal */ + $this->message = __($message); + } +} diff --git a/Model/User/Builder.php b/Model/User/Builder.php new file mode 100644 index 000000000..fa3ffbc7d --- /dev/null +++ b/Model/User/Builder.php @@ -0,0 +1,88 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Model\User; + +use Exception; +use Magento\Backend\Model\Auth\Session; +use Magento\Framework\Event\ManagerInterface; +use Nosto\Model\User; +use Nosto\Tagging\Logger\Logger as NostoLogger; + +class Builder +{ + private NostoLogger $logger; + private Session $backendAuthSession; + private ManagerInterface $eventManager; + + /** + * @param Session $backendAuthSession + * @param NostoLogger $logger + * @param ManagerInterface $eventManager + */ + public function __construct( + Session $backendAuthSession, + NostoLogger $logger, + ManagerInterface $eventManager + ) { + $this->backendAuthSession = $backendAuthSession; + $this->logger = $logger; + $this->eventManager = $eventManager; + } + + /** + * @return User + */ + public function build() + { + $metaData = new User(); + + try { + $user = $this->backendAuthSession->getUser(); + if ($user !== null) { + $metaData->setFirstName($user->getFirstName()); + $metaData->setLastName($user->getLastName()); + $metaData->setEmail($user->getEmail()); + } + } catch (Exception $e) { + $this->logger->exception($e); + } + + $this->eventManager->dispatch('nosto_user_load_after', ['user' => $metaData]); + + return $metaData; + } +} diff --git a/Observer/Adminhtml/Config.php b/Observer/Adminhtml/Config.php new file mode 100644 index 000000000..2036c34bb --- /dev/null +++ b/Observer/Adminhtml/Config.php @@ -0,0 +1,174 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Observer\Adminhtml; + +use Exception; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Module\Manager as ModuleManager; +use Magento\Store\Model\Store; +use Nosto\Tagging\Helper\Account as NostoAccountHelper; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Indexer\QueueIndexer; + +/** + * Observer to mark all indexed products as dirty if settings have changed + */ +class Config implements ObserverInterface +{ + public const WEBSITE_SCOPE_KEY = 'website'; + public const STORE_SCOPE_KEY = 'store'; + + /** @var NostoLogger */ + private NostoLogger $logger; + + /** @var ModuleManager */ + private ModuleManager $moduleManager; + + /** @var NostoHelperScope */ + private NostoHelperScope $nostoHelperScope; + + /** @var NostoAccountHelper */ + private NostoAccountHelper $nostoAccountHelper; + + /** var QueueIndexer */ + private QueueIndexer $queueIndexer; + + /** @var IndexerRegistry */ + private IndexerRegistry $indexerRegistry; + + /** + * Config Constructor. + * + * @param NostoLogger $logger + * @param ModuleManager $moduleManager + * @param NostoHelperScope $nostoHelperScope + * @param NostoAccountHelper $nostoAccountHelper + * @param IndexerRegistry $indexerRegistry + * @param QueueIndexer $queueIndexer + */ + public function __construct( + NostoLogger $logger, + ModuleManager $moduleManager, + NostoHelperScope $nostoHelperScope, + NostoAccountHelper $nostoAccountHelper, + IndexerRegistry $indexerRegistry, + QueueIndexer $queueIndexer + ) { + $this->logger = $logger; + $this->moduleManager = $moduleManager; + $this->nostoHelperScope = $nostoHelperScope; + $this->nostoAccountHelper = $nostoAccountHelper; + $this->queueIndexer = $queueIndexer; + $this->indexerRegistry = $indexerRegistry; + } + + /** + * Observer method to mark all indexed products as dirty on the index table + * + * @param Observer $observer the dispatched event + * @throws LocalizedException + */ + public function execute(Observer $observer) + { + $changedConfig = $observer->getData('changed_paths'); + // If array of changes contains only indexer allow memory, we can skip + if (empty($changedConfig) + || !$this->moduleManager->isEnabled(NostoHelperData::MODULE_NAME) + || (count($changedConfig) === 1 && $changedConfig[0] === NostoHelperData::XML_PATH_INDEXER_MEMORY) + ) { + return; + } + $storeRequest = $observer->getData(self::STORE_SCOPE_KEY); + $websiteRequest = $observer->getData(self::WEBSITE_SCOPE_KEY); + // If $storeRequest && $websiteRequest are empty strings, means we're in a global scope. + // Mark as dirty for all stores if config is different than the one just saved + if (empty($storeRequest) && empty($websiteRequest)) { // Global scope + $stores = $this->nostoHelperScope->getStores(); + foreach ($stores as $store) { + $this->reindexAll($store); + } + } elseif (!empty($websiteRequest) && empty($storeRequest)) { // Website Level + // Get stores from the website and mark them all as dirty + $website = $this->nostoHelperScope->getWebsite($websiteRequest); + $stores = $website->getStores(); + foreach ($stores as $store) { + $this->reindexAll($store); + } + } else { // Store View Level + $store = $this->nostoHelperScope->getStore($storeRequest); + $this->reindexAll($store); + } + } + + /** + * Wrapper to log and mark all products as dirty after configuration has changed + * @param Store $store + */ + private function reindexAll(Store $store) + { + if ($this->nostoAccountHelper->nostoInstalledAndEnabled($store)) { + $this->logger->infoWithSource( + sprintf( + 'Nosto Settings updated, marking all indexed products as dirty for store %s', + $store->getName() + ), + ['storeId' => $store->getId()], + $this + ); + + $indexer = $this->indexerRegistry->get(QueueIndexer::INDEXER_ID); + if (!$indexer->isScheduled()) { + $this->logger->infoWithSource( + 'Not performing full Nosto reindex as the indexer is not scheduled', + ['storeId' => $store->getId()], + $this + ); + } else { + try { + $this->queueIndexer->doIndex($store); + } catch (Exception $e) { + $this->logger->exception($e); + } + } + } + } +} diff --git a/Observer/Cart/Add.php b/Observer/Cart/Add.php new file mode 100644 index 000000000..10453cb84 --- /dev/null +++ b/Observer/Cart/Add.php @@ -0,0 +1,173 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Observer\Cart; + +use Exception; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Module\Manager as ModuleManager; +use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Quote\Model\Quote\Item; +use Nosto\Helper\SerializationHelper; +use Nosto\Model\Event\Cart\Update; +use Nosto\Request\Http\HttpRequest; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Cart\Builder as NostoCartBuilder; +use Nosto\Tagging\Model\Cart\Item\Builder as NostoCartItemBuilder; +use Nosto\Tagging\Model\Customer\Customer as NostoCustomer; + +class Add implements ObserverInterface +{ + private NostoHelperData $nostoHelperData; + private NostoHelperAccount $nostoHelperAccount; + private NostoLogger $logger; + private ModuleManager $moduleManager; + private NostoHelperScope $nostoHelperScope; + private CookieManagerInterface $cookieManager; + private NostoCartItemBuilder $nostoCartItemBuilder; + private NostoCartBuilder $nostoCartBuilder; + private CookieMetadataFactory $cookieMetadataFactory; + public const COOKIE_NAME = 'nosto.itemsAddedToCart'; + + /** + * Constructor. + * + * @param NostoHelperData $nostoHelperData + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope + * @param NostoLogger $logger + * @param ModuleManager $moduleManager + * @param CookieManagerInterface $cookieManager + * @param NostoCartItemBuilder $nostoCartItemBuilder + * @param NostoCartBuilder $nostoCartBuilder + * @param CookieMetadataFactory $cookieMetadataFactory + */ + public function __construct( + NostoHelperData $nostoHelperData, + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, + NostoLogger $logger, + ModuleManager $moduleManager, + CookieManagerInterface $cookieManager, + NostoCartItemBuilder $nostoCartItemBuilder, + NostoCartBuilder $nostoCartBuilder, + CookieMetadataFactory $cookieMetadataFactory + ) { + $this->nostoHelperData = $nostoHelperData; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->logger = $logger; + $this->moduleManager = $moduleManager; + $this->nostoHelperScope = $nostoHelperScope; + $this->cookieManager = $cookieManager; + $this->nostoCartItemBuilder = $nostoCartItemBuilder; + $this->nostoCartBuilder = $nostoCartBuilder; + $this->cookieMetadataFactory = $cookieMetadataFactory; + } + + /** + * Event handler for the "checkout_cart_product_add_after" and event. + * Sends a cart update API call to Nosto. + * + * @param Observer $observer + * @return void + * @suppress PhanDeprecatedFunction + */ + public function execute(Observer $observer) + { + try { + if ($this->moduleManager->isEnabled(NostoHelperData::MODULE_NAME)) { + $nostoAccount = $this->nostoHelperAccount->findAccount( + $this->nostoHelperScope->getStore() + ); + + if (!$nostoAccount || !$nostoAccount->isConnectedToNosto()) { + return; + } + + HttpRequest::buildUserAgent( + 'Magento', + $this->nostoHelperData->getPlatformVersion(), + $this->nostoHelperData->getModuleVersion() + ); + + $nostoCustomerId = $this->cookieManager->getCookie(NostoCustomer::COOKIE_NAME); + if (!$nostoCustomerId) { + $this->logger->info('Cannot find customer id from cookie.'); + return; + } + + /** @noinspection PhpUndefinedMethodInspection */ + $quoteItem = $observer->getQuoteItem(); + if (!$quoteItem instanceof Item) { + $this->logger->info('Cannot find quote item from the event.'); + return; + } + + $store = $this->nostoHelperScope->getStore(); + $cartUpdate = new Update(); + $addedItem = $this->nostoCartItemBuilder->build( + $quoteItem, + $store->getCurrentCurrencyCode() ?: $store->getDefaultCurrencyCode() + ); + $cartUpdate->setAddedItems([$addedItem]); + + if (!headers_sent()) { + //use the cookie way + $metadata = $this->cookieMetadataFactory + ->createPublicCookieMetadata() + ->setDuration(60) + ->setSecure(false) + ->setHttpOnly(false) + ->setPath('/'); + $this->cookieManager->setPublicCookie( + self::COOKIE_NAME, + SerializationHelper::serialize($cartUpdate), + $metadata + ); + } else { + $this->logger->info('Headers sent already. Cannot set the cookie.'); + } + } + } catch (Exception $e) { + $this->logger->exception($e); + } + } +} diff --git a/Observer/Customer/Save.php b/Observer/Customer/Save.php new file mode 100644 index 000000000..4da7cba85 --- /dev/null +++ b/Observer/Customer/Save.php @@ -0,0 +1,83 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Observer\Customer; + +use Magento\Customer\Model\Data\Customer; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Module\Manager as ModuleManager; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Util\Customer as CustomerUtil; + +class Save implements ObserverInterface +{ + private ModuleManager $moduleManger; + + /** + * Save constructor. + * @param ModuleManager $moduleManger + */ + public function __construct( + ModuleManager $moduleManger + ) { + $this->moduleManger = $moduleManger; + } + + /** + * @param Observer $observer + */ + public function execute(Observer $observer) + { + if ($this->moduleManger->isEnabled(NostoHelperData::MODULE_NAME)) { + /** @var Customer $customer */ + /** @noinspection PhpUndefinedMethodInspection */ + $customer = $observer->getCustomer(); + $customerReference = $customer->getCustomAttribute( + NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME + ); + + if ($customerReference === null) { + $customerUtil = new CustomerUtil(); + $customerReference = $customerUtil->generateCustomerReference($customer); + $customer->setData( + NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME, + $customerReference + ); + } + } + } +} diff --git a/Observer/Customer/UpdateMarketingPermission.php b/Observer/Customer/UpdateMarketingPermission.php new file mode 100644 index 000000000..91d75fa74 --- /dev/null +++ b/Observer/Customer/UpdateMarketingPermission.php @@ -0,0 +1,124 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Observer\Customer; + +use Exception; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Module\Manager as ModuleManager; +use Magento\Newsletter\Model\Subscriber; +use Nosto\Operation\MarketingPermission; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Logger\Logger as NostoLogger; + +class UpdateMarketingPermission implements ObserverInterface +{ + private NostoHelperAccount $nostoHelperAccount; + private NostoLogger $logger; + private ModuleManager $moduleManager; + private NostoHelperScope $nostoHelperScope; + + /** + * UpdateMarketingPermission constructor. + * + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoHelperScope $nostoHelperScope + * @param NostoLogger $logger + * @param ModuleManager $moduleManager + */ + public function __construct( + NostoHelperAccount $nostoHelperAccount, + NostoHelperScope $nostoHelperScope, + NostoLogger $logger, + ModuleManager $moduleManager + ) { + $this->nostoHelperAccount = $nostoHelperAccount; + $this->nostoHelperScope = $nostoHelperScope; + $this->logger = $logger; + $this->moduleManager = $moduleManager; + } + + /** + * Event handler for the "newsletter_subscriber_save_commit_after" event. + * Sends a customer update API call to Nosto. + * + * @param Observer $observer + * @return void + * @throws NoSuchEntityException + */ + public function execute(Observer $observer) + { + /** @noinspection PhpUndefinedMethodInspection */ + /** @var Subscriber $subscriber */ + $subscriber = $observer->getEvent()->getSubscriber(); + $currentStore = $this->nostoHelperScope->getStore(); + $stores = $currentStore->getWebsite()->getStores(); + if (!$subscriber instanceof Subscriber + || !$this->moduleManager->isEnabled(NostoHelperData::MODULE_NAME) + || $stores === [] + ) { + return; + } + foreach ($stores as $store) { + $nostoAccount = $this->nostoHelperAccount->findAccount( + $store + ); + if ($nostoAccount === null) { + continue; + } + $operation = new MarketingPermission($nostoAccount); + $isSubscribed = $subscriber->getSubscriberStatus() === Subscriber::STATUS_SUBSCRIBED; + try { + $operation->update( + $subscriber->getSubscriberEmail(), + $isSubscribed + ); + } catch (Exception $e) { + $this->logger->error( + sprintf( + "Failed to update customer marketing permission. + Message was: %s", + $e->getMessage() + ) + ); + } + } + } +} diff --git a/Observer/Order/Save.php b/Observer/Order/Save.php index 0da9c675f..712e32b89 100644 --- a/Observer/Order/Save.php +++ b/Observer/Order/Save.php @@ -1,154 +1,309 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Observer\Order; +use DateTime; +use Exception; +use Magento\Customer\Api\CustomerRepositoryInterface as MagentoCustomerRepository; use Magento\Framework\Event\Observer; -use Magento\Payment\Model\Cart\SalesModel\Order; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\Module\Manager as ModuleManager; -use Nosto\Tagging\Helper\Data as DataHelper; -use Nosto\Tagging\Helper\Account as AccountHelper; use Magento\Framework\Event\ObserverInterface; -use Nosto\Tagging\Model\Order\Builder; -use Psr\Log\LoggerInterface; -use Nosto\Tagging\Model\Order\Builder as OrderBuilder; -use Nosto\Tagging\Model\CustomerFactory; -use Nosto\Tagging\Api\Data\CustomerInterface as NostoCustomer; +use Magento\Framework\Indexer\IndexerInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Module\Manager as ModuleManager; +use Magento\Sales\Model\Order; +use Magento\Store\Model\Store; +use Nosto\Model\Order\Buyer; +use Nosto\Model\Order\Order as NostoOrder; +use Nosto\Operation\Order\OrderCreate as NostoOrderCreate; +use Nosto\Operation\Order\OrderStatus as NostoOrderUpdate; +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\Customer\Customer as NostoCustomer; +use Nosto\Tagging\Model\Customer\Repository as CustomerRepository; +use Nosto\Tagging\Model\Indexer\QueueIndexer as QueueIndexer; +use Nosto\Tagging\Model\Order\Builder as NostoOrderBuilder; +use Nosto\Tagging\Model\Order\Status\Builder as NostoOrderStatusBuilder; +use Nosto\Types\Signup\AccountInterface; -/** - * Class Save - * @package Nosto\Tagging\Observer - */ class Save implements ObserverInterface { - /** - * @var DataHelper - */ - protected $_dataHelper; + private NostoHelperData $nostoHelperData; + private NostoHelperAccount $nostoHelperAccount; + private NostoLogger $logger; + private NostoOrderBuilder $nostoOrderBuilder; + private ModuleManager $moduleManager; + private CustomerRepository $customerRepository; + private IndexerInterface $indexer; + private NostoHelperUrl $nostoHelperUrl; + private MagentoCustomerRepository $magentoCustomerRepository; + private NostoOrderStatusBuilder $orderStatusBuilder; + private static array $sent = []; + private int $intervalForNew; /** - * @var AccountHelper + * Save constructor. + * @param NostoHelperData $nostoHelperData + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoLogger $logger + * @param ModuleManager $moduleManager + * @param CustomerRepository $customerRepository + * @param NostoOrderBuilder $orderBuilder + * @param NostoOrderStatusBuilder $orderStatusBuilder + * @param IndexerRegistry $indexerRegistry + * @param NostoHelperUrl $nostoHelperUrl + * @param MagentoCustomerRepository $magentoCustomerRepository + * @param int $intervalForNew */ - protected $_accountHelper; + public function __construct( + NostoHelperData $nostoHelperData, + NostoHelperAccount $nostoHelperAccount, + NostoLogger $logger, + ModuleManager $moduleManager, + CustomerRepository $customerRepository, + NostoOrderBuilder $orderBuilder, + NostoOrderStatusBuilder $orderStatusBuilder, + IndexerRegistry $indexerRegistry, + NostoHelperUrl $nostoHelperUrl, + MagentoCustomerRepository $magentoCustomerRepository, + int $intervalForNew + ) { + $this->nostoHelperData = $nostoHelperData; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->logger = $logger; + $this->moduleManager = $moduleManager; + $this->nostoOrderBuilder = $orderBuilder; + $this->orderStatusBuilder = $orderStatusBuilder; + $this->customerRepository = $customerRepository; + $this->indexer = $indexerRegistry->get(QueueIndexer::INDEXER_ID); + $this->nostoHelperUrl = $nostoHelperUrl; + $this->magentoCustomerRepository = $magentoCustomerRepository; + $this->intervalForNew = $intervalForNew; + } /** - * @var StoreManagerInterface + * Event handler for the "catalog_product_save_after" and event. + * Sends a product update API call to Nosto. + * + * @param Observer $observer + * @return void + * @suppress PhanDeprecatedFunction + * @suppress PhanTypeMismatchArgument */ - protected $_storeManager; + public function execute(Observer $observer) + { + if ($this->moduleManager->isEnabled(NostoHelperData::MODULE_NAME)) { + HttpRequest::buildUserAgent( + 'Magento', + $this->nostoHelperData->getPlatformVersion(), + $this->nostoHelperData->getModuleVersion() + ); - /** - * @var LoggerInterface - */ - protected $_logger; + /* @var Order $order */ + /** @noinspection PhpUndefinedMethodInspection */ + $order = $observer->getOrder(); + + //Check if order has been sent once + if (in_array($order->getId(), self::$sent)) { + return; + } + $store = $order->getStore(); + $nostoAccount = $this->nostoHelperAccount->findAccount( + $store + ); + if ($nostoAccount !== null) { + //Check if order is new or updated + if ($this->isNewOrder($order)) { + $this->sendNewOrder($order, $nostoAccount, $store); + } else { + $this->sendOrderStatusUpdated($order, $nostoAccount); + } + self::$sent[] = $order->getId(); + } + } + } /** - * @var Builder + * Detects if the order is new (the first time the order is saved) + * + * @param Order $order + * @return bool */ - protected $_orderBuilder; + public function isNewOrder(Order $order) + { + try { + $updated = new DateTime($order->getUpdatedAt()); + $created = new DateTime($order->getCreatedAt()); + $diff = $updated->getTimestamp() - $created->getTimestamp(); + return $order->getState() === Order::STATE_NEW && $diff <= $this->intervalForNew; + } catch (Exception $e) { + $this->logger->exception($e); + return true; + } + } /** - * @var ModuleManager + * Handles the inventory level update to Nosto + * + * @param NostoOrder $nostoOrder */ - protected $_moduleManager; + private function handleInventoryLevelUpdate(NostoOrder $nostoOrder) + { + //update inventory level + if (!$this->indexer->isScheduled() && $this->nostoHelperData->isInventoryTaggingEnabled()) { + $items = $nostoOrder->getPurchasedItems(); + if ($items) { + $productIds = []; + foreach ($items as $item) { + if ($item->getProductId() !== '-1') { + $productIds[] = $item->getProductId(); + } + } + + /** @phan-suppress-next-line PhanDeprecatedFunction */ + $this->indexer->reindexList($productIds); + } + } + } /** - * @var CustomerFactory + * @param Order $order + * @return string|null */ - protected $_customerFactory; + private function getCustomerReference(Order $order) + { + $customerId = $order->getCustomerId(); + $nostoCustomerId = null; + try { + $magentoCustomer = $this->magentoCustomerRepository->getById($customerId); + // Get the value of `customer_reference` + $customerReferenceAttribute = $magentoCustomer->getCustomAttribute( + NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME + ); + if ($customerReferenceAttribute !== null) { + $nostoCustomerId = $customerReferenceAttribute->getValue(); + } + } catch (Exception $e) { + $this->logger->exception($e); + } + return $nostoCustomerId; + } /** - * Constructor. + * Send new order to Nosto * - * @param DataHelper $dataHelper - * @param AccountHelper $accountHelper - * @param StoreManagerInterface $storeManager - * @param LoggerInterface $logger - * @param ModuleManager $moduleManager - * @param CustomerFactory $customerFactory - * @param OrderBuilder $orderBuilder + * @param Order $order + * @param AccountInterface $nostoAccount + * @param Store $store */ - public function __construct( - DataHelper $dataHelper, - AccountHelper $accountHelper, - StoreManagerInterface $storeManager, - LoggerInterface $logger, - ModuleManager $moduleManager, - CustomerFactory $customerFactory, - OrderBuilder $orderBuilder - ) { - $this->_dataHelper = $dataHelper; - $this->_accountHelper = $accountHelper; - $this->_storeManager = $storeManager; - $this->_logger = $logger; - $this->_moduleManager = $moduleManager; - $this->_orderBuilder = $orderBuilder; - $this->_customerFactory = $customerFactory; + private function sendNewOrder(Order $order, AccountInterface $nostoAccount, Store $store) + { + /** @var NostoCustomer $nostoCustomer */ + $nostoCustomer = $this->customerRepository + ->getOneByQuoteId($order->getQuoteId()); + $nostoCustomerId = null; + $nostoCustomerIdentifier = NostoOrderCreate::IDENTIFIER_BY_CID; + if ($nostoCustomer instanceof NostoCustomer) { + $nostoCustomerId = $nostoCustomer->getNostoId(); + } + // If the id is still null, fetch the `customer_reference` + if ($nostoCustomerId === null && + $this->nostoHelperData->isMultiChannelOrderTrackingEnabled($store) + ) { + $nostoCustomerId = $this->getCustomerReference($order); + $nostoCustomerIdentifier = NostoOrderCreate::IDENTIFIER_BY_REF; + } + $nostoOrder = $this->nostoOrderBuilder->build($order); + $nostoOrder->setCustomer(new Buyer()); // Remove customer data from order API calls + if ($nostoCustomerId !== null) { + try { + $orderService = new NostoOrderCreate( + $nostoOrder, + $nostoAccount, + $nostoCustomerIdentifier, + $nostoCustomerId, + $this->nostoHelperUrl->getActiveDomain($store) + ); + $orderService->execute(); + } catch (Exception $e) { + $this->logger->error( + sprintf( + 'Failed to save order with quote #%s for customer #%s. + Message was: %s', + $order->getQuoteId(), + (string)$nostoCustomerId, + $e->getMessage() + ) + ); + } + } else { + $this->logger->warn( + sprintf( + 'Could not resolve Nosto customer id for order #%s', + $order->getQuoteId() + ) + ); + } + $this->handleInventoryLevelUpdate($nostoOrder); } /** - * Event handler for the "catalog_product_save_after" and event. - * Sends a product update API call to Nosto. + * Send updated order status to Nosto * - * @param Observer $observer - * @return void + * @param Order $order + * @param AccountInterface $nostoAccount */ - public function execute(Observer $observer) + private function sendOrderStatusUpdated(Order $order, AccountInterface $nostoAccount) { - if ($this->_moduleManager->isEnabled(DataHelper::MODULE_NAME)) { - /* @var Order $order */ - $order = $observer->getOrder(); - $nostoOrder = $this->_orderBuilder->build($order); - $nostoAccount = $this->_accountHelper->findAccount( - $this->_storeManager->getStore() + try { + $orderStatus = $this->orderStatusBuilder->build($order); + $orderService = new NostoOrderUpdate($nostoAccount, $orderStatus); + $orderService->execute(); + } catch (Exception $e) { + $this->logger->error( + sprintf( + 'Failed to update order with quote #%s. + Message was: %s', + $order->getQuoteId(), + $e->getMessage() + ) ); - if ($nostoAccount !== null) { - $quoteId = $order->getQuoteId(); - $nostoCustomer = $this->_customerFactory - ->create() - ->load($quoteId, NostoCustomer::QUOTE_ID); - - $orderService = new \NostoServiceOrder($nostoAccount); - try { - $orderService->confirm($nostoOrder, - $nostoCustomer->getNostoId()); - - } catch (\Exception $e) { - $this->_logger->error( - sprintf( - "Failed to save order with quote #%s for customer #%s. - Message was: %s", - $quoteId, - $nostoCustomer->getNostoId(), - $e->getMessage() - ) - ); - } - } } } } diff --git a/Observer/Product/Base.php b/Observer/Product/Base.php index 3b91cc21b..57e2a882b 100644 --- a/Observer/Product/Base.php +++ b/Observer/Product/Base.php @@ -1,159 +1,117 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ namespace Nosto\Tagging\Observer\Product; +use Exception; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductRepository; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Indexer\IndexerInterface; +use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Module\Manager as ModuleManager; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use Nosto\Tagging\Helper\Account as AccountHelper; -use Nosto\Tagging\Helper\Data as DataHelper; -use Nosto\Tagging\Model\Product\Builder as ProductBuilder; -use Psr\Log\LoggerInterface; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Model\Indexer\QueueIndexer as QueueIndexer; abstract class Base implements ObserverInterface { - /** - * @var DataHelper - */ - protected $_dataHelper; + /** @var ModuleManager $moduleManager */ + public ModuleManager $moduleManager; - /** - * @var AccountHelper - */ - protected $_accountHelper; + /** @var ProductRepository $productRepository */ + public ProductRepository $productRepository; - /** - * @var ProductBuilder - */ - protected $_productBuilder; + /** @var NostoHelperData $dataHelper */ + public NostoHelperData $dataHelper; - /** - * @var StoreManagerInterface - */ - protected $_storeManager; + /** @var IndexerInterface */ + public IndexerInterface $indexer; - /** - * @var LoggerInterface - */ - protected $_logger; + /** @var QueueIndexer $queueIndexer */ + public QueueIndexer $queueIndexer; /** - * @var ModuleManager - */ - protected $_moduleManager; - - /** - * Constructor. - * - * @param DataHelper $dataHelper - * @param AccountHelper $accountHelper - * @param ProductBuilder $productBuilder - * @param StoreManagerInterface $storeManager - * @param LoggerInterface $logger + * Base constructor. * @param ModuleManager $moduleManager + * @param ProductRepository $productRepository + * @param NostoHelperData $dataHelper + * @param IndexerRegistry $indexerRegistry + * @param QueueIndexer $indexerInvalidate */ public function __construct( - DataHelper $dataHelper, - AccountHelper $accountHelper, - ProductBuilder $productBuilder, - StoreManagerInterface $storeManager, - LoggerInterface $logger, - ModuleManager $moduleManager + ModuleManager $moduleManager, + ProductRepository $productRepository, + NostoHelperData $dataHelper, + IndexerRegistry $indexerRegistry, + QueueIndexer $indexerInvalidate ) { - $this->_dataHelper = $dataHelper; - $this->_accountHelper = $accountHelper; - $this->_productBuilder = $productBuilder; - $this->_storeManager = $storeManager; - $this->_logger = $logger; - $this->_moduleManager = $moduleManager; + $this->moduleManager = $moduleManager; + $this->productRepository = $productRepository; + $this->dataHelper = $dataHelper; + $this->indexer = $indexerRegistry->get(QueueIndexer::INDEXER_ID); + $this->queueIndexer = $indexerInvalidate; } /** - * Event handler for the "catalog_product_save_after" and event. - * Sends a product update API call to Nosto. - * * @param Observer $observer - * @return void + * @throws Exception */ public function execute(Observer $observer) { - if ($this->_moduleManager->isEnabled(DataHelper::MODULE_NAME)) { - // Always "delete" the product for all stores it is available in. - // This is done to avoid data inconsistencies as even if a product - // is edited for only one store, the updated data can reflect in - // other stores as well. - /* @var \Magento\Catalog\Model\Product $product */ - /** @noinspection PhpUndefinedMethodInspection */ - $product = $observer->getProduct(); - foreach ($product->getStoreIds() as $storeId) { - /** @var Store $store */ - $store = $this->_storeManager->getStore($storeId); - /** @var \NostoAccount $account */ - $account = $this->_accountHelper->findAccount($store); - if ($account === null) { - continue; - } + if ($this->moduleManager->isEnabled(NostoHelperData::MODULE_NAME) + && !$this->indexer->isScheduled() + ) { + /* @var Product $product */ + $product = $this->extractProduct($observer); - if (!$this->validateProduct($product)) { - continue; - } - - // Load the product model for this particular store view. - /** @var \NostoProduct $model */ - $metaProduct = $this->_productBuilder->build($product, $store); - if (is_null($metaProduct)) { - continue; - } - - try { - $op = new \NostoServiceProduct($account); - $op->addProduct($metaProduct); - $this->doRequest($op); - } catch (\NostoException $e) { - $this->_logger->error($e, ['exception' => $e]); - } + if ($product instanceof Product && $product->getId()) { + $this->queueIndexer->executeRow($product->getId()); } } } /** - * Validate whether the event should be handled or not - * - * @param Product $product the product from the event - */ - abstract protected function validateProduct(Product $product); - - /** - * @param \NostoServiceProduct $operation + * Default method for extracting product from the observer + * @param Observer $observer * @return mixed */ - abstract protected function doRequest(\NostoServiceProduct $operation); -} \ No newline at end of file + public function extractProduct(Observer $observer) + { + /** @noinspection PhpUndefinedMethodInspection */ + return $observer->getProduct(); + } +} diff --git a/Observer/Product/Delete.php b/Observer/Product/Delete.php deleted file mode 100644 index 2916758b9..000000000 --- a/Observer/Product/Delete.php +++ /dev/null @@ -1,58 +0,0 @@ - - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) - */ - -namespace Nosto\Tagging\Observer\Product; - -use Magento\Catalog\Model\Product; -use Nosto\Tagging\Observer\Product\Base as ProductObserver; - -/** - * Delete event observer model. - * Used to interact with Magento events. - * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - */ -class Delete extends ProductObserver -{ - /** - * @inheritdoc - */ - protected function doRequest(\NostoServiceProduct $operation) - { - $operation->delete(); - } - - /** - * @inheritdoc - */ - protected function validateProduct(Product $product) - { - return true; - } -} \ No newline at end of file diff --git a/Observer/Product/MassProductAttributeUpdate.php b/Observer/Product/MassProductAttributeUpdate.php new file mode 100644 index 000000000..67820f117 --- /dev/null +++ b/Observer/Product/MassProductAttributeUpdate.php @@ -0,0 +1,129 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Observer\Product; + +use Exception; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Store\Model\Store; +use Nosto\Tagging\Helper\Account as NostoHelperAccount; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\ResourceModel\Magento\Product\Collection as ProductCollection; +use Nosto\Tagging\Model\ResourceModel\Magento\Product\CollectionBuilder; +use Nosto\Tagging\Model\Service\Update\QueueService; + +class MassProductAttributeUpdate implements ObserverInterface +{ + + /** @var QueueService */ + private QueueService $queueService; + + /** @var CollectionBuilder */ + private CollectionBuilder $productCollectionBuilder; + + /** @var NostoLogger */ + private NostoLogger $logger; + + /** @var NostoHelperAccount */ + private NostoHelperAccount $nostoHelperAccount; + + /** + * MassProductAttributeUpdate constructor. + * @param QueueService $queueService + * @param CollectionBuilder $productCollectionBuilder + * @param NostoHelperAccount $nostoHelperAccount + * @param NostoLogger $logger + */ + public function __construct( + QueueService $queueService, + CollectionBuilder $productCollectionBuilder, + NostoHelperAccount $nostoHelperAccount, + NostoLogger $logger + ) { + $this->queueService = $queueService; + $this->productCollectionBuilder = $productCollectionBuilder; + $this->nostoHelperAccount = $nostoHelperAccount; + $this->logger = $logger; + } + + /** + * @param Observer $observer + */ + public function execute(Observer $observer) + { + $ids = $observer->getData('product_ids'); + + if (!is_array($ids)) { + $this->logger->debug("Could not add mass updated products to nosto indexer"); + return; + } + + $stores = $this->nostoHelperAccount->getStoresWithNosto(); + foreach ($stores as $store) { + $this->indexProductsPerStore($store, $ids); + } + } + + /** + * @param Store $store + * @param array $ids + */ + private function indexProductsPerStore(Store $store, array $ids) + { + $collection = $this->getCollection($store, $ids); + try { + $this->queueService->addCollectionToUpsertQueue( + $collection, + $store + ); + } catch (Exception $e) { + $this->logger->exception($e); + } + } + + /** + * @param Store $store + * @param array $ids + * @return ProductCollection + */ + private function getCollection(Store $store, array $ids): ProductCollection + { + return $this->productCollectionBuilder->initDefault($store) + ->withIds($ids) + ->build(); + } +} diff --git a/Observer/Product/Review.php b/Observer/Product/Review.php new file mode 100644 index 000000000..5391f4725 --- /dev/null +++ b/Observer/Product/Review.php @@ -0,0 +1,68 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Observer\Product; + +use Magento\Framework\Event\Observer; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Review\Model\Review as ReviewModel; + +/** + * Product update model for Reviews and Ratings + * + * @author Nosto Solutions Ltd + */ +class Review extends Base +{ + /** + * @inheritDoc + * @throws NoSuchEntityException + */ + public function extractProduct(Observer $observer) + { + /* @var ReviewModel $review */ + /** @noinspection PhpUndefinedMethodInspection */ + $review = $observer->getObject(); + $product = null; + if ($review instanceof ReviewModel + && $this->dataHelper->isRatingTaggingEnabled() + ) { + $product = $this->productRepository->getById($review->getEntityPkValue()); + } + + return $product; + } +} diff --git a/Observer/Product/Update.php b/Observer/Product/Update.php deleted file mode 100644 index e59b64aaa..000000000 --- a/Observer/Product/Update.php +++ /dev/null @@ -1,58 +0,0 @@ - - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) - */ - -namespace Nosto\Tagging\Observer\Product; - -use Magento\Catalog\Model\Product; -use Nosto\Tagging\Observer\Product\Base as ProductObserver; - -/** - * Upsert event observer model. - * Used to interact with Magento events. - * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - */ -class Update extends ProductObserver -{ - /** - * @inheritdoc - */ - protected function doRequest(\NostoServiceProduct $operation) - { - $operation->upsert(); - } - - /** - * @inheritdoc - */ - protected function validateProduct(Product $product) - { - return $product->isVisibleInSiteVisibility(); - } -} \ No newline at end of file diff --git a/Observer/Rates/Update.php b/Observer/Rates/Update.php new file mode 100644 index 000000000..5cdebf0a8 --- /dev/null +++ b/Observer/Rates/Update.php @@ -0,0 +1,100 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Observer\Rates; + +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Module\Manager as ModuleManager; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Rates\Service as NostoRatesService; + +/** + * Observer to update the exchange rates for each of the store views if the module is enabled and + * an account exists for the store view. + */ +class Update implements ObserverInterface +{ + private NostoLogger $logger; + private ModuleManager $moduleManager; + private NostoRatesService $nostoRatesService; + private NostoHelperScope $nostoHelperScope; + + /** + * Constructor. + * + * @param NostoLogger $logger + * @param ModuleManager $moduleManager + * @param NostoHelperScope $nostoHelperScope + * @param NostoRatesService $nostoRatesService + */ + public function __construct( + NostoLogger $logger, + ModuleManager $moduleManager, + NostoHelperScope $nostoHelperScope, + NostoRatesService $nostoRatesService + ) { + $this->logger = $logger; + $this->moduleManager = $moduleManager; + $this->nostoRatesService = $nostoRatesService; + $this->nostoHelperScope = $nostoHelperScope; + } + + /** + * Observer method to update the exchange rates for each for the store views by invoking the + * rates management service + * + * @param Observer $observer the dispatched event + */ + public function execute(Observer $observer) // @codingStandardsIgnoreLine + { + if (!$this->moduleManager->isEnabled(NostoHelperData::MODULE_NAME)) { + return; + } + + $this->logger->debug('Updating settings to Nosto for all store views'); + foreach ($this->nostoHelperScope->getStores(false) as $store) { + $this->logger->debug('Updating settings for ' . $store->getName()); + if ($this->nostoRatesService->update($store)) { + $this->logger->debug('Successfully updated the settings for the store view'); + } else { + $this->logger->warning('Unable to update the settings for the store view'); + } + } + } +} diff --git a/Observer/Settings/Update.php b/Observer/Settings/Update.php new file mode 100644 index 000000000..130291627 --- /dev/null +++ b/Observer/Settings/Update.php @@ -0,0 +1,100 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Observer\Settings; + +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Module\Manager as ModuleManager; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Logger\Logger as NostoLogger; +use Nosto\Tagging\Model\Meta\Account\Settings\Service as NostoSettingsService; + +/** + * Observer to update the account settings for each of the store views if the module is enabled and + * an account exists for the store view. + */ +class Update implements ObserverInterface +{ + private NostoLogger $logger; + private ModuleManager $moduleManager; + private NostoSettingsService $nostoSettingsService; + private NostoHelperScope $nostoHelperScope; + + /** + * Constructor. + * + * @param NostoLogger $logger + * @param ModuleManager $moduleManager + * @param NostoHelperScope $nostoHelperScope + * @param NostoSettingsService $nostoSettingsService + */ + public function __construct( + NostoLogger $logger, + ModuleManager $moduleManager, + NostoHelperScope $nostoHelperScope, + NostoSettingsService $nostoSettingsService + ) { + $this->logger = $logger; + $this->moduleManager = $moduleManager; + $this->nostoSettingsService = $nostoSettingsService; + $this->nostoHelperScope = $nostoHelperScope; + } + + /** + * Observer method to update the account settings for each for the store views by invoking the + * settings management service + * + * @param Observer $observer the dispatched event + */ + public function execute(Observer $observer) // @codingStandardsIgnoreLine + { + if (!$this->moduleManager->isEnabled(NostoHelperData::MODULE_NAME)) { + return; + } + + $this->logger->info('Updating settings to Nosto for all store views'); + foreach ($this->nostoHelperScope->getStores(false) as $store) { + $this->logger->info('Updating settings for ' . $store->getName()); + if ($this->nostoSettingsService->update($store)) { + $this->logger->info('Successfully updated the settings for the store view'); + } else { + $this->logger->warning('Unable to update the settings for the store view'); + } + } + } +} diff --git a/Plugin/ProductQueueUpdate.php b/Plugin/ProductQueueUpdate.php new file mode 100644 index 000000000..36b259250 --- /dev/null +++ b/Plugin/ProductQueueUpdate.php @@ -0,0 +1,89 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Plugin; + +use Closure; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Model\AbstractModel; +use Nosto\Tagging\Model\Indexer\QueueProcessorIndexer; +use Nosto\Tagging\Model\ResourceModel\Product\Update\Queue as QueueResource; + +/** + * Plugin for product updates + */ +class ProductQueueUpdate +{ + /** @var IndexerRegistry */ + private IndexerRegistry $indexerRegistry; + + /** @var QueueProcessorIndexer */ + private QueueProcessorIndexer $queueProcessorIndexer; + + /** + * ProductInvalidate constructor. + * @param IndexerRegistry $indexerRegistry + * @param QueueProcessorIndexer $queueProcessorIndexer + */ + public function __construct( + IndexerRegistry $indexerRegistry, + QueueProcessorIndexer $queueProcessorIndexer + ) { + $this->indexerRegistry = $indexerRegistry; + $this->queueProcessorIndexer = $queueProcessorIndexer; + } + + /** + * @param QueueResource $queueResource + * @param Closure $proceed + * @param AbstractModel $queue + * @return mixed + */ + public function aroundSave( + QueueResource $queueResource, + Closure $proceed, + AbstractModel $queue + ) { + $mageIndexer = $this->indexerRegistry->get(QueueProcessorIndexer::INDEXER_ID); + if (!$mageIndexer->isScheduled()) { + $queueResource->addCommitCallback(function () use ($queue) { + $this->queueProcessorIndexer->executeRow($queue->getId()); + }); + } + + return $proceed($queue); + } +} diff --git a/Plugin/ProductUpdate.php b/Plugin/ProductUpdate.php new file mode 100644 index 000000000..b98a965b6 --- /dev/null +++ b/Plugin/ProductUpdate.php @@ -0,0 +1,142 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Plugin; + +use Closure; +use Magento\Catalog\Model\ResourceModel\Product as MagentoResourceProduct; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Model\AbstractModel; +use Nosto\Tagging\Exception\ParentProductDisabledException; +use Nosto\Tagging\Model\Indexer\QueueIndexer; +use Nosto\Tagging\Model\Product\Repository as NostoProductRepository; +use Nosto\Tagging\Logger\Logger as NostoLogger; + +/** + * Plugin for product updates + */ +class ProductUpdate +{ + /** @var IndexerRegistry */ + private IndexerRegistry $indexerRegistry; + + /** @var QueueIndexer */ + private QueueIndexer $queueIndexer; + + /** @var NostoProductRepository */ + private NostoProductRepository $nostoProductRepository; + + /** @var NostoLogger */ + private NostoLogger $logger; + + /** + * ProductUpdate constructor. + * @param IndexerRegistry $indexerRegistry + * @param QueueIndexer $queueIndexer + * @param NostoProductRepository $nostoProductRepository + * @param NostoLogger $logger + */ + public function __construct( + IndexerRegistry $indexerRegistry, + QueueIndexer $queueIndexer, + NostoProductRepository $nostoProductRepository, + NostoLogger $logger + ) { + $this->indexerRegistry = $indexerRegistry; + $this->queueIndexer = $queueIndexer; + $this->nostoProductRepository = $nostoProductRepository; + $this->logger = $logger; + } + + /** + * @param MagentoResourceProduct $productResource + * @param Closure $proceed + * @param AbstractModel $product + * @return mixed + */ + public function aroundSave( + MagentoResourceProduct $productResource, + Closure $proceed, + AbstractModel $product + ) { + $mageIndexer = $this->indexerRegistry->get(QueueIndexer::INDEXER_ID); + if (!$mageIndexer->isScheduled()) { + $productResource->addCommitCallback(function () use ($product) { + $this->queueIndexer->executeRow($product->getId()); + }); + } + + return $proceed($product); + } + + /** + * @param MagentoResourceProduct $productResource + * @param Closure $proceed + * @param AbstractModel $product + * @return mixed + * @suppress PhanTypeMismatchArgument + * @noinspection PhpParamsInspection + */ + public function aroundDelete( + MagentoResourceProduct $productResource, + Closure $proceed, + AbstractModel $product + ) { + $mageIndexer = $this->indexerRegistry->get(QueueIndexer::INDEXER_ID); + if (!$mageIndexer->isScheduled()) { + + try { + $productIds = $this->nostoProductRepository->resolveParentProductIds($product); + } catch (ParentProductDisabledException $e) { + $this->logger->debug($e->getMessage()); + return $proceed($product); + } + + if (empty($productIds)) { + $productResource->addCommitCallback(function () use ($product) { + $this->queueIndexer->executeRow($product->getId()); + }); + } + if (is_array($productIds) && !empty($productIds)) { + $productResource->addCommitCallback(function () use ($productIds) { + $this->queueIndexer->executeList($productIds); + }); + } + } + + return $proceed($product); + } +} diff --git a/Plugin/Sales/OrderRepository.php b/Plugin/Sales/OrderRepository.php new file mode 100644 index 000000000..07e3c15ae --- /dev/null +++ b/Plugin/Sales/OrderRepository.php @@ -0,0 +1,74 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Plugin\Sales; + +use Magento\Framework\Event\ManagerInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; + +class OrderRepository +{ + /** + * @var ManagerInterface + */ + private ManagerInterface $eventManager; + + /** + * OrderRepository constructor. + * @param ManagerInterface $eventManager + */ + public function __construct( + ManagerInterface $eventManager + ) { + $this->eventManager = $eventManager; + } + + /** + * @param OrderRepositoryInterface $subject + * @param OrderInterface|Order $order + * @return OrderInterface + */ + public function afterSave( + /** @noinspection PhpUnusedParameterInspection */ + OrderRepositoryInterface $subject, + OrderInterface $order + ) { + $this->eventManager->dispatch('nosto_sales_save_after', ['order' => $order]); + return $order; + } +} diff --git a/README.md b/README.md index 51cd2e89e..df9fd80e7 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,21 @@ -# Nosto extension for Magento 2 - -## Changelog - -* 1.0.1 - * Add "js stub" for Nosto script - * Fix issue with orders when Nosto module is installed but Nosto account is not connected - -* 1.0.0 - * Make the plug-in compatible with Magento 2.1.0 - -* 1.0.0-RC4 - * Remove variation tagging - -* 1.0.0-RC3 - * Fix store resolving issue(#18) - -* 1.0.0-RC2 - * Fix javascript include issue (#16) - * Fix multi store issue (#15) - -* 1.0.0-RC - * Rename the package to nosto/module-nostotagging - -* 0.2.0 - * Dispatch event after Nosto product is loaded - * Improve exception handling - * Fix acl issues - -* 0.1.1 - * Fix the composer files to autoload Nosto PHP SDK correctly - -* 0.1.0 - * First implementation of Magento 2 extension +# Nosto module for Magento 2 + +Increase your conversion rate and average order value by delivering your +customers personalized product recommendations throughout their shopping +journey. + +Nosto allows you to deliver every customer a personalized shopping experience +through recommendations based on their unique user behavior - increasing +conversion, average order value and customer retention as a result. + +[http://nosto.com](http://nosto.com/) + +## Installing + +The preferred way of installing the extension is via [Composer](https://getcomposer.org/). If you don't have composer installed yet you can get it by following [these instructions](https://getcomposer.org/doc/00-intro.md). It's recommended to install composer globally. You will also need public key and private key from Magento Marketplace or Magento Connect in order to install packages to Magento 2 via Composer. Please follow these instructions to get public key and private key http://devdocs.magento.com/guides/v2.1/install-gde/prereq/connect-auth.html. Once you have composer installed you can install Nosto extension (nosto/module-nostotagging). + +For complete installation instructions please see our [Wiki](https://github.com/Nosto/nosto-magento2/wiki) + +## License + +Open Software License ("OSL") v3.0 \ No newline at end of file diff --git a/Setup/InstallSchema.php b/Setup/InstallSchema.php deleted file mode 100644 index 064be1c5c..000000000 --- a/Setup/InstallSchema.php +++ /dev/null @@ -1,104 +0,0 @@ - - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) - */ - -namespace Nosto\Tagging\Setup; - -use Magento\Framework\DB\Adapter\AdapterInterface; -use Magento\Framework\Setup\InstallSchemaInterface; -use Magento\Framework\Setup\ModuleContextInterface; -use Magento\Framework\Setup\SchemaSetupInterface; -use Magento\Framework\DB\Ddl\Table; -use Nosto\Tagging\Api\Data\CustomerInterface; - -class InstallSchema implements InstallSchemaInterface -{ - /** - * Installs DB schema for Nosto Tagging module - * - * @param SchemaSetupInterface $setup - * @param ModuleContextInterface $context - * @return void - */ - public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) - { - $installer = $setup; - $installer->startSetup(); - $table = $installer->getConnection() - ->newTable($installer->getTable('nosto_tagging_customer')) - ->addColumn( - CustomerInterface::CUSTOMER_ID, - Table::TYPE_INTEGER, - null, - [ - 'identity' => true, - 'nullable' => false, - 'primary' => true, - 'unsigned' => true - ], - 'Customer ID' - ) - ->addColumn( - CustomerInterface::QUOTE_ID, - Table::TYPE_INTEGER, - null, - ['nullable' => false, 'unsigned' => true] - ) - ->addColumn( - CustomerInterface::NOSTO_ID, - Table::TYPE_TEXT, - 255, - ['nullable' => false], - 'Nosto customer ID' - ) - ->addColumn( - CustomerInterface::CREATED_AT, - Table::TYPE_DATETIME, - null, - ['nullable' => false], - 'Creation Time' - ) - ->addColumn( - CustomerInterface::UPDATED_AT, - Table::TYPE_DATETIME, - null, - ['nullable' => false], - 'Updated Time' - ) - ->addIndex( - $installer->getIdxName( - 'nosto_tagging_customer', - ['quote_id', 'nosto_id'] - ), - ['quote_id','nosto_id'], - ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] - ) - ->setComment('Nosto customer and order mapping'); - - $installer->getConnection()->createTable($table); - $installer->endSetup(); - } -} diff --git a/Setup/Patch/Data/AddCustomerReference.php b/Setup/Patch/Data/AddCustomerReference.php new file mode 100644 index 000000000..5fe1e3e29 --- /dev/null +++ b/Setup/Patch/Data/AddCustomerReference.php @@ -0,0 +1,162 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Setup\Patch\Data; + +use Magento\Customer\Model\Customer; +use Magento\Customer\Setup\CustomerSetupFactory; +use Magento\Eav\Model\Entity\Attribute\SetFactory as AttributeSetFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\PatchVersionInterface; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Zend_Validate_Exception; +use Exception; + +class AddCustomerReference implements DataPatchInterface, PatchVersionInterface +{ + private array $customerReferenceForms = ['adminhtml_customer']; + /** @var CustomerSetupFactory */ + private CustomerSetupFactory $customerSetupFactory; + + /** @var AttributeSetFactory */ + private AttributeSetFactory $attributeSetFactory; + + /** @var ModuleDataSetupInterface */ + private ModuleDataSetupInterface $moduleDataSetup; + + /** + * @param CustomerSetupFactory $customerSetupFactory + * @param AttributeSetFactory $attributeSetFactory + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct( + CustomerSetupFactory $customerSetupFactory, + AttributeSetFactory $attributeSetFactory, + ModuleDataSetupInterface $moduleDataSetup + ) { + $this->customerSetupFactory = $customerSetupFactory; + $this->attributeSetFactory = $attributeSetFactory; + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * {@inheritdoc} + */ + public function getAliases(): array + { + return []; + } + + /** + * {@inheritdoc} + */ + public static function getDependencies(): array + { + return []; + } + + /** + * {@inheritdoc} + */ + public function apply() + { + $this->moduleDataSetup->getConnection()->startSetup(); + /** @noinspection PhpUnhandledExceptionInspection */ + $this->addCustomerReference(); + $this->moduleDataSetup->getConnection()->endSetup(); + } + + /** + * @throws LocalizedException + * @throws Zend_Validate_Exception + * @throws Exception + */ + public function addCustomerReference() + { + $customerEavSetup = $this->customerSetupFactory->create(['setup' => $this->moduleDataSetup]); + + $customerEntity = $customerEavSetup->getEavConfig()->getEntityType(Customer::ENTITY); + $attributeSetId = (int)$customerEntity->getDefaultAttributeSetId(); + + $attributeSet = $this->attributeSetFactory->create(); + $attributeGroupId = $attributeSet->getDefaultGroupId($attributeSetId); + + $customerEavSetup->addAttribute( + Customer::ENTITY, + NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME, + [ + 'type' => 'varchar', + 'label' => 'Nosto Customer Reference', + 'input' => 'text', + 'required' => false, + 'sort_order' => 120, + 'position' => 120, + 'visible' => true, + 'user_defined' => true, + 'unique' => true, + 'system' => false, + ] + ); + + $attribute = $customerEavSetup->getEavConfig()->getAttribute( + Customer::ENTITY, + NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME + ); + + /** @noinspection NullPointerExceptionInspection */ + $attribute->addData( + [ + 'attribute_set_id' => $attributeSetId, + 'attribute_group_id' => $attributeGroupId, + 'used_in_forms' => $this->customerReferenceForms, + ] + ); + + /** @noinspection PhpDeprecationInspection */ + /** @noinspection NullPointerExceptionInspection */ + $attribute->save(); + } + + /** + * {@inheritdoc} + */ + public static function getVersion(): string + { + return '6.0.0'; + } +} diff --git a/Setup/Patch/Data/AlterCustomerReferenceNonEditable.php b/Setup/Patch/Data/AlterCustomerReferenceNonEditable.php new file mode 100644 index 000000000..0d6bdc06e --- /dev/null +++ b/Setup/Patch/Data/AlterCustomerReferenceNonEditable.php @@ -0,0 +1,125 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Setup\Patch\Data; + +use Magento\Customer\Model\Customer; +use Magento\Customer\Setup\CustomerSetupFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\PatchVersionInterface; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Exception; + +class AlterCustomerReferenceNonEditable implements DataPatchInterface, PatchVersionInterface +{ + private array $customerReferenceForms = ['adminhtml_customer']; + + /** @var CustomerSetupFactory */ + private CustomerSetupFactory $customerSetupFactory; + + /** @var ModuleDataSetupInterface */ + private ModuleDataSetupInterface $moduleDataSetup; + + /** + * @param CustomerSetupFactory $customerSetupFactory + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct( + CustomerSetupFactory $customerSetupFactory, + ModuleDataSetupInterface $moduleDataSetup + ) { + $this->customerSetupFactory = $customerSetupFactory; + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * {@inheritdoc} + */ + public function getAliases(): array + { + return []; + } + + /** + * {@inheritdoc} + */ + public static function getDependencies(): array + { + return []; + } + + /** + * {@inheritdoc} + */ + public function apply() + { + $this->moduleDataSetup->getConnection()->startSetup(); + /** @noinspection PhpUnhandledExceptionInspection */ + $this->alterCustomerReferenceNonEditable(); + $this->moduleDataSetup->getConnection()->endSetup(); + } + + /** + * Sets the attribute Nosto customer reference to be only editable in admin + * + * @throws LocalizedException + * @throws Exception + */ + public function alterCustomerReferenceNonEditable() + { + $customerSetup = $this->customerSetupFactory->create(['setup' => $this->moduleDataSetup]); + $attribute = $customerSetup->getEavConfig()->getAttribute( + Customer::ENTITY, + NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME + ); + $attribute->addData( + [ + 'used_in_forms' => $this->customerReferenceForms + ] + ); + $attribute->save(); + } + + /** + * {@inheritdoc} + */ + public static function getVersion(): string + { + return '6.0.0'; + } +} diff --git a/Setup/Patch/Data/PopulateCustomerReference.php b/Setup/Patch/Data/PopulateCustomerReference.php new file mode 100644 index 000000000..501dba339 --- /dev/null +++ b/Setup/Patch/Data/PopulateCustomerReference.php @@ -0,0 +1,149 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Setup\Patch\Data; + +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\PatchVersionInterface; +use Nosto\Tagging\Helper\Data as NostoHelperData; +use Nosto\Tagging\Logger\Logger; +use Exception; +use Nosto\Tagging\Util\PagingIterator; +use Nosto\Tagging\Util\Customer as CustomerUtil; + +class PopulateCustomerReference implements DataPatchInterface, PatchVersionInterface +{ + /** @var ModuleDataSetupInterface */ + private ModuleDataSetupInterface $moduleDataSetup; + + /** @var CustomerCollectionFactory */ + private CustomerCollectionFactory $customerCollectionFactory; + + /** @var CustomerResource */ + private CustomerResource $customerResource; + + /** @var Logger */ + private Logger $logger; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param CustomerCollectionFactory $customerCollectionFactory + * @param CustomerResource $customerResource + * @param Logger $logger + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + CustomerCollectionFactory $customerCollectionFactory, + CustomerResource $customerResource, + Logger $logger + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->customerCollectionFactory = $customerCollectionFactory; + $this->customerResource = $customerResource; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public function getAliases(): array + { + return []; + } + + /** + * {@inheritdoc} + */ + public static function getDependencies(): array + { + return []; + } + + /** + * {@inheritdoc} + */ + public function apply() + { + $this->moduleDataSetup->getConnection()->startSetup(); + /** @noinspection PhpUnhandledExceptionInspection */ + $this->populateCustomerReference(); + $this->moduleDataSetup->getConnection()->endSetup(); + } + + /** + * @throws LocalizedException + * @throws Exception + */ + public function populateCustomerReference() + { + $customerCollection = $this->customerCollectionFactory->create() + ->addAttributeToSelect(NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME) + ->setPageSize(1000); + $iterator = new PagingIterator($customerCollection); + /* @var Customer $customer */ + foreach ($iterator as $page) { + foreach ($page as $customer) { + if (!$customer->getData(NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME)) { + $customer->setData( + NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME, + (new CustomerUtil)->generateCustomerReference($customer) + ); + try { + $this->customerResource->saveAttribute( + $customer, + NostoHelperData::NOSTO_CUSTOMER_REFERENCE_ATTRIBUTE_NAME + ); + } catch (Exception $e) { + $this->logger->exception($e); + } + } + } + } + } + + /** + * {@inheritdoc} + */ + public static function getVersion(): string + { + return '6.0.0'; + } +} diff --git a/Test/Unit/Helper/AccountTest.php b/Test/Unit/Helper/AccountTest.php new file mode 100644 index 000000000..5164dae80 --- /dev/null +++ b/Test/Unit/Helper/AccountTest.php @@ -0,0 +1,132 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +declare(strict_types=1); + +namespace Nosto\Tagging\Test\Unit\Helper; + +use Magento\Backend\Model\UrlInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +//use Nosto\Request\Api\Token; +use Nosto\Tagging\Helper\Account; +use Magento\Framework\App\Helper\Context; +use Nosto\Tagging\Helper\Scope as NostoHelperScope; +use Nosto\Tagging\Helper\Url as NostoHelperUrl; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use Nosto\Model\Signup\Account as SignupAccount; +use Magento\Store\Model\Store; +use Magento\Framework\Module\Manager; +use Nosto\Tagging\Logger\Logger; + +class AccountTest extends TestCase +{ + /** @var Context|MockObject */ + protected $contextMock; + + /** @var WriterInterface|MockObject */ + protected $appConfig; + + /** @var NostoHelperScope|MockObject */ + protected $nostoHelperScope; + + /** @var NostoHelperUrl|MockObject */ + protected $nostoHelperUrl; + + /** @var UrlInterface|MockObject */ + protected $urlInterface; + + /** @var Logger */ + protected Logger $loggerMock; + + /** @var MockObject */ + protected MockObject $moduleManagerMock; + + /** @var Account */ + protected Account $account; + + /** + * SetUp test + * + * @return void + */ + protected function setUp(): void + { + $this->contextMock = $this->createMock(Context::class); + $this->appConfig = $this->createMock(WriterInterface::class); + $this->nostoHelperScope = $this->createMock(NostoHelperScope::class); + $this->nostoHelperUrl = $this->createMock(NostoHelperUrl::class); + $this->urlInterface = $this->createMock(UrlInterface::class); + + $this->loggerMock = $this->createMock(Logger::class); + $this->moduleManagerMock = $this->getMockBuilder(Manager::class)->disableOriginalConstructor() + ->getMock(); + $this->contextMock->expects($this->any())->method('getModuleManager')->willReturn($this->moduleManagerMock); + $this->contextMock->expects($this->any())->method('getLogger')->willReturn($this->loggerMock); + + $this->account = new Account($this->contextMock, + $this->appConfig, + $this->nostoHelperScope, + $this->nostoHelperUrl, + $this->urlInterface + ); + } + + /** + * @covers Account::saveAccount() + * @return void + */ + public function testSaveAccount() + { + $account = new SignupAccount('magento-test-account'); + $store = $this->createMock(Store::class); + $store->method('getId')->willReturn(1); +// $tokens[] = new Token(Token::API_SSO, 'ssoToken'); +// $tokens[] = new Token(Token::API_PRODUCTS, 'productsToken'); +// $tokens[] = new Token(Token::API_EXCHANGE_RATES, 'ratesToken'); +// $tokens[] = new Token(Token::API_SETTINGS, 'settingsToken'); +// $tokens[] = new Token(Token::API_EMAIL, 'emailToken'); +// $account->setTokens($tokens); + + $result = $this->account->saveAccount($account, $store); +// $accountName = $store->getConfig(Account::XML_PATH_ACCOUNT); +// $savedTokens = $store->getConfig(Account::XML_PATH_TOKENS); +// $savedDomain = $store->getConfig(Account::XML_PATH_DOMAIN); + + $this->assertTrue($result); + } +} diff --git a/Test/Unit/Util/UrlTest.php b/Test/Unit/Util/UrlTest.php new file mode 100644 index 000000000..5a9fb5335 --- /dev/null +++ b/Test/Unit/Util/UrlTest.php @@ -0,0 +1,64 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +declare(strict_types=1); + +namespace Nosto\Tagging\Test\Unit\Util; + +use Nosto\Tagging\Util\Url; +use PHPUnit\Framework\TestCase; + +class UrlTest extends TestCase +{ + private Url $url; + + public function setUp(): void + { + $this->url = new Url(); + } + + /** + * @covers Url::removePubFromUrl() + * @return void + */ + public function testRemovePubFromUrl() + { + $urlToRemovePub = 'https://magento2.dev.nos.to/media/pub/18263871.jpg'; + $result = $this->url->removePubFromUrl($urlToRemovePub); + $this->assertEquals('https://magento2.dev.nos.to/media/18263871.jpg', $result); + } +} diff --git a/Util/Benchmark.php b/Util/Benchmark.php new file mode 100644 index 000000000..0a255f846 --- /dev/null +++ b/Util/Benchmark.php @@ -0,0 +1,205 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Util; + +use Nosto\NostoException; + +class Benchmark +{ + /** @var array */ + private array $times = []; + /** @var array */ + private array $ticks = []; + /** @var array */ + private array $checkpoints = []; + /** @var array */ + private array $checkpointTimes = []; + /** @var Benchmark|null */ + private static ?Benchmark $instance = null; + + /** + * Prevent instance + */ + private function __construct() + { + // Private + } + + /** + * Returns singleton instance + * + * @return Benchmark + */ + public static function getInstance(): Benchmark + { + if (self::$instance === null) { + self::$instance = new Benchmark(); + } + return self::$instance; + } + + /** + * Start instrumentation + * + * @param string $name + * @param int $checkpoint + */ + public function startInstrumentation(string $name, int $checkpoint) + { + $this->resetTimer($name); + $this->checkpoints[$name] = $checkpoint; + $this->checkpointTimes[$name] = []; + $this->ticks[$name] = 0; + } + + /** + * Reset timer + * + * @param $name + * @return void + */ + public function resetTimer($name) + { + $this->times[$name] = microtime(true); + } + + /** + * Stop instrumentation + * + * @param string $name + */ + public function stopInstrumentation(string $name) + { + $this->checkpointTimes[$name][] = $this->getElapsed($name); + } + + /** + * Return elapsed time for timer + * + * @param string $name + * @return float + */ + public function getElapsed(string $name) + { + if (empty($this->times[$name])) { + return 0; + } + return microtime(true) - $this->times[$name]; + } + + /** + * Calculates one call for given name. When checkpoint is reached the elapsed time + * is stored into the checkpoints array. Returns elapsed time when checkpoint is reached, otherwise + * null. + * + * @param string $name + * @return float|null + */ + public function tick(string $name) + { + ++$this->ticks[$name]; + if ($this->ticks[$name] % $this->checkpoints[$name] === 0) { + $elapsed = $this->getElapsed($name); + $this->checkpointTimes[$name][] = $elapsed; + $this->resetTimer($name); + return $elapsed; + } + + return null; + } + + /** + * Returns recorded times in for a specific measurement name + * + * @param string $name + * @return array + * @throws NostoException + */ + public function getCheckpointTimes(string $name): array + { + if (!isset($this->checkpointTimes[$name])) { + throw new NostoException(sprintf('No breakpoints found for %s', $name)); + } + return $this->checkpointTimes[$name]; + } + + /** + * Returns the amount of ticks for given name + * + * @param string $name + * @return int + * @throws NostoException + */ + public function getTickCount(string $name): int + { + if (!isset($this->ticks[$name])) { + throw new NostoException(sprintf('No ticks defined for %s', $name)); + } + return $this->ticks[$name]; + } + + /** + * Returns the avg time for each tick + * + * @param string $name + * @return float + * @throws NostoException + */ + public function getAvgTickTime(string $name): float + { + if (!isset($this->checkpointTimes[$name])) { + throw new NostoException(sprintf('No breakpoints found for %s', $name)); + } + $ticks = $this->getTickCount($name) > 0 ? $this->getTickCount($name) : 1; + return round($this->getTotalTime($name) / $ticks, 6); + } + + /** + * Returns the total recorded time for given name + * + * @param string $name + * @return float + * @throws NostoException + */ + public function getTotalTime(string $name): float + { + if (!isset($this->checkpointTimes[$name])) { + throw new NostoException(sprintf('No breakpoints found for %s', $name)); + } + return round(array_sum($this->checkpointTimes[$name]), 4); + } +} diff --git a/Util/Customer.php b/Util/Customer.php new file mode 100644 index 000000000..792cdcebd --- /dev/null +++ b/Util/Customer.php @@ -0,0 +1,57 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Util; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Backend\Customer\Interceptor as CustomerInterceptor; + +class Customer +{ + public const CUSTOMER_REFERENCE_HASH_ALGO = 'sha256'; + + /** + * @param CustomerInterface|CustomerInterceptor $customer + * @return string + */ + public function generateCustomerReference($customer) + { + return hash( + self::CUSTOMER_REFERENCE_HASH_ALGO, + $customer->getId() . $customer->getEmail() + ); + } +} diff --git a/Util/PagingIterator.php b/Util/PagingIterator.php new file mode 100644 index 000000000..fd1391b19 --- /dev/null +++ b/Util/PagingIterator.php @@ -0,0 +1,138 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Util; + +use Iterator; +use Magento\Framework\Data\Collection; +use Nosto\NostoException; + +class PagingIterator implements Iterator +{ + private Collection $collection; + + /** @var int */ + private int $currentPageNumber; + + /** @var int */ + private int $lastPageNumber; + + /** + * Iterator constructor. + * @param Collection $collection + * @throws NostoException + */ + public function __construct(Collection $collection) + { + if (!is_numeric($collection->getPageSize())) { + throw new NostoException('Page size not defined or not an integer'); + } + $this->collection = $collection; + $this->lastPageNumber = $this->collection->getLastPageNumber(); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function current() + { + return $this->collection; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function next() + { + ++$this->currentPageNumber; + $this->page($this->currentPageNumber); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function key() + { + return $this->collection->getCurPage(); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function valid(): bool + { + return $this->currentPageNumber <= $this->lastPageNumber; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function rewind() + { + $this->page(1); + } + + /** + * @param int $pageNumber + */ + private function page(int $pageNumber) + { + $this->collection->clear(); + $this->collection->setCurPage($pageNumber); + $this->currentPageNumber = $pageNumber; + } + + /** + * @return int + */ + public function getLastPageNumber(): int + { + return $this->lastPageNumber; + } + + /** + * @return int + */ + public function getCurrentPageNumber(): int + { + return $this->currentPageNumber; + } +} diff --git a/Util/Repository.php b/Util/Repository.php new file mode 100644 index 000000000..82ef719dd --- /dev/null +++ b/Util/Repository.php @@ -0,0 +1,88 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Util; + +use Magento\Framework\Api\Search\SearchResult; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\App\ResourceConnection\SourceProviderInterface; +use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; + +class Repository +{ + /** + * Performs search + * + * @param AbstractCollection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param SearchResult $searchResults + * + * @return SearchResult + */ + public function search( + AbstractCollection $collection, + SearchCriteriaInterface $searchCriteria, + SearchResult $searchResults + ) { + $this->addFiltersToCollection($searchCriteria, $collection); + $collection->load(); + $searchResults->setSearchCriteria($searchCriteria); + $searchResults->setItems($collection->getItems()); + $searchResults->setTotalCount($collection->getSize()); + + return $searchResults; + } + + /** + * Adds filters to given collection + * + * @param SearchCriteriaInterface $searchCriteria + * @param SourceProviderInterface $collection + */ + private function addFiltersToCollection( + SearchCriteriaInterface $searchCriteria, + SourceProviderInterface $collection + ) { + foreach ($searchCriteria->getFilterGroups() as $filterGroup) { + $fields = $conditions = []; + foreach ($filterGroup->getFilters() as $filter) { + $fields[] = $filter->getField(); + $conditions[] = [$filter->getConditionType() => $filter->getValue()]; + } + $collection->addFieldToFilter($fields, $conditions); + } + } +} diff --git a/Util/StringUtil.php b/Util/StringUtil.php new file mode 100644 index 000000000..2ed331cb0 --- /dev/null +++ b/Util/StringUtil.php @@ -0,0 +1,59 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Util; + +class StringUtil +{ + /** + * Strips out all whitespace and line breaks from a given string + * + * @param string $string + * @return string + */ + public function stripWhitespaceAndLinebreaks(string $string): string + { + return preg_replace( + '/[ \t]+/', + ' ', + preg_replace( + '/[\r\n]+/', + "\n", + $string + ) + ); + } +} diff --git a/Util/Url.php b/Util/Url.php new file mode 100644 index 000000000..6d95b7fea --- /dev/null +++ b/Util/Url.php @@ -0,0 +1,57 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +namespace Nosto\Tagging\Util; + +class Url +{ + /** + * Remove the first "/pub/" from a URL + * + * @param string $url + * @return mixed + */ + public function removePubFromUrl(string $url) + { + $path = '/pub/'; + $pos = strpos($url, $path); + if ($pos !== false) { + return substr_replace($url, '/', $pos, strlen($path)); + } + + return $url; + } +} diff --git a/build.xml b/build.xml new file mode 100644 index 000000000..54546cfed --- /dev/null +++ b/build.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compile.sh b/compile.sh new file mode 100755 index 000000000..cafe58e0d --- /dev/null +++ b/compile.sh @@ -0,0 +1,15 @@ +#!/bin/bash -x +BRANCH_NAME=$(git branch --show-current) +PROJECT_NAME=$(cat composer.json| jq --raw-output .name) +APP_DIR=$(pwd) +TMP_DIR=$(mktemp -d -t ci-XXXXXXXXXX) +cd $TMP_DIR || exit 0 +composer create-project magento/community-edition=2.4.4 . +composer config minimum-stability dev +composer config prefer-stable true +composer require --no-update ${PROJECT_NAME}:dev-${BRANCH_NAME} +composer require --no-update magento/module-asynchronous-operations:@stable +composer update --no-dev +bin/magento module:enable --all +bin/magento setup:di:compile +mv -f generated $APP_DIR/vendor/magento/generated diff --git a/composer.json b/composer.json index e939b747b..e6aaf3f1c 100644 --- a/composer.json +++ b/composer.json @@ -1,20 +1,88 @@ { - "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": "1.0.1", - "license": [ - "OSL-3.0" - ], - "minimum-stability": "dev", - "require": { - "php": ">=5.5.0", - "nosto/php-sdk": "2.4.2" - }, - "autoload": { - "psr-4": { - "Nosto\\Tagging\\": "" - }, - "files": [ "registration.php" ] + "name": "wearejh/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": "6.0.0", + "require-dev": { + "phpmd/phpmd": "^2.5", + "sebastian/phpcpd": "*", + "phing/phing": "2.*", + "magento-ecg/coding-standard": "4.5.*", + "magento/module-catalog": "104.0.2", + "magento/module-sales": "103.0.3", + "magento/module-sales-inventory": "100.4.0.*", + "magento/module-sales-rule": "101.2.3", + "magento/module-store": "101.1.3", + "magento/module-configurable-product": "100.4.3", + "magento/module-directory": "100.4.3", + "magento/module-bundle": "101.0.3", + "magento/module-search": "101.1.3", + "magento/module-catalog-search": "102.0.3", + "magento/module-quote": "101.2.3", + "magento/module-review": "100.4.3", + "magento/module-grouped-product": "100.4.3", + "magento/zendframework1": "1.14.3", + "mridang/pmd-annotations": "^0.0.2", + "staabm/annotate-pull-request-from-checkstyle": "^1.1", + "magento/magento-coding-standard": "^5.0", + "magento/module-asynchronous-operations": "100.4.3", + "phan/phan": "5.3.0", + "drenso/phan-extensions": "3.5.1", + "yotpo/module-review": "^2.9", + "phpunit/phpunit": "~9.5.18" + }, + "suggest": { + "magento/product-community-edition": "2.*", + "yotpo/module-review": "^2.9" + }, + "license": [ + "OSL-3.0" + ], + "require": { + "nosto/php-sdk": ">=5.8.1", + "php": ">=7.4.0", + "magento/framework": ">=101.0.6|~102.0", + "ext-json": "*" + }, + "repositories": [ + { + "type": "composer", + "url": "https://repo.magento.com/" } -} \ No newline at end of file + ], + "autoload": { + "psr-4": { + "Nosto\\Tagging\\": "" + }, + "files": [ + "registration.php" + ] + }, + "archive": { + "exclude": [ + "!composer.*", + "Jenkinsfile", + "default.conf", + "Dockerfile", + ".DS_STORE", + ".idea", + ".phan", + ".docker", + "ruleset.xml", + "phan.*", + ".gitignore", + "build.xml", + ".github", + "supervisord.conf", + "entrypoint.sh", + "/magento" + ] + }, + "config": { + "process-timeout":3600 + }, + "scripts": { + "di:compile": "./compile.sh", + "ci:inspect": "./inspect.sh" + } +} diff --git a/composer.lock b/composer.lock index 60883aff0..a11955785 100644 --- a/composer.lock +++ b/composer.lock @@ -1,51 +1,12151 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "hash": "a05e72fb980f0746e54cead050ae07a9", - "content-hash": "2ce53ced5b1bf7bad78a8245a0a1955b", + "content-hash": "7544921f1848c9733c78bb7c13336333", "packages": [ + { + "name": "brick/math", + "version": "0.9.3", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/ca57d18f028f84f777b2168cd1911b0dee2343ae", + "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0", + "vimeo/psalm": "4.9.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "brick", + "math" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.9.3" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/brick/math", + "type": "tidelift" + } + ], + "time": "2021-08-15T20:50:18+00:00" + }, + { + "name": "brick/varexporter", + "version": "0.3.5", + "source": { + "type": "git", + "url": "https://github.com/brick/varexporter.git", + "reference": "05241f28dfcba2b51b11e2d750e296316ebbe518" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/varexporter/zipball/05241f28dfcba2b51b11e2d750e296316ebbe518", + "reference": "05241f28dfcba2b51b11e2d750e296316ebbe518", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.0", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^8.5 || ^9.0", + "vimeo/psalm": "4.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\VarExporter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A powerful alternative to var_export(), which can export closures and objects without __set_state()", + "keywords": [ + "var_export" + ], + "support": { + "issues": "https://github.com/brick/varexporter/issues", + "source": "https://github.com/brick/varexporter/tree/0.3.5" + }, + "time": "2021-02-10T13:53:07+00:00" + }, + { + "name": "colinmollenhour/credis", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/colinmollenhour/credis.git", + "reference": "afec8e58ec93d2291c127fa19709a048f28641e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/colinmollenhour/credis/zipball/afec8e58ec93d2291c127fa19709a048f28641e5", + "reference": "afec8e58ec93d2291c127fa19709a048f28641e5", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "suggest": { + "ext-redis": "Improved performance for communicating with redis" + }, + "type": "library", + "autoload": { + "classmap": [ + "Client.php", + "Cluster.php", + "Sentinel.php", + "Module.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Colin Mollenhour", + "email": "colin@mollenhour.com" + } + ], + "description": "Credis is a lightweight interface to the Redis key-value store which wraps the phpredis library when available for better performance.", + "homepage": "https://github.com/colinmollenhour/credis", + "support": { + "issues": "https://github.com/colinmollenhour/credis/issues", + "source": "https://github.com/colinmollenhour/credis/tree/v1.13.0" + }, + "time": "2022-04-07T14:57:22+00:00" + }, + { + "name": "colinmollenhour/php-redis-session-abstract", + "version": "v1.4.5", + "source": { + "type": "git", + "url": "https://github.com/colinmollenhour/php-redis-session-abstract.git", + "reference": "77ad0c1637ae6ea059f1f8e9fbdac6469242a16d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/colinmollenhour/php-redis-session-abstract/zipball/77ad0c1637ae6ea059f1f8e9fbdac6469242a16d", + "reference": "77ad0c1637ae6ea059f1f8e9fbdac6469242a16d", + "shasum": "" + }, + "require": { + "colinmollenhour/credis": "~1.6", + "php": "^5.5 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-0": { + "Cm\\RedisSession\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin Mollenhour" + } + ], + "description": "A Redis-based session handler with optimistic locking", + "homepage": "https://github.com/colinmollenhour/php-redis-session-abstract", + "support": { + "issues": "https://github.com/colinmollenhour/php-redis-session-abstract/issues", + "source": "https://github.com/colinmollenhour/php-redis-session-abstract/tree/v1.4.5" + }, + "time": "2021-12-01T21:16:01+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b", + "reference": "4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "psr/log": "^1.0", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.3.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-10-28T20:44:15+00:00" + }, + { + "name": "composer/composer", + "version": "2.2.12", + "source": { + "type": "git", + "url": "https://github.com/composer/composer.git", + "reference": "ba61e768b410736efe61df01b61f1ec44f51474f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/composer/zipball/ba61e768b410736efe61df01b61f1ec44f51474f", + "reference": "ba61e768b410736efe61df01b61f1ec44f51474f", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "composer/metadata-minifier": "^1.0", + "composer/pcre": "^1.0", + "composer/semver": "^3.0", + "composer/spdx-licenses": "^1.2", + "composer/xdebug-handler": "^2.0 || ^3.0", + "justinrainbow/json-schema": "^5.2.11", + "php": "^5.3.2 || ^7.0 || ^8.0", + "psr/log": "^1.0 || ^2.0", + "react/promise": "^1.2 || ^2.7", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.0", + "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0", + "symfony/filesystem": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0" + }, + "require-dev": { + "phpspec/prophecy": "^1.10", + "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" + }, + "suggest": { + "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", + "ext-zip": "Enabling the zip extension allows you to unzip archives", + "ext-zlib": "Allow gzip compression of HTTP requests" + }, + "bin": [ + "bin/composer" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\": "src/Composer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "https://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", + "homepage": "https://getcomposer.org/", + "keywords": [ + "autoload", + "dependency", + "package" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/composer/issues", + "source": "https://github.com/composer/composer/tree/2.2.12" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-04-13T14:42:25+00:00" + }, + { + "name": "composer/metadata-minifier", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/metadata-minifier.git", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/c549d23829536f0d0e984aaabbf02af91f443207", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2", + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\MetadataMinifier\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Small utility library that handles metadata minification and expansion.", + "keywords": [ + "composer", + "compression" + ], + "support": { + "issues": "https://github.com/composer/metadata-minifier/issues", + "source": "https://github.com/composer/metadata-minifier/tree/1.0.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-04-07T13:37:33+00:00" + }, + { + "name": "composer/pcre", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/67a32d7d6f9f560b726ab25a061b38ff3a80c560", + "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/1.0.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-01-21T20:24:37+00:00" + }, + { + "name": "composer/semver", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-04-01T19:23:25+00:00" + }, + { + "name": "composer/spdx-licenses", + "version": "1.5.6", + "source": { + "type": "git", + "url": "https://github.com/composer/spdx-licenses.git", + "reference": "a30d487169d799745ca7280bc90fdfa693536901" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/a30d487169d799745ca7280bc90fdfa693536901", + "reference": "a30d487169d799745ca7280bc90fdfa693536901", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Spdx\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "SPDX licenses list and validation library.", + "keywords": [ + "license", + "spdx", + "validator" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/spdx-licenses/issues", + "source": "https://github.com/composer/spdx-licenses/tree/1.5.6" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-11-18T10:14:14+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "9e36aeed4616366d2b690bdce11f71e9178c579a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/9e36aeed4616366d2b690bdce11f71e9178c579a", + "reference": "9e36aeed4616366d2b690bdce11f71e9178c579a", + "shasum": "" + }, + "require": { + "composer/pcre": "^1", + "php": "^5.3.2 || ^7.0 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/2.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-24T20:20:32+00:00" + }, + { + "name": "fgrosse/phpasn1", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/fgrosse/PHPASN1.git", + "reference": "eef488991d53e58e60c9554b09b1201ca5ba9296" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/eef488991d53e58e60c9554b09b1201ca5ba9296", + "reference": "eef488991d53e58e60c9554b09b1201ca5ba9296", + "shasum": "" + }, + "require": { + "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "~2.0", + "phpunit/phpunit": "^6.3 || ^7.0 || ^8.0" + }, + "suggest": { + "ext-bcmath": "BCmath is the fallback extension for big integer calculations", + "ext-curl": "For loading OID information from the web if they have not bee defined statically", + "ext-gmp": "GMP is the preferred extension for big integer calculations", + "phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "FG\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Friedrich Große", + "email": "friedrich.grosse@gmail.com", + "homepage": "https://github.com/FGrosse", + "role": "Author" + }, + { + "name": "All contributors", + "homepage": "https://github.com/FGrosse/PHPASN1/contributors" + } + ], + "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.", + "homepage": "https://github.com/FGrosse/PHPASN1", + "keywords": [ + "DER", + "asn.1", + "asn1", + "ber", + "binary", + "decoding", + "encoding", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/fgrosse/PHPASN1/issues", + "source": "https://github.com/fgrosse/PHPASN1/tree/v2.4.0" + }, + "time": "2021-12-11T12:41:06+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.5.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.6.1", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.17.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/6.5" + }, + "time": "2020-06-16T21:01:06+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2021-10-22T20:56:57+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.8.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "337e3ad8e5716c15f9657bd214d16cc5e69df268" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/337e3ad8e5716c15f9657bd214d16cc5e69df268", + "reference": "337e3ad8e5716c15f9657bd214d16cc5e69df268", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.8.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2022-03-20T21:51:18+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "5.2.12", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "ad87d5a5ca981228e0e205c2bc7dfb8e24559b60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/ad87d5a5ca981228e0e205c2bc7dfb8e24559b60", + "reference": "ad87d5a5ca981228e0e205c2bc7dfb8e24559b60", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/5.2.12" + }, + "time": "2022-04-13T08:02:27+00:00" + }, + { + "name": "laminas/laminas-code", + "version": "3.5.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-code.git", + "reference": "b549b70c0bb6e935d497f84f750c82653326ac77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-code/zipball/b549b70c0bb6e935d497f84f750c82653326ac77", + "reference": "b549b70c0bb6e935d497f84f750c82653326ac77", + "shasum": "" + }, + "require": { + "laminas/laminas-eventmanager": "^3.3", + "laminas/laminas-zendframework-bridge": "^1.1", + "php": "^7.3 || ~8.0.0" + }, + "conflict": { + "phpspec/prophecy": "<1.9.0" + }, + "replace": { + "zendframework/zend-code": "^3.4.1" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4", + "ext-phar": "*", + "laminas/laminas-coding-standard": "^1.0.0", + "laminas/laminas-stdlib": "^3.3.0", + "phpunit/phpunit": "^9.4.2" + }, + "suggest": { + "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", + "laminas/laminas-stdlib": "Laminas\\Stdlib component" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Code\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", + "homepage": "https://laminas.dev", + "keywords": [ + "code", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-code/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-code/issues", + "rss": "https://github.com/laminas/laminas-code/releases.atom", + "source": "https://github.com/laminas/laminas-code" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-11-30T20:16:31+00:00" + }, + { + "name": "laminas/laminas-config", + "version": "3.7.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-config.git", + "reference": "e43d13dcfc273d4392812eb395ce636f73f34dfd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-config/zipball/e43d13dcfc273d4392812eb395ce636f73f34dfd", + "reference": "e43d13dcfc273d4392812eb395ce636f73f34dfd", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laminas/laminas-stdlib": "^3.6", + "php": "^7.3 || ~8.0.0 || ~8.1.0", + "psr/container": "^1.0" + }, + "conflict": { + "container-interop/container-interop": "<1.2.0", + "zendframework/zend-config": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "laminas/laminas-filter": "^2.7.2", + "laminas/laminas-i18n": "^2.10.3", + "laminas/laminas-servicemanager": "^3.7", + "phpunit/phpunit": "^9.5.5" + }, + "suggest": { + "laminas/laminas-filter": "^2.7.2; install if you want to use the Filter processor", + "laminas/laminas-i18n": "^2.7.4; install if you want to use the Translator processor", + "laminas/laminas-servicemanager": "^2.7.8 || ^3.3; if you need an extensible plugin manager for use with the Config Factory" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Config\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "provides a nested object property based user interface for accessing this configuration data within application code", + "homepage": "https://laminas.dev", + "keywords": [ + "config", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-config/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-config/issues", + "rss": "https://github.com/laminas/laminas-config/releases.atom", + "source": "https://github.com/laminas/laminas-config" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-10-01T16:07:46+00:00" + }, + { + "name": "laminas/laminas-crypt", + "version": "3.8.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-crypt.git", + "reference": "0972bb907fd555c16e2a65309b66720acf2b8699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-crypt/zipball/0972bb907fd555c16e2a65309b66720acf2b8699", + "reference": "0972bb907fd555c16e2a65309b66720acf2b8699", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "laminas/laminas-math": "^3.4", + "laminas/laminas-servicemanager": "^3.11.2", + "laminas/laminas-stdlib": "^3.6", + "php": "^7.4 || ~8.0.0 || ~8.1.0", + "psr/container": "^1.1" + }, + "conflict": { + "zendframework/zend-crypt": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.3.0", + "phpunit/phpunit": "^9.5.11" + }, + "suggest": { + "ext-openssl": "Required for most features of Laminas\\Crypt" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Crypt\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Strong cryptography tools and password hashing", + "homepage": "https://laminas.dev", + "keywords": [ + "crypt", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-crypt/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-crypt/issues", + "rss": "https://github.com/laminas/laminas-crypt/releases.atom", + "source": "https://github.com/laminas/laminas-crypt" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-04-12T14:28:29+00:00" + }, + { + "name": "laminas/laminas-escaper", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-escaper.git", + "reference": "5e04bc5ae5990b17159d79d331055e2c645e5cc5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/5e04bc5ae5990b17159d79d331055e2c645e5cc5", + "reference": "5e04bc5ae5990b17159d79d331055e2c645e5cc5", + "shasum": "" + }, + "require": { + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^7.3 || ~8.0.0" + }, + "replace": { + "zendframework/zend-escaper": "^2.6.1" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "phpunit/phpunit": "^9.3", + "psalm/plugin-phpunit": "^0.12.2", + "vimeo/psalm": "^3.16" + }, + "suggest": { + "ext-iconv": "*", + "ext-mbstring": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Escaper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", + "homepage": "https://laminas.dev", + "keywords": [ + "escaper", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-escaper/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-escaper/issues", + "rss": "https://github.com/laminas/laminas-escaper/releases.atom", + "source": "https://github.com/laminas/laminas-escaper" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-11-17T21:26:43+00:00" + }, + { + "name": "laminas/laminas-eventmanager", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-eventmanager.git", + "reference": "41f7209428f37cab9573365e361f4078209aaafa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-eventmanager/zipball/41f7209428f37cab9573365e361f4078209aaafa", + "reference": "41f7209428f37cab9573365e361f4078209aaafa", + "shasum": "" + }, + "require": { + "php": "^7.4 || ~8.0.0 || ~8.1.0" + }, + "conflict": { + "container-interop/container-interop": "<1.2", + "zendframework/zend-eventmanager": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.2.1", + "laminas/laminas-stdlib": "^3.6", + "phpbench/phpbench": "^1.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5.5", + "psr/container": "^1.1.2 || ^2.0.2" + }, + "suggest": { + "laminas/laminas-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature", + "psr/container": "^1.1.2 || ^2.0.2, to use the lazy listeners feature" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\EventManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Trigger and listen to events within a PHP application", + "homepage": "https://laminas.dev", + "keywords": [ + "event", + "eventmanager", + "events", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-eventmanager/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-eventmanager/issues", + "rss": "https://github.com/laminas/laminas-eventmanager/releases.atom", + "source": "https://github.com/laminas/laminas-eventmanager" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-04-06T21:05:17+00:00" + }, + { + "name": "laminas/laminas-http", + "version": "2.14.3", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-http.git", + "reference": "bfaab8093e382274efed7fdc3ceb15f09ba352bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-http/zipball/bfaab8093e382274efed7fdc3ceb15f09ba352bb", + "reference": "bfaab8093e382274efed7fdc3ceb15f09ba352bb", + "shasum": "" + }, + "require": { + "laminas/laminas-loader": "^2.5.1", + "laminas/laminas-stdlib": "^3.2.1", + "laminas/laminas-uri": "^2.5.2", + "laminas/laminas-validator": "^2.10.1", + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^7.3 || ~8.0.0" + }, + "replace": { + "zendframework/zend-http": "^2.11.2" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "laminas/laminas-config": "^3.1 || ^2.6", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "paragonie/certainty": "For automated management of cacert.pem" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Http\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Provides an easy interface for performing Hyper-Text Transfer Protocol (HTTP) requests", + "homepage": "https://laminas.dev", + "keywords": [ + "http", + "http client", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-http/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-http/issues", + "rss": "https://github.com/laminas/laminas-http/releases.atom", + "source": "https://github.com/laminas/laminas-http" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-02-18T21:58:11+00:00" + }, + { + "name": "laminas/laminas-json", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-json.git", + "reference": "9a0ce9f330b7d11e70c4acb44d67e8c4f03f437f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-json/zipball/9a0ce9f330b7d11e70c4acb44d67e8c4f03f437f", + "reference": "9a0ce9f330b7d11e70c4acb44d67e8c4f03f437f", + "shasum": "" + }, + "require": { + "php": "^7.3 || ~8.0.0 || ~8.1.0" + }, + "conflict": { + "zendframework/zend-json": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.2.1", + "laminas/laminas-stdlib": "^2.7.7 || ^3.1", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "laminas/laminas-json-server": "For implementing JSON-RPC servers", + "laminas/laminas-xml2json": "For converting XML documents to JSON" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Json\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "provides convenience methods for serializing native PHP to JSON and decoding JSON to native PHP", + "homepage": "https://laminas.dev", + "keywords": [ + "json", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-json/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-json/issues", + "rss": "https://github.com/laminas/laminas-json/releases.atom", + "source": "https://github.com/laminas/laminas-json" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-09-02T18:02:31+00:00" + }, + { + "name": "laminas/laminas-loader", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-loader.git", + "reference": "d0589ec9dd48365fd95ad10d1c906efd7711c16b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-loader/zipball/d0589ec9dd48365fd95ad10d1c906efd7711c16b", + "reference": "d0589ec9dd48365fd95ad10d1c906efd7711c16b", + "shasum": "" + }, + "require": { + "php": "^7.3 || ~8.0.0 || ~8.1.0" + }, + "conflict": { + "zendframework/zend-loader": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.2.1", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Loader\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Autoloading and plugin loading strategies", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "loader" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-loader/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-loader/issues", + "rss": "https://github.com/laminas/laminas-loader/releases.atom", + "source": "https://github.com/laminas/laminas-loader" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-09-02T18:30:53+00:00" + }, + { + "name": "laminas/laminas-mail", + "version": "2.16.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-mail.git", + "reference": "1ee1a384b96c8af29ecad9b3a7adc27a150ebc49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/1ee1a384b96c8af29ecad9b3a7adc27a150ebc49", + "reference": "1ee1a384b96c8af29ecad9b3a7adc27a150ebc49", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "laminas/laminas-loader": "^2.8", + "laminas/laminas-mime": "^2.9.1", + "laminas/laminas-stdlib": "^3.6", + "laminas/laminas-validator": "^2.15", + "php": "^7.3 || ~8.0.0 || ~8.1.0", + "symfony/polyfill-intl-idn": "^1.24.0", + "symfony/polyfill-mbstring": "^1.12.0", + "webmozart/assert": "^1.10" + }, + "conflict": { + "zendframework/zend-mail": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "laminas/laminas-crypt": "^2.6 || ^3.4", + "laminas/laminas-db": "^2.13.3", + "laminas/laminas-servicemanager": "^3.7", + "phpunit/phpunit": "^9.5.5", + "psalm/plugin-phpunit": "^0.15.1", + "symfony/process": "^5.3.7", + "vimeo/psalm": "^4.7" + }, + "suggest": { + "laminas/laminas-crypt": "Crammd5 support in SMTP Auth", + "laminas/laminas-servicemanager": "^2.7.10 || ^3.3.1 when using SMTP to deliver messages" + }, + "type": "library", + "extra": { + "laminas": { + "component": "Laminas\\Mail", + "config-provider": "Laminas\\Mail\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Mail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Provides generalized functionality to compose and send both text and MIME-compliant multipart e-mail messages", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "mail" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-mail/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-mail/issues", + "rss": "https://github.com/laminas/laminas-mail/releases.atom", + "source": "https://github.com/laminas/laminas-mail" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-02-23T21:08:17+00:00" + }, + { + "name": "laminas/laminas-math", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-math.git", + "reference": "146d8187ab247ae152e811a6704a953d43537381" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-math/zipball/146d8187ab247ae152e811a6704a953d43537381", + "reference": "146d8187ab247ae152e811a6704a953d43537381", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.3 || ~8.0.0 || ~8.1.0" + }, + "conflict": { + "zendframework/zend-math": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "phpunit/phpunit": "^9.5.5" + }, + "suggest": { + "ext-bcmath": "If using the bcmath functionality", + "ext-gmp": "If using the gmp functionality" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev", + "dev-develop": "3.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Create cryptographically secure pseudo-random numbers, and manage big integers", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "math" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-math/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-math/issues", + "rss": "https://github.com/laminas/laminas-math/releases.atom", + "source": "https://github.com/laminas/laminas-math" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-12-06T02:02:07+00:00" + }, + { + "name": "laminas/laminas-mime", + "version": "2.9.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-mime.git", + "reference": "72d21a1b4bb7086d4a4d7058c0abca180b209184" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-mime/zipball/72d21a1b4bb7086d4a4d7058c0abca180b209184", + "reference": "72d21a1b4bb7086d4a4d7058c0abca180b209184", + "shasum": "" + }, + "require": { + "laminas/laminas-stdlib": "^2.7 || ^3.0", + "php": "^7.3 || ~8.0.0 || ~8.1.0" + }, + "conflict": { + "zendframework/zend-mime": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.2.1", + "laminas/laminas-mail": "^2.12", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "laminas/laminas-mail": "Laminas\\Mail component" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Mime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Create and parse MIME messages and parts", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "mime" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-mime/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-mime/issues", + "rss": "https://github.com/laminas/laminas-mime/releases.atom", + "source": "https://github.com/laminas/laminas-mime" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-09-20T21:19:24+00:00" + }, + { + "name": "laminas/laminas-modulemanager", + "version": "2.11.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-modulemanager.git", + "reference": "6acf5991d10b0b38a2edb08729ed48981b2a5dad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-modulemanager/zipball/6acf5991d10b0b38a2edb08729ed48981b2a5dad", + "reference": "6acf5991d10b0b38a2edb08729ed48981b2a5dad", + "shasum": "" + }, + "require": { + "brick/varexporter": "^0.3.2", + "laminas/laminas-config": "^3.7", + "laminas/laminas-eventmanager": "^3.4", + "laminas/laminas-stdlib": "^3.6", + "php": "^7.3 || ~8.0.0 || ~8.1.0", + "webimpress/safe-writer": "^1.0.2 || ^2.1" + }, + "conflict": { + "zendframework/zend-modulemanager": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "^2.3", + "laminas/laminas-loader": "^2.8", + "laminas/laminas-mvc": "^3.1.1", + "laminas/laminas-servicemanager": "^3.7", + "phpunit/phpunit": "^9.5.5" + }, + "suggest": { + "laminas/laminas-console": "Laminas\\Console component", + "laminas/laminas-loader": "Laminas\\Loader component if you are not using Composer autoloading for your modules", + "laminas/laminas-mvc": "Laminas\\Mvc component", + "laminas/laminas-servicemanager": "Laminas\\ServiceManager component" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\ModuleManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Modular application system for laminas-mvc applications", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "modulemanager" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-modulemanager/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-modulemanager/issues", + "rss": "https://github.com/laminas/laminas-modulemanager/releases.atom", + "source": "https://github.com/laminas/laminas-modulemanager" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-10-13T17:05:17+00:00" + }, + { + "name": "laminas/laminas-mvc", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-mvc.git", + "reference": "88da7200cf8f5a970c35d91717a5c4db94981e5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-mvc/zipball/88da7200cf8f5a970c35d91717a5c4db94981e5e", + "reference": "88da7200cf8f5a970c35d91717a5c4db94981e5e", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "^1.2", + "laminas/laminas-eventmanager": "^3.2", + "laminas/laminas-http": "^2.7", + "laminas/laminas-modulemanager": "^2.8", + "laminas/laminas-router": "^3.0.2", + "laminas/laminas-servicemanager": "^3.3", + "laminas/laminas-stdlib": "^3.2.1", + "laminas/laminas-view": "^2.11.3", + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^7.3 || ~8.0.0" + }, + "replace": { + "zendframework/zend-mvc": "^3.1.1" + }, + "require-dev": { + "http-interop/http-middleware": "^0.4.1", + "laminas/laminas-coding-standard": "^1.0.0", + "laminas/laminas-json": "^2.6.1 || ^3.0", + "laminas/laminas-psr7bridge": "^1.0", + "laminas/laminas-stratigility": ">=2.0.1 <2.2", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.4.2" + }, + "suggest": { + "laminas/laminas-json": "(^2.6.1 || ^3.0) To auto-deserialize JSON body content in AbstractRestfulController extensions, when json_decode is unavailable", + "laminas/laminas-log": "^2.9.1 To provide log functionality via LogFilterManager, LogFormatterManager, and LogProcessorManager", + "laminas/laminas-mvc-console": "laminas-mvc-console provides the ability to expose laminas-mvc as a console application", + "laminas/laminas-mvc-i18n": "laminas-mvc-i18n provides integration with laminas-i18n, including a translation bridge and translatable route segments", + "laminas/laminas-mvc-middleware": "To dispatch middleware in your laminas-mvc application", + "laminas/laminas-mvc-plugin-fileprg": "To provide Post/Redirect/Get functionality around forms that container file uploads", + "laminas/laminas-mvc-plugin-flashmessenger": "To provide flash messaging capabilities between requests", + "laminas/laminas-mvc-plugin-identity": "To access the authenticated identity (per laminas-authentication) in controllers", + "laminas/laminas-mvc-plugin-prg": "To provide Post/Redirect/Get functionality within controllers", + "laminas/laminas-paginator": "^2.7 To provide pagination functionality via PaginatorPluginManager", + "laminas/laminas-servicemanager-di": "laminas-servicemanager-di provides utilities for integrating laminas-di and laminas-servicemanager in your laminas-mvc application" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Mvc\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Laminas's event-driven MVC layer, including MVC Applications, Controllers, and Plugins", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "mvc" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-mvc/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-mvc/issues", + "rss": "https://github.com/laminas/laminas-mvc/releases.atom", + "source": "https://github.com/laminas/laminas-mvc" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-12-14T21:54:40+00:00" + }, + { + "name": "laminas/laminas-router", + "version": "3.4.5", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-router.git", + "reference": "aaf2eb364eedeb5c4d5b9ee14cd2938d0f7e89b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-router/zipball/aaf2eb364eedeb5c4d5b9ee14cd2938d0f7e89b7", + "reference": "aaf2eb364eedeb5c4d5b9ee14cd2938d0f7e89b7", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "^1.2", + "laminas/laminas-http": "^2.8.1", + "laminas/laminas-servicemanager": "^2.7.8 || ^3.3", + "laminas/laminas-stdlib": "^3.3", + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^7.3 || ~8.0.0" + }, + "replace": { + "zendframework/zend-router": "^3.3.0" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "laminas/laminas-i18n": "^2.7.4", + "phpunit/phpunit": "^9.4" + }, + "suggest": { + "laminas/laminas-i18n": "^2.7.4, if defining translatable HTTP path segments" + }, + "type": "library", + "extra": { + "laminas": { + "component": "Laminas\\Router", + "config-provider": "Laminas\\Router\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Router\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Flexible routing system for HTTP and console applications", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "routing" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-router/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-router/issues", + "rss": "https://github.com/laminas/laminas-router/releases.atom", + "source": "https://github.com/laminas/laminas-router" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-04-19T16:06:00+00:00" + }, + { + "name": "laminas/laminas-servicemanager", + "version": "3.11.2", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-servicemanager.git", + "reference": "8a1f4d53ec93b2e18174f6f186922ef44d11a75a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/8a1f4d53ec93b2e18174f6f186922ef44d11a75a", + "reference": "8a1f4d53ec93b2e18174f6f186922ef44d11a75a", + "shasum": "" + }, + "require": { + "laminas/laminas-stdlib": "^3.2.1", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0", + "psr/container": "^1.0" + }, + "conflict": { + "ext-psr": "*", + "laminas/laminas-code": "<3.3.1", + "zendframework/zend-code": "<3.3.1", + "zendframework/zend-servicemanager": "*" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "replace": { + "container-interop/container-interop": "^1.2.0" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.0", + "laminas/laminas-coding-standard": "~2.3.0", + "laminas/laminas-container-config-test": "^0.6", + "laminas/laminas-dependency-plugin": "^2.1.2", + "mikey179/vfsstream": "^1.6.10@alpha", + "ocramius/proxy-manager": "^2.11", + "phpbench/phpbench": "^1.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5.5", + "psalm/plugin-phpunit": "^0.16.1", + "vimeo/psalm": "^4.8" + }, + "suggest": { + "ocramius/proxy-manager": "ProxyManager ^2.1.1 to handle lazy initialization of services" + }, + "bin": [ + "bin/generate-deps-for-config-factory", + "bin/generate-factory-for-class" + ], + "type": "library", + "autoload": { + "files": [ + "src/autoload.php" + ], + "psr-4": { + "Laminas\\ServiceManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Factory-Driven Dependency Injection Container", + "homepage": "https://laminas.dev", + "keywords": [ + "PSR-11", + "dependency-injection", + "di", + "dic", + "laminas", + "service-manager", + "servicemanager" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-servicemanager/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-servicemanager/issues", + "rss": "https://github.com/laminas/laminas-servicemanager/releases.atom", + "source": "https://github.com/laminas/laminas-servicemanager" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-04-07T17:21:25+00:00" + }, + { + "name": "laminas/laminas-stdlib", + "version": "3.7.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-stdlib.git", + "reference": "bcd869e2fe88d567800057c1434f2380354fe325" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/bcd869e2fe88d567800057c1434f2380354fe325", + "reference": "bcd869e2fe88d567800057c1434f2380354fe325", + "shasum": "" + }, + "require": { + "php": "^7.3 || ~8.0.0 || ~8.1.0" + }, + "conflict": { + "zendframework/zend-stdlib": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.3.0", + "phpbench/phpbench": "^1.0", + "phpunit/phpunit": "^9.3.7", + "psalm/plugin-phpunit": "^0.16.0", + "vimeo/psalm": "^4.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Stdlib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "SPL extensions, array utilities, error handlers, and more", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "stdlib" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-stdlib/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-stdlib/issues", + "rss": "https://github.com/laminas/laminas-stdlib/releases.atom", + "source": "https://github.com/laminas/laminas-stdlib" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-01-21T15:50:46+00:00" + }, + { + "name": "laminas/laminas-uri", + "version": "2.8.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-uri.git", + "reference": "79bd4c614c8cf9a6ba715a49fca8061e84933d87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-uri/zipball/79bd4c614c8cf9a6ba715a49fca8061e84933d87", + "reference": "79bd4c614c8cf9a6ba715a49fca8061e84933d87", + "shasum": "" + }, + "require": { + "laminas/laminas-escaper": "^2.5", + "laminas/laminas-validator": "^2.10", + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^7.3 || ~8.0.0" + }, + "replace": { + "zendframework/zend-uri": "^2.7.1" + }, + "require-dev": { + "laminas/laminas-coding-standard": "^2.1", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Uri\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "A component that aids in manipulating and validating » Uniform Resource Identifiers (URIs)", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "uri" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-uri/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-uri/issues", + "rss": "https://github.com/laminas/laminas-uri/releases.atom", + "source": "https://github.com/laminas/laminas-uri" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-02-17T21:53:05+00:00" + }, + { + "name": "laminas/laminas-validator", + "version": "2.17.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-validator.git", + "reference": "bdd503adc83d814a5c94e598ea0eb9fc7ca56339" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-validator/zipball/bdd503adc83d814a5c94e598ea0eb9fc7ca56339", + "reference": "bdd503adc83d814a5c94e598ea0eb9fc7ca56339", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "^1.1", + "laminas/laminas-stdlib": "^3.6", + "php": "^7.3 || ~8.0.0 || ~8.1.0" + }, + "conflict": { + "zendframework/zend-validator": "*" + }, + "require-dev": { + "laminas/laminas-cache": "^2.6.1", + "laminas/laminas-coding-standard": "~2.2.1", + "laminas/laminas-db": "^2.7", + "laminas/laminas-filter": "^2.6", + "laminas/laminas-http": "^2.14.2", + "laminas/laminas-i18n": "^2.6", + "laminas/laminas-math": "^2.6", + "laminas/laminas-servicemanager": "^2.7.11 || ^3.0.3", + "laminas/laminas-session": "^2.8", + "laminas/laminas-uri": "^2.7", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5.5", + "psalm/plugin-phpunit": "^0.15.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "vimeo/psalm": "^4.3" + }, + "suggest": { + "laminas/laminas-db": "Laminas\\Db component, required by the (No)RecordExists validator", + "laminas/laminas-filter": "Laminas\\Filter component, required by the Digits validator", + "laminas/laminas-i18n": "Laminas\\I18n component to allow translation of validation error messages", + "laminas/laminas-i18n-resources": "Translations of validator messages", + "laminas/laminas-math": "Laminas\\Math component, required by the Csrf validator", + "laminas/laminas-servicemanager": "Laminas\\ServiceManager component to allow using the ValidatorPluginManager and validator chains", + "laminas/laminas-session": "Laminas\\Session component, ^2.8; required by the Csrf validator", + "laminas/laminas-uri": "Laminas\\Uri component, required by the Uri and Sitemap\\Loc validators", + "psr/http-message": "psr/http-message, required when validating PSR-7 UploadedFileInterface instances via the Upload and UploadFile validators" + }, + "type": "library", + "extra": { + "laminas": { + "component": "Laminas\\Validator", + "config-provider": "Laminas\\Validator\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Validator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Validation classes for a wide range of domains, and the ability to chain validators to create complex validation criteria", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "validator" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-validator/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-validator/issues", + "rss": "https://github.com/laminas/laminas-validator/releases.atom", + "source": "https://github.com/laminas/laminas-validator" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-03-08T18:16:51+00:00" + }, + { + "name": "laminas/laminas-view", + "version": "2.20.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-view.git", + "reference": "2cd6973a3e042be3d244260fe93f435668f5c2b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-view/zipball/2cd6973a3e042be3d244260fe93f435668f5c2b4", + "reference": "2cd6973a3e042be3d244260fe93f435668f5c2b4", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "^1.2", + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "laminas/laminas-escaper": "^2.5", + "laminas/laminas-eventmanager": "^3.4", + "laminas/laminas-json": "^3.3", + "laminas/laminas-servicemanager": "^3.10", + "laminas/laminas-stdlib": "^3.6", + "php": "^7.4 || ~8.0.0 || ~8.1.0", + "psr/container": "^1 || ^2" + }, + "conflict": { + "container-interop/container-interop": "<1.2", + "laminas/laminas-router": "<3.0.1", + "laminas/laminas-servicemanager": "<3.3", + "laminas/laminas-session": "<2.12", + "zendframework/zend-view": "*" + }, + "require-dev": { + "laminas/laminas-authentication": "^2.5", + "laminas/laminas-coding-standard": "~2.3.0", + "laminas/laminas-console": "^2.6", + "laminas/laminas-feed": "^2.15", + "laminas/laminas-filter": "^2.13.0", + "laminas/laminas-http": "^2.15", + "laminas/laminas-i18n": "^2.6", + "laminas/laminas-modulemanager": "^2.7.1", + "laminas/laminas-mvc": "^3.0", + "laminas/laminas-mvc-i18n": "^1.1", + "laminas/laminas-mvc-plugin-flashmessenger": "^1.5.0", + "laminas/laminas-navigation": "^2.13.1", + "laminas/laminas-paginator": "^2.11.0", + "laminas/laminas-permissions-acl": "^2.6", + "laminas/laminas-router": "^3.0.1", + "laminas/laminas-uri": "^2.5", + "phpspec/prophecy": "^1.12", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5.5", + "psalm/plugin-phpunit": "^0.16.1", + "vimeo/psalm": "^4.10" + }, + "suggest": { + "laminas/laminas-authentication": "Laminas\\Authentication component", + "laminas/laminas-escaper": "Laminas\\Escaper component", + "laminas/laminas-feed": "Laminas\\Feed component", + "laminas/laminas-filter": "Laminas\\Filter component", + "laminas/laminas-http": "Laminas\\Http component", + "laminas/laminas-i18n": "Laminas\\I18n component", + "laminas/laminas-mvc": "Laminas\\Mvc component", + "laminas/laminas-mvc-plugin-flashmessenger": "laminas-mvc-plugin-flashmessenger component, if you want to use the FlashMessenger view helper with laminas-mvc versions 3 and up", + "laminas/laminas-navigation": "Laminas\\Navigation component", + "laminas/laminas-paginator": "Laminas\\Paginator component", + "laminas/laminas-permissions-acl": "Laminas\\Permissions\\Acl component", + "laminas/laminas-servicemanager": "Laminas\\ServiceManager component", + "laminas/laminas-uri": "Laminas\\Uri component" + }, + "bin": [ + "bin/templatemap_generator.php" + ], + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\View\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Flexible view layer supporting and providing multiple view layers, helpers, and more", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "view" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-view/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-view/issues", + "rss": "https://github.com/laminas/laminas-view/releases.atom", + "source": "https://github.com/laminas/laminas-view" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-02-22T13:52:44+00:00" + }, + { + "name": "laminas/laminas-zendframework-bridge", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-zendframework-bridge.git", + "reference": "7f049390b756d34ba5940a8fb47634fbb51f79ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/7f049390b756d34ba5940a8fb47634fbb51f79ab", + "reference": "7f049390b756d34ba5940a8fb47634fbb51f79ab", + "shasum": "" + }, + "require": { + "php": ">=7.4, <8.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5.14", + "psalm/plugin-phpunit": "^0.15.2", + "squizlabs/php_codesniffer": "^3.6.2", + "vimeo/psalm": "^4.21.0" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\ZendFrameworkBridge" + } + }, + "autoload": { + "files": [ + "src/autoload.php" + ], + "psr-4": { + "Laminas\\ZendFrameworkBridge\\": "src//" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Alias legacy ZF class names to Laminas Project equivalents.", + "keywords": [ + "ZendFramework", + "autoloading", + "laminas", + "zf" + ], + "support": { + "forum": "https://discourse.laminas.dev/", + "issues": "https://github.com/laminas/laminas-zendframework-bridge/issues", + "rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom", + "source": "https://github.com/laminas/laminas-zendframework-bridge" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-02-22T22:17:01+00:00" + }, + { + "name": "magento/framework", + "version": "103.0.3-p2", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/framework/magento-framework-103.0.3.0-patch2.zip", + "shasum": "415322da8c479e67ee0b6dc4e7199d71839a765e" + }, + "require": { + "colinmollenhour/php-redis-session-abstract": "~1.4.0", + "composer/composer": "^1.9 || ^2.0", + "ext-bcmath": "*", + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-hash": "*", + "ext-iconv": "*", + "ext-intl": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "ext-xsl": "*", + "guzzlehttp/guzzle": "^6.3.3", + "laminas/laminas-code": "^3.5.1", + "laminas/laminas-crypt": "^3.4.0", + "laminas/laminas-escaper": "2.7.0", + "laminas/laminas-http": "^2.6.0", + "laminas/laminas-mail": "^2.9.0", + "laminas/laminas-mime": "^2.8.0", + "laminas/laminas-mvc": "^3.2.0", + "laminas/laminas-stdlib": "^3.2.1", + "laminas/laminas-uri": "^2.5.1", + "laminas/laminas-validator": "^2.6.0", + "lib-libxml": "*", + "magento/zendframework1": "~1.14.2", + "monolog/monolog": "^1.17", + "php": "~7.3.0||~7.4.0", + "ramsey/uuid": "~4.1.0", + "symfony/console": "~4.4.0", + "symfony/process": "~4.4.0", + "tedivm/jshrink": "~1.4.0", + "web-token/jwt-framework": "^v2.2.7", + "wikimedia/less.php": "^3.0.0" + }, + "suggest": { + "ext-imagick": "Use Image Magick >=3.0.0 as an optional alternative image processing library" + }, + "type": "magento2-library", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Framework\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/zendframework1", + "version": "1.14.3", + "source": { + "type": "git", + "url": "https://github.com/magento/zf1.git", + "reference": "726855dfb080089dc7bc7b016624129f8e7bc4e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/magento/zf1/zipball/726855dfb080089dc7bc7b016624129f8e7bc4e5", + "reference": "726855dfb080089dc7bc7b016624129f8e7bc4e5", + "shasum": "" + }, + "require": { + "php": ">=5.2.11" + }, + "require-dev": { + "phpunit/dbunit": "1.3.*", + "phpunit/phpunit": "3.7.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.12.x-dev" + } + }, + "autoload": { + "psr-0": { + "Zend_": "library/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "library/" + ], + "license": [ + "BSD-3-Clause" + ], + "description": "Magento Zend Framework 1", + "homepage": "http://framework.zend.com/", + "keywords": [ + "ZF1", + "framework" + ], + "support": { + "issues": "https://github.com/magento/zf1/issues", + "source": "https://github.com/magento/zf1/tree/1.14.3" + }, + "time": "2019-11-26T15:09:40+00:00" + }, + { + "name": "monolog/monolog", + "version": "1.27.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "52ebd235c1f7e0d5e1b16464b695a28335f8e44a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/52ebd235c1f7e0d5e1b16464b695a28335f8e44a", + "reference": "52ebd235c1f7e0d5e1b16464b695a28335f8e44a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpstan/phpstan": "^0.12.59", + "phpunit/phpunit": "~4.5", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/1.27.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2022-03-13T20:29:46+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.13.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "210577fe3cf7badcc5814d99455df46564f3c077" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", + "reference": "210577fe3cf7badcc5814d99455df46564f3c077", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" + }, + "time": "2021-11-30T19:35:32+00:00" + }, { "name": "nosto/php-sdk", - "version": "2.4.1", + "version": "5.8.1", + "source": { + "type": "git", + "url": "https://github.com/Nosto/nosto-php-sdk.git", + "reference": "7312c672c892b3d40474b42f01e654f4246b4118" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nosto/nosto-php-sdk/zipball/7312c672c892b3d40474b42f01e654f4246b4118", + "reference": "7312c672c892b3d40474b42f01e654f4246b4118", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "php": ">=5.5", + "phpseclib/phpseclib": "~3.0.9", + "vlucas/phpdotenv": "^3.6" + }, + "require-dev": { + "codeception/c3": "^2.6", + "codeception/codeception": "4.1.*", + "codeception/module-asserts": "1.3.*", + "codeception/specify": "^0.4.6", + "mridang/pmd-annotations": "^0.0.2", + "phan/phan": "2.6", + "phing/phing": "2.*", + "phpmd/phpmd": "^2.6", + "sebastian/phpcpd": "^3.0", + "squizlabs/php_codesniffer": "^2.6", + "staabm/annotate-pull-request-from-checkstyle": "^1.1", + "symfony/console": "3.*", + "wimg/php-compatibility": "^9.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/bootstrap.php" + ], + "psr-4": { + "Nosto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "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/5.8.1" + }, + "time": "2022-05-02T14:48:49+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "9229e15f2e6ba772f0c55dd6986c563b937170a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/9229e15f2e6ba772f0c55dd6986c563b937170a8", + "reference": "9229e15f2e6ba772f0c55dd6986c563b937170a8", + "shasum": "" + }, + "require": { + "php": "^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^1|^2|^3|^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2022-01-17T05:32:27+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.8.1", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "eab7a0df01fe2344d172bff4cd6dbd3f8b84ad15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/eab7a0df01fe2344d172bff4cd6dbd3f8b84ad15", + "reference": "eab7a0df01fe2344d172bff4cd6dbd3f8b84ad15", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "phpunit/phpunit": "^6.5.14 || ^7.5.20 || ^8.5.19 || ^9.5.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.8.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2021-12-04T23:24:31+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.14", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "2f0b7af658cbea265cbb4a791d6c29a6613f98ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2f0b7af658cbea265cbb4a791d6c29a6613f98ef", + "reference": "2f0b7af658cbea265cbb4a791d6c29a6613f98ef", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.14" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2022-04-04T05:15:45+00:00" + }, + { + "name": "psr/container", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.2" + }, + "time": "2021-11-05T16:50:12+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/cccc74ee5e328031b15640b51056ee8d3bb66c0a", + "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8", + "symfony/polyfill-php81": "^1.23" + }, + "require-dev": { + "captainhook/captainhook": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "ergebnis/composer-normalize": "^2.6", + "fakerphp/faker": "^1.5", + "hamcrest/hamcrest-php": "^2", + "jangregor/phpstan-prophecy": "^0.8", + "mockery/mockery": "^1.3", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^0.12.32", + "phpstan/phpstan-mockery": "^0.12.5", + "phpstan/phpstan-phpunit": "^0.12.11", + "phpunit/phpunit": "^8.5 || ^9", + "psy/psysh": "^0.10.4", + "slevomat/coding-standard": "^6.3", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/1.2.2" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2021-10-10T03:01:02+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.1.3", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "2df6bbdf0133247bfa0063ccbcf59185a243f52d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/2df6bbdf0133247bfa0063ccbcf59185a243f52d", + "reference": "2df6bbdf0133247bfa0063ccbcf59185a243f52d", + "shasum": "" + }, + "require": { + "brick/math": "^0.8 || ^0.9", + "ext-json": "*", + "php": "^7.2 || ^8.0", + "ramsey/collection": "^1.0", + "symfony/polyfill-ctype": "^1.8" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "codeception/aspect-mock": "^3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7.0", + "doctrine/annotations": "^1.8", + "goaop/framework": "^2", + "mockery/mockery": "^1.3", + "moontoast/math": "^1.1", + "paragonie/random-lib": "^2", + "php-mock/php-mock-mockery": "^1.3", + "php-mock/php-mock-phpunit": "^2.5", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^0.17.1", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-mockery": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^8.5", + "psy/psysh": "^0.10.0", + "slevomat/coding-standard": "^6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "3.9.4" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-ctype": "Enables faster processing of character classification using ctype functions.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "homepage": "https://github.com/ramsey/uuid", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "rss": "https://github.com/ramsey/uuid/releases.atom", + "source": "https://github.com/ramsey/uuid" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2021-09-25T23:00:53+00:00" + }, + { + "name": "react/promise", + "version": "v2.9.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/234f8fd1023c9158e2314fa9d7d0e6a83db42910", + "reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v2.9.0" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-02-11T10:27:51+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "4211420d25eba80712bff236a98960ef68b866b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/4211420d25eba80712bff236a98960ef68b866b7", + "reference": "4211420d25eba80712bff236a98960ef68b866b7", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2022-04-01T13:37:23+00:00" + }, + { + "name": "seld/phar-utils", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "9f3452c93ff423469c0d56450431562ca423dcee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/9f3452c93ff423469c0d56450431562ca423dcee", + "reference": "9f3452c93ff423469c0d56450431562ca423dcee", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phar" + ], + "support": { + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.0" + }, + "time": "2021-12-10T11:20:11+00:00" + }, + { + "name": "spomky-labs/aes-key-wrap", + "version": "v6.0.0", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/aes-key-wrap.git", + "reference": "97388255a37ad6fb1ed332d07e61fa2b7bb62e0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/aes-key-wrap/zipball/97388255a37ad6fb1ed332d07e61fa2b7bb62e0d", + "reference": "97388255a37ad6fb1ed332d07e61fa2b7bb62e0d", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "lib-openssl": "*", + "php": ">=7.2", + "thecodingmachine/safe": "^1.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-beberlei-assert": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^7.0|^8.0|^9.0", + "thecodingmachine/phpstan-safe-rule": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "AESKW\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky-Labs/aes-key-wrap/contributors" + } + ], + "description": "AES Key Wrap for PHP.", + "homepage": "https://github.com/Spomky-Labs/aes-key-wrap", + "keywords": [ + "A128KW", + "A192KW", + "A256KW", + "RFC3394", + "RFC5649", + "aes", + "key", + "padding", + "wrap" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/aes-key-wrap/issues", + "source": "https://github.com/Spomky-Labs/aes-key-wrap/tree/v6.0.0" + }, + "time": "2020-08-01T14:07:55+00:00" + }, + { + "name": "spomky-labs/base64url", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/base64url.git", + "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d", + "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.11|^0.12", + "phpstan/phpstan-beberlei-assert": "^0.11|^0.12", + "phpstan/phpstan-deprecation-rules": "^0.11|^0.12", + "phpstan/phpstan-phpunit": "^0.11|^0.12", + "phpstan/phpstan-strict-rules": "^0.11|^0.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Base64Url\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky-Labs/base64url/contributors" + } + ], + "description": "Base 64 URL Safe Encoding/Decoding PHP Library", + "homepage": "https://github.com/Spomky-Labs/base64url", + "keywords": [ + "base64", + "rfc4648", + "safe", + "url" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/base64url/issues", + "source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2020-11-03T09:10:25+00:00" + }, + { + "name": "symfony/config", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "9f8964f56f7234f8ace16f66cb3fbae950c04e68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/9f8964f56f7234f8ace16f66cb3fbae950c04e68", + "reference": "9f8964f56f7234f8ace16f66cb3fbae950c04e68", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.16", + "symfony/polyfill-php81": "^1.22" + }, + "conflict": { + "symfony/finder": "<4.4" + }, + "require-dev": { + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/yaml": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-12T16:02:29+00:00" + }, + { + "name": "symfony/console", + "version": "v4.4.41", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0e1e62083b20ccb39c2431293de060f756af905c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0e1e62083b20ccb39c2431293de060f756af905c", + "reference": "0e1e62083b20ccb39c2431293de060f756af905c", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2" + }, + "conflict": { + "psr/log": ">=3", + "symfony/dependency-injection": "<3.4", + "symfony/event-dispatcher": "<4.3|>=5", + "symfony/lock": "<4.4", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" + }, + "require-dev": { + "psr/log": "^1|^2", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/event-dispatcher": "^4.3", + "symfony/lock": "^4.4|^5.0", + "symfony/process": "^3.4|^4.0|^5.0", + "symfony/var-dumper": "^4.3|^5.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/console/tree/v4.4.41" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-12T15:19:55+00:00" + }, + { + "name": "symfony/debug", + "version": "v4.4.41", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "6637e62480b60817b9a6984154a533e8e64c6bd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/6637e62480b60817b9a6984154a533e8e64c6bd5", + "reference": "6637e62480b60817b9a6984154a533e8e64c6bd5", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "symfony/http-kernel": "<3.4" + }, + "require-dev": { + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/debug/tree/v4.4.41" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-12T15:19:55+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "855e29cd715ad62bb840c9841fe09a7cde50811f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/855e29cd715ad62bb840c9841fe09a7cde50811f", + "reference": "855e29cd715ad62bb840c9841fe09a7cde50811f", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16", + "symfony/polyfill-php81": "^1.22", + "symfony/service-contracts": "^1.1.6|^2" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<5.3", + "symfony/finder": "<4.4", + "symfony/proxy-manager-bridge": "<4.4", + "symfony/yaml": "<4.4.26" + }, + "provide": { + "psr/container-implementation": "1.0", + "symfony/service-implementation": "1.0|2.0" + }, + "require-dev": { + "symfony/config": "^5.3|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/yaml": "^4.4.26|^5.0|^6.0" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-26T13:08:29+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v4.4.41", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "529feb0e03133dbd5fd3707200147cc4903206da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/529feb0e03133dbd5fd3707200147cc4903206da", + "reference": "529feb0e03133dbd5fd3707200147cc4903206da", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "^1|^2|^3", + "symfony/debug": "^4.4.5", + "symfony/var-dumper": "^4.4|^5.0" + }, + "require-dev": { + "symfony/http-kernel": "^4.4|^5.0", + "symfony/serializer": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v4.4.41" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-12T15:19:55+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v4.4.37", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "3ccfcfb96ecce1217d7b0875a0736976bc6e63dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3ccfcfb96ecce1217d7b0875a0736976bc6e63dc", + "reference": "3ccfcfb96ecce1217d7b0875a0736976bc6e63dc", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/event-dispatcher-contracts": "^1.1", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/dependency-injection": "<3.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "1.1" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/error-handler": "~3.4|~4.4", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/service-contracts": "^1.1|^2", + "symfony/stopwatch": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.37" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:41:36+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v1.1.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "1d5cd762abaa6b2a4169d3e77610193a7157129e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/1d5cd762abaa6b2a4169d3e77610193a7157129e", + "reference": "1d5cd762abaa6b2a4169d3e77610193a7157129e", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "suggest": { + "psr/event-dispatcher": "", + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v1.1.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:41:36+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v5.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "3a4442138d80c9f7b600fb297534ac718b61d37f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/3a4442138d80c9f7b600fb297534ac718b61d37f", + "reference": "3a4442138d80c9f7b600fb297534ac718b61d37f", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-01T12:33:59+00:00" + }, + { + "name": "symfony/finder", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/9b630f3427f3ebe7cd346c277a1408b00249dad9", + "reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-15T08:07:45+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "1a4f708e4e87f335d1b1be6148060739152f0bd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/1a4f708e4e87f335d1b1be6148060739152f0bd5", + "reference": "1a4f708e4e87f335d1b1be6148060739152f0bd5", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "symfony/http-client-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-13T20:07:29+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "ff2818d1c3d49860bcae1f2cbb5eb00fcd3bf9e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ff2818d1c3d49860bcae1f2cbb5eb00fcd3bf9e2", + "reference": "ff2818d1c3d49860bcae1f2cbb5eb00fcd3bf9e2", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "predis/predis": "~1.0", + "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/mime": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/mime": "To use the file extension guesser" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-22T08:14:12+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v4.4.41", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "7f8ce5bffc3939c63b7da32de5a546c98eb67698" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/7f8ce5bffc3939c63b7da32de5a546c98eb67698", + "reference": "7f8ce5bffc3939c63b7da32de5a546c98eb67698", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "^1|^2", + "symfony/error-handler": "^4.4", + "symfony/event-dispatcher": "^4.4", + "symfony/http-client-contracts": "^1.1|^2", + "symfony/http-foundation": "^4.4.30|^5.3.7", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/browser-kit": "<4.3", + "symfony/config": "<3.4", + "symfony/console": ">=5", + "symfony/dependency-injection": "<4.3", + "symfony/translation": "<4.2", + "twig/twig": "<1.43|<2.13,>=2" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^4.3|^5.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/console": "^3.4|^4.0", + "symfony/css-selector": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^4.3|^5.0", + "symfony/dom-crawler": "^3.4|^4.0|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/finder": "^3.4|^4.0|^5.0", + "symfony/process": "^3.4|^4.0|^5.0", + "symfony/routing": "^3.4|^4.0|^5.0", + "symfony/stopwatch": "^3.4|^4.0|^5.0", + "symfony/templating": "^3.4|^4.0|^5.0", + "symfony/translation": "^4.2|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "twig/twig": "^1.43|^2.13|^3.0.4" + }, + "suggest": { + "symfony/browser-kit": "", + "symfony/config": "", + "symfony/console": "", + "symfony/dependency-injection": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v4.4.41" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-27T17:13:11+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "30885182c981ab175d4d034db0f6f469898070ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-10-20T20:35:02+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "749045c69efb97c70d25d7463abba812e91f3a44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/749045c69efb97c70d25d7463abba812e91f3a44", + "reference": "749045c69efb97c70d25d7463abba812e91f3a44", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-09-14T14:02:44+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-19T12:13:01+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-30T18:21:41+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "9a142215a36a3888e30d0a9eeea9766764e96976" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976", + "reference": "9a142215a36a3888e30d0a9eeea9766764e96976", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-05-27T09:17:38+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-06-05T21:20:04+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-04T08:16:47+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-09-13T13:58:11+00:00" + }, + { + "name": "symfony/process", + "version": "v4.4.41", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "9eedd60225506d56e42210a70c21bb80ca8456ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/9eedd60225506d56e42210a70c21bb80ca8456ce", + "reference": "9eedd60225506d56e42210a70c21bb80ca8456ce", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v4.4.41" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-04T10:19:07+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/24d9dc654b83e91aa59f9d167b131bc3b5bea24c", + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-13T20:07:29+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "cdcadd343d31ad16fc5e006b0de81ea307435053" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cdcadd343d31ad16fc5e006b0de81ea307435053", + "reference": "cdcadd343d31ad16fc5e006b0de81ea307435053", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "phpunit/phpunit": "<5.4.3", + "symfony/console": "<4.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/uid": "^5.1|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", + "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-26T13:19:20+00:00" + }, + { + "name": "tedivm/jshrink", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/tedious/JShrink.git", + "reference": "0513ba1407b1f235518a939455855e6952a48bbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tedious/JShrink/zipball/0513ba1407b1f235518a939455855e6952a48bbc", + "reference": "0513ba1407b1f235518a939455855e6952a48bbc", + "shasum": "" + }, + "require": { + "php": "^5.6|^7.0|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.8", + "php-coveralls/php-coveralls": "^1.1.0", + "phpunit/phpunit": "^6" + }, + "type": "library", + "autoload": { + "psr-0": { + "JShrink": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Robert Hafner", + "email": "tedivm@tedivm.com" + } + ], + "description": "Javascript Minifier built in PHP", + "homepage": "http://github.com/tedious/JShrink", + "keywords": [ + "javascript", + "minifier" + ], + "support": { + "issues": "https://github.com/tedious/JShrink/issues", + "source": "https://github.com/tedious/JShrink/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/tedivm/jshrink", + "type": "tidelift" + } + ], + "time": "2020-11-30T18:10:21+00:00" + }, + { + "name": "thecodingmachine/safe", + "version": "v1.3.3", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "phpstan/phpstan": "^0.12", + "squizlabs/php_codesniffer": "^3.2", + "thecodingmachine/phpstan-strict-rules": "^0.12" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.1-dev" + } + }, + "autoload": { + "files": [ + "deprecated/apc.php", + "deprecated/libevent.php", + "deprecated/mssql.php", + "deprecated/stats.php", + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/ingres-ii.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/msql.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/mysqlndMs.php", + "generated/mysqlndQc.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/password.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pdf.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/simplexml.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "psr-4": { + "Safe\\": [ + "lib/", + "deprecated/", + "generated/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3" + }, + "time": "2020-10-28T17:51:34+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v3.6.10", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "5b547cdb25825f10251370f57ba5d9d924e6f68e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/5b547cdb25825f10251370f57ba5d9d924e6f68e", + "reference": "5b547cdb25825f10251370f57ba5d9d924e6f68e", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0", + "phpoption/phpoption": "^1.5.2", + "symfony/polyfill-ctype": "^1.17" + }, + "require-dev": { + "ext-filter": "*", + "ext-pcre": "*", + "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.21" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator.", + "ext-pcre": "Required to use most of the library." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v3.6.10" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2021-12-12T23:02:06+00:00" + }, + { + "name": "web-token/jwt-framework", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-framework.git", + "reference": "643cced197e32471418bd89e7a44b69fd04eb9de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-framework/zipball/643cced197e32471418bd89e7a44b69fd04eb9de", + "reference": "643cced197e32471418bd89e7a44b69fd04eb9de", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.17|^0.9", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-sodium": "*", + "fgrosse/phpasn1": "^2.0", + "php": ">=7.2", + "psr/event-dispatcher": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "spomky-labs/aes-key-wrap": "^5.0|^6.0", + "spomky-labs/base64url": "^1.0|^2.0", + "symfony/config": "^4.2|^5.0", + "symfony/console": "^4.2|^5.0", + "symfony/dependency-injection": "^4.2|^5.0", + "symfony/event-dispatcher": "^4.2|^5.0", + "symfony/http-kernel": "^4.2|^5.0", + "symfony/polyfill-mbstring": "^1.12" + }, + "conflict": { + "spomky-labs/jose": "*" + }, + "replace": { + "web-token/encryption-pack": "self.version", + "web-token/jwt-bundle": "self.version", + "web-token/jwt-checker": "self.version", + "web-token/jwt-console": "self.version", + "web-token/jwt-core": "self.version", + "web-token/jwt-easy": "self.version", + "web-token/jwt-encryption": "self.version", + "web-token/jwt-encryption-algorithm-aescbc": "self.version", + "web-token/jwt-encryption-algorithm-aesgcm": "self.version", + "web-token/jwt-encryption-algorithm-aesgcmkw": "self.version", + "web-token/jwt-encryption-algorithm-aeskw": "self.version", + "web-token/jwt-encryption-algorithm-dir": "self.version", + "web-token/jwt-encryption-algorithm-ecdh-es": "self.version", + "web-token/jwt-encryption-algorithm-experimental": "self.version", + "web-token/jwt-encryption-algorithm-pbes2": "self.version", + "web-token/jwt-encryption-algorithm-rsa": "self.version", + "web-token/jwt-key-mgmt": "self.version", + "web-token/jwt-nested-token": "self.version", + "web-token/jwt-signature": "self.version", + "web-token/jwt-signature-algorithm-ecdsa": "self.version", + "web-token/jwt-signature-algorithm-eddsa": "self.version", + "web-token/jwt-signature-algorithm-experimental": "self.version", + "web-token/jwt-signature-algorithm-hmac": "self.version", + "web-token/jwt-signature-algorithm-none": "self.version", + "web-token/jwt-signature-algorithm-rsa": "self.version", + "web-token/jwt-util-ecc": "self.version", + "web-token/signature-pack": "self.version" + }, + "require-dev": { + "bjeavons/zxcvbn-php": "^1.0", + "blackfire/php-sdk": "^1.14", + "ext-curl": "*", + "ext-gmp": "*", + "friendsofphp/php-cs-fixer": "^2.16", + "infection/infection": "^0.15|^0.16|^0.17|^0.18|^0.19|^0.20", + "matthiasnoback/symfony-config-test": "^3.1|^4.0", + "nyholm/psr7": "^1.3", + "php-coveralls/php-coveralls": "^2.0", + "php-http/mock-client": "^1.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^8.0|^9.0", + "symfony/browser-kit": "^4.2|^5.0", + "symfony/finder": "^4.2|^5.0", + "symfony/framework-bundle": "^4.2|^5.0", + "symfony/http-client": "^5.2", + "symfony/phpunit-bridge": "^4.2|^5.0", + "symfony/serializer": "^4.2|^5.0", + "symfony/var-dumper": "^4.2|^5.0" + }, + "suggest": { + "bjeavons/zxcvbn-php": "Adds key quality check for oct keys.", + "ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys", + "php-http/httplug": "To enable JKU/X5U support.", + "php-http/httplug-bundle": "To enable JKU/X5U support.", + "php-http/message-factory": "To enable JKU/X5U support.", + "symfony/serializer": "Use the Symfony serializer to serialize/unserialize JWS and JWE tokens.", + "symfony/var-dumper": "Used to show data on the debug toolbar." + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Jose\\": "src/", + "Jose\\Component\\Core\\Util\\Ecc\\": [ + "src/Ecc" + ], + "Jose\\Component\\Signature\\Algorithm\\": [ + "src/SignatureAlgorithm/ECDSA", + "src/SignatureAlgorithm/EdDSA", + "src/SignatureAlgorithm/HMAC", + "src/SignatureAlgorithm/None", + "src/SignatureAlgorithm/RSA", + "src/SignatureAlgorithm/Experimental" + ], + "Jose\\Component\\Encryption\\Algorithm\\": [ + "src/EncryptionAlgorithm/Experimental" + ], + "Jose\\Component\\Encryption\\Algorithm\\KeyEncryption\\": [ + "src/EncryptionAlgorithm/KeyEncryption/AESGCMKW", + "src/EncryptionAlgorithm/KeyEncryption/AESKW", + "src/EncryptionAlgorithm/KeyEncryption/Direct", + "src/EncryptionAlgorithm/KeyEncryption/ECDHES", + "src/EncryptionAlgorithm/KeyEncryption/PBES2", + "src/EncryptionAlgorithm/KeyEncryption/RSA" + ], + "Jose\\Component\\Encryption\\Algorithm\\ContentEncryption\\": [ + "src/EncryptionAlgorithm/ContentEncryption/AESGCM", + "src/EncryptionAlgorithm/ContentEncryption/AESCBC" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "JSON Object Signing and Encryption library for PHP and Symfony Bundle.", + "homepage": "https://github.com/web-token/jwt-framework", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "issues": "https://github.com/web-token/jwt-framework/issues", + "source": "https://github.com/web-token/jwt-framework/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + } + ], + "time": "2021-06-25T15:59:52+00:00" + }, + { + "name": "webimpress/safe-writer", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/webimpress/safe-writer.git", + "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webimpress/safe-writer/zipball/9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", + "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5.4", + "vimeo/psalm": "^4.7", + "webimpress/coding-standard": "^1.2.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev", + "dev-develop": "2.3.x-dev", + "dev-release-1.0": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Webimpress\\SafeWriter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Tool to write files safely, to avoid race conditions", + "keywords": [ + "concurrent write", + "file writer", + "race condition", + "safe writer", + "webimpress" + ], + "support": { + "issues": "https://github.com/webimpress/safe-writer/issues", + "source": "https://github.com/webimpress/safe-writer/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2021-04-19T16:34:45+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.10.0" + }, + "time": "2021-03-09T10:59:23+00:00" + }, + { + "name": "wikimedia/less.php", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/wikimedia/less.php.git", + "reference": "a486d78b9bd16b72f237fc6093aa56d69ce8bd13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wikimedia/less.php/zipball/a486d78b9bd16b72f237fc6093aa56d69ce8bd13", + "reference": "a486d78b9bd16b72f237fc6093aa56d69ce8bd13", + "shasum": "" + }, + "require": { + "php": ">=7.2.9" + }, + "require-dev": { + "mediawiki/mediawiki-codesniffer": "34.0.0", + "mediawiki/minus-x": "1.0.0", + "php-parallel-lint/php-console-highlighter": "0.5.0", + "php-parallel-lint/php-parallel-lint": "1.2.0", + "phpunit/phpunit": "^8.5" + }, + "bin": [ + "bin/lessc" + ], + "type": "library", + "autoload": { + "psr-0": { + "Less": "lib/" + }, + "classmap": [ + "lessc.inc.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Josh Schmidt", + "homepage": "https://github.com/oyejorge" + }, + { + "name": "Matt Agar", + "homepage": "https://github.com/agar" + }, + { + "name": "Martin Jantošovič", + "homepage": "https://github.com/Mordred" + } + ], + "description": "PHP port of the Javascript version of LESS http://lesscss.org (Originally maintained by Josh Schmidt)", + "keywords": [ + "css", + "less", + "less.js", + "lesscss", + "php", + "stylesheet" + ], + "support": { + "issues": "https://github.com/wikimedia/less.php/issues", + "source": "https://github.com/wikimedia/less.php/tree/v3.1.0" + }, + "time": "2020-12-11T19:33:31+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.22" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-03-03T08:28:38+00:00" + }, + { + "name": "drenso/phan-extensions", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/Drenso/PhanExtensions.git", + "reference": "848a4f2d8c52a29a14320df5452c1558f44b2cfe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Drenso/PhanExtensions/zipball/848a4f2d8c52a29a14320df5452c1558f44b2cfe", + "reference": "848a4f2d8c52a29a14320df5452c1558f44b2cfe", + "shasum": "" + }, + "require-dev": { + "phan/phan": "~2|~3" + }, + "type": "project", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob van de Vijver", + "email": "bob@drenso.nl" + }, + { + "name": "Tobias Feijten", + "email": "tobias@drenso.nl" + } + ], + "description": "This project contains several extensions (stubs/plugins) to be used with Phan for static PHP analysis", + "homepage": "https://github.com/Drenso/PhanExtensions", + "keywords": [ + "phan", + "plugin", + "plugins", + "stub", + "stubs", + "symfony" + ], + "support": { + "issues": "https://github.com/Drenso/PhanExtensions/issues", + "source": "https://github.com/Drenso/PhanExtensions/tree/v3.5.1" + }, + "time": "2021-06-08T10:46:31+00:00" + }, + { + "name": "felixfbecker/advanced-json-rpc", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "php": "^7.1 || ^8.0", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" + }, + "time": "2021-06-11T22:34:44+00:00" + }, + { + "name": "laminas/laminas-captcha", + "version": "2.12.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-captcha.git", + "reference": "b07e499a7df73795768aa89e0138757a7ddb9195" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-captcha/zipball/b07e499a7df73795768aa89e0138757a7ddb9195", + "reference": "b07e499a7df73795768aa89e0138757a7ddb9195", + "shasum": "" + }, + "require": { + "laminas/laminas-math": "^2.7 || ^3.0", + "laminas/laminas-recaptcha": "^3.0", + "laminas/laminas-session": "^2.12", + "laminas/laminas-stdlib": "^3.6", + "laminas/laminas-text": "^2.8", + "laminas/laminas-validator": "^2.14", + "php": "^7.3 || ~8.0.0 || ~8.1.0" + }, + "conflict": { + "zendframework/zend-captcha": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.1.4", + "phpunit/phpunit": "^9.4.3", + "psalm/plugin-phpunit": "^0.15.1", + "vimeo/psalm": "^4.6" + }, + "suggest": { + "laminas/laminas-i18n-resources": "Translations of captcha messages" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Captcha\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Generate and validate CAPTCHAs using Figlets, images, ReCaptcha, and more", + "homepage": "https://laminas.dev", + "keywords": [ + "captcha", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-captcha/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-captcha/issues", + "rss": "https://github.com/laminas/laminas-captcha/releases.atom", + "source": "https://github.com/laminas/laminas-captcha" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-04-07T10:41:09+00:00" + }, + { + "name": "laminas/laminas-db", + "version": "2.15.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-db.git", + "reference": "1125ef2e55108bdfcc1f0030d3a0f9b895e09606" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-db/zipball/1125ef2e55108bdfcc1f0030d3a0f9b895e09606", + "reference": "1125ef2e55108bdfcc1f0030d3a0f9b895e09606", + "shasum": "" + }, + "require": { + "laminas/laminas-stdlib": "^3.7.1", + "php": "^7.3 || ~8.0.0 || ~8.1.0" + }, + "conflict": { + "zendframework/zend-db": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.2.1", + "laminas/laminas-eventmanager": "^3.4.0", + "laminas/laminas-hydrator": "^3.2 || ^4.3", + "laminas/laminas-servicemanager": "^3.7.0", + "phpunit/phpunit": "^9.5.19" + }, + "suggest": { + "laminas/laminas-eventmanager": "Laminas\\EventManager component", + "laminas/laminas-hydrator": "(^3.2 || ^4.3) Laminas\\Hydrator component for using HydratingResultSets", + "laminas/laminas-servicemanager": "Laminas\\ServiceManager component" + }, + "type": "library", + "extra": { + "laminas": { + "component": "Laminas\\Db", + "config-provider": "Laminas\\Db\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Db\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Database abstraction layer, SQL abstraction, result set abstraction, and RowDataGateway and TableDataGateway implementations", + "homepage": "https://laminas.dev", + "keywords": [ + "db", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-db/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-db/issues", + "rss": "https://github.com/laminas/laminas-db/releases.atom", + "source": "https://github.com/laminas/laminas-db" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-04-11T13:26:20+00:00" + }, + { + "name": "laminas/laminas-recaptcha", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-recaptcha.git", + "reference": "8ada2b91c44daa0ef4c909e9f88c88076c045ff8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-recaptcha/zipball/8ada2b91c44daa0ef4c909e9f88c88076c045ff8", + "reference": "8ada2b91c44daa0ef4c909e9f88c88076c045ff8", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laminas/laminas-http": "^2.14", + "laminas/laminas-json": "^3.2", + "laminas/laminas-stdlib": "^3.3", + "laminas/laminas-zendframework-bridge": "^1.1", + "php": "^7.3 || ~8.0.0" + }, + "replace": { + "zendframework/zendservice-recaptcha": "^3.2.0" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.1.4", + "laminas/laminas-config": "^3.4", + "laminas/laminas-validator": "^2.14", + "phpunit/phpunit": "^9.4.3" + }, + "suggest": { + "laminas/laminas-validator": "~2.0, if using ReCaptcha's Mailhide API" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\ReCaptcha\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "OOP wrapper for the ReCaptcha web service", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "recaptcha" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-recaptcha/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-recaptcha/issues", + "rss": "https://github.com/laminas/laminas-recaptcha/releases.atom", + "source": "https://github.com/laminas/laminas-recaptcha" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-11-28T18:07:15+00:00" + }, + { + "name": "laminas/laminas-session", + "version": "2.12.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-session.git", + "reference": "888c6a344e9a4c9f34ab6e09346640eac9be3fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-session/zipball/888c6a344e9a4c9f34ab6e09346640eac9be3fcf", + "reference": "888c6a344e9a4c9f34ab6e09346640eac9be3fcf", + "shasum": "" + }, + "require": { + "laminas/laminas-eventmanager": "^3.4", + "laminas/laminas-stdlib": "^3.6", + "php": "^7.3 || ~8.0.0 || ~8.1.0" + }, + "conflict": { + "zendframework/zend-session": "*" + }, + "require-dev": { + "container-interop/container-interop": "^1.1", + "laminas/laminas-cache": "3.0.x-dev", + "laminas/laminas-cache-storage-adapter-memory": "2.0.x-dev", + "laminas/laminas-coding-standard": "~2.2.1", + "laminas/laminas-db": "^2.13.4", + "laminas/laminas-http": "^2.15", + "laminas/laminas-servicemanager": "^3.7", + "laminas/laminas-validator": "^2.15", + "mongodb/mongodb": "v1.9.x-dev", + "php-mock/php-mock-phpunit": "^1.1.2 || ^2.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5.9" + }, + "suggest": { + "laminas/laminas-cache": "Laminas\\Cache component", + "laminas/laminas-db": "Laminas\\Db component", + "laminas/laminas-http": "Laminas\\Http component", + "laminas/laminas-servicemanager": "Laminas\\ServiceManager component", + "laminas/laminas-validator": "Laminas\\Validator component", + "mongodb/mongodb": "If you want to use the MongoDB session save handler" + }, + "type": "library", + "extra": { + "laminas": { + "component": "Laminas\\Session", + "config-provider": "Laminas\\Session\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Session\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Object-oriented interface to PHP sessions and storage", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "session" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-session/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-session/issues", + "rss": "https://github.com/laminas/laminas-session/releases.atom", + "source": "https://github.com/laminas/laminas-session" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-02-15T16:38:29+00:00" + }, + { + "name": "laminas/laminas-text", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-text.git", + "reference": "8879e75d03e09b0d6787e6680cfa255afd4645a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-text/zipball/8879e75d03e09b0d6787e6680cfa255afd4645a7", + "reference": "8879e75d03e09b0d6787e6680cfa255afd4645a7", + "shasum": "" + }, + "require": { + "laminas/laminas-servicemanager": "^3.4", + "laminas/laminas-stdlib": "^3.6", + "php": "^7.3 || ~8.0.0 || ~8.1.0" + }, + "conflict": { + "zendframework/zend-text": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Text\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Create FIGlets and text-based tables", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "text" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-text/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-text/issues", + "rss": "https://github.com/laminas/laminas-text/releases.atom", + "source": "https://github.com/laminas/laminas-text" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-09-02T16:50:53+00:00" + }, + { + "name": "magento-ecg/coding-standard", + "version": "4.5.0", + "source": { + "type": "git", + "url": "https://github.com/magento-ecg/coding-standard.git", + "reference": "fe4185e22e2fec6988b69b5ef05e5dc331ae84e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/magento-ecg/coding-standard/zipball/fe4185e22e2fec6988b69b5ef05e5dc331ae84e8", + "reference": "fe4185e22e2fec6988b69b5ef05e5dc331ae84e8", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Magento Expert Consulting Group", + "homepage": "http://magentocommerce.com/consulting", + "role": "Maintainer" + } + ], + "description": "A set of PHP_CodeSniffer rules and sniffs.", + "homepage": "https://github.com/magento-ecg/coding-standard", + "support": { + "issues": "https://github.com/magento-ecg/coding-standard/issues", + "source": "https://github.com/magento-ecg/coding-standard/tree/4.5.0" + }, + "time": "2022-01-29T10:23:37+00:00" + }, + { + "name": "magento/framework-bulk", + "version": "101.0.1", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/framework-bulk/magento-framework-bulk-101.0.1.0.zip", + "shasum": "0509f701466b6c6403b97f625a723029ae922754" + }, + "require": { + "magento/framework": "103.0.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-library", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Framework\\Bulk\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/framework-message-queue", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/framework-message-queue/magento-framework-message-queue-100.4.4.0.zip", + "shasum": "38ba5f46176d13cea8e3a52b6c293fb6c6e3c93d" + }, + "require": { + "magento/framework": "103.0.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-library", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Framework\\MessageQueue\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/magento-coding-standard", + "version": "5", + "source": { + "type": "git", + "url": "https://github.com/magento/magento-coding-standard.git", + "reference": "da46c5d57a43c950dfa364edc7f1f0436d5353a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/magento/magento-coding-standard/zipball/da46c5d57a43c950dfa364edc7f1f0436d5353a5", + "reference": "da46c5d57a43c950dfa364edc7f1f0436d5353a5", + "shasum": "" + }, + "require": { + "php": ">=5.6.0", + "squizlabs/php_codesniffer": "^3.4", + "webonyx/graphql-php": ">=0.12.6 <1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "Magento2\\": "Magento2/" + }, + "classmap": [ + "PHP_CodeSniffer/Tokenizers/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "A set of Magento specific PHP CodeSniffer rules.", + "support": { + "issues": "https://github.com/magento/magento-coding-standard/issues", + "source": "https://github.com/magento/magento-coding-standard/tree/v5" + }, + "time": "2019-11-04T22:08:27+00:00" + }, + { + "name": "magento/module-asynchronous-operations", + "version": "100.4.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-asynchronous-operations/magento-module-asynchronous-operations-100.4.3.0.zip", + "shasum": "ce1bbcf47020689fae6dd8e2e34dd18a01dd67cf" + }, + "require": { + "magento/framework": "103.0.*", + "magento/framework-bulk": "101.0.*", + "magento/framework-message-queue": "100.4.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-ui": "101.2.*", + "php": "~7.3.0||~7.4.0" + }, + "suggest": { + "magento/module-admin-notification": "100.4.*", + "magento/module-logging": "*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\AsynchronousOperations\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-authorization", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-authorization/magento-module-authorization-100.4.4.0.zip", + "shasum": "7f94d3c40f8d836c84bd6547889047e00692ca09" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Authorization\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "Authorization module provides access to Magento ACL functionality." + }, + { + "name": "magento/module-backend", + "version": "102.0.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-backend/magento-module-backend-102.0.4.0.zip", + "shasum": "4f75d59880b3a8af1c8b0299e47f322e4b38ebba" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backup": "100.4.*", + "magento/module-catalog": "104.0.*", + "magento/module-cms": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-developer": "100.4.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-quote": "101.2.*", + "magento/module-reports": "100.4.*", + "magento/module-require-js": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-security": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-translation": "100.4.*", + "magento/module-ui": "101.2.*", + "magento/module-user": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-theme": "101.1.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php", + "cli_commands.php" + ], + "psr-4": { + "Magento\\Backend\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-backup", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-backup/magento-module-backup-100.4.4.0.zip", + "shasum": "2ed47abed34b081913c248bc52ba6dafc151f809" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-cron": "100.4.*", + "magento/module-store": "101.1.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Backup\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-bundle", + "version": "101.0.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-bundle/magento-module-bundle-101.0.3.0.zip", + "shasum": "9cf9f2d600b119095ae3eeeb7f248720985bbe2b" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-catalog-rule": "101.2.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-gift-message": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-ui": "101.2.*", + "php": "~7.3.0||~7.4.0" + }, + "suggest": { + "magento/module-bundle-sample-data": "Sample Data version: 100.4.*", + "magento/module-sales-rule": "101.2.*", + "magento/module-webapi": "100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Bundle\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-captcha", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-captcha/magento-module-captcha-100.4.4.0.zip", + "shasum": "290e51dafbf7038c28629ff5cd8e312176e984d1" + }, + "require": { + "laminas/laminas-captcha": "^2.11.0", + "laminas/laminas-db": "^2.13.4", + "laminas/laminas-session": "^2.12.0", + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Captcha\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-catalog", + "version": "104.0.2", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-catalog/magento-module-catalog-104.0.2.0.zip", + "shasum": "91a30c6dea91786cb3239f42a5f5a597b2dd37ac" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-asynchronous-operations": "100.4.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-catalog-rule": "101.2.*", + "magento/module-catalog-url-rewrite": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-cms": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-indexer": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-msrp": "100.4.*", + "magento/module-page-cache": "100.4.*", + "magento/module-product-alert": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-url-rewrite": "102.0.*", + "magento/module-widget": "101.2.*", + "magento/module-wishlist": "101.2.*", + "php": "~7.3.0||~7.4.0" + }, + "suggest": { + "magento/module-catalog-sample-data": "Sample Data version: 100.4.*", + "magento/module-cookie": "100.4.*", + "magento/module-sales": "103.0.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Catalog\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-catalog-import-export", + "version": "101.1.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-catalog-import-export/magento-module-catalog-import-export-101.1.4.0.zip", + "shasum": "7de989e9b9128c4048d95d3add36ea42996ded52" + }, + "require": { + "ext-ctype": "*", + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-catalog-url-rewrite": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-import-export": "101.0.*", + "magento/module-media-storage": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CatalogImportExport\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-catalog-inventory", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-catalog-inventory/magento-module-catalog-inventory-100.4.4.0.zip", + "shasum": "f6fe6467ba5fb05307ef1071466375d1d045ed2a" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-quote": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CatalogInventory\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-catalog-rule", + "version": "101.2.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-catalog-rule/magento-module-catalog-rule-101.2.4.0.zip", + "shasum": "0b46c28faedfbb1ad98dfa7928f7207901592b5e" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-rule": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-catalog-rule-sample-data": "Sample Data version: 100.4.*", + "magento/module-import-export": "101.0.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CatalogRule\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-catalog-search", + "version": "102.0.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-catalog-search/magento-module-catalog-search-102.0.3.0.zip", + "shasum": "6ea97d986b9ae0564dc2fe8cc8c8786043465751" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-indexer": "100.4.*", + "magento/module-search": "101.1.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.3.0||~7.4.0" + }, + "suggest": { + "magento/module-config": "101.2.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CatalogSearch\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "Catalog search" + }, + { + "name": "magento/module-catalog-url-rewrite", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-catalog-url-rewrite/magento-module-catalog-url-rewrite-100.4.4.0.zip", + "shasum": "ea899afea444a981d3e468118038a280c75f86a8" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-import-export": "101.1.*", + "magento/module-eav": "102.1.*", + "magento/module-import-export": "101.0.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-url-rewrite": "102.0.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-webapi": "100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CatalogUrlRewrite\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-checkout", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-checkout/magento-module-checkout-100.4.4.0.zip", + "shasum": "7565754344c67aea344a07fb2058cf53561127e3" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-captcha": "100.4.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-msrp": "100.4.*", + "magento/module-page-cache": "100.4.*", + "magento/module-payment": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-sales-rule": "101.2.*", + "magento/module-security": "100.4.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-cookie": "100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Checkout\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-cms", + "version": "104.0.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-cms/magento-module-cms-104.0.4.0.zip", + "shasum": "c3b0a5b87f4245732334fd9571d41a382f3bf5db" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-email": "101.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-variable": "100.4.*", + "magento/module-widget": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-cms-sample-data": "Sample Data version: 100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Cms\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-cms-url-rewrite", + "version": "100.4.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-cms-url-rewrite/magento-module-cms-url-rewrite-100.4.3.0.zip", + "shasum": "5a8de8093d7d4e6ffe8f9cb9bc42f8259dee97a0" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-cms": "104.0.*", + "magento/module-store": "101.1.*", + "magento/module-url-rewrite": "102.0.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CmsUrlRewrite\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-config", + "version": "101.2.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-config/magento-module-config-101.2.4.0.zip", + "shasum": "9392da7243f39fad1e90ff4709394ab3dd6657d7" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-cron": "100.4.*", + "magento/module-deploy": "100.4.*", + "magento/module-directory": "100.4.*", + "magento/module-email": "101.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-store": "101.1.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Config\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-configurable-product", + "version": "100.4.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-configurable-product/magento-module-configurable-product-100.4.3.0.zip", + "shasum": "cbf4b4091c63bac3481728b33913de08b9577cba" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.3.0||~7.4.0" + }, + "suggest": { + "magento/module-configurable-sample-data": "Sample Data version: 100.4.*", + "magento/module-msrp": "100.4.*", + "magento/module-product-links-sample-data": "Sample Data version: 100.4.*", + "magento/module-product-video": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-sales-rule": "101.2.*", + "magento/module-tax": "100.4.*", + "magento/module-webapi": "100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\ConfigurableProduct\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-contact", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-contact/magento-module-contact-100.4.4.0.zip", + "shasum": "f59890ba23fff0b4174eca28e9eb9631da272fdf" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-cms": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-store": "101.1.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Contact\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-cron", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-cron/magento-module-cron-100.4.4.0.zip", + "shasum": "3ac0f4fc89416ac589e7a22749f1825bf8c0ae36" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-store": "101.1.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-config": "101.2.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Cron\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-customer", + "version": "103.0.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-customer/magento-module-customer-103.0.4.0.zip", + "shasum": "a9b5e4fb9a4bd904bc6c4fd8951a42c5f28f1f4f" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-integration": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-newsletter": "100.4.*", + "magento/module-page-cache": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-wishlist": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-cookie": "100.4.*", + "magento/module-customer-sample-data": "Sample Data version: 100.4.*", + "magento/module-webapi": "100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Customer\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-deploy", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-deploy/magento-module-deploy-100.4.4.0.zip", + "shasum": "d019c83f5d2117b74ede903f9e8e4f9efc807886" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-config": "101.2.*", + "magento/module-require-js": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-user": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "cli_commands.php", + "registration.php" + ], + "psr-4": { + "Magento\\Deploy\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-developer", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-developer/magento-module-developer-100.4.4.0.zip", + "shasum": "130d066e02afc49ea5e499a38c2d207b316897bf" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-config": "101.2.*", + "magento/module-store": "101.1.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Developer\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-directory", + "version": "100.4.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-directory/magento-module-directory-100.4.3.0.zip", + "shasum": "5664ebbfb0c6314099bf69e70e5d4227c1a122df" + }, + "require": { + "lib-libxml": "*", + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-config": "101.2.*", + "magento/module-store": "101.1.*", + "php": "~7.3.0||~7.4.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Directory\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-downloadable", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-downloadable/magento-module-downloadable-100.4.4.0.zip", + "shasum": "9612442d3c202c19dfbbced3e118cc084cef6878" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-gift-message": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-downloadable-sample-data": "Sample Data version: 100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Downloadable\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-eav", + "version": "102.1.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-eav/magento-module-eav-102.1.4.0.zip", + "shasum": "c3be158f50ef1f618bfde852c22555cb12e31840" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-media-storage": "100.4.*", + "magento/module-store": "101.1.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Eav\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-email", + "version": "101.1.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-email/magento-module-email-101.1.4.0.zip", + "shasum": "d1af5680086a5a9bf12f25164d4b4e9acbb10688" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-cms": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-media-storage": "100.4.*", + "magento/module-require-js": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-variable": "100.4.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-theme": "101.1.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Email\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-gift-message", + "version": "100.4.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-gift-message/magento-module-gift-message-100.4.3.0.zip", + "shasum": "599c56fecf3c26ff9d1b59011255ab32eb9ed4e3" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-eav": "102.1.*", + "magento/module-multishipping": "100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\GiftMessage\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-grouped-product", + "version": "100.4.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-grouped-product/magento-module-grouped-product-100.4.3.0.zip", + "shasum": "e494a6a9df2f0094fb0334cfa27da9b275abc214" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-msrp": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-wishlist": "101.2.*", + "php": "~7.3.0||~7.4.0" + }, + "suggest": { + "magento/module-grouped-product-sample-data": "Sample Data version: 100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\GroupedProduct\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-import-export", + "version": "101.0.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-import-export/magento-module-import-export-101.0.4.0.zip", + "shasum": "3087bdbb3e5e28efa45f1fd7b7e0b347480d2225" + }, + "require": { + "ext-ctype": "*", + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-eav": "102.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\ImportExport\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-indexer", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-indexer/magento-module-indexer-100.4.4.0.zip", + "shasum": "d5fd2a2d9db69e8f9901b9b84059fc7b50a003f3" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Indexer\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-integration", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-integration/magento-module-integration-100.4.4.0.zip", + "shasum": "9822538189688906a2a4805b9d29c50823305517" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-customer": "103.0.*", + "magento/module-security": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-user": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Integration\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-media-storage", + "version": "100.4.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-media-storage/magento-module-media-storage-100.4.3.0.zip", + "shasum": "d6d7bda754468621063b5b238fc3e84079cae0ee" + }, + "require": { + "magento/framework": "103.0.*", + "magento/framework-bulk": "101.0.*", + "magento/module-asynchronous-operations": "100.4.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaStorage\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-msrp", + "version": "100.4.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-msrp/magento-module-msrp-100.4.3.0.zip", + "shasum": "5e15e57618e975581e0096857d1624d0f6d5a010" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-downloadable": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-bundle": "101.0.*", + "magento/module-msrp-sample-data": "Sample Data version: 100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Msrp\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-newsletter", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-newsletter/magento-module-newsletter-100.4.4.0.zip", + "shasum": "cf43af43a4d0f074c640c1f9b25fe0305fe736b5" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-cms": "104.0.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-email": "101.1.*", + "magento/module-require-js": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-widget": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Newsletter\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-page-cache", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-page-cache/magento-module-page-cache-100.4.4.0.zip", + "shasum": "86488eb5329f143529d35dfbb6a8108e471d8198" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-config": "101.2.*", + "magento/module-store": "101.1.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\PageCache\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-payment", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-payment/magento-module-payment-100.4.4.0.zip", + "shasum": "ba16255ab4a4e232de422128c83fb3817ea4aea0" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-directory": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Payment\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-product-alert", + "version": "100.4.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-product-alert/magento-module-product-alert-100.4.3.0.zip", + "shasum": "4d6c67208028fdb74bcc1bde4336ae70b2246d9e" + }, + "require": { + "magento/framework": "103.0.*", + "magento/framework-bulk": "101.0.*", + "magento/module-asynchronous-operations": "100.4.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-customer": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-config": "101.2.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\ProductAlert\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-quote", + "version": "101.2.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-quote/magento-module-quote-101.2.3.0.zip", + "shasum": "91522269af2fae7d9916299abc70fdb4fa31fa87" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-payment": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-sales-sequence": "100.4.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "php": "~7.3.0||~7.4.0" + }, + "suggest": { + "magento/module-webapi": "100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Quote\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-reports", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-reports/magento-module-reports-100.4.4.0.zip", + "shasum": "54cdf7898e9ce88835c70bd98e6bf3768da1c0a8" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-cms": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-downloadable": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-quote": "101.2.*", + "magento/module-review": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-sales-rule": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-widget": "101.2.*", + "magento/module-wishlist": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Reports\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-require-js", + "version": "100.4.1", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-require-js/magento-module-require-js-100.4.1.0.zip", + "shasum": "8a573426813a22a6a1253711bda515303e6f7796" + }, + "require": { + "magento/framework": "103.0.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\RequireJs\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-review", + "version": "100.4.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-review/magento-module-review-100.4.3.0.zip", + "shasum": "e79c47dad8cd17e501251854f308816420505573" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-newsletter": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.3.0||~7.4.0" + }, + "suggest": { + "magento/module-cookie": "100.4.*", + "magento/module-review-sample-data": "Sample Data version: 100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Review\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-rss", + "version": "100.4.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-rss/magento-module-rss-100.4.3.0.zip", + "shasum": "dc0efb744c3bc59bdec1b8e3dc8d07695dcf92bb" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-customer": "103.0.*", + "magento/module-store": "101.1.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Rss\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-rule", + "version": "100.4.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-rule/magento-module-rule-100.4.3.0.zip", + "shasum": "1165df5b96f157a0cc5fad73926fc5385b26d90b" + }, + "require": { + "lib-libxml": "*", + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-eav": "102.1.*", + "magento/module-store": "101.1.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Rule\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-sales", + "version": "103.0.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-sales/magento-module-sales-103.0.3.0.zip", + "shasum": "9986e510fc18b5b9ed79cd4a3e1025aa10a00e47" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-bundle": "101.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-gift-message": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-payment": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-reports": "100.4.*", + "magento/module-sales-rule": "101.2.*", + "magento/module-sales-sequence": "100.4.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-widget": "101.2.*", + "magento/module-wishlist": "101.2.*", + "php": "~7.3.0||~7.4.0" + }, + "suggest": { + "magento/module-sales-sample-data": "Sample Data version: 100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Sales\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-sales-inventory", + "version": "100.4.0", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-sales-inventory/magento-module-sales-inventory-100.4.0.0.zip", + "shasum": "f00ad78a70ca2dd02dcb8fc3b1f8166aabb9aa27" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "php": "~7.3.0||~7.4.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\SalesInventory\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-sales-rule", + "version": "101.2.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-sales-rule/magento-module-sales-rule-101.2.3.0.zip", + "shasum": "a2d5ea16744531a74fe1469431050ced9b198ce6" + }, + "require": { + "magento/framework": "103.0.*", + "magento/framework-bulk": "101.0.*", + "magento/module-asynchronous-operations": "100.4.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-captcha": "100.4.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-rule": "101.2.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-payment": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-reports": "100.4.*", + "magento/module-rule": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-widget": "101.2.*", + "php": "~7.3.0||~7.4.0" + }, + "suggest": { + "magento/module-sales-rule-sample-data": "Sample Data version: 100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\SalesRule\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-sales-sequence", + "version": "100.4.2", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-sales-sequence/magento-module-sales-sequence-100.4.2.0.zip", + "shasum": "4e5880119eecf16b3e66dba1f9e9985f07d2d58d" + }, + "require": { + "magento/framework": "103.0.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\SalesSequence\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-search", + "version": "101.1.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-search/magento-module-search-101.1.3.0.zip", + "shasum": "95ef4fb554a1096bb53234673ba9290aa35c4a11" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog-search": "102.0.*", + "magento/module-reports": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.3.0||~7.4.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Search\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-security", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-security/magento-module-security-100.4.4.0.zip", + "shasum": "8dc34acc5886991e372557e64df325bae8ff1e68" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-config": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-user": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-customer": "103.0.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Security\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "Security management module" + }, + { + "name": "magento/module-shipping", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-shipping/magento-module-shipping-100.4.4.0.zip", + "shasum": "503a898158ff301be00856671a270ca5dff7bda9" + }, + "require": { + "ext-gd": "*", + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-contact": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-payment": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-ui": "101.2.*", + "magento/module-user": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-config": "101.2.*", + "magento/module-fedex": "100.4.*", + "magento/module-ups": "100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Shipping\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-store", + "version": "101.1.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-store/magento-module-store-101.1.3.0.zip", + "shasum": "b019ec5adac8c32657a1b1b18e75ec10d3597233" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-ui": "101.2.*", + "php": "~7.3.0||~7.4.0" + }, + "suggest": { + "magento/module-deploy": "100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Store\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-tax", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-tax/magento-module-tax-100.4.4.0.zip", + "shasum": "a65794f2053094a757a16a33dba14c4588e1d5e1" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-page-cache": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-reports": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-tax-sample-data": "Sample Data version: 100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Tax\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-theme", + "version": "101.1.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-theme/magento-module-theme-101.1.4.0.zip", + "shasum": "0d42df06aec2580a16f87d2fb0deaa46fcf64fe3" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-cms": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-require-js": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-widget": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-deploy": "100.4.*", + "magento/module-directory": "100.4.*", + "magento/module-theme-sample-data": "Sample Data version: 100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Theme\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-translation", + "version": "100.4.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-translation/magento-module-translation-100.4.4.0.zip", + "shasum": "cf12c0a7493629dcf952f7af91a95eb30784f194" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-developer": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-deploy": "100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Translation\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-ui", + "version": "101.2.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-ui/magento-module-ui-101.2.4.0.zip", + "shasum": "285bbc4d9c6241512eaf9dafbf0c1259fefcab03" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-eav": "102.1.*", + "magento/module-store": "101.1.*", + "magento/module-user": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-config": "101.2.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Ui\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-url-rewrite", + "version": "102.0.3", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-url-rewrite/magento-module-url-rewrite-102.0.3.0.zip", + "shasum": "f624555ea5fbb891aacd64901b43b472bd2f8aab" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-url-rewrite": "100.4.*", + "magento/module-cms": "104.0.*", + "magento/module-cms-url-rewrite": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\UrlRewrite\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-user", + "version": "101.2.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-user/magento-module-user-101.2.4.0.zip", + "shasum": "787d34763f773826e23a9e4d696507e677aff4da" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-email": "101.1.*", + "magento/module-integration": "100.4.*", + "magento/module-security": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\User\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-variable", + "version": "100.4.2", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-variable/magento-module-variable-100.4.2.0.zip", + "shasum": "b67c8e4a7e13590bbf6040844ae8e2a189687a8b" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-config": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Variable\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-widget", + "version": "101.2.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-widget/magento-module-widget-101.2.4.0.zip", + "shasum": "b803c2b00d38a63b52c9d0ec45a58043f41d5d02" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-cms": "104.0.*", + "magento/module-email": "101.1.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-variable": "100.4.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-widget-sample-data": "Sample Data version: 100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Widget\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "magento/module-wishlist", + "version": "101.2.4", + "dist": { + "type": "zip", + "url": "https://repo.magento.com/archives/magento/module-wishlist/magento-module-wishlist-101.2.4.0.zip", + "shasum": "5d298143aab4b1abbd2e0618cbdc9a61bbe99f1d" + }, + "require": { + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-captcha": "100.4.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-rss": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "php": "~7.4.0||~8.1.0" + }, + "suggest": { + "magento/module-bundle": "101.0.*", + "magento/module-configurable-product": "100.4.*", + "magento/module-cookie": "100.4.*", + "magento/module-downloadable": "100.4.*", + "magento/module-grouped-product": "100.4.*", + "magento/module-wishlist-sample-data": "Sample Data version: 100.4.*" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Wishlist\\": "" + } + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "N/A" + }, + { + "name": "microsoft/tolerant-php-parser", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/microsoft/tolerant-php-parser.git", + "reference": "6a965617cf484355048ac6d2d3de7b6ec93abb16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/microsoft/tolerant-php-parser/zipball/6a965617cf484355048ac6d2d3de7b6ec93abb16", + "reference": "6a965617cf484355048ac6d2d3de7b6ec93abb16", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.15" + }, + "type": "library", + "autoload": { + "psr-4": { + "Microsoft\\PhpParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Lourens", + "email": "roblou@microsoft.com" + } + ], + "description": "Tolerant PHP-to-AST parser designed for IDE usage scenarios", + "support": { + "issues": "https://github.com/microsoft/tolerant-php-parser/issues", + "source": "https://github.com/microsoft/tolerant-php-parser/tree/v0.1.1" + }, + "time": "2021-07-16T21:28:12+00:00" + }, + { + "name": "mridang/pmd-annotations", + "version": "0.0.2", + "source": { + "type": "git", + "url": "https://github.com/mridang/pmd-annotations.git", + "reference": "0fcaf0e31698c4d81ddb6fa2538edfda8123a7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mridang/pmd-annotations/zipball/0fcaf0e31698c4d81ddb6fa2538edfda8123a7a2", + "reference": "0fcaf0e31698c4d81ddb6fa2538edfda8123a7a2", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5" + }, + "bin": [ + "pmd2pr" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mridang Agarwalla" + } + ], + "description": "Turns PMD style XML reports into Github pull-request annotations via the Checks API. This script is meant for use within your Github Action.", + "keywords": [ + "actions", + "annotations", + "github", + "pmd" + ], + "support": { + "issues": "https://github.com/mridang/pmd-annotations/issues", + "source": "https://github.com/mridang/pmd-annotations/tree/0.0.2" + }, + "funding": [ + { + "url": "https://github.com/mridang", + "type": "github" + } + ], + "time": "2020-03-31T08:41:21+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2022-03-03T13:19:32+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d", + "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v4.0.0" + }, + "time": "2020-12-01T19:48:11+00:00" + }, + { + "name": "pdepend/pdepend", + "version": "2.10.3", + "source": { + "type": "git", + "url": "https://github.com/pdepend/pdepend.git", + "reference": "da3166a06b4a89915920a42444f707122a1584c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/da3166a06b4a89915920a42444f707122a1584c9", + "reference": "da3166a06b4a89915920a42444f707122a1584c9", + "shasum": "" + }, + "require": { + "php": ">=5.3.7", + "symfony/config": "^2.3.0|^3|^4|^5|^6.0", + "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0", + "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0|^1.2.3", + "gregwar/rst": "^1.0", + "phpunit/phpunit": "^4.8.36|^5.7.27", + "squizlabs/php_codesniffer": "^2.0.0" + }, + "bin": [ + "src/bin/pdepend" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PDepend\\": "src/main/php/PDepend" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Official version of pdepend to be handled with Composer", + "support": { + "issues": "https://github.com/pdepend/pdepend/issues", + "source": "https://github.com/pdepend/pdepend/tree/2.10.3" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend", + "type": "tidelift" + } + ], + "time": "2022-02-23T07:53:09+00:00" + }, + { + "name": "phan/phan", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/phan/phan.git", + "reference": "42ee10d1d456d4c26b72035a8051487c864d712b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phan/phan/zipball/42ee10d1d456d4c26b72035a8051487c864d712b", + "reference": "42ee10d1d456d4c26b72035a8051487c864d712b", + "shasum": "" + }, + "require": { + "composer/semver": "^1.4|^2.0|^3.0", + "composer/xdebug-handler": "^1.3.2|^2.0.0", + "ext-filter": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "felixfbecker/advanced-json-rpc": "^3.0.4", + "microsoft/tolerant-php-parser": "^0.1.0", + "netresearch/jsonmapper": "^1.6.0|^2.0|^3.0|^4.0", + "php": "^7.2.0|^8.0.0", + "sabre/event": "^5.0.3", + "symfony/console": "^3.2|^4.0|^5.0", + "symfony/polyfill-mbstring": "^1.11.0", + "symfony/polyfill-php80": "^1.20.0", + "tysonandre/var_representation_polyfill": "^0.0.2|^0.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.0" + }, + "suggest": { + "ext-ast": "Needed for parsing ASTs (unless --use-fallback-parser is used). 1.0.1+ is needed, 1.0.14+ is recommended.", + "ext-iconv": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8", + "ext-igbinary": "Improves performance of polyfill when ext-ast is unavailable", + "ext-mbstring": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8", + "ext-tokenizer": "Needed for fallback/polyfill parser support and file/line-based suppressions.", + "ext-var_representation": "Suggested for converting values to strings in issue messages" + }, + "bin": [ + "phan", + "phan_client", + "tocheckstyle" + ], + "type": "project", + "autoload": { + "psr-4": { + "Phan\\": "src/Phan" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tyson Andre" + }, + { + "name": "Rasmus Lerdorf" + }, + { + "name": "Andrew S. Morrison" + } + ], + "description": "A static analyzer for PHP", + "keywords": [ + "analyzer", + "php", + "static" + ], + "support": { + "issues": "https://github.com/phan/phan/issues", + "source": "https://github.com/phan/phan/tree/5.3.0" + }, + "time": "2021-11-13T16:53:42+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phing/phing", + "version": "2.17.2", + "source": { + "type": "git", + "url": "https://github.com/phingofficial/phing.git", + "reference": "8b8cee3eb12c24502fc4c227ac5889746248a140" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phingofficial/phing/zipball/8b8cee3eb12c24502fc4c227ac5889746248a140", + "reference": "8b8cee3eb12c24502fc4c227ac5889746248a140", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "ext-pdo_sqlite": "*", + "mikey179/vfsstream": "^1.6", + "pdepend/pdepend": "2.x", + "pear/archive_tar": "1.4.x", + "pear/http_request2": "dev-trunk", + "pear/net_growl": "dev-trunk", + "pear/pear-core-minimal": "1.10.1", + "pear/versioncontrol_git": "@dev", + "pear/versioncontrol_svn": "~0.5", + "phpdocumentor/phpdocumentor": "2.x", + "phploc/phploc": "~2.0.6", + "phpmd/phpmd": "~2.2", + "phpunit/phpunit": ">=3.7", + "sebastian/git": "~1.0", + "sebastian/phpcpd": "2.x", + "siad007/versioncontrol_hg": "^1.0", + "simpletest/simpletest": "^1.1", + "squizlabs/php_codesniffer": "~2.2", + "symfony/yaml": "^2.8 || ^3.1 || ^4.0" + }, + "suggest": { + "pdepend/pdepend": "PHP version of JDepend", + "pear/archive_tar": "Tar file management class", + "pear/versioncontrol_git": "A library that provides OO interface to handle Git repository", + "pear/versioncontrol_svn": "A simple OO-style interface for Subversion, the free/open-source version control system", + "phpdocumentor/phpdocumentor": "Documentation Generator for PHP", + "phploc/phploc": "A tool for quickly measuring the size of a PHP project", + "phpmd/phpmd": "PHP version of PMD tool", + "phpunit/php-code-coverage": "Library that provides collection, processing, and rendering functionality for PHP code coverage information", + "phpunit/phpunit": "The PHP Unit Testing Framework", + "sebastian/phpcpd": "Copy/Paste Detector (CPD) for PHP code", + "siad007/versioncontrol_hg": "A library for interfacing with Mercurial repositories.", + "tedivm/jshrink": "Javascript Minifier built in PHP" + }, + "bin": [ + "bin/phing" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.16.x-dev" + } + }, + "autoload": { + "classmap": [ + "classes/phing/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "classes" + ], + "license": [ + "LGPL-3.0-only" + ], + "authors": [ + { + "name": "Michiel Rook", + "email": "mrook@php.net" + }, + { + "name": "Phing Community", + "homepage": "https://www.phing.info/trac/wiki/Development/Contributors" + } + ], + "description": "PHing Is Not GNU make; it's a PHP project build system or build tool based on Apache Ant.", + "homepage": "https://www.phing.info/", + "keywords": [ + "build", + "phing", + "task", + "tool" + ], + "support": { + "irc": "irc://irc.freenode.net/phing", + "issues": "https://www.phing.info/trac/report", + "source": "https://github.com/phingofficial/phing/tree/2.17.2" + }, + "funding": [ + { + "url": "https://github.com/mrook", + "type": "github" + }, + { + "url": "https://github.com/siad007", + "type": "github" + }, + { + "url": "https://www.patreon.com/michielrook", + "type": "patreon" + } + ], + "time": "2022-02-09T09:50:58+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + }, + "time": "2021-10-19T17:43:47+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "77a32518733312af16a44300404e945338981de3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", + "reference": "77a32518733312af16a44300404e945338981de3", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" + }, + "time": "2022-03-15T21:29:03+00:00" + }, + { + "name": "phpmd/phpmd", + "version": "2.12.0", + "source": { + "type": "git", + "url": "https://github.com/phpmd/phpmd.git", + "reference": "c0b678ba71902f539c27c14332aa0ddcf14388ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/c0b678ba71902f539c27c14332aa0ddcf14388ec", + "reference": "c0b678ba71902f539c27c14332aa0ddcf14388ec", + "shasum": "" + }, + "require": { + "composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0", + "ext-xml": "*", + "pdepend/pdepend": "^2.10.3", + "php": ">=5.3.9" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0 || ^1.3.2", + "ext-json": "*", + "ext-simplexml": "*", + "gregwar/rst": "^1.0", + "mikey179/vfsstream": "^1.6.8", + "phpunit/phpunit": "^4.8.36 || ^5.7.27", + "squizlabs/php_codesniffer": "^2.0" + }, + "bin": [ + "src/bin/phpmd" + ], + "type": "library", + "autoload": { + "psr-0": { + "PHPMD\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Manuel Pichler", + "email": "github@manuel-pichler.de", + "homepage": "https://github.com/manuelpichler", + "role": "Project Founder" + }, + { + "name": "Marc Würth", + "email": "ravage@bluewin.ch", + "homepage": "https://github.com/ravage84", + "role": "Project Maintainer" + }, + { + "name": "Other contributors", + "homepage": "https://github.com/phpmd/phpmd/graphs/contributors", + "role": "Contributors" + } + ], + "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.", + "homepage": "https://phpmd.org/", + "keywords": [ + "mess detection", + "mess detector", + "pdepend", + "phpmd", + "pmd" + ], + "support": { + "irc": "irc://irc.freenode.org/phpmd", + "issues": "https://github.com/phpmd/phpmd/issues", + "source": "https://github.com/phpmd/phpmd/tree/2.12.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd", + "type": "tidelift" + } + ], + "time": "2022-03-24T13:33:01+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", + "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2 || ~8.0, <8.2", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0 || ^7.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" + }, + "time": "2021-12-08T12:19:24+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.15", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.13.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-03-07T09:28:20+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.5.20", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/12bc8879fb65aef2138b26fc633cb1e3620cffba", + "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpspec/prophecy": "^1.12.1", + "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.5", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.3", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.0", + "sebastian/version": "^3.0.2" + }, + "require-dev": { + "ext-pdo": "*", + "phpspec/prophecy-phpunit": "^2.0.1" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.20" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-04-01T12:37:26+00:00" + }, + { + "name": "sabre/event", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "d7da22897125d34d7eddf7977758191c06a74497" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/d7da22897125d34d7eddf7977758191c06a74497", + "reference": "d7da22897125d34d7eddf7977758191c06a74497", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "type": "library", + "autoload": { + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ], + "psr-4": { + "Sabre\\Event\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/event/issues", + "source": "https://github.com/fruux/sabre-event" + }, + "time": "2021-11-04T06:51:17+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:49:45+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:10:38+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-04-03T09:37:03+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-11-11T14:18:36+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-14T08:28:10+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", "source": { "type": "git", - "url": "https://github.com/Nosto/php-sdk.git", - "reference": "813ae14c2a70d7ed6ed5ba8260feda558b6d1963" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nosto/php-sdk/zipball/813ae14c2a70d7ed6ed5ba8260feda558b6d1963", - "reference": "813ae14c2a70d7ed6ed5ba8260feda558b6d1963", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", "shasum": "" }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, "require-dev": { - "codeception/aspect-mock": "0.5.3", - "codeception/codeception": "2.0.0", - "codeception/specify": "0.4.1" + "phpunit/phpunit": "^9.3" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "description": "PHP SDK for developing Nosto modules for e-commerce platforms", + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/phpcpd", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpcpd.git", + "reference": "f3683aa0db2e8e09287c2bb33a595b2873ea9176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpcpd/zipball/f3683aa0db2e8e09287c2bb33a595b2873ea9176", + "reference": "f3683aa0db2e8e09287c2bb33a595b2873ea9176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0", + "phpunit/php-timer": "^5.0", + "sebastian/cli-parser": "^1.0", + "sebastian/version": "^3.0" + }, + "bin": [ + "phpcpd" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Copy/Paste Detector (CPD) for PHP code.", + "homepage": "https://github.com/sebastianbergmann/phpcpd", + "support": { + "issues": "https://github.com/sebastianbergmann/phpcpd/issues", + "source": "https://github.com/sebastianbergmann/phpcpd/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-12-07T05:39:23+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:17:30+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-03-15T09:54:48+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.6.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2021-12-12T21:44:58+00:00" + }, + { + "name": "staabm/annotate-pull-request-from-checkstyle", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/staabm/annotate-pull-request-from-checkstyle.git", + "reference": "0a4a54c13d3db7e389981e866d20c3db03776acb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/annotate-pull-request-from-checkstyle/zipball/0a4a54c13d3db7e389981e866d20c3db03776acb", + "reference": "0a4a54c13d3db7e389981e866d20c3db03776acb", + "shasum": "" + }, + "require": { + "ext-libxml": "*", + "ext-simplexml": "*", + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16.1" + }, + "bin": [ + "cs2pr" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Markus Staab" + } + ], + "support": { + "issues": "https://github.com/staabm/annotate-pull-request-from-checkstyle/issues", + "source": "https://github.com/staabm/annotate-pull-request-from-checkstyle/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2022-01-03T09:04:06+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + }, + { + "name": "tysonandre/var_representation_polyfill", + "version": "0.1.1", + "source": { + "type": "git", + "url": "https://github.com/TysonAndre/var_representation_polyfill.git", + "reference": "0a942e74e18af5514749895507bc6ca7ab96399a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TysonAndre/var_representation_polyfill/zipball/0a942e74e18af5514749895507bc6ca7ab96399a", + "reference": "0a942e74e18af5514749895507bc6ca7ab96399a", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.2.0|^8.0.0" + }, + "require-dev": { + "phan/phan": "^4.0", + "phpunit/phpunit": "^8.5.0" + }, + "suggest": { + "ext-var_representation": "*" + }, + "type": "library", + "autoload": { + "files": [ + "src/var_representation.php" + ], + "psr-4": { + "VarRepresentation\\": "src/VarRepresentation" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tyson Andre" + } + ], + "description": "Polyfill for var_representation: convert a variable to a string in a way that fixes the shortcomings of var_export", + "keywords": [ + "var_export", + "var_representation" + ], + "support": { + "issues": "https://github.com/TysonAndre/var_representation_polyfill/issues", + "source": "https://github.com/TysonAndre/var_representation_polyfill/tree/0.1.1" + }, + "time": "2021-08-16T00:12:50+00:00" + }, + { + "name": "webonyx/graphql-php", + "version": "v0.13.9", + "source": { + "type": "git", + "url": "https://github.com/webonyx/graphql-php.git", + "reference": "d9a94fddcad0a35d4bced212b8a44ad1bc59bdf3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/d9a94fddcad0a35d4bced212b8a44ad1bc59bdf3", + "reference": "d9a94fddcad0a35d4bced212b8a44ad1bc59bdf3", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^7.1||^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpbench/phpbench": "^0.14.0", + "phpstan/phpstan": "^0.11.4", + "phpstan/phpstan-phpunit": "^0.11.0", + "phpstan/phpstan-strict-rules": "^0.11.0", + "phpunit/phpcov": "^5.0", + "phpunit/phpunit": "^7.2", + "psr/http-message": "^1.0", + "react/promise": "2.*" + }, + "suggest": { + "psr/http-message": "To use standard GraphQL server", + "react/promise": "To leverage async resolving on React PHP platform" + }, + "type": "library", + "autoload": { + "psr-4": { + "GraphQL\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP port of GraphQL reference implementation", + "homepage": "https://github.com/webonyx/graphql-php", + "keywords": [ + "api", + "graphql" + ], + "support": { + "issues": "https://github.com/webonyx/graphql-php/issues", + "source": "https://github.com/webonyx/graphql-php/tree/0.13.x" + }, + "funding": [ + { + "url": "https://opencollective.com/webonyx-graphql-php", + "type": "open_collective" + } + ], + "time": "2020-07-02T05:49:25+00:00" + }, + { + "name": "yotpo/module-review", + "version": "2.9.2", + "source": { + "type": "git", + "url": "https://github.com/YotpoLtd/magento2-module-yotpo-reviews.git", + "reference": "b9a56557ed8a9533b3f13ec54044d12d83e23b92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/YotpoLtd/magento2-module-yotpo-reviews/zipball/b9a56557ed8a9533b3f13ec54044d12d83e23b92", + "reference": "b9a56557ed8a9533b3f13ec54044d12d83e23b92", + "shasum": "" + }, + "require": { + "magento/framework": ">=101.0.0", + "php": "~5.6.0|^7.0" + }, + "type": "magento2-module", + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Yotpo\\Yotpo\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "Yotpo Reviews extension for Magento2", "support": { - "source": "https://github.com/Nosto/php-sdk/tree/2.4.1", - "issues": "https://github.com/Nosto/php-sdk/issues" + "issues": "https://github.com/YotpoLtd/magento2-module-yotpo-reviews/issues", + "source": "https://github.com/YotpoLtd/magento2-module-yotpo-reviews/tree/master" }, - "time": "2016-02-10 13:44:03" + "time": "2019-11-14T09:42:06+00:00" } ], - "packages-dev": [], "aliases": [], - "minimum-stability": "dev", + "minimum-stability": "stable", "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "~5.5.0|~5.6.0" + "php": ">=7.4.0", + "ext-json": "*" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "2.1.0" } diff --git a/default.conf b/default.conf new file mode 100644 index 000000000..5665e29ec --- /dev/null +++ b/default.conf @@ -0,0 +1,14 @@ + + ServerAdmin mage-admin@localhost + DocumentRoot /var/www/html + DirectoryIndex index.php index.html index.htm + + Require all granted + Options FollowSymLinks MultiViews + AllowOverride FileInfo AuthConfig Limit Indexes Options=All,MultiViews + + AddDefaultCharset UTF-8 + SetOutputFilter DEFLATE + ErrorLog /var/log/apache2/error.log + CustomLog /var/log/apache2/access.log combined + diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 000000000..159f4abff --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +if [ "$1" != "" ]; then + exec "$@" +else + exec supervisord -c /etc/supervisord.conf +fi diff --git a/etc/acl.xml b/etc/acl.xml index be6b85fb5..6a9a86883 100755 --- a/etc/acl.xml +++ b/etc/acl.xml @@ -1,38 +1,58 @@ - + - - + + + + + + + + + diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml new file mode 100644 index 000000000..4513320ec --- /dev/null +++ b/etc/adminhtml/di.xml @@ -0,0 +1,46 @@ + + + + + + + + Nosto\Tagging\Model\System\Message\Notification\InvalidAccount + + + + + diff --git a/etc/adminhtml/events.xml b/etc/adminhtml/events.xml index ac7a1074e..7cafc8a47 100644 --- a/etc/adminhtml/events.xml +++ b/etc/adminhtml/events.xml @@ -1,36 +1,50 @@ - - - + + + + - - + + diff --git a/etc/adminhtml/menu.xml b/etc/adminhtml/menu.xml index b9015481a..98d3b4987 100755 --- a/etc/adminhtml/menu.xml +++ b/etc/adminhtml/menu.xml @@ -1,34 +1,48 @@ - + - - + + \ No newline at end of file diff --git a/etc/adminhtml/routes.xml b/etc/adminhtml/routes.xml index e00d280a3..c73c54ed8 100755 --- a/etc/adminhtml/routes.xml +++ b/etc/adminhtml/routes.xml @@ -1,32 +1,42 @@ - + diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml new file mode 100644 index 000000000..eecce215c --- /dev/null +++ b/etc/adminhtml/system.xml @@ -0,0 +1,297 @@ + + + + + + +
+ + service + Nosto_Tagging::config_nosto + + + + + Select the image version that you would like to use for the + recommendation thumbnails + + Nosto\Tagging\Model\Config\Source\Image + + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + + + Select the attribute that you would like to use as your brand + attribute. Tagging the brand allows you to use brand data as filtering + conditions filtering in recommendations. + + Nosto\Tagging\Model\Config\Source\Brand + + + + Select the attribute that you would like to use as your supplier-cost + attribute. Tagging the supplier-cost enables us to deduce the margin and + allows you to use margin data as filtering conditions in recommendations. + + Nosto\Tagging\Model\Config\Source\Margin + + + + Select the attribute that you would like to use as your GTIN + attribute. + + Nosto\Tagging\Model\Config\Source\Gtin + + + + Select the attribute that you would like to use as your Google Category + attribute. + + Nosto\Tagging\Model\Config\Source\GoogleCategory + + + + + Feature flags to enable or disable optional tagging data. Toggling any of + these fields will require a full catalog reindex. Please contact a customer + service representative of Nosto to request a full catalog reindex. + + + + Warning! Enabling variation data + tagging may minutely impact performance.]]> + + Magento\Config\Model\Config\Source\Yesno + + + + Send attributes from product attribute set to Nosto as custom fields + + Magento\Config\Model\Config\Source\Yesno + + + + Enable tagging alternate images data for use in recommendations. + + Magento\Config\Model\Config\Source\Yesno + + + + Enable tagging rating and review data for use in recommendations. + + Nosto\Tagging\Model\Config\Source\Ratings + + + + + Warning! Enabling variation data + tagging may minutely impact performance.]]> + + Magento\Config\Model\Config\Source\Yesno + + + + Warning! Disabling this feature may + lead to products with incorrect pricing and availability data to be displayed + in recommendations.]]> + + Magento\Config\Model\Config\Source\Yesno + + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + + + Nosto\Tagging\Model\Config\Source\Memory + + + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + Choose attributes that will be added to tags when present + + + + Choose attributes that will be added to tags 1 when present + + Nosto\Tagging\Model\Config\Source\Tags + + + + Choose attributes that will be added to tag 2 when present + + Nosto\Tagging\Model\Config\Source\Tags + + + + Choose attributes that will be added to tag 3 when present + + Nosto\Tagging\Model\Config\Source\Tags + + + + + + + Set this to "Exchange rates" if your store uses Magento's exchange rates. If the store view uses only one currency set this to + "Single currency". If you have a custom pricing handling set this to "Disabled" and Nosto will not try to do any currency conversions. + + Nosto\Tagging\Model\Config\Source\MultiCurrency + Nosto\Tagging\Model\Config\Backend\MultiCurrency + + + + disabled + + + Warning! It cannot be enabled if multi-currency is enabled.]]> + + Magento\Config\Model\Config\Source\Yesno + + + + + + + Nosto\Tagging\Block\Adminhtml\Form\Field\Tokens + + +
+
+
diff --git a/etc/cache.xml b/etc/cache.xml new file mode 100644 index 000000000..a59d2d84b --- /dev/null +++ b/etc/cache.xml @@ -0,0 +1,42 @@ + + + + + + + Cache for Nosto product data + + \ No newline at end of file diff --git a/etc/communication.xml b/etc/communication.xml new file mode 100644 index 000000000..bbbd74187 --- /dev/null +++ b/etc/communication.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/etc/config.xml b/etc/config.xml new file mode 100644 index 000000000..8848eaa1d --- /dev/null +++ b/etc/config.xml @@ -0,0 +1,70 @@ + + + + + + + + + image + 0 + + + 1 + + + 1 + 1 + 1 + 1 + 0 + 1 + 0 + 1 + 50 + 0 + 0 + 1 + 0 + + + undefined + 0 + + + + diff --git a/etc/crontab.xml b/etc/crontab.xml new file mode 100644 index 000000000..3e9f51951 --- /dev/null +++ b/etc/crontab.xml @@ -0,0 +1,45 @@ + + + + + + + + 4 * * * * + + + diff --git a/etc/csp_whitelist.xml b/etc/csp_whitelist.xml new file mode 100644 index 000000000..37fdfd386 --- /dev/null +++ b/etc/csp_whitelist.xml @@ -0,0 +1,41 @@ + + + + + + *.nosto.com + *.nos.to + + + + + *.nosto.com + *.nos.to + + + + + *.nosto.com + *.nos.to + + + + + *.nosto.com + *.nos.to + + + + + *.nosto.com + *.nos.to + + + + + *.nosto.com + *.nos.to + + + + diff --git a/etc/db_schema.xml b/etc/db_schema.xml new file mode 100644 index 000000000..515843412 --- /dev/null +++ b/etc/db_schema.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ +
diff --git a/etc/di.xml b/etc/di.xml index b769866b1..cee504e79 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -1,10 +1,257 @@ - - + ~ Copyright (c) 2020, Nosto Solutions Ltd + ~ All rights reserved. + ~ + ~ Redistribution and use in source and binary forms, with or without modification, + ~ are permitted provided that the following conditions are met: + ~ + ~ 1. Redistributions of source code must retain the above copyright notice, + ~ this list of conditions and the following disclaimer. + ~ + ~ 2. Redistributions in binary form must reproduce the above copyright notice, + ~ this list of conditions and the following disclaimer in the documentation + ~ and/or other materials provided with the distribution. + ~ + ~ 3. Neither the name of the copyright holder nor the names of its contributors + ~ may be used to endorse or promote products derived from this software without + ~ specific prior written permission. + ~ + ~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ~ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + ~ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + ~ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ~ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + ~ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + ~ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ~ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ~ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + ~ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ~ + ~ @author Nosto Solutions Ltd + ~ @copyright 2020 Nosto Solutions Ltd + ~ @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + ~ + --> + + + + + + + + + + + + + + + + + + + + + + + + + Nosto\Tagging\Helper\Account\Proxy + Nosto\Tagging\Helper\Scope\Proxy + + + + + Nosto\Tagging\Helper\Account\Proxy + Nosto\Tagging\Helper\Scope\Proxy + Magento\Framework\App\Config\Storage\Writer\Proxy + Nosto\Tagging\Helper\Cache\Proxy + + + + + Magento\Customer\Model\CustomerFactory\Proxy + + + + + nosto + + Magento\Framework\Logger\Handler\System + Magento\Framework\Logger\Handler\Debug + + + + + + 1 + + + + + + + Nosto\Tagging\Console\Command\NostoAccountConnectCommand + + + Nosto\Tagging\Console\Command\NostoAccountRemoveCommand + + + Nosto\Tagging\Console\Command\NostoGenerateCustomerReferenceCommand + + + + + + + + + + Nosto\Tagging\Helper\Account\Proxy + Nosto\Tagging\Helper\Data\Proxy + + + + + Magento\Customer\Model\Session\Proxy + + + + + + 604800 + + + + + + Nosto\Tagging\Model\Service\Product\Attribute\DefaultAttributeService + + 100 + + + + + + Nosto\Tagging\Model\Service\Stock\Provider\DefaultStockProvider + + 1000 + + + + + + Nosto\Tagging\Model\Service\Product\Category\DefaultCategoryService + + + + + + + Nosto\Tagging\Model\Service\Product\DefaultProductService + + + + + + + Nosto\Tagging\Model\Service\Product\CachingProductService + + + + + + + Nosto\Tagging\Model\Service\Product\SanitizingProductService + + + + + + + Nosto\Tagging\Model\Service\Product\CachingProductService + + + + + + + + Nosto\Tagging\Model\Indexer\Dimensions\Queue\ModeSwitcher + + + Nosto\Tagging\Model\Indexer\Dimensions\QueueProcessor\ModeSwitcher + + + + + + + + nosto_index_product_queue + nosto_index_product_queue_processor + + + + + + + Nosto\Tagging\Model\Indexer\Dimensions\StoreDimensionProvider + + + + + + + Nosto\Tagging\Model\Indexer\Dimensions\StoreDimensionProvider + + + + + + + Nosto\Tagging\Model\Service\Sync\Upsert\AsyncBulkConsumer + + + + + + + Nosto\Tagging\Model\Service\Sync\Delete\AsyncBulkConsumer + + + + + + + Nosto\Tagging\Model\Service\Product\DefaultProductService + + 50 + 60 + + + + + 100 + + + + + + Nosto\Tagging\Model\Service\Sync\Upsert\AsyncBulkPublisher + + + Nosto\Tagging\Model\Service\Sync\Delete\AsyncBulkPublisher + + 1000 + 8 + + + + + 500 + + diff --git a/etc/events.xml b/etc/events.xml index 815dd1ae5..79d76cdfe 100644 --- a/etc/events.xml +++ b/etc/events.xml @@ -1,33 +1,64 @@ - + - + + + + + + + + + + + + + + + + + + + + + + diff --git a/etc/frontend/di.xml b/etc/frontend/di.xml index 17815eb6b..eac4b3ec0 100644 --- a/etc/frontend/di.xml +++ b/etc/frontend/di.xml @@ -1,17 +1,52 @@ - + ~ Copyright (c) 2020, Nosto Solutions Ltd + ~ All rights reserved. + ~ + ~ Redistribution and use in source and binary forms, with or without modification, + ~ are permitted provided that the following conditions are met: + ~ + ~ 1. Redistributions of source code must retain the above copyright notice, + ~ this list of conditions and the following disclaimer. + ~ + ~ 2. Redistributions in binary form must reproduce the above copyright notice, + ~ this list of conditions and the following disclaimer in the documentation + ~ and/or other materials provided with the distribution. + ~ + ~ 3. Neither the name of the copyright holder nor the names of its contributors + ~ may be used to endorse or promote products derived from this software without + ~ specific prior written permission. + ~ + ~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ~ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + ~ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + ~ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ~ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + ~ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + ~ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ~ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ~ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + ~ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ~ + ~ @author Nosto Solutions Ltd + ~ @copyright 2020 Nosto Solutions Ltd + ~ @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + ~ + --> + + + Nosto\Tagging\CustomerData\CustomerTagging Nosto\Tagging\CustomerData\CartTagging + Nosto\Tagging\CustomerData\ActiveVariationTagging + + + diff --git a/etc/frontend/routes.xml b/etc/frontend/routes.xml index 082891efc..a8b6d7e7d 100755 --- a/etc/frontend/routes.xml +++ b/etc/frontend/routes.xml @@ -1,35 +1,45 @@ - + - + \ No newline at end of file diff --git a/etc/frontend/sections.xml b/etc/frontend/sections.xml index 14a9ec06d..67c77680a 100644 --- a/etc/frontend/sections.xml +++ b/etc/frontend/sections.xml @@ -1,17 +1,48 @@ + ~ Copyright (c) 2020, Nosto Solutions Ltd + ~ All rights reserved. + ~ + ~ Redistribution and use in source and binary forms, with or without modification, + ~ are permitted provided that the following conditions are met: + ~ + ~ 1. Redistributions of source code must retain the above copyright notice, + ~ this list of conditions and the following disclaimer. + ~ + ~ 2. Redistributions in binary form must reproduce the above copyright notice, + ~ this list of conditions and the following disclaimer in the documentation + ~ and/or other materials provided with the distribution. + ~ + ~ 3. Neither the name of the copyright holder nor the names of its contributors + ~ may be used to endorse or promote products derived from this software without + ~ specific prior written permission. + ~ + ~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ~ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + ~ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + ~ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ~ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + ~ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + ~ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ~ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ~ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + ~ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ~ + ~ @author Nosto Solutions Ltd + ~ @copyright 2020 Nosto Solutions Ltd + ~ @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + ~ + --> + + + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd">
+
diff --git a/etc/indexer.xml b/etc/indexer.xml new file mode 100644 index 000000000..ee9c8b336 --- /dev/null +++ b/etc/indexer.xml @@ -0,0 +1,45 @@ + + + + + Nosto Product Queue + Populates queue for product ids to be sent to Nosto + + + Nosto Product Queue Processor + Processes the update queue + + diff --git a/etc/module.xml b/etc/module.xml index 4db3eda6e..f4da127bd 100755 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,31 +1,41 @@ - - - \ No newline at end of file + + + diff --git a/etc/mview.xml b/etc/mview.xml new file mode 100644 index 000000000..fceca2af2 --- /dev/null +++ b/etc/mview.xml @@ -0,0 +1,73 @@ + + + + + + + +
+
+
+
+
+
+
+
+ + +
+ + +
+ + +
+
+ + +
+ + +
+ + + + + +
+ + + diff --git a/etc/queue.xml b/etc/queue.xml new file mode 100644 index 000000000..95666bc8a --- /dev/null +++ b/etc/queue.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + diff --git a/etc/queue_consumer.xml b/etc/queue_consumer.xml new file mode 100644 index 000000000..d6c596e3c --- /dev/null +++ b/etc/queue_consumer.xml @@ -0,0 +1,50 @@ + + + + + + + diff --git a/etc/queue_publisher.xml b/etc/queue_publisher.xml new file mode 100644 index 000000000..0d044bc71 --- /dev/null +++ b/etc/queue_publisher.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/etc/queue_topology.xml b/etc/queue_topology.xml new file mode 100644 index 000000000..8f70a8190 --- /dev/null +++ b/etc/queue_topology.xml @@ -0,0 +1,42 @@ + + + + + + + + + diff --git a/inspect.sh b/inspect.sh new file mode 100755 index 000000000..bbcfbd208 --- /dev/null +++ b/inspect.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker run --env GITHUB_WORKSPACE=/app --volume="$(pwd)"/.:/app docker.pkg.github.com/mridang/action-phpstorm/stormy:latest /app /app/.idea/inspectionProfiles/CI.xml /tmp v2 Inspection diff --git a/phan.php b/phan.php new file mode 100644 index 000000000..f45a11f0f --- /dev/null +++ b/phan.php @@ -0,0 +1,90 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +return [ + 'backward_compatibility_checks' => false, + 'signature-compatibility' => true, + 'progress-bar' => true, + 'simplify_ast' => false, + 'dead_code_detection' => false, + 'exclude_file_regex' => '@^vendor/.*/(tests|test|Tests|Test)/@', + 'directory_list' => [ + 'Api', + 'Block', + 'Console', + 'Controller', + 'Cron', + 'CustomerData', + 'Exception', + 'Helper', + 'Logger', + 'Model', + 'Observer', + 'Plugin', + 'Util', + 'vendor/vlucas', + 'vendor/nosto/php-sdk', + 'vendor/phpseclib', + 'vendor/magento', + 'vendor/monolog', + 'vendor/zendframework', + 'vendor/laminas', + 'vendor/psr', + 'vendor/symfony/console', + 'magento/generated' + ], + 'exclude_file_list' => [ + 'vendor/magento/laminas/laminas-validator/src/Hostname/Biz.php', + 'vendor/magento/laminas/laminas-validator/src/Hostname/Cn.php', + 'vendor/magento/laminas/laminas-validator/src/Hostname/Com.php', + 'vendor/magento/laminas/laminas-validator/src/Hostname/Jp.php', + 'vendor/magento/zendframework1/library/Zend/Validate/Hostname/Biz.php', + 'vendor/magento/zendframework1/library/Zend/Validate/Hostname/Cn.php', + 'vendor/magento/zendframework1/library/Zend/Validate/Hostname/Com.php', + 'vendor/magento/zendframework1/library/Zend/Validate/Hostname/Jp.php', + + ], + 'exclude_analysis_directory_list' => [ + 'vendor/' + ], + 'suppress_issue_types' => [ + 'PhanParamSignatureMismatch' + ], + "color_issue_messages_if_supported" => true, + 'plugins' => [ + 'vendor/drenso/phan-extensions/Plugin/DocComment/InlineVarPlugin.php' + ] +]; diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 000000000..5e0d501a7 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,41 @@ + + + + + Test/Unit + + + + + + Api + Block + Console + Controller + Cron + CustomerData + Exception + Helper + Logger + Model + Observer + Plugin + Util + Setup + view + + + diff --git a/registration.php b/registration.php index 9a3d3df43..780fac82e 100755 --- a/registration.php +++ b/registration.php @@ -1,34 +1,46 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ +use Dotenv\Dotenv; use Magento\Framework\Component\ComponentRegistrar; -ComponentRegistrar::register( - ComponentRegistrar::MODULE, - 'Nosto_Tagging', - __DIR__ -); +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Nosto_Tagging', __DIR__); + +if (file_exists(dirname(__FILE__) . DIRECTORY_SEPARATOR . '.env')) { + /** @noinspection PhpParamsInspection */ + $dotenv = new Dotenv(dirname(__FILE__)); // @codingStandardsIgnoreLine + $dotenv->overload(); +} diff --git a/ruleset.xml b/ruleset.xml index 9cba5f4c8..4f289356a 100644 --- a/ruleset.xml +++ b/ruleset.xml @@ -1,8 +1,81 @@ + + - The Magento coding standard. - - - + ./Api + ./Block + ./Console + ./Controller + ./Cron + ./CustomerData + ./Exception + ./Helper + ./Logger + ./Model + ./Observer + ./Plugin + ./Util + ./Setup + ./view + *.js + *.css + + The Magento coding standard. + + + + + + + + + + + + + + + + + + + + + + diff --git a/supervisord.conf b/supervisord.conf new file mode 100644 index 000000000..8c66c4f3c --- /dev/null +++ b/supervisord.conf @@ -0,0 +1,13 @@ +[supervisord] +nodaemon=true + +[program:mysql] +command =/usr/sbin/mysqld +autorestart=true + + +[program:apache2] +command=apachectl -D "FOREGROUND" -k start +redirect_stderr=true +autostart=true +autorestart=false diff --git a/view/adminhtml/layout/nosto_account_index.xml b/view/adminhtml/layout/nosto_account_index.xml index 445ad43a6..d9b9fa08e 100644 --- a/view/adminhtml/layout/nosto_account_index.xml +++ b/view/adminhtml/layout/nosto_account_index.xml @@ -1,42 +1,52 @@ - + + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - + + - + - \ No newline at end of file + diff --git a/view/adminhtml/requirejs-config.js b/view/adminhtml/requirejs-config.js index 31f86c4eb..59cd58514 100644 --- a/view/adminhtml/requirejs-config.js +++ b/view/adminhtml/requirejs-config.js @@ -1,35 +1,43 @@ - /* - * Magento + * Copyright (c) 2020, Nosto Solutions Ltd + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. * - * NOTICE OF LICENSE + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * - * This source file is subject to the Open Software License (OSL 3.0) - * that is bundled with this package in the file LICENSE.txt. - * It is also available through the world-wide-web at this URL: - * http://opensource.org/licenses/osl-3.0.php - * If you did not receive a copy of the license and are unable to - * obtain it through the world-wide-web, please send an email - * to license@magentocommerce.com so we can send you a copy immediately. + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. * - * DISCLAIMER + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * - * Do not edit or add to this file if you wish to upgrade Magento to newer - * versions in the future. If you wish to customize Magento for your - * needs please refer to http://www.magentocommerce.com for more information. + * @author Nosto Solutions Ltd + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ -var config = { - map: { - '*': { - iframe_handler: 'Nosto_Tagging/js/iframe_handler', - iframe_resizer: 'Nosto_Tagging/js/iframe_resizer' - } +const config = { + map: { + '*': { + iframe_handler: 'Nosto_Tagging/js/iframe_handler', + iframe_resizer: 'Nosto_Tagging/js/iframe_resizer' } -}; \ No newline at end of file + } +}; diff --git a/view/adminhtml/templates/config.phtml b/view/adminhtml/templates/config.phtml new file mode 100644 index 000000000..d8a728918 --- /dev/null +++ b/view/adminhtml/templates/config.phtml @@ -0,0 +1,48 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +use Nosto\Tagging\Block\Adminhtml\Account\Config; + +/** + * @var Config $block + */ +?> + + diff --git a/view/adminhtml/templates/iframe.phtml b/view/adminhtml/templates/iframe.phtml index 2520f7e7b..dd0b32ad2 100644 --- a/view/adminhtml/templates/iframe.phtml +++ b/view/adminhtml/templates/iframe.phtml @@ -1,39 +1,52 @@ - + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ /** - * @var \Nosto\Tagging\Block\Adminhtml\Account\Iframe $this + * @var Iframe $block */ ?> + + data-mage-init='escapeHtml(json_encode($block->getIframeConfig())); ?>'> diff --git a/view/adminhtml/templates/tokens.phtml b/view/adminhtml/templates/tokens.phtml new file mode 100644 index 000000000..9091d4aa0 --- /dev/null +++ b/view/adminhtml/templates/tokens.phtml @@ -0,0 +1,96 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +/** + * @var $block Nosto\Tagging\Block\Adminhtml\Form\Field\Tokens + */ +?> + + +
+ getAccountDetails(); ?> + +
+ escapeHtml(__('There is no account connected')) ?> +
+ +
+ + + + + getTokens() as $token): ?> + + + + + + +
+ escapeHtml(__('Account ID')); ?> + + +
+ escapeHtml($token->getName()) ?> Token + + +
+ + + diff --git a/view/adminhtml/web/js/iframe_handler.js b/view/adminhtml/web/js/iframe_handler.js index dcbd337aa..459fe05b3 100644 --- a/view/adminhtml/web/js/iframe_handler.js +++ b/view/adminhtml/web/js/iframe_handler.js @@ -1,146 +1,238 @@ +/* + * Copyright (c) 2020, Nosto Solutions Ltd + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @author Nosto Solutions Ltd + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + /*jshint browser:true*/ +// noinspection JSUnresolvedFunction define([ - 'jquery', - 'iframe_resizer' + 'jquery', + 'iframe_resizer' ], function ($) { - 'use strict'; + 'use strict'; - var TYPE_NEW_ACCOUNT = 'newAccount', - TYPE_CONNECT_ACCOUNT = 'connectAccount', - TYPE_SYNC_ACCOUNT = 'syncAccount', - TYPE_REMOVE_ACCOUNT = 'removeAccount'; + const TYPE_NEW_ACCOUNT = 'newAccount', + TYPE_CONNECT_ACCOUNT = 'connectAccount', + TYPE_SYNC_ACCOUNT = 'syncAccount', + TYPE_REMOVE_ACCOUNT = 'removeAccount'; - /** - * @type {Object} - */ - var settings = { - origin: '', - xhrParams: {}, - urls: { - createAccount: '', - connectAccount: '', - syncAccount: '', - deleteAccount: '' - }, - element: null - }; + /** + * @type {Object} + */ + const settings = { + origin: '', + xhrParams: {}, + urls: { + createAccount: '', + connectAccount: '', + syncAccount: '', + deleteAccount: '' + }, + element: null + }; + + /** + * Window.postMessage() event handler for catching messages from nosto. + * + * Supported messages must come from nosto.com and be formatted according + * to the following example: + * + * '[Nosto]{ 'type': 'the message action', 'params': {} }' + * + * @param {Object} event + */ + const receiveMessage = function (event) { + // If the message does not start with '[Nosto]', then it is not for us. + if (('' + event.data).substr(0, 7) !== '[Nosto]') { + return; + } + + // Check the origin to prevent cross-site scripting. + const originRegexp = new RegExp(settings.origin); + if (!originRegexp.test(event.origin)) { + console.warn('Requested URL does not matches iframe origin'); + return; + } + + const json = ('' + event.data).substr(7); + const data = JSON.parse(json); /** - * Window.postMessage() event handler for catching messages from nosto. - * - * Supported messages must come from nosto.com and be formatted according - * to the following example: - * - * '[Nosto]{ 'type': 'the message action', 'params': {} }' - * - * @param {Object} event + * @param {{redirect_url:string, success}} response */ - var receiveMessage = function (event) { - // Check the origin to prevent cross-site scripting. - var originRegexp = new RegExp(settings.origin); - if (!originRegexp.test(event.origin)) { - return; - } - // If the message does not start with '[Nosto]', then it is not for us. - if (('' + event.data).substr(0, 7) !== '[Nosto]') { - return; + switch (data.type) { + case TYPE_NEW_ACCOUNT: + // noinspection JSUnresolvedVariable + const post_data = {email: data.params.email}; + // noinspection JSUnresolvedVariable + if (data.params.details) { + // noinspection JSUnresolvedVariable + post_data.details = JSON.stringify(data.params.details); } + xhr(settings.urls.createAccount, { + data: post_data, + success: function (response) { + if (response.redirect_url) { + settings.element.src = response.redirect_url; + } + } + }); + break; - var json = ('' + event.data).substr(7); - var data = JSON.parse(json); - if (typeof data === 'object' && data.type) { - switch (data.type) { - case TYPE_NEW_ACCOUNT: - $.ajax({ - url: settings.urls.createAccount, - type: 'POST', - dataType: 'json', - data: $.extend(settings.xhrParams, {email: data.params.email}), - showLoader: false - }).done(function (response) { - if (response.redirect_url) { - settings.element.src = response.redirect_url; - } else { - throw new Error('Nosto: failed to handle account creation.'); - } - }); - break; - - case TYPE_CONNECT_ACCOUNT: - $.ajax({ - url: settings.urls.connectAccount, - type: 'POST', - dataType: 'json', - data: settings.xhrParams, - showLoader: false - }).done(function (response) { - if (response.redirect_url) { - if (response.success && response.success === true) { - window.location.href = response.redirect_url; - } else { - settings.element.src = response.redirect_url; - } - } else { - throw new Error('Nosto: failed to handle account connection.'); - } - }); - break; - - case TYPE_SYNC_ACCOUNT: - $.ajax({ - url: settings.urls.syncAccount, - type: 'POST', - dataType: 'json', - data: settings.xhrParams, - showLoader: false - }).done(function (response) { - if (response.redirect_url) { - if (response.success && response.success === true) { - window.location.href = response.redirect_url; - } else { - settings.element.src = response.redirect_url; - } - } else { - throw new Error('Nosto: failed to handle account sync.'); - } - }); - break; - - case TYPE_REMOVE_ACCOUNT: - $.ajax({ - url: settings.urls.deleteAccount, - type: 'POST', - dataType: 'json', - data: settings.xhrParams, - showLoader: false - }).done(function (response) { - if (response.redirect_url) { - settings.element.src = response.redirect_url; - } else { - throw new Error('Nosto: failed to handle account deletion.'); - } - }); - break; - - default: - throw new Error('Nosto: invalid postMessage `type`.'); + case TYPE_CONNECT_ACCOUNT: + xhr(settings.urls.connectAccount, { + success: function (response) { + if (response.success && response.redirect_url) { + window.location.href = response.redirect_url; + } else if (!response.success && response.redirect_url) { + settings.element.src = response.redirect_url; } - } - }; + } + }); + break; - /** - * @param {Object} config - * @param {Element} element - */ - return function (config, element) { - // Init the iframe re-sizer. - $(element).iFrameResize({heightCalculationMethod: 'bodyScroll'}); + case TYPE_SYNC_ACCOUNT: + xhr(settings.urls.syncAccount, { + success: function (response) { + if (response.success && response.redirect_url) { + window.location.href = response.redirect_url; + } else if (!response.success && response.redirect_url) { + settings.element.src = response.redirect_url; + } + } + }); + break; + + case TYPE_REMOVE_ACCOUNT: + xhr(settings.urls.deleteAccount, { + success: function (response) { + if (response.success && response.redirect_url) { + settings.element.src = response.redirect_url; + } + } + }); + break; + + default: + throw new Error("Nosto: invalid postMessage `type`."); + } + }; + + /** + * Creates a new XMLHttpRequest. + * + * Usage example: + * + * xhr("http://localhost/target.html", { + * "method": "POST", + * "data": {"key": "value"}, + * "success": function (response) { // handle success request } + * }); + * + * @param {String} url the url to call. + * @param {Object} params optional params. + */ + function xhr(url, params) { + const options = extendObject({ + method: "POST", + async: true, + data: {} + }, params); + // Always add the Magento form_key property for request authorization. + // noinspection JSUnresolvedVariable + options.data.form_key = window.FORM_KEY; + const oReq = new XMLHttpRequest(); + if (typeof options.success === "function") { + oReq.addEventListener("load", function (e) { + // noinspection JSUnresolvedVariable + options.success(JSON.parse(e.target.response)); + }, false); + } + oReq.open(options.method, decodeURIComponent(url), options.async); + oReq.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + oReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + oReq.send(buildQueryString(options.data)); + } - // Configure the iframe API. - $.extend(settings, config); - settings.element = element; + /** + * Extends a literal object with data from the other object. + * + * @param {Object} obj1 the object to extend. + * @param {Object} obj2 the object to extend from. + * @returns {Object} + */ + function extendObject(obj1, obj2) { + for (let key in obj2) { + if (obj2.hasOwnProperty(key)) { + obj1[key] = obj2[key]; + } + } + return obj1; + } - // Register event handler for window.postMessage() messages from nosto. - window.addEventListener('message', receiveMessage, false); + /** + * Builds a query string based on params. + * + * @param {Object} params the params to turn into a query string. + * @returns {string} the built query string. + */ + function buildQueryString(params) { + let queryString = ""; + for (let key in params) { + if (params.hasOwnProperty(key)) { + if (queryString !== "") { + queryString += "&"; + } + queryString += encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); + } } -}); \ No newline at end of file + return queryString; + } + + /** + * @param {Object} config + * @param {Element} element + */ + return function (config, element) { + // Init the iframe re-sizer. + $(element).iFrameResize({heightCalculationMethod: 'bodyScroll'}); + + // Configure the iframe API. + // noinspection JSCheckFunctionSignatures + $.extend(settings, config); + settings.element = element; + + // Register event handler for window.postMessage() messages from nosto. + window.addEventListener('message', receiveMessage, false); + } +}); diff --git a/view/frontend/layout/catalog_category_view.xml b/view/frontend/layout/catalog_category_view.xml index f7818947d..ea7b599b8 100644 --- a/view/frontend/layout/catalog_category_view.xml +++ b/view/frontend/layout/catalog_category_view.xml @@ -1,47 +1,61 @@ - + + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - - - - nostoId - nosto-page-category1 - + + + category + + + + + + nosto-page-category1 + - - - nostoId - nosto-page-category2 - + + + nosto-page-category2 + diff --git a/view/frontend/layout/catalog_product_view.xml b/view/frontend/layout/catalog_product_view.xml index 3e5088c2a..9e216ae0b 100644 --- a/view/frontend/layout/catalog_product_view.xml +++ b/view/frontend/layout/catalog_product_view.xml @@ -1,53 +1,70 @@ - + + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - - - - nostoId - nosto-page-product1 - + + + product + + + + + + + + + nosto-page-product1 + - - - nostoId - nosto-page-product2 - + + + nosto-page-product2 + - - - nostoId - nosto-page-product3 - + + + nosto-page-product3 + diff --git a/view/frontend/layout/catalogsearch_result_index.xml b/view/frontend/layout/catalogsearch_result_index.xml index ae413ddc6..e29cf5519 100644 --- a/view/frontend/layout/catalogsearch_result_index.xml +++ b/view/frontend/layout/catalogsearch_result_index.xml @@ -1,48 +1,62 @@ - + + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - - - - nostoId - nosto-page-search1 - + + + search + + + + + + nosto-page-search1 + - - - nostoId - nosto-page-search2 - + + + nosto-page-search2 + diff --git a/view/frontend/layout/checkout_cart_index.xml b/view/frontend/layout/checkout_cart_index.xml index e6f774780..eec9bda7c 100644 --- a/view/frontend/layout/checkout_cart_index.xml +++ b/view/frontend/layout/checkout_cart_index.xml @@ -1,52 +1,66 @@ - + + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - - - nostoId - nosto-page-cart1 - + + + cart + + + + + nosto-page-cart1 + - - - nostoId - nosto-page-cart2 - + + + nosto-page-cart2 + - - - nostoId - nosto-page-cart3 - + + + nosto-page-cart3 + diff --git a/view/frontend/layout/checkout_index_index.xml b/view/frontend/layout/checkout_index_index.xml new file mode 100644 index 000000000..3123dfcb1 --- /dev/null +++ b/view/frontend/layout/checkout_index_index.xml @@ -0,0 +1,61 @@ + + + + + + + + + + checkout + + + + + checkout-nosto-1 + + + + + checkout-nosto-2 + + + + + diff --git a/view/frontend/layout/checkout_onepage_success.xml b/view/frontend/layout/checkout_onepage_success.xml index 424adfcbd..7d26c7214 100644 --- a/view/frontend/layout/checkout_onepage_success.xml +++ b/view/frontend/layout/checkout_onepage_success.xml @@ -1,47 +1,61 @@ - + + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - - - - nostoId - thankyou-nosto-1 - + + + order + + + + + + thankyou-nosto-1 + - - - nostoId - thankyou-nosto-2 - + + + thankyou-nosto-2 + diff --git a/view/frontend/layout/cms_index_index.xml b/view/frontend/layout/cms_index_index.xml new file mode 100644 index 000000000..6c8f77dca --- /dev/null +++ b/view/frontend/layout/cms_index_index.xml @@ -0,0 +1,73 @@ + + + + + + + + + + front + + + + + frontpage-nosto-1 + + + + + frontpage-nosto-2 + + + + + frontpage-nosto-3 + + + + + frontpage-nosto-4 + + + + + \ No newline at end of file diff --git a/view/frontend/layout/cms_noroute_index.xml b/view/frontend/layout/cms_noroute_index.xml index fdbae5e89..7f9d5e805 100644 --- a/view/frontend/layout/cms_noroute_index.xml +++ b/view/frontend/layout/cms_noroute_index.xml @@ -1,52 +1,66 @@ - + + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - - - nostoId - notfound-nosto-1 - + + + notfound + + + + + notfound-nosto-1 + - - - nostoId - notfound-nosto-2 - + + + notfound-nosto-2 + - - - nostoId - notfound-nosto-3 - + + + notfound-nosto-3 + diff --git a/view/frontend/layout/default.xml b/view/frontend/layout/default.xml index 78c093dbd..c288f4837 100755 --- a/view/frontend/layout/default.xml +++ b/view/frontend/layout/default.xml @@ -1,40 +1,56 @@ - + + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - - - - - + + + + + + + + @@ -59,6 +75,18 @@ + + + + + + Nosto_Tagging/js/view/variation-tagging + + + + + \ No newline at end of file diff --git a/view/frontend/requirejs-config.js b/view/frontend/requirejs-config.js index 95be6335a..cf5cd770c 100644 --- a/view/frontend/requirejs-config.js +++ b/view/frontend/requirejs-config.js @@ -1,33 +1,43 @@ /* - * Magento + * Copyright (c) 2020, Nosto Solutions Ltd + * All rights reserved. * - * NOTICE OF LICENSE + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: * - * This source file is subject to the Open Software License (OSL 3.0) - * that is bundled with this package in the file LICENSE.txt. - * It is also available through the world-wide-web at this URL: - * http://opensource.org/licenses/osl-3.0.php - * If you did not receive a copy of the license and are unable to - * obtain it through the world-wide-web, please send an email - * to license@magentocommerce.com so we can send you a copy immediately. + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. * - * DISCLAIMER + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * - * Do not edit or add to this file if you wish to upgrade Magento to newer - * versions in the future. If you wish to customize Magento for your - * needs please refer to http://www.magentocommerce.com for more information. + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @author Nosto Solutions Ltd + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ -var config = { - map: { - '*': { - nostojs: 'Nosto_Tagging/js/nostojs' - } +const config = { + map: { + '*': { + nostojs: 'Nosto_Tagging/js/nostojs', + recobuy: 'Nosto_Tagging/js/recobuy' } -}; \ No newline at end of file + } +}; diff --git a/view/frontend/templates/addtocart.phtml b/view/frontend/templates/addtocart.phtml new file mode 100644 index 000000000..bc51e128b --- /dev/null +++ b/view/frontend/templates/addtocart.phtml @@ -0,0 +1,49 @@ + + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause + * + */ + +use Nosto\Tagging\Block\Addtocart; + +/** @var Addtocart $block */ +?> +
+ getBlockHtml('formkey') ?> +
+ + diff --git a/view/frontend/templates/cart.phtml b/view/frontend/templates/cart.phtml index b4bd35e8c..98222cdc3 100644 --- a/view/frontend/templates/cart.phtml +++ b/view/frontend/templates/cart.phtml @@ -1,28 +1,37 @@ + * @copyright 2020 Nosto Solutions Ltd + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause * - * @category Nosto - * @package Nosto_Tagging - * @author Nosto Solutions Ltd - * @copyright Copyright (c) 2013-2016 Nosto Solutions Ltd (http://www.nosto.com) - * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ /** @@ -30,22 +39,42 @@ * Magento's KnockoutJS logic. * * @see \Nosto\Tagging\CustomerData\CartTagging + * @var \Nosto\Tagging\Block\Knockout $block + * @noinspection PhpFullyQualifiedNameUsageInspection */ ?> -