diff --git a/EventListener/LoadClassMetadataListener.php b/EventListener/LoadClassMetadataListener.php new file mode 100644 index 00000000..6b9e7ee0 --- /dev/null +++ b/EventListener/LoadClassMetadataListener.php @@ -0,0 +1,22 @@ +getClassMetadata(); + + if (is_a($classMetadata->getName(), File::class, true)) { + $classMetadata->table['indexes']['oro_akeneo_file_parent_index'] = [ + 'columns' => ['parent_entity_class', 'parent_entity_id'], + ]; + + $classMetadata->fieldMappings[$classMetadata->getFieldForColumn('parent_entity_class')]['length'] = 255; + } + } +} diff --git a/ImportExport/Processor/ProductImageImportProcessor.php b/ImportExport/Processor/ProductImageImportProcessor.php index 393932ed..7e54832d 100644 --- a/ImportExport/Processor/ProductImageImportProcessor.php +++ b/ImportExport/Processor/ProductImageImportProcessor.php @@ -2,13 +2,25 @@ namespace Oro\Bundle\AkeneoBundle\ImportExport\Processor; +use Oro\Bundle\BatchBundle\Item\Support\ClosableInterface; use Oro\Bundle\IntegrationBundle\ImportExport\Processor\StepExecutionAwareImportProcessor; use Oro\Bundle\ProductBundle\Entity\Product; use Oro\Bundle\ProductBundle\Entity\ProductImage; use Oro\Bundle\ProductBundle\Entity\ProductImageType; -class ProductImageImportProcessor extends StepExecutionAwareImportProcessor +class ProductImageImportProcessor extends StepExecutionAwareImportProcessor implements ClosableInterface { + public function close() + { + if ($this->strategy instanceof ClosableInterface) { + $this->strategy->close(); + } + + if ($this->dataConverter instanceof ClosableInterface) { + $this->dataConverter->close(); + } + } + /** * {@inheritdoc} */ @@ -70,6 +82,14 @@ private function mergeImages(Product $product, array $images): Product continue; } + if (!is_a($image->getImage()->getParentEntityClass(), ProductImage::class, true)) { + $image->setImage(null); + + $product->removeImage($image); + + continue; + } + $filename = $image->getImage()->getOriginalFilename(); if (!in_array($filename, array_keys($images))) { $product->removeImage($image); @@ -77,16 +97,16 @@ private function mergeImages(Product $product, array $images): Product continue; } - if ($hasMain && $image->hasType(ProductImageType::TYPE_MAIN)) { + if ($hasMain && $this->hasType($image, ProductImageType::TYPE_MAIN)) { $image->removeType(ProductImageType::TYPE_MAIN); } - if ($hasListing && $image->hasType(ProductImageType::TYPE_LISTING)) { + if ($hasListing && $this->hasType($image, ProductImageType::TYPE_LISTING)) { $image->removeType(ProductImageType::TYPE_LISTING); } - $hasMain = $hasMain || $image->hasType(ProductImageType::TYPE_MAIN); - $hasListing = $hasListing || $image->hasType(ProductImageType::TYPE_LISTING); + $hasMain = $hasMain || $this->hasType($image, ProductImageType::TYPE_MAIN); + $hasListing = $hasListing || $this->hasType($image, ProductImageType::TYPE_LISTING); unset($images[$filename]); } @@ -113,4 +133,15 @@ private function mergeImages(Product $product, array $images): Product return $product; } + + private function hasType(ProductImage $image, string $type): bool + { + foreach ($image->getTypes() as $imageType) { + if ($imageType->getType() === $type) { + return true; + } + } + + return false; + } } diff --git a/ImportExport/Strategy/BrandImportStrategy.php b/ImportExport/Strategy/BrandImportStrategy.php index 17f0fcbd..85220b96 100644 --- a/ImportExport/Strategy/BrandImportStrategy.php +++ b/ImportExport/Strategy/BrandImportStrategy.php @@ -32,7 +32,7 @@ public function beforeProcessEntity($entity) { $this->setOwner($entity); - return $entity; + return parent::beforeProcessEntity($entity); } protected function afterProcessEntity($entity) @@ -47,7 +47,12 @@ protected function afterProcessEntity($entity) $this->addSlug($entity, $entity->getDefaultName()); } - return $entity; + $result = parent::afterProcessEntity($entity); + if (!$result && $entity) { + $this->processValidationErrors($entity, []); + } + + return $result; } private function addSlug(Brand $brand, LocalizedFallbackValue $localizedName): void diff --git a/ImportExport/Strategy/ProductImageImportStrategy.php b/ImportExport/Strategy/ProductImageImportStrategy.php index a4738fa7..14a22296 100644 --- a/ImportExport/Strategy/ProductImageImportStrategy.php +++ b/ImportExport/Strategy/ProductImageImportStrategy.php @@ -2,16 +2,28 @@ namespace Oro\Bundle\AkeneoBundle\ImportExport\Strategy; +use Oro\Bundle\BatchBundle\Item\Support\ClosableInterface; use Oro\Bundle\ImportExportBundle\Strategy\Import\ConfigurableAddOrReplaceStrategy; +use Oro\Bundle\ProductBundle\Entity\Product; use Oro\Bundle\ProductBundle\Entity\ProductImage; /** * Strategy to import product images. */ -class ProductImageImportStrategy extends ConfigurableAddOrReplaceStrategy +class ProductImageImportStrategy extends ConfigurableAddOrReplaceStrategy implements ClosableInterface { use ImportStrategyAwareHelperTrait; + /** + * @var Product[] + */ + private $existingProducts = []; + + public function close() + { + $this->existingProducts = []; + } + /** * @param ProductImage $entity * @@ -39,10 +51,14 @@ protected function beforeProcessEntity($entity) continue; } + if (!is_a($image->getImage()->getParentEntityClass(), ProductImage::class, true)) { + continue; + } + if ($image->getImage()->getOriginalFilename() === $entity->getImage()->getOriginalFilename()) { $itemData['image']['uuid'] = $image->getImage()->getUuid(); - $entity = $image; + $this->fieldHelper->setObjectValue($entity, 'id', $image->getId()); } } $this->context->setValue('itemData', $itemData); @@ -76,4 +92,45 @@ protected function validateBeforeProcess($entity) return $entity; } + + protected function isFieldExcluded($entityName, $fieldName, $itemData = null) + { + $excludeImageFields = ['updatedAt', 'types']; + + if (is_a($entityName, ProductImage::class, true) && in_array($fieldName, $excludeImageFields)) { + return true; + } + + return parent::isFieldExcluded($entityName, $fieldName, $itemData); + } + + protected function findExistingEntity($entity, array $searchContext = []) + { + if ($entity instanceof Product && array_key_exists($entity->getSku(), $this->existingProducts)) { + return $this->existingProducts[$entity->getSku()]; + } + + $entity = parent::findExistingEntity($entity, $searchContext); + + if ($entity instanceof Product) { + $this->existingProducts[$entity->getSku()] = $entity; + } + + return $entity; + } + + protected function findExistingEntityByIdentityFields($entity, array $searchContext = []) + { + if ($entity instanceof Product && array_key_exists($entity->getSku(), $this->existingProducts)) { + return $this->existingProducts[$entity->getSku()]; + } + + $entity = parent::findExistingEntityByIdentityFields($entity, $searchContext); + + if ($entity instanceof Product) { + $this->existingProducts[$entity->getSku()] = $entity; + } + + return $entity; + } } diff --git a/ImportExport/Writer/AttributeWriter.php b/ImportExport/Writer/AttributeWriter.php index ba7caeb6..2aeacc70 100644 --- a/ImportExport/Writer/AttributeWriter.php +++ b/ImportExport/Writer/AttributeWriter.php @@ -371,6 +371,7 @@ private function saveAttachmentConfig(string $className, string $fieldName, stri $attachmentConfig = $attachmentProvider->getConfig($className, $fieldName); $attachmentConfig->set('file_applications', ['default', 'commerce']); $attachmentConfig->set('acl_protected', true); + $attachmentConfig->set('use_dam', false); $attachmentConfig->set('maxsize', self::MAX_SIZE); $attachmentConfig->set('width', self::MAX_WIDTH); $attachmentConfig->set('height', self::MAX_HEIGHT); diff --git a/Migrations/Data/ORM/DisableDAM.php b/Migrations/Data/ORM/DisableDAM.php new file mode 100644 index 00000000..f1eae992 --- /dev/null +++ b/Migrations/Data/ORM/DisableDAM.php @@ -0,0 +1,27 @@ +container->get('oro_entity_config.config_manager'); + $attachmentProvider = $configManager->getProvider('attachment'); + $attachmentConfig = $attachmentProvider->getConfig(ProductImage::class, 'image'); + $attachmentConfig->set('use_dam', false); + + $configManager->persist($attachmentConfig); + $configManager->flush(); + } +} diff --git a/Migrations/Schema/OroAkeneoBundleInstaller.php b/Migrations/Schema/OroAkeneoBundleInstaller.php index e198ac7c..c60c2b2a 100644 --- a/Migrations/Schema/OroAkeneoBundleInstaller.php +++ b/Migrations/Schema/OroAkeneoBundleInstaller.php @@ -48,7 +48,7 @@ class OroAkeneoBundleInstaller implements Installation, ExtendExtensionAwareInte */ public function getMigrationVersion() { - return 'v1_13'; + return 'v1_14'; } /** diff --git a/Migrations/Schema/v1_14/OroAkeneoMigration.php b/Migrations/Schema/v1_14/OroAkeneoMigration.php new file mode 100644 index 00000000..cda64358 --- /dev/null +++ b/Migrations/Schema/v1_14/OroAkeneoMigration.php @@ -0,0 +1,71 @@ +addPostQuery( + "UPDATE oro_attachment_file +SET owner_user_id = (SELECT default_user_owner_id FROM oro_integration_channel WHERE type = 'oro_akeneo' LIMIT 1) +WHERE owner_user_id IS NULL + AND parent_entity_class = 'Oro\Bundle\ProductBundle\Entity\Product' + AND parent_entity_field_name LIKE 'Akeneo%';" + ); + + if ($this->platform instanceof MySqlPlatform) { + $queries->addPostQuery( + "UPDATE oro_attachment_file +SET uuid = UUID(); +WHERE parent_entity_class = 'Oro\Bundle\ProductBundle\Entity\Product' +AND parent_entity_field_name LIKE 'Akeneo%';" + ); + } elseif ($this->platform instanceof PostgreSqlPlatform) { + $queries->addPostQuery( + "UPDATE oro_attachment_file +SET uuid = uuid_generate_v4() +WHERE parent_entity_class = 'Oro\Bundle\ProductBundle\Entity\Product' +AND parent_entity_field_name LIKE 'Akeneo%';" + ); + } + + $queries->addPostQuery( + "UPDATE oro_attachment_file +SET owner_user_id = (SELECT default_user_owner_id FROM oro_integration_channel WHERE type = 'oro_akeneo' LIMIT 1) +WHERE owner_user_id IS null + AND parent_entity_class = 'Oro\Bundle\ProductBundle\Entity\ProductImage' + AND parent_entity_field_name = 'image';" + ); + + if ($this->platform instanceof MySqlPlatform) { + $queries->addPostQuery( + "UPDATE oro_attachment_file +SET uuid = UUID(); +WHERE parent_entity_class = 'Oro\Bundle\ProductBundle\Entity\ProductImage' +AND parent_entity_field_name = 'image';" + ); + } elseif ($this->platform instanceof PostgreSqlPlatform) { + $queries->addPostQuery( + "UPDATE oro_attachment_file +SET uuid = uuid_generate_v4() +WHERE parent_entity_class = 'Oro\Bundle\ProductBundle\Entity\ProductImage' +AND parent_entity_field_name = 'image';" + ); + } + + $table = $schema->getTable('oro_attachment_file'); + $table->getColumn('parent_entity_class')->setLength(255); + $table->addIndex(['parent_entity_class', 'parent_entity_id'], 'oro_akeneo_file_parent_index'); + } +} diff --git a/Resources/config/services.yml b/Resources/config/services.yml index f03ebcdb..bd9d059c 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -74,6 +74,24 @@ services: tags: - { name: validator.constraint_validator, alias: oro_akeneo.attribute_mapping_validator } + oro_akeneo.validator.unique_variant_links: + class: 'Oro\Bundle\AkeneoBundle\Validator\UniqueProductVariantLinksValidator' + decorates: oro_product.validator.unique_variant_links + arguments: + - '@oro_entity.doctrine_helper' + - '@oro_akeneo.validator.unique_variant_links.inner' + tags: + - { name: validator.constraint_validator, alias: oro_product_unique_variant_links } + + oro_akeneo.validator.unique_variant_links_simple_product: + class: 'Oro\Bundle\AkeneoBundle\Validator\UniqueVariantLinksSimpleProductValidator' + decorates: oro_product.validator.unique_variant_links_simple_product + arguments: + - '@oro_entity.doctrine_helper' + - '@oro_akeneo.validator.unique_variant_links_simple_product.inner' + tags: + - { name: validator.constraint_validator, alias: oro_product_unique_variant_links_simple_product } + oro_akeneo.event_subscriber.doctrine: class: 'Oro\Bundle\AkeneoBundle\EventSubscriber\DoctrineSubscriber' calls: @@ -131,6 +149,11 @@ services: arguments: - '@oro_akeneo.event_listener.import_export_tags_subscriber.decorator.inner' + oro_akeneo.event_listener.load_class_metadata: + class: Oro\Bundle\AkeneoBundle\EventListener\LoadClassMetadataListener + tags: + - { name: doctrine.event_listener, event: loadClassMetadata } + oro_akeneo.event_listener.product_collection_variant_reindex_message_send_listener.decorator: class: Oro\Bundle\AkeneoBundle\EventListener\ProductCollectionVariantReindexMessageSendListenerDecorator decorates: oro_product.entity.event_listener.product_collection_variant_reindex_message_send_listener diff --git a/Validator/UniqueProductVariantLinksValidator.php b/Validator/UniqueProductVariantLinksValidator.php new file mode 100644 index 00000000..e6e8b001 --- /dev/null +++ b/Validator/UniqueProductVariantLinksValidator.php @@ -0,0 +1,58 @@ +doctrineHelper = $doctrineHelper; + $this->validator = $validator; + } + + public function initialize(ExecutionContextInterface $context) + { + $this->validator->initialize($context); + + parent::initialize($context); + } + + public function validate($value, Constraint $constraint) + { + $product = $this->getConfigurableProduct($value, $constraint); + if ($product === null) { + return; + } + if (count($product->getVariantFields()) === 0) { + return null; + } + + $uow = $this->doctrineHelper->getEntityManagerForClass(Product::class)->getUnitOfWork(); + $collections = array_merge($uow->getScheduledCollectionUpdates(), $uow->getScheduledCollectionDeletions()); + if ( + !in_array($value->getVariantLinks(), $collections) + && !in_array($value->getParentVariantLinks(), $collections) + && empty($uow->getEntityChangeSet($value)['variantFields']) + ) { + return; + } + + $this->validator->validate($value, $constraint); + } +} diff --git a/Validator/UniqueVariantLinksSimpleProductValidator.php b/Validator/UniqueVariantLinksSimpleProductValidator.php new file mode 100644 index 00000000..edac4ba9 --- /dev/null +++ b/Validator/UniqueVariantLinksSimpleProductValidator.php @@ -0,0 +1,58 @@ +doctrineHelper = $doctrineHelper; + $this->validator = $validator; + } + + public function initialize(ExecutionContextInterface $context) + { + $this->validator->initialize($context); + + parent::initialize($context); + } + + public function validate($value, Constraint $constraint) + { + if (!is_a($value, Product::class)) { + throw new \InvalidArgumentException(sprintf('Entity must be instance of "%s", "%s" given', Product::class, is_object($value) ? get_class($value) : gettype($value))); + } + + if ($value->isConfigurable() || $value->getParentVariantLinks()->count() === 0) { + return; + } + + $uow = $this->doctrineHelper->getEntityManagerForClass(Product::class)->getUnitOfWork(); + $collections = array_merge($uow->getScheduledCollectionUpdates(), $uow->getScheduledCollectionDeletions()); + if ( + !in_array($value->getVariantLinks(), $collections) + && !in_array($value->getParentVariantLinks(), $collections) + && empty($uow->getEntityChangeSet($value)['variantFields']) + ) { + return; + } + + $this->validator->validate($value, $constraint); + } +}