From ed08999be7fe5c983e373071a3dc7ead5b945ea0 Mon Sep 17 00:00:00 2001 From: Georg Wurz Date: Wed, 17 Jul 2024 17:13:08 +0200 Subject: [PATCH 01/11] [VariantBundle] start implementation of variant creator --- .../Resources/public/pimcore/js/resource.js | 198 +++++++++++++++++ .../Controller/VariantController.php | 202 ++++++++++++++++++ .../Messenger/CreateVariantMessage.php | 60 ++++++ .../Handler/CreateVariantMessageHandler.php | 63 ++++++ .../Resources/config/pimcore/config.yml | 2 + .../Resources/config/pimcore/messenger.yml | 18 ++ .../Resources/config/pimcore/routing.yml | 13 +- .../Resources/config/services.yml | 20 +- .../Resources/translations/admin.de.yaml | 5 +- .../Resources/translations/admin.en.yaml | 4 +- .../Service/VariantGeneratorService.php | 107 ++++++++++ .../VariantGeneratorServiceInterface.php | 32 +++ .../Variant/Model/AttributeGroup.php | 11 + .../Variant/Model/AttributeGroupInterface.php | 2 + 14 files changed, 730 insertions(+), 7 deletions(-) create mode 100644 src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php create mode 100644 src/CoreShop/Bundle/VariantBundle/Messenger/CreateVariantMessage.php create mode 100644 src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php create mode 100755 src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/config.yml create mode 100755 src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/messenger.yml create mode 100644 src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php create mode 100644 src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorServiceInterface.php diff --git a/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js b/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js index b3401830f3..108adcf7b0 100644 --- a/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js +++ b/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js @@ -189,6 +189,204 @@ coreshop.core.resource = Class.create(coreshop.resource, { new coreshop.product.workflow.variantUnitDefinitionSolidifier(tab.data, tab.tab); }.bind(this, tab) }); + + const variantHandler = () => { + const store = Ext.create('Ext.data.TreeStore', { + proxy: { + type: 'ajax', + url: Routing.generate('coreshop_admin_variant_attributes', {id: tab.id}), + reader: { + type: 'json', + rootProperty: 'data' + }, + }, + sorters: [{ + property: 'sorting', + direction: 'ASC' + }] + }); + + store.on('load', (store, records, successful, operation, eOpts) => { + if(!store) { + console.error('no data found'); + } + }); + + const tree = Ext.create('Ext.tree.Panel', { + hideHeaders: true, + rootVisible: false, + store: store, + layout: 'fit', + }); + + const panel = new Ext.Panel({ + layout: 'fit', + header: false, + bodyStyle: "padding:10px", + border: false, + buttons: [ + { + text: t("apply"), + iconCls: "pimcore_icon_accept", + handler: function() { + const groupedAttributes = (tree.getView().getChecked()).reduce((acc, obj) => { + const groupId = obj.data.group_id; + if (!acc[groupId]) { + acc[groupId] = []; + } + acc[groupId].push(obj.data.id); + return acc; + }, {}); + + Ext.Ajax.request({ + url: Routing.generate('coreshop_admin_variant_generator'), + jsonData: { id: tab.id, attributes: groupedAttributes }, + method: 'POST', + success: function (response) { + var res = Ext.decode(response.responseText); + if (res.success === true) { + Ext.Msg.alert(t('success'), res.message); + window.destroy(); + } else { + Ext.Msg.alert(t('error'), res.message); + } + }.bind(this) + }); + } + }, + { + text: t("close"), + iconCls: "pimcore_icon_cancel", + handler: function() { + window.destroy(); + } + } + ], + frame: false, + items: [ + tree + ], + }); + + const window = new Ext.window.Window({ + closeAction: 'close', + height: 600, + width: 400, + layout: 'fit', + items: [ + panel + ], + modal: false, + plain: true, + title: t('coreshop.variant_generator.generate'), + }); + + window.show(); + }; + + productMoreButtons.push({ + text: t('coreshop.variant_generator.generate'), + scale: 'medium', + handler: variantHandler.bind(this, tab) + }); + + const variantHandler = () => { + const store = Ext.create('Ext.data.TreeStore', { + proxy: { + type: 'ajax', + url: Routing.generate('coreshop_admin_variant_attributes', {id: tab.id}), + reader: { + type: 'json', + rootProperty: 'data' + }, + }, + sorters: [{ + property: 'sorting', + direction: 'ASC' + }] + }); + + store.on('load', (store, records, successful, operation, eOpts) => { + // TODO + if(!store) { + console.error('no data found'); + } + }); + + const tree = Ext.create('Ext.tree.Panel', { + hideHeaders: true, + rootVisible: false, + store: store, + }); + + const panel = new Ext.Panel({ + autoscroll: true, + header: false, + bodyStyle: "padding:10px", + border: false, + buttons: [ + { + text: t("apply"), + iconCls: "pimcore_icon_accept", + handler: function() { + const checked = (tree.getView().getChecked()).map((checked) => { + return { + id: checked.data.id, + group_id: checked.data.group_id, + }; + }) + + Ext.Ajax.request({ + url: Routing.generate('coreshop_admin_variant_generator', { id: tab.id, attributes: checked }), + method: 'GET', + success: function (response) { + var res = Ext.decode(response.responseText); + if (res.success === true) { + //this.checkStatus(res); + } else { + Ext.Msg.alert(t('error'), res.message); + } + }.bind(this) + }); + } + }, + { + text: t("close"), + iconCls: "pimcore_icon_cancel", + handler: function() { + window.destroy(); + } + } + ], + frame: false, + items: [ + tree + ], + }); + + const window = new Ext.window.Window({ + autoscroll: true, + closeAction: 'close', + height: 400, + items: [ + panel + ], + layout: 'fit', + modal: false, + plain: true, + title: t('coreshop.variant_generator.generate'), + width: 560, + }); + + window.show(); + }; + + productMoreButtons.push({ + text: t('coreshop.variant_generator.generate'), + scale: 'medium', + //iconCls: 'coreshop_icon_product_unit', + handler: variantHandler.bind(this, tab) + }); } if (productMoreButtons.length === 0) { diff --git a/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php b/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php new file mode 100644 index 0000000000..2a3c6dfda5 --- /dev/null +++ b/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php @@ -0,0 +1,202 @@ +getParameterFromRequest($request, 'id'); + + if(!$id) { + throw new \InvalidArgumentException('no product id given'); + } + + $product = DataObject::getById($id); + + if(!$product instanceof ProductVariantAwareInterface) { + throw new NotFoundHttpException('no product found'); + } + + if (AbstractObject::OBJECT_TYPE_VARIANT === $product->getType()) { + $product = $product->getVariantParent(); + } + + $attributeGroups = $product->getAllowedAttributeGroups(); + + $data = array_map(static function(AttributeGroupInterface $group) { + return [ + 'text' => sprintf('%s (ID: %s)', $group->getKey(), $group->getId()), + 'sorting' => $group->getSorting(), + 'leaf' => false, + 'iconCls' => 'pimcore_icon_object', + 'data' => array_map(static function(AttributeInterface $attribute) use ($group) { + return [ + 'text' => sprintf('%s (ID: %s)', $attribute->getKey(), $attribute->getId()), + 'id' => $attribute->getId(), + 'group_id' => $group->getId(), + 'sorting' => $attribute->getSorting(), + 'leaf' => true, + 'checked' => false, + 'iconCls' => 'pimcore_icon_object', + ]; + }, $group->getAttributes()) + ]; + }, $attributeGroups); + + return $this->json( + [ + 'success' => true, + 'data' => $data + ] + ); + } + public function generateVariantsAction(Request $request) + { + $id = $this->getParameterFromRequest($request, 'id'); + $attributes = $this->getParameterFromRequest($request, 'attributes'); + + if(!$id) { + throw new \InvalidArgumentException('no product id given'); + } + + if(!$attributes) { + throw new \InvalidArgumentException('no attributes given'); + } + + $product = DataObject::getById($id); + + if(!$product instanceof ProductVariantAwareInterface) { + throw new NotFoundHttpException('no product found'); + } + + if (AbstractObject::OBJECT_TYPE_VARIANT === $product->getType()) { + $product = $product->getVariantParent(); + } + + $groupedAttributes = []; + foreach ($attributes as $attribute) { + $groupedAttributes[$attribute['group_id']][] = $attribute; + } + + $combinations = []; + $this->generateCombinations($groupedAttributes, [], 0, $combinations); + $this->generateVariants($combinations, $product); + + $data = []; + + return $this->json( + [ + 'success' => true, + 'data' => $data + ] + ); + } + + protected function generateVariants(array $combinations, ProductVariantAwareInterface $product): array + { + $variants = []; + foreach($combinations as $combinationAttribute) { + $attributes = array_map(static function($combination) { + return DataObject::getById($combination['id']); + }, $combinationAttribute); + + // TODO: search variant by attributes + $variants = DataObject\CoreShopProduct::getByAttributes(array_map(static function($attribute) { + + }, $attributes), 1); + + + // TODO + //$variants = $product->getChildren([DataObject::OBJECT_TYPE_VARIANT]); + //$variants->addConditionParam('attributes') + $variant = null; + + if(!$variant instanceof ProductVariantAwareInterface) { + $class = get_class($product); + /** + * @var ProductVariantAwareInterface $variant + */ + $variant = new $class(); + } + + $key = implode(' ', array_map(static function(AttributeInterface $attribute) { + return $attribute->getKey(); + }, $attributes)); + + foreach(Tool::getValidLanguages() as $language) { + $name = implode(' ', array_map(static function(AttributeInterface $attribute) { + return $attribute->getName(); + }, $attributes)); + + $variant->setName(sprintf('%s %s', $product->getName(), $name), $language); + } + + $variant->setKey($key); + $variant->setParent($product); + $variant->setPublished(false); + $variant->setType(DataObject::OBJECT_TYPE_VARIANT); + $variant->setAttributes($attributes); + $variant->save(); + $variants[] = $variant; + } + + return $variants; + } + + private function generateCombinations($groupedAttributes, $currentCombination, $groupIndex, &$combinations): void + { + if ($groupIndex >= count($groupedAttributes)) { + // Base case: reached the end of groups, add the combination to the result + $combinations[] = $currentCombination; + return; + } + + $currentGroup = array_values($groupedAttributes)[$groupIndex]; + + foreach ($currentGroup as $attribute) { + // Include the current attribute in the combination + $currentCombination[] = $attribute; + + // Recur to the next group + $this->generateCombinations($groupedAttributes, $currentCombination, $groupIndex + 1, $combinations); + + // Backtrack: remove the current attribute from the combination + array_pop($currentCombination); + } + } + +} diff --git a/src/CoreShop/Bundle/VariantBundle/Messenger/CreateVariantMessage.php b/src/CoreShop/Bundle/VariantBundle/Messenger/CreateVariantMessage.php new file mode 100644 index 0000000000..6fc4a27f00 --- /dev/null +++ b/src/CoreShop/Bundle/VariantBundle/Messenger/CreateVariantMessage.php @@ -0,0 +1,60 @@ +objectId; + } + + public function setObjectId(int $objectId): void + { + $this->objectId = $objectId; + } + + public function getAttributeIds(): array + { + return $this->attributeIds; + } + + public function setAttributeIds(array $attributeIds): void + { + $this->attributeIds = $attributeIds; + } + + public function getUserId(): ?int + { + return $this->userId; + } + + public function setUserId(?int $userId): void + { + $this->userId = $userId; + } + +} diff --git a/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php b/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php new file mode 100644 index 0000000000..e50546c7e8 --- /dev/null +++ b/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php @@ -0,0 +1,63 @@ +getObjectId()); + + if (!$object instanceof ProductVariantAwareInterface) { + return; + } + + $attributeIds = $message->getAttributeIds(); + if (!$attributeIds) { + return; + } + + $variant = $this->variantGeneratorService->generateVariant($attributeIds, $object); + + // TODO: check if needed, because of too many notifications for massive variant generation + if (null !== $variant && null !== $message->getUserId()) { + $this->notificationService->sendToUser( + $message->getUserId(), + 0, + sprintf('Variant %s generated', $variant->getName()), + sprintf('Variant %s with ID %s for Product %s with ID %s has been generated', $variant->getName(), + $variant->getId(), $object->getSku(), $object->getId()) + ); + } + } +} diff --git a/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/config.yml b/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/config.yml new file mode 100755 index 0000000000..d114c15215 --- /dev/null +++ b/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/config.yml @@ -0,0 +1,2 @@ +imports: + - { resource: messenger.yml } diff --git a/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/messenger.yml b/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/messenger.yml new file mode 100755 index 0000000000..db3e97bdd8 --- /dev/null +++ b/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/messenger.yml @@ -0,0 +1,18 @@ +framework: + messenger: + transports: + coreshop_variant: + dsn: "doctrine://default?queue_name=coreshop_variant" + failure_transport: coreshop_variant_failed + retry_strategy: + max_retries: 3 + delay: 300000 + multiplier: 2 + # we store failed messages here for admins to manually review them later + coreshop_variant_failed: + dsn: "doctrine://default?queue_name=coreshop_variant_failed" + retry_strategy: + max_retries: 0 + + routing: + 'CoreShop\Bundle\VariantBundle\Messenger\CreateVariantMessage': coreshop_variant \ No newline at end of file diff --git a/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/routing.yml b/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/routing.yml index b5c15a581b..86095693f1 100644 --- a/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/routing.yml +++ b/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/routing.yml @@ -1,5 +1,12 @@ -coreshop_variant_attribute: - path: /product/attribute/{product} - defaults: { _controller: CoreShop\Bundle\VariantBundle\Controller\ProductController::attributeAction } +coreshop_admin_variant_attributes: + path: /admin/variant/attributes + defaults: { _controller: CoreShop\Bundle\VariantBundle\Controller\VariantController::getAttributesAction } + options: + expose: true + +coreshop_admin_variant_generator: + path: /admin/variant/generate + defaults: { _controller: CoreShop\Bundle\VariantBundle\Controller\VariantController::generateVariantsAction } + methods: [POST] options: expose: true \ No newline at end of file diff --git a/src/CoreShop/Bundle/VariantBundle/Resources/config/services.yml b/src/CoreShop/Bundle/VariantBundle/Resources/config/services.yml index 3a60927528..85c6831c00 100755 --- a/src/CoreShop/Bundle/VariantBundle/Resources/config/services.yml +++ b/src/CoreShop/Bundle/VariantBundle/Resources/config/services.yml @@ -47,4 +47,22 @@ services: decorates: 'CoreShop\Component\Pimcore\DataObject\CompositeLinkGenerator' arguments: - '@CoreShop\Bundle\VariantBundle\Pimcore\VariantLinkGenerator.inner' - - '%coreshop.variant.redirect_to_main_variant%' \ No newline at end of file + - '%coreshop.variant.redirect_to_main_variant%' + + CoreShop\Bundle\VariantBundle\Controller\VariantController: + parent: CoreShop\Bundle\ResourceBundle\Controller\AdminController + arguments: + - '@Symfony\Contracts\Translation\TranslatorInterface' + tags: + - { name: container.service_subscriber } + - { name: controller.service_arguments } + + CoreShop\Bundle\VariantBundle\Service\VariantGeneratorServiceInterface: '@CoreShop\Bundle\VariantBundle\Service\VariantGeneratorService' + CoreShop\Bundle\VariantBundle\Service\VariantGeneratorService: ~ + + CoreShop\Bundle\VariantBundle\Messenger\Handler\CreateVariantMessageHandler: + arguments: + - '@CoreShop\Bundle\VariantBundle\Service\VariantGeneratorServiceInterface' + - '@Pimcore\Model\Notification\Service\NotificationService' + tags: + - { name: messenger.message_handler } \ No newline at end of file diff --git a/src/CoreShop/Bundle/VariantBundle/Resources/translations/admin.de.yaml b/src/CoreShop/Bundle/VariantBundle/Resources/translations/admin.de.yaml index e151315190..ce8cc09b90 100644 --- a/src/CoreShop/Bundle/VariantBundle/Resources/translations/admin.de.yaml +++ b/src/CoreShop/Bundle/VariantBundle/Resources/translations/admin.de.yaml @@ -13,5 +13,6 @@ coreshop: value_color: "Farbe" attribute_group: "Attribut Gruppe" - - + variant_generator: + generate: "Varianten generieren" + generate_in_background: "Varianten werden im Hintergrund erstellt" diff --git a/src/CoreShop/Bundle/VariantBundle/Resources/translations/admin.en.yaml b/src/CoreShop/Bundle/VariantBundle/Resources/translations/admin.en.yaml index ca979e475e..b1d1d3c47a 100644 --- a/src/CoreShop/Bundle/VariantBundle/Resources/translations/admin.en.yaml +++ b/src/CoreShop/Bundle/VariantBundle/Resources/translations/admin.en.yaml @@ -13,5 +13,7 @@ coreshop: value_color: "Color" attribute_group: "Attribute Groupe" - + variant_generator: + generate: "Generate variants" + generate_in_background: "Variants are generated in background" diff --git a/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php new file mode 100644 index 0000000000..8f85aff088 --- /dev/null +++ b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php @@ -0,0 +1,107 @@ +generateVariant($attributeIds, $product); + + if($variant) { + $variants[] = $variant; + } + } + + return $variants; + } + + public function generateVariant(array $attributeIds, ProductVariantAwareInterface $product): ?ProductVariantAwareInterface + { + $class = get_class($product); + + $existingVariants = new ($class . '\Listing')(); + $existingVariants->setCondition('path LIKE \''.$product->getFullPath().'/%\''); + $attributeCondition = implode(' AND ', array_map(static function($id) { + return 'attributes LIKE "%object|' . $id . '%"'; + }, $attributeIds)); + $existingVariants->addConditionParam($attributeCondition); + $existingVariants->setLimit(1); + + if(!$existingVariants->count()) { + /** + * @var ProductVariantAwareInterface $variant + */ + $variant = new $class(); + + $attributes = array_filter(array_map(static function($attributeId) { + $attribute = DataObject::getById($attributeId); + return $attribute instanceof AttributeInterface ? $attribute : null; + }, $attributeIds)); + + $key = implode(' ', array_map(static function(AttributeInterface $attribute) { + return $attribute->getKey(); + }, $attributes)); + + foreach(Tool::getValidLanguages() as $language) { + $name = implode(' ', array_map(static function(AttributeInterface $attribute) use ($language) { + return $attribute->getName($language); + }, $attributes)); + + $variant->setName(sprintf('%s %s', $product->getName($language), $name), $language); + } + + $variant->setKey($key); + $variant->setParent($product); + $variant->setPublished(false); + $variant->setType(AbstractObject::OBJECT_TYPE_VARIANT); + $variant->setAttributes($attributes); + $variant->save(); + + return $variant; + } + + return null; + } + + public function generateCombinations(array $groupedAttributes, array $currentCombination, int $groupIndex, array &$combinations): void + { + if ($groupIndex >= count($groupedAttributes)) { + $combinations[] = $currentCombination; + return; + } + + $currentGroup = array_values($groupedAttributes)[$groupIndex]; + + foreach ($currentGroup as $attribute) { + $currentCombination[] = $attribute; + $this->generateCombinations($groupedAttributes, $currentCombination, $groupIndex + 1, $combinations); + array_pop($currentCombination); + } + } +} \ No newline at end of file diff --git a/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorServiceInterface.php b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorServiceInterface.php new file mode 100644 index 0000000000..6c2a6d4537 --- /dev/null +++ b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorServiceInterface.php @@ -0,0 +1,32 @@ +getChildren([self::OBJECT_TYPE_OBJECT]) as $object) { + if($object instanceof AttributeInterface) { + $attributes[] = $object; + } + } + + return $attributes; + } } diff --git a/src/CoreShop/Component/Variant/Model/AttributeGroupInterface.php b/src/CoreShop/Component/Variant/Model/AttributeGroupInterface.php index e4ed8489b8..989ac09c0e 100644 --- a/src/CoreShop/Component/Variant/Model/AttributeGroupInterface.php +++ b/src/CoreShop/Component/Variant/Model/AttributeGroupInterface.php @@ -33,4 +33,6 @@ public function setSorting(?float $sorting): static; public function getShowInList(): ?bool; public function setShowInList(?bool $showInList): static; + + public function getAttributes(): array; } From 629113faa42416d1c48560e6e1dcb56ef5f8ab9b Mon Sep 17 00:00:00 2001 From: Georg Wurz Date: Thu, 8 Aug 2024 17:11:48 +0200 Subject: [PATCH 02/11] [VariantBundle] messages, tweaks, etc. --- .../Resources/public/pimcore/js/resource.js | 315 +++++++----------- .../Controller/VariantController.php | 109 ++---- .../Resources/config/services.yml | 4 +- .../Resources/translations/admin.en.yaml | 2 +- .../Service/VariantGeneratorService.php | 2 +- 5 files changed, 154 insertions(+), 278 deletions(-) diff --git a/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js b/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js index 108adcf7b0..01e871c5a6 100644 --- a/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js +++ b/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js @@ -190,203 +190,142 @@ coreshop.core.resource = Class.create(coreshop.resource, { }.bind(this, tab) }); - const variantHandler = () => { - const store = Ext.create('Ext.data.TreeStore', { - proxy: { - type: 'ajax', - url: Routing.generate('coreshop_admin_variant_attributes', {id: tab.id}), - reader: { - type: 'json', - rootProperty: 'data' + if(tab?.data?.data?.allowedAttributeGroups?.length) { + const variantHandler = () => { + const store = Ext.create('Ext.data.TreeStore', { + proxy: { + type: 'ajax', + url: Routing.generate('coreshop_admin_variant_attributes', {id: tab.id}), + reader: { + type: 'json', + rootProperty: 'data' + }, }, - }, - sorters: [{ - property: 'sorting', - direction: 'ASC' - }] - }); - - store.on('load', (store, records, successful, operation, eOpts) => { - if(!store) { - console.error('no data found'); - } - }); - - const tree = Ext.create('Ext.tree.Panel', { - hideHeaders: true, - rootVisible: false, - store: store, - layout: 'fit', - }); - - const panel = new Ext.Panel({ - layout: 'fit', - header: false, - bodyStyle: "padding:10px", - border: false, - buttons: [ - { - text: t("apply"), - iconCls: "pimcore_icon_accept", - handler: function() { - const groupedAttributes = (tree.getView().getChecked()).reduce((acc, obj) => { - const groupId = obj.data.group_id; - if (!acc[groupId]) { - acc[groupId] = []; + sorters: [{ + property: 'sorting', + direction: 'ASC' + }] + }); + + store.on('load', (store, records, successful, operation, eOpts) => { + if(!store) { + console.error('no data found'); + } + }); + + const applyButton = Ext.create('Ext.Button', { + text: t("apply"), + iconCls: "pimcore_icon_accept", + disabled: true, + handler: function() { + const groupedAttributes = (tree.getView().getChecked()).reduce((acc, obj) => { + const groupId = obj.data.group_id; + if (!acc[groupId]) { + acc[groupId] = []; + } + acc[groupId].push(obj.data.id); + return acc; + }, {}); + + Ext.Ajax.request({ + url: Routing.generate('coreshop_admin_variant_generator'), + jsonData: { id: tab.id, attributes: groupedAttributes }, + method: 'POST', + success: function (response) { + var res = Ext.decode(response.responseText); + if (res.success === true) { + Ext.Msg.alert(t('success'), res.message); + window.destroy(); + } else { + Ext.Msg.alert(t('error'), res.message); } - acc[groupId].push(obj.data.id); - return acc; - }, {}); - - Ext.Ajax.request({ - url: Routing.generate('coreshop_admin_variant_generator'), - jsonData: { id: tab.id, attributes: groupedAttributes }, - method: 'POST', - success: function (response) { - var res = Ext.decode(response.responseText); - if (res.success === true) { - Ext.Msg.alert(t('success'), res.message); - window.destroy(); - } else { - Ext.Msg.alert(t('error'), res.message); - } - }.bind(this) - }); - } - }, - { - text: t("close"), - iconCls: "pimcore_icon_cancel", - handler: function() { - window.destroy(); - } + }.bind(this) + }); } - ], - frame: false, - items: [ - tree - ], - }); - - const window = new Ext.window.Window({ - closeAction: 'close', - height: 600, - width: 400, - layout: 'fit', - items: [ - panel - ], - modal: false, - plain: true, - title: t('coreshop.variant_generator.generate'), - }); - - window.show(); - }; - - productMoreButtons.push({ - text: t('coreshop.variant_generator.generate'), - scale: 'medium', - handler: variantHandler.bind(this, tab) - }); - - const variantHandler = () => { - const store = Ext.create('Ext.data.TreeStore', { - proxy: { - type: 'ajax', - url: Routing.generate('coreshop_admin_variant_attributes', {id: tab.id}), - reader: { - type: 'json', - rootProperty: 'data' - }, - }, - sorters: [{ - property: 'sorting', - direction: 'ASC' - }] - }); - - store.on('load', (store, records, successful, operation, eOpts) => { - // TODO - if(!store) { - console.error('no data found'); - } - }); - - const tree = Ext.create('Ext.tree.Panel', { - hideHeaders: true, - rootVisible: false, - store: store, - }); - - const panel = new Ext.Panel({ - autoscroll: true, - header: false, - bodyStyle: "padding:10px", - border: false, - buttons: [ - { - text: t("apply"), - iconCls: "pimcore_icon_accept", - handler: function() { - const checked = (tree.getView().getChecked()).map((checked) => { - return { - id: checked.data.id, - group_id: checked.data.group_id, - }; - }) - - Ext.Ajax.request({ - url: Routing.generate('coreshop_admin_variant_generator', { id: tab.id, attributes: checked }), - method: 'GET', - success: function (response) { - var res = Ext.decode(response.responseText); - if (res.success === true) { - //this.checkStatus(res); - } else { - Ext.Msg.alert(t('error'), res.message); + }) + + const tree = Ext.create('Ext.tree.Panel', { + hideHeaders: true, + rootVisible: false, + store: store, + layout: 'fit', + listeners: { + checkchange: function () { + const rootNode = tree.getRootNode(); + let allParentsHaveSelection = true; + + rootNode.eachChild(function (parentNode) { + var hasCheckedLeaf = false; + + parentNode.eachChild(function (childNode) { + if (childNode.get('checked')) { + hasCheckedLeaf = true; + return false; } - }.bind(this) + }); + + if (!hasCheckedLeaf) { + allParentsHaveSelection = false; + return false; + } }); - } - }, - { - text: t("close"), - iconCls: "pimcore_icon_cancel", - handler: function() { - window.destroy(); + + if (allParentsHaveSelection) { + applyButton.enable(); + //Ext.Msg.alert('Success', 'At least one leaf from each parent node is selected.'); + } else { + applyButton.disable(); + //Ext.Msg.alert('Error', 'Please select at least one leaf from each parent node.'); + } } } - ], - frame: false, - items: [ - tree - ], - }); - - const window = new Ext.window.Window({ - autoscroll: true, - closeAction: 'close', - height: 400, - items: [ - panel - ], - layout: 'fit', - modal: false, - plain: true, - title: t('coreshop.variant_generator.generate'), - width: 560, + }); + + const panel = new Ext.Panel({ + layout: 'fit', + header: false, + bodyStyle: "padding:10px", + border: false, + buttons: [ + applyButton, + { + text: t("close"), + iconCls: "pimcore_icon_cancel", + handler: function() { + window.destroy(); + } + } + ], + frame: false, + items: [ + tree + ], + }); + + const window = new Ext.window.Window({ + closeAction: 'close', + height: 400, + width: 600, + layout: 'fit', + items: [ + panel + ], + modal: false, + plain: true, + title: t('coreshop.variant_generator.generate'), + }); + + window.show(); + }; + + productMoreButtons.push({ + text: t('coreshop.variant_generator.generate'), + scale: 'medium', + iconCls: 'pimcore_icon_variant', + handler: variantHandler.bind(this, tab) }); - window.show(); - }; - - productMoreButtons.push({ - text: t('coreshop.variant_generator.generate'), - scale: 'medium', - //iconCls: 'coreshop_icon_product_unit', - handler: variantHandler.bind(this, tab) - }); + } } if (productMoreButtons.length === 0) { diff --git a/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php b/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php index 2a3c6dfda5..0a2ba734a7 100644 --- a/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php +++ b/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php @@ -19,25 +19,38 @@ namespace CoreShop\Bundle\VariantBundle\Controller; use CoreShop\Bundle\ResourceBundle\Controller\AdminController; +use CoreShop\Bundle\ResourceBundle\Controller\ViewHandlerInterface; +use CoreShop\Bundle\VariantBundle\Messenger\CreateVariantMessage; +use CoreShop\Bundle\VariantBundle\Service\VariantGeneratorService; use CoreShop\Component\Variant\Model\AttributeGroupInterface; use CoreShop\Component\Variant\Model\AttributeInterface; use CoreShop\Component\Variant\Model\ProductVariantAwareInterface; use Pimcore\Model\DataObject; use Pimcore\Model\DataObject\AbstractObject; -use Pimcore\Tool; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @psalm-suppress InternalClass */ class VariantController extends AdminController { - private function mapData($object) { - + public function __construct( + \Psr\Container\ContainerInterface $container, + protected ViewHandlerInterface $viewHandler, + protected ParameterBagInterface $parameterBag, + protected VariantGeneratorService $variantGeneratorService, + protected MessageBusInterface $messageBus, + protected TranslatorInterface $translator, + ) { + + parent::__construct($container, $viewHandler, $parameterBag); } - public function getAttributes(Request $request) + public function getAttributesAction(Request $request) { $id = $this->getParameterFromRequest($request, 'id'); @@ -107,96 +120,18 @@ public function generateVariantsAction(Request $request) $product = $product->getVariantParent(); } - $groupedAttributes = []; - foreach ($attributes as $attribute) { - $groupedAttributes[$attribute['group_id']][] = $attribute; - } - $combinations = []; - $this->generateCombinations($groupedAttributes, [], 0, $combinations); - $this->generateVariants($combinations, $product); + $this->variantGeneratorService->generateCombinations($attributes, [], 0, $combinations); - $data = []; + foreach($combinations as $attributeIds) { + $this->messageBus->dispatch(new CreateVariantMessage($product->getId(), $attributeIds, $this->getAdminUser()->getId())); + } return $this->json( [ 'success' => true, - 'data' => $data + 'message' => $this->translator->trans('coreshop.variant_generator.generate_in_background', [], 'admin') ] ); } - - protected function generateVariants(array $combinations, ProductVariantAwareInterface $product): array - { - $variants = []; - foreach($combinations as $combinationAttribute) { - $attributes = array_map(static function($combination) { - return DataObject::getById($combination['id']); - }, $combinationAttribute); - - // TODO: search variant by attributes - $variants = DataObject\CoreShopProduct::getByAttributes(array_map(static function($attribute) { - - }, $attributes), 1); - - - // TODO - //$variants = $product->getChildren([DataObject::OBJECT_TYPE_VARIANT]); - //$variants->addConditionParam('attributes') - $variant = null; - - if(!$variant instanceof ProductVariantAwareInterface) { - $class = get_class($product); - /** - * @var ProductVariantAwareInterface $variant - */ - $variant = new $class(); - } - - $key = implode(' ', array_map(static function(AttributeInterface $attribute) { - return $attribute->getKey(); - }, $attributes)); - - foreach(Tool::getValidLanguages() as $language) { - $name = implode(' ', array_map(static function(AttributeInterface $attribute) { - return $attribute->getName(); - }, $attributes)); - - $variant->setName(sprintf('%s %s', $product->getName(), $name), $language); - } - - $variant->setKey($key); - $variant->setParent($product); - $variant->setPublished(false); - $variant->setType(DataObject::OBJECT_TYPE_VARIANT); - $variant->setAttributes($attributes); - $variant->save(); - $variants[] = $variant; - } - - return $variants; - } - - private function generateCombinations($groupedAttributes, $currentCombination, $groupIndex, &$combinations): void - { - if ($groupIndex >= count($groupedAttributes)) { - // Base case: reached the end of groups, add the combination to the result - $combinations[] = $currentCombination; - return; - } - - $currentGroup = array_values($groupedAttributes)[$groupIndex]; - - foreach ($currentGroup as $attribute) { - // Include the current attribute in the combination - $currentCombination[] = $attribute; - - // Recur to the next group - $this->generateCombinations($groupedAttributes, $currentCombination, $groupIndex + 1, $combinations); - - // Backtrack: remove the current attribute from the combination - array_pop($currentCombination); - } - } - } diff --git a/src/CoreShop/Bundle/VariantBundle/Resources/config/services.yml b/src/CoreShop/Bundle/VariantBundle/Resources/config/services.yml index 85c6831c00..78369cd089 100755 --- a/src/CoreShop/Bundle/VariantBundle/Resources/config/services.yml +++ b/src/CoreShop/Bundle/VariantBundle/Resources/config/services.yml @@ -52,7 +52,9 @@ services: CoreShop\Bundle\VariantBundle\Controller\VariantController: parent: CoreShop\Bundle\ResourceBundle\Controller\AdminController arguments: - - '@Symfony\Contracts\Translation\TranslatorInterface' + - '@CoreShop\Bundle\VariantBundle\Service\VariantGeneratorService' + - '@messenger.default_bus' + - '@translator' tags: - { name: container.service_subscriber } - { name: controller.service_arguments } diff --git a/src/CoreShop/Bundle/VariantBundle/Resources/translations/admin.en.yaml b/src/CoreShop/Bundle/VariantBundle/Resources/translations/admin.en.yaml index b1d1d3c47a..d378fde5d7 100644 --- a/src/CoreShop/Bundle/VariantBundle/Resources/translations/admin.en.yaml +++ b/src/CoreShop/Bundle/VariantBundle/Resources/translations/admin.en.yaml @@ -15,5 +15,5 @@ coreshop: variant_generator: generate: "Generate variants" - generate_in_background: "Variants are generated in background" + generate_in_background: "Variants will be generated in background" diff --git a/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php index 8f85aff088..eeb14fee8d 100644 --- a/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php +++ b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php @@ -64,7 +64,7 @@ public function generateVariant(array $attributeIds, ProductVariantAwareInterfac return $attribute instanceof AttributeInterface ? $attribute : null; }, $attributeIds)); - $key = implode(' ', array_map(static function(AttributeInterface $attribute) { + $key = implode('-', array_map(static function(AttributeInterface $attribute) { return $attribute->getKey(); }, $attributes)); From 0141fb0949bc0c492b5e6b228643edebbe2f57eb Mon Sep 17 00:00:00 2001 From: Georg Wurz Date: Thu, 8 Aug 2024 17:26:56 +0200 Subject: [PATCH 03/11] [VariantBundle] check also unpublished variants --- .../Messenger/Handler/CreateVariantMessageHandler.php | 2 +- .../Bundle/VariantBundle/Service/VariantGeneratorService.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php b/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php index e50546c7e8..f80a0b0d65 100644 --- a/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php +++ b/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php @@ -56,7 +56,7 @@ public function __invoke(CreateVariantMessage $message) 0, sprintf('Variant %s generated', $variant->getName()), sprintf('Variant %s with ID %s for Product %s with ID %s has been generated', $variant->getName(), - $variant->getId(), $object->getSku(), $object->getId()) + $variant->getId(), $object->getKey(), $object->getId()) ); } } diff --git a/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php index eeb14fee8d..3f5c2ef8ec 100644 --- a/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php +++ b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php @@ -52,6 +52,7 @@ public function generateVariant(array $attributeIds, ProductVariantAwareInterfac }, $attributeIds)); $existingVariants->addConditionParam($attributeCondition); $existingVariants->setLimit(1); + $existingVariants->setUnpublished(true); if(!$existingVariants->count()) { /** From a61dde352b26868b4ab710c762031ad339f7c0dc Mon Sep 17 00:00:00 2001 From: Georg Wurz Date: Thu, 8 Aug 2024 17:27:39 +0200 Subject: [PATCH 04/11] [VariantBundle] set default key --- .../Bundle/VariantBundle/Service/VariantGeneratorService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php index 3f5c2ef8ec..c1ebff413c 100644 --- a/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php +++ b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php @@ -65,7 +65,7 @@ public function generateVariant(array $attributeIds, ProductVariantAwareInterfac return $attribute instanceof AttributeInterface ? $attribute : null; }, $attributeIds)); - $key = implode('-', array_map(static function(AttributeInterface $attribute) { + $key = implode(' - ', array_map(static function(AttributeInterface $attribute) { return $attribute->getKey(); }, $attributes)); From 9803fc5d987e7d310e3f80b6b6eeff557b055fad Mon Sep 17 00:00:00 2001 From: Georg Wurz Date: Thu, 8 Aug 2024 17:35:42 +0200 Subject: [PATCH 05/11] [VariantBundle] check for correct amount of attributes --- .../VariantBundle/Service/VariantGeneratorService.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php index c1ebff413c..d79dabc064 100644 --- a/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php +++ b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php @@ -43,6 +43,10 @@ public function generateVariants(array $combinations, ProductVariantAwareInterfa public function generateVariant(array $attributeIds, ProductVariantAwareInterface $product): ?ProductVariantAwareInterface { + if (count($product->getAllowedAttributeGroups()) !== count($attributeIds)) { + return null; + } + $class = get_class($product); $existingVariants = new ($class . '\Listing')(); @@ -54,7 +58,7 @@ public function generateVariant(array $attributeIds, ProductVariantAwareInterfac $existingVariants->setLimit(1); $existingVariants->setUnpublished(true); - if(!$existingVariants->count()) { + if (!$existingVariants->count()) { /** * @var ProductVariantAwareInterface $variant */ From 2a98901f068ffe122d4ce202601dd0ca0f49fb30 Mon Sep 17 00:00:00 2001 From: Georg Wurz Date: Fri, 9 Aug 2024 09:10:17 +0200 Subject: [PATCH 06/11] [VariantBundle] remove TODOs and comments --- .../Bundle/CoreBundle/Resources/public/pimcore/js/resource.js | 2 -- .../Messenger/Handler/CreateVariantMessageHandler.php | 1 - 2 files changed, 3 deletions(-) diff --git a/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js b/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js index 01e871c5a6..38e472a63b 100644 --- a/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js +++ b/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js @@ -272,10 +272,8 @@ coreshop.core.resource = Class.create(coreshop.resource, { if (allParentsHaveSelection) { applyButton.enable(); - //Ext.Msg.alert('Success', 'At least one leaf from each parent node is selected.'); } else { applyButton.disable(); - //Ext.Msg.alert('Error', 'Please select at least one leaf from each parent node.'); } } } diff --git a/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php b/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php index f80a0b0d65..335bb436b2 100644 --- a/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php +++ b/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php @@ -49,7 +49,6 @@ public function __invoke(CreateVariantMessage $message) $variant = $this->variantGeneratorService->generateVariant($attributeIds, $object); - // TODO: check if needed, because of too many notifications for massive variant generation if (null !== $variant && null !== $message->getUserId()) { $this->notificationService->sendToUser( $message->getUserId(), From 7621d5e436daae838781fdd59ec483f77bd05818 Mon Sep 17 00:00:00 2001 From: Georg Wurz Date: Fri, 9 Aug 2024 09:29:22 +0200 Subject: [PATCH 07/11] [VariantBundle] check for Pimcore\DataObject\Concrete in generateVariant --- .../Service/VariantGeneratorService.php | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php index d79dabc064..821e301a89 100644 --- a/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php +++ b/src/CoreShop/Bundle/VariantBundle/Service/VariantGeneratorService.php @@ -43,13 +43,15 @@ public function generateVariants(array $combinations, ProductVariantAwareInterfa public function generateVariant(array $attributeIds, ProductVariantAwareInterface $product): ?ProductVariantAwareInterface { - if (count($product->getAllowedAttributeGroups()) !== count($attributeIds)) { + if(!$product instanceof DataObject\Concrete) { return null; } - $class = get_class($product); + if (count($product->getAllowedAttributeGroups()) !== count($attributeIds)) { + return null; + } - $existingVariants = new ($class . '\Listing')(); + $existingVariants = $product::getList(); $existingVariants->setCondition('path LIKE \''.$product->getFullPath().'/%\''); $attributeCondition = implode(' AND ', array_map(static function($id) { return 'attributes LIKE "%object|' . $id . '%"'; @@ -58,40 +60,41 @@ public function generateVariant(array $attributeIds, ProductVariantAwareInterfac $existingVariants->setLimit(1); $existingVariants->setUnpublished(true); - if (!$existingVariants->count()) { - /** - * @var ProductVariantAwareInterface $variant - */ - $variant = new $class(); + // there is already a variant with the given attributes + if ($existingVariants->count()) { + return null; + } - $attributes = array_filter(array_map(static function($attributeId) { - $attribute = DataObject::getById($attributeId); - return $attribute instanceof AttributeInterface ? $attribute : null; - }, $attributeIds)); + /** + * @var ProductVariantAwareInterface $variant + */ + $variant = new ($product::class)(); - $key = implode(' - ', array_map(static function(AttributeInterface $attribute) { - return $attribute->getKey(); - }, $attributes)); + $attributes = array_filter(array_map(static function($attributeId) { + $attribute = DataObject::getById($attributeId); + return $attribute instanceof AttributeInterface ? $attribute : null; + }, $attributeIds)); - foreach(Tool::getValidLanguages() as $language) { - $name = implode(' ', array_map(static function(AttributeInterface $attribute) use ($language) { - return $attribute->getName($language); - }, $attributes)); + $key = implode(' - ', array_map(static function(AttributeInterface $attribute) { + return $attribute->getKey(); + }, $attributes)); - $variant->setName(sprintf('%s %s', $product->getName($language), $name), $language); - } + foreach(Tool::getValidLanguages() as $language) { + $name = implode(' ', array_map(static function(AttributeInterface $attribute) use ($language) { + return $attribute->getName($language); + }, $attributes)); - $variant->setKey($key); - $variant->setParent($product); - $variant->setPublished(false); - $variant->setType(AbstractObject::OBJECT_TYPE_VARIANT); - $variant->setAttributes($attributes); - $variant->save(); - - return $variant; + $variant->setName(sprintf('%s %s', $product->getName($language), $name), $language); } - return null; + $variant->setKey($key); + $variant->setParent($product); + $variant->setPublished(false); + $variant->setType(AbstractObject::OBJECT_TYPE_VARIANT); + $variant->setAttributes($attributes); + $variant->save(); + + return $variant; } public function generateCombinations(array $groupedAttributes, array $currentCombination, int $groupIndex, array &$combinations): void From 3d94c7a4713e06947ff6c4217e0db5a8332fc89c Mon Sep 17 00:00:00 2001 From: Dominik Pfaffenbauer Date: Fri, 9 Aug 2024 12:17:50 +0200 Subject: [PATCH 08/11] [VariantBundle] ignore internal pimcore methods --- .../Bundle/VariantBundle/Controller/VariantController.php | 5 ++++- .../Messenger/Handler/CreateVariantMessageHandler.php | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php b/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php index 0a2ba734a7..dee7fad6e2 100644 --- a/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php +++ b/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php @@ -124,7 +124,10 @@ public function generateVariantsAction(Request $request) $this->variantGeneratorService->generateCombinations($attributes, [], 0, $combinations); foreach($combinations as $attributeIds) { - $this->messageBus->dispatch(new CreateVariantMessage($product->getId(), $attributeIds, $this->getAdminUser()->getId())); + /** + * @psalm-suppress InternalMethod + */ + $this->messageBus->dispatch(new CreateVariantMessage($product->getId(), $attributeIds, $this->getAdminUser()?->getId())); } return $this->json( diff --git a/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php b/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php index 335bb436b2..06330ba562 100644 --- a/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php +++ b/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php @@ -50,6 +50,9 @@ public function __invoke(CreateVariantMessage $message) $variant = $this->variantGeneratorService->generateVariant($attributeIds, $object); if (null !== $variant && null !== $message->getUserId()) { + /** + * @psalm-suppress InternalMethod + */ $this->notificationService->sendToUser( $message->getUserId(), 0, From 0abf279897525192cc1c6b316971acdd2d41ebbc Mon Sep 17 00:00:00 2001 From: Dominik Pfaffenbauer Date: Fri, 9 Aug 2024 13:21:10 +0200 Subject: [PATCH 09/11] [VariantBundle] smaller refactoring --- .../Controller/VariantController.php | 51 ++++++++----------- .../Handler/CreateVariantMessageHandler.php | 2 - 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php b/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php index dee7fad6e2..b0d53b644a 100644 --- a/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php +++ b/src/CoreShop/Bundle/VariantBundle/Controller/VariantController.php @@ -19,7 +19,6 @@ namespace CoreShop\Bundle\VariantBundle\Controller; use CoreShop\Bundle\ResourceBundle\Controller\AdminController; -use CoreShop\Bundle\ResourceBundle\Controller\ViewHandlerInterface; use CoreShop\Bundle\VariantBundle\Messenger\CreateVariantMessage; use CoreShop\Bundle\VariantBundle\Service\VariantGeneratorService; use CoreShop\Component\Variant\Model\AttributeGroupInterface; @@ -27,7 +26,6 @@ use CoreShop\Component\Variant\Model\ProductVariantAwareInterface; use Pimcore\Model\DataObject; use Pimcore\Model\DataObject\AbstractObject; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Messenger\MessageBusInterface; @@ -38,29 +36,17 @@ */ class VariantController extends AdminController { - public function __construct( - \Psr\Container\ContainerInterface $container, - protected ViewHandlerInterface $viewHandler, - protected ParameterBagInterface $parameterBag, - protected VariantGeneratorService $variantGeneratorService, - protected MessageBusInterface $messageBus, - protected TranslatorInterface $translator, - ) { - - parent::__construct($container, $viewHandler, $parameterBag); - } - public function getAttributesAction(Request $request) { $id = $this->getParameterFromRequest($request, 'id'); - if(!$id) { + if (!$id) { throw new \InvalidArgumentException('no product id given'); } $product = DataObject::getById($id); - if(!$product instanceof ProductVariantAwareInterface) { + if (!$product instanceof ProductVariantAwareInterface) { throw new NotFoundHttpException('no product found'); } @@ -70,13 +56,13 @@ public function getAttributesAction(Request $request) $attributeGroups = $product->getAllowedAttributeGroups(); - $data = array_map(static function(AttributeGroupInterface $group) { + $data = array_map(static function (AttributeGroupInterface $group) { return [ 'text' => sprintf('%s (ID: %s)', $group->getKey(), $group->getId()), 'sorting' => $group->getSorting(), 'leaf' => false, 'iconCls' => 'pimcore_icon_object', - 'data' => array_map(static function(AttributeInterface $attribute) use ($group) { + 'data' => array_map(static function (AttributeInterface $attribute) use ($group) { return [ 'text' => sprintf('%s (ID: %s)', $attribute->getKey(), $attribute->getId()), 'id' => $attribute->getId(), @@ -86,33 +72,38 @@ public function getAttributesAction(Request $request) 'checked' => false, 'iconCls' => 'pimcore_icon_object', ]; - }, $group->getAttributes()) + }, $group->getAttributes()), ]; }, $attributeGroups); return $this->json( [ 'success' => true, - 'data' => $data + 'data' => $data, ] ); } - public function generateVariantsAction(Request $request) - { + + public function generateVariantsAction( + Request $request, + VariantGeneratorService $variantGeneratorService, + MessageBusInterface $messageBus, + TranslatorInterface $translator, + ) { $id = $this->getParameterFromRequest($request, 'id'); $attributes = $this->getParameterFromRequest($request, 'attributes'); - if(!$id) { + if (!$id) { throw new \InvalidArgumentException('no product id given'); } - if(!$attributes) { + if (!$attributes) { throw new \InvalidArgumentException('no attributes given'); } $product = DataObject::getById($id); - if(!$product instanceof ProductVariantAwareInterface) { + if (!$product instanceof ProductVariantAwareInterface) { throw new NotFoundHttpException('no product found'); } @@ -121,19 +112,21 @@ public function generateVariantsAction(Request $request) } $combinations = []; - $this->variantGeneratorService->generateCombinations($attributes, [], 0, $combinations); + $variantGeneratorService->generateCombinations($attributes, [], 0, $combinations); - foreach($combinations as $attributeIds) { + foreach ($combinations as $attributeIds) { /** * @psalm-suppress InternalMethod */ - $this->messageBus->dispatch(new CreateVariantMessage($product->getId(), $attributeIds, $this->getAdminUser()?->getId())); + $messageBus->dispatch( + new CreateVariantMessage($product->getId(), $attributeIds, $this->getAdminUser()?->getId()) + ); } return $this->json( [ 'success' => true, - 'message' => $this->translator->trans('coreshop.variant_generator.generate_in_background', [], 'admin') + 'message' => $translator->trans('coreshop.variant_generator.generate_in_background', [], 'admin'), ] ); } diff --git a/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php b/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php index 06330ba562..2a5e3f2289 100644 --- a/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php +++ b/src/CoreShop/Bundle/VariantBundle/Messenger/Handler/CreateVariantMessageHandler.php @@ -23,9 +23,7 @@ use CoreShop\Component\Variant\Model\ProductVariantAwareInterface; use Pimcore\Model\DataObject; use Pimcore\Model\Notification\Service\NotificationService; -use Symfony\Component\Messenger\Attribute\AsMessageHandler; -#[AsMessageHandler] class CreateVariantMessageHandler { public function __construct( From 9d8fa7c479a51d20591d7e9972264b1774f7e111 Mon Sep 17 00:00:00 2001 From: Dominik Pfaffenbauer Date: Sat, 10 Aug 2024 16:50:55 +0200 Subject: [PATCH 10/11] [VariantBundle] add js to variant bundle --- .../Resources/public/pimcore/js/resource.js | 137 +------------ .../Resources/public/pimcore/js/broker.js | 11 +- .../DependencyInjection/Configuration.php | 2 + .../Resources/config/pimcore/admin.yml | 4 + .../Resources/config/pimcore/config.yml | 1 + .../Resources/public/pimcore/js/resource.js | 186 ++++++++++++++++++ 6 files changed, 202 insertions(+), 139 deletions(-) create mode 100644 src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/admin.yml create mode 100644 src/CoreShop/Bundle/VariantBundle/Resources/public/pimcore/js/resource.js diff --git a/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js b/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js index 38e472a63b..a2fe666bda 100644 --- a/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js +++ b/src/CoreShop/Bundle/CoreBundle/Resources/public/pimcore/js/resource.js @@ -14,7 +14,7 @@ pimcore.registerNS('coreshop.core.resource'); coreshop.core.resource = Class.create(coreshop.resource, { initialize: function () { coreshop.broker.addListener('pimcore.ready', this.pimcoreReady, this); - coreshop.broker.addListener('pimcore.postOpenObject', this.postOpenObject, this); + coreshop.broker.addListener('pimcore.postOpenObject', this.postOpenObject, this, false, -10); coreshop.broker.fireEvent('resource.register', 'coreshop.core', this); }, @@ -189,141 +189,6 @@ coreshop.core.resource = Class.create(coreshop.resource, { new coreshop.product.workflow.variantUnitDefinitionSolidifier(tab.data, tab.tab); }.bind(this, tab) }); - - if(tab?.data?.data?.allowedAttributeGroups?.length) { - const variantHandler = () => { - const store = Ext.create('Ext.data.TreeStore', { - proxy: { - type: 'ajax', - url: Routing.generate('coreshop_admin_variant_attributes', {id: tab.id}), - reader: { - type: 'json', - rootProperty: 'data' - }, - }, - sorters: [{ - property: 'sorting', - direction: 'ASC' - }] - }); - - store.on('load', (store, records, successful, operation, eOpts) => { - if(!store) { - console.error('no data found'); - } - }); - - const applyButton = Ext.create('Ext.Button', { - text: t("apply"), - iconCls: "pimcore_icon_accept", - disabled: true, - handler: function() { - const groupedAttributes = (tree.getView().getChecked()).reduce((acc, obj) => { - const groupId = obj.data.group_id; - if (!acc[groupId]) { - acc[groupId] = []; - } - acc[groupId].push(obj.data.id); - return acc; - }, {}); - - Ext.Ajax.request({ - url: Routing.generate('coreshop_admin_variant_generator'), - jsonData: { id: tab.id, attributes: groupedAttributes }, - method: 'POST', - success: function (response) { - var res = Ext.decode(response.responseText); - if (res.success === true) { - Ext.Msg.alert(t('success'), res.message); - window.destroy(); - } else { - Ext.Msg.alert(t('error'), res.message); - } - }.bind(this) - }); - } - }) - - const tree = Ext.create('Ext.tree.Panel', { - hideHeaders: true, - rootVisible: false, - store: store, - layout: 'fit', - listeners: { - checkchange: function () { - const rootNode = tree.getRootNode(); - let allParentsHaveSelection = true; - - rootNode.eachChild(function (parentNode) { - var hasCheckedLeaf = false; - - parentNode.eachChild(function (childNode) { - if (childNode.get('checked')) { - hasCheckedLeaf = true; - return false; - } - }); - - if (!hasCheckedLeaf) { - allParentsHaveSelection = false; - return false; - } - }); - - if (allParentsHaveSelection) { - applyButton.enable(); - } else { - applyButton.disable(); - } - } - } - }); - - const panel = new Ext.Panel({ - layout: 'fit', - header: false, - bodyStyle: "padding:10px", - border: false, - buttons: [ - applyButton, - { - text: t("close"), - iconCls: "pimcore_icon_cancel", - handler: function() { - window.destroy(); - } - } - ], - frame: false, - items: [ - tree - ], - }); - - const window = new Ext.window.Window({ - closeAction: 'close', - height: 400, - width: 600, - layout: 'fit', - items: [ - panel - ], - modal: false, - plain: true, - title: t('coreshop.variant_generator.generate'), - }); - - window.show(); - }; - - productMoreButtons.push({ - text: t('coreshop.variant_generator.generate'), - scale: 'medium', - iconCls: 'pimcore_icon_variant', - handler: variantHandler.bind(this, tab) - }); - - } } if (productMoreButtons.length === 0) { diff --git a/src/CoreShop/Bundle/PimcoreBundle/Resources/public/pimcore/js/broker.js b/src/CoreShop/Bundle/PimcoreBundle/Resources/public/pimcore/js/broker.js index 5e599b636b..9abbe856d7 100644 --- a/src/CoreShop/Bundle/PimcoreBundle/Resources/public/pimcore/js/broker.js +++ b/src/CoreShop/Bundle/PimcoreBundle/Resources/public/pimcore/js/broker.js @@ -6,7 +6,7 @@ * files that are distributed with this source code. * * @copyright Copyright (c) CoreShop GmbH (https://www.coreshop.org) - * @license https://www.coreshop.org/license GPLv3 and CCL + * @license https://www.coreshop.org/license GNU General Public License version 3 (GPLv3) * */ @@ -27,6 +27,10 @@ coreshop.broker = { var list = coreshop.broker._listeners[name]; + list = list.sort(function(a, b) { + return a.priority - b.priority; + }); + //copy arguments var args = []; for (var j = 1; j < arguments.length; j++) { @@ -59,7 +63,7 @@ coreshop.broker = { } }, - addListener: function (name, func, scope, once) { + addListener: function (name, func, scope, once, priority) { if (coreshop.broker._listeners[name] === undefined) { coreshop.broker._listeners[name] = []; } @@ -67,7 +71,8 @@ coreshop.broker = { coreshop.broker._listeners[name].push({ func: func, scope: scope, - once: Ext.isDefined(once) ? once : false + once: Ext.isDefined(once) ? once : false, + priority: priority ?? 0 }); }, diff --git a/src/CoreShop/Bundle/VariantBundle/DependencyInjection/Configuration.php b/src/CoreShop/Bundle/VariantBundle/DependencyInjection/Configuration.php index b364537e80..5b0ba813c7 100644 --- a/src/CoreShop/Bundle/VariantBundle/DependencyInjection/Configuration.php +++ b/src/CoreShop/Bundle/VariantBundle/DependencyInjection/Configuration.php @@ -24,6 +24,7 @@ use CoreShop\Component\Variant\Model\AttributeGroupInterface; use CoreShop\Component\Variant\Model\AttributeInterface; use CoreShop\Component\Variant\Model\AttributeValueInterface; +use CoreShop\Component\Variant\Model\ProductVariantAwareInterface; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -58,6 +59,7 @@ private function addStack(ArrayNodeDefinition $node): void ->children() ->scalarNode('attribute_group')->defaultValue(AttributeGroupInterface::class)->cannotBeEmpty()->end() ->scalarNode('attribute')->defaultValue(AttributeInterface::class)->cannotBeEmpty()->end() + ->scalarNode('variant_aware')->defaultValue(ProductVariantAwareInterface::class)->cannotBeEmpty()->end() ->end() ->end() ->end() diff --git a/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/admin.yml b/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/admin.yml new file mode 100644 index 0000000000..20ee98d439 --- /dev/null +++ b/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/admin.yml @@ -0,0 +1,4 @@ +core_shop_variant: + pimcore_admin: + js: + resource: '/bundles/coreshopvariant/pimcore/js/resource.js' diff --git a/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/config.yml b/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/config.yml index d114c15215..4d84d20e16 100755 --- a/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/config.yml +++ b/src/CoreShop/Bundle/VariantBundle/Resources/config/pimcore/config.yml @@ -1,2 +1,3 @@ imports: + - { resource: admin.yml } - { resource: messenger.yml } diff --git a/src/CoreShop/Bundle/VariantBundle/Resources/public/pimcore/js/resource.js b/src/CoreShop/Bundle/VariantBundle/Resources/public/pimcore/js/resource.js new file mode 100644 index 0000000000..56055f1ab5 --- /dev/null +++ b/src/CoreShop/Bundle/VariantBundle/Resources/public/pimcore/js/resource.js @@ -0,0 +1,186 @@ +/* + * CoreShop. + * + * This source file is subject to the GNU General Public License version 3 (GPLv3) + * For the full copyright and license information, please view the LICENSE.md and gpl-3.0.txt + * files that are distributed with this source code. + * + * @copyright Copyright (c) CoreShop GmbH (https://www.coreshop.org) + * @license https://www.coreshop.org/license GPLv3 and CCL + * + */ +pimcore.registerNS('coreshop.variant'); +pimcore.registerNS('coreshop.variant.resource'); +coreshop.variant.resource = Class.create(coreshop.resource, { + initialize: function () { + coreshop.broker.fireEvent('resource.register', 'coreshop.variant', this); + coreshop.broker.addListener('pimcore.postOpenObject', this.postOpenObject, this); + }, + + postOpenObject: function (tab) { + const className = tab.data.general.className; + + if (!coreshop.stack.coreshop.variant_aware.includes(className)) { + return; + } + + this._enrichProductObject(tab); + + pimcore.layout.refresh(); + }, + + _enrichProductObject: function (tab) { + + if (tab.data.general.type === 'object') { + if(tab?.data?.data?.allowedAttributeGroups?.length) { + const variantHandler = () => { + const store = Ext.create('Ext.data.TreeStore', { + proxy: { + type: 'ajax', + url: Routing.generate('coreshop_admin_variant_attributes', {id: tab.id}), + reader: { + type: 'json', + rootProperty: 'data' + }, + }, + sorters: [{ + property: 'sorting', + direction: 'ASC' + }] + }); + + store.on('load', (store, records, successful, operation, eOpts) => { + if(!store) { + console.error('no data found'); + } + }); + + const applyButton = Ext.create('Ext.Button', { + text: t("apply"), + iconCls: "pimcore_icon_accept", + disabled: true, + handler: function() { + const groupedAttributes = (tree.getView().getChecked()).reduce((acc, obj) => { + const groupId = obj.data.group_id; + if (!acc[groupId]) { + acc[groupId] = []; + } + acc[groupId].push(obj.data.id); + return acc; + }, {}); + + Ext.Ajax.request({ + url: Routing.generate('coreshop_admin_variant_generator'), + jsonData: { id: tab.id, attributes: groupedAttributes }, + method: 'POST', + success: function (response) { + var res = Ext.decode(response.responseText); + if (res.success === true) { + Ext.Msg.alert(t('success'), res.message); + window.destroy(); + } else { + Ext.Msg.alert(t('error'), res.message); + } + }.bind(this) + }); + } + }) + + const tree = Ext.create('Ext.tree.Panel', { + hideHeaders: true, + rootVisible: false, + store: store, + layout: 'fit', + listeners: { + checkchange: function () { + const rootNode = tree.getRootNode(); + let allParentsHaveSelection = true; + + rootNode.eachChild(function (parentNode) { + var hasCheckedLeaf = false; + + parentNode.eachChild(function (childNode) { + if (childNode.get('checked')) { + hasCheckedLeaf = true; + return false; + } + }); + + if (!hasCheckedLeaf) { + allParentsHaveSelection = false; + return false; + } + }); + + if (allParentsHaveSelection) { + applyButton.enable(); + } else { + applyButton.disable(); + } + } + } + }); + + const panel = new Ext.Panel({ + layout: 'fit', + header: false, + bodyStyle: "padding:10px", + border: false, + buttons: [ + applyButton, + { + text: t("close"), + iconCls: "pimcore_icon_cancel", + handler: function() { + window.destroy(); + } + } + ], + frame: false, + items: [ + tree + ], + }); + + const window = new Ext.window.Window({ + closeAction: 'close', + height: 400, + width: 600, + layout: 'fit', + items: [ + panel + ], + modal: false, + plain: true, + title: t('coreshop.variant_generator.generate'), + }); + + window.show(); + }; + + if (tab.toolbar.child('[iconCls="coreshop_icon_logo"]')) { + tab.toolbar.child('[iconCls="coreshop_icon_logo"]').menu.add({ + text: t('coreshop.variant_generator.generate'), + scale: 'medium', + iconCls: 'pimcore_icon_variant', + handler: variantHandler.bind(this, tab) + }); + } + else { + tab.toolbar.insert(tab.toolbar.items.length, '-'); + + tab.toolbar.insert(tab.toolbar.items.length, { + text: t('coreshop.variant_generator.generate'), + scale: 'medium', + iconCls: 'pimcore_icon_variant', + handler: variantHandler.bind(this, tab) + }); + } + } + } + } +}); + +coreshop.broker.addListener('pimcore.ready', function () { + new coreshop.variant.resource(); +}); From 53f046a12bb0023a7f70605e61d1848591ad9b80 Mon Sep 17 00:00:00 2001 From: Dominik Pfaffenbauer Date: Mon, 12 Aug 2024 08:40:34 +0200 Subject: [PATCH 11/11] [VariantBundle] add messenger to consumer to installation guide --- docs/01_Getting_Started/00_Installation.md | 2 +- docs/03_Bundles/Variant_Bundle.md | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/01_Getting_Started/00_Installation.md b/docs/01_Getting_Started/00_Installation.md index 963f160ef4..066027fb88 100644 --- a/docs/01_Getting_Started/00_Installation.md +++ b/docs/01_Getting_Started/00_Installation.md @@ -78,7 +78,7 @@ CoreShop also uses Symfony Messenger for async tasks like sending E-Mails or Pro Please run these 2 transports to process the data ```yaml -bin/console messenger:consume coreshop_notification coreshop_index --time-limit=300 +bin/console messenger:consume coreshop_notification coreshop_index coreshop_variant --time-limit=300 ``` ## Payment diff --git a/docs/03_Bundles/Variant_Bundle.md b/docs/03_Bundles/Variant_Bundle.md index 8319193132..0f81520314 100644 --- a/docs/03_Bundles/Variant_Bundle.md +++ b/docs/03_Bundles/Variant_Bundle.md @@ -72,3 +72,15 @@ include these fields: The Variant Bundle significantly enhances the flexibility of product management in CoreShop, allowing for detailed and diverse product variant configurations. + +## Variant Generator +The Variant Generator is a tool that automatically generates variants for a VariantAware Class based on the attribute groups +defined in the Data Object. The Generator is available in the Pimcore backend at the Toolbar on your VariantAware Class. + +### Installation + +Variant Generator uses Symfony Messenger for async processing, you can run it with the following command: + +```yaml +bin/console messenger:consume coreshop_variant --time-limit=300 +``` \ No newline at end of file