From d2c1a19de020e6e5e610ac8be3437aa55f2be290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Poirier=20Th=C3=A9or=C3=AAt?= Date: Thu, 25 Apr 2024 17:10:59 -0400 Subject: [PATCH] [SonataImportBundle] Normalize structure of code --- composer.json | 4 +- composer.lock | 158 +++++------ config/services_test.yaml | 10 + .../DoctrineConfigurationRegistryTest.php | 69 +---- .../Admin/ColumnAdmin.php | 6 +- .../Admin/ImportAdmin.php | 191 +------------ .../Column/BaseColumnExtractor.php | 28 ++ .../DoctrineAssociationColumnExtractor.php} | 76 +++--- .../DoctrineFieldColumnExtractor.php | 66 +++++ .../DoctrineTranslationColumnExtractor.php} | 52 ++-- .../ColumnBuilder/ColumnBuilderInterface.php | 12 - .../Column/ColumnBuilder/ColumnInfo.php | 7 - .../DoctrineAssociationColumnBuilder.php | 43 --- .../DoctrineAssociationFieldColumnBuilder.php | 38 --- .../DoctrineAssociationPathColumnBuilder.php | 64 ----- .../Doctrine/DoctrineFieldColumnBuilder.php | 47 ---- .../MappedToOptionColumnBuilder.php | 34 --- .../NamedBaseIdentifierColumnBuilder.php | 21 -- .../ColumnBuilder/ReflectionColumnBuilder.php | 75 ------ .../Column/ColumnExtractorInterface.php | 38 +++ .../Column/ColumnFactory.php | 11 +- .../Extractor/ExactMatchColumnExtractor.php | 36 +++ .../Extractor/PropertyPathColumnExtractor.php | 29 ++ .../SetterMethodReflectionColumnExtractor.php | 119 +++++++++ .../MappedToOptionBuilderInterface.php | 17 -- .../MappedToOptionBuilderAggregator.php | 33 --- .../DrawSonataImportExtension.php | 10 +- .../Event/AttributeImportEvent.php | 34 --- .../sonata-import-bundle/Import/Importer.php | 251 ++++++++++++++++++ ...DoctrineAssociationColumnExtractorTest.php | 108 ++++++++ .../DoctrineFieldColumnExtractorTest.php | 147 ++++++++++ ...DoctrineTranslationColumnExtractorTest.php | 94 +++++++ .../Extractor/Fixtures/TranslatableEntity.php | 32 +++ .../TranslatableEntityTranslation.php | 47 ++++ .../ExactMatchColumnExtractorTest.php | 122 +++++++++ .../PropertyPathColumnExtractorTest.php | 75 ++++++ ...terMethodReflectionColumnExtractorTest.php | 171 ++++++++++++ .../Tests/ColumnFactoryTest.php | 39 +-- packages/sonata-import-bundle/composer.json | 4 + .../BaseTemporaryEntityCleaner.php | 37 +++ packages/tester/DoctrineOrmTrait.php | 72 +++++ phpunit.xml.dist | 2 +- tests/SonataImportBundle/ImporterTest.php | 86 ++++++ 43 files changed, 1739 insertions(+), 876 deletions(-) create mode 100644 packages/sonata-import-bundle/Column/BaseColumnExtractor.php rename packages/sonata-import-bundle/Column/{MappedToOptionBuilder/DoctrineMappedToOptionBuilder.php => Bridge/Doctrine/Extractor/DoctrineAssociationColumnExtractor.php} (59%) create mode 100644 packages/sonata-import-bundle/Column/Bridge/Doctrine/Extractor/DoctrineFieldColumnExtractor.php rename packages/sonata-import-bundle/Column/{MappedToOptionBuilder/DoctrineTranslationMappedToOptionBuilder.php => Bridge/KnpDoctrineBehaviors/Extractor/DoctrineTranslationColumnExtractor.php} (70%) delete mode 100644 packages/sonata-import-bundle/Column/ColumnBuilder/ColumnBuilderInterface.php delete mode 100644 packages/sonata-import-bundle/Column/ColumnBuilder/ColumnInfo.php delete mode 100644 packages/sonata-import-bundle/Column/ColumnBuilder/Doctrine/DoctrineAssociationColumnBuilder.php delete mode 100644 packages/sonata-import-bundle/Column/ColumnBuilder/Doctrine/DoctrineAssociationFieldColumnBuilder.php delete mode 100644 packages/sonata-import-bundle/Column/ColumnBuilder/Doctrine/DoctrineAssociationPathColumnBuilder.php delete mode 100644 packages/sonata-import-bundle/Column/ColumnBuilder/Doctrine/DoctrineFieldColumnBuilder.php delete mode 100644 packages/sonata-import-bundle/Column/ColumnBuilder/MappedToOptionColumnBuilder.php delete mode 100644 packages/sonata-import-bundle/Column/ColumnBuilder/NamedBaseIdentifierColumnBuilder.php delete mode 100644 packages/sonata-import-bundle/Column/ColumnBuilder/ReflectionColumnBuilder.php create mode 100644 packages/sonata-import-bundle/Column/ColumnExtractorInterface.php create mode 100644 packages/sonata-import-bundle/Column/Extractor/ExactMatchColumnExtractor.php create mode 100644 packages/sonata-import-bundle/Column/Extractor/PropertyPathColumnExtractor.php create mode 100644 packages/sonata-import-bundle/Column/Extractor/SetterMethodReflectionColumnExtractor.php delete mode 100644 packages/sonata-import-bundle/Column/MappedToOptionBuilder/MappedToOptionBuilderInterface.php delete mode 100644 packages/sonata-import-bundle/Column/MappedToOptionBuilderAggregator.php delete mode 100644 packages/sonata-import-bundle/Event/AttributeImportEvent.php create mode 100644 packages/sonata-import-bundle/Import/Importer.php create mode 100644 packages/sonata-import-bundle/Tests/Column/Bridge/Doctrine/Extractor/DoctrineAssociationColumnExtractorTest.php create mode 100644 packages/sonata-import-bundle/Tests/Column/Bridge/Doctrine/Extractor/DoctrineFieldColumnExtractorTest.php create mode 100644 packages/sonata-import-bundle/Tests/Column/Bridge/KnpDoctrineBehaviors/Extractor/DoctrineTranslationColumnExtractorTest.php create mode 100644 packages/sonata-import-bundle/Tests/Column/Bridge/KnpDoctrineBehaviors/Extractor/Fixtures/TranslatableEntity.php create mode 100644 packages/sonata-import-bundle/Tests/Column/Bridge/KnpDoctrineBehaviors/Extractor/Fixtures/TranslatableEntityTranslation.php create mode 100644 packages/sonata-import-bundle/Tests/Column/Extractor/ExactMatchColumnExtractorTest.php create mode 100644 packages/sonata-import-bundle/Tests/Column/Extractor/PropertyPathColumnExtractorTest.php create mode 100644 packages/sonata-import-bundle/Tests/Column/Extractor/SetterMethodReflectionColumnExtractorTest.php create mode 100644 packages/tester-bundle/PHPUnit/Extension/DeleteTemporaryEntity/BaseTemporaryEntityCleaner.php create mode 100644 tests/SonataImportBundle/ImporterTest.php diff --git a/composer.json b/composer.json index 301bc5be8..ab6d63e76 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,8 @@ "nesbot/carbon": "^2.0", "doctrine/mongodb-odm-bundle": "*", "fidry/cpu-core-counter": "^1.1", - "knplabs/doctrine-behaviors": "*" + "knplabs/doctrine-behaviors": "*", + "symfony/notifier": "^6.4.0" }, "require-dev": { "ext-pcntl": "*", @@ -121,7 +122,6 @@ "scheb/2fa-email": "^6.0", "colinodell/psr-testlogger": "^1.1", "cweagans/composer-patches": "^1.7", - "symfony/notifier": "^6.4.0", "symfony/maker-bundle": "^1.58" }, "replace": { diff --git a/composer.lock b/composer.lock index 3529adfab..2e463a1c7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f27d61a8609e8d6d0545bba8aeddcc89", + "content-hash": "0db9e6a087ae9ac17dc8fd4e98056a2d", "packages": [ { "name": "aws/aws-crt-php", @@ -9086,6 +9086,84 @@ ], "time": "2024-03-21T19:36:20+00:00" }, + { + "name": "symfony/notifier", + "version": "v6.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/notifier.git", + "reference": "1c6c7a744483c939f0e75446446f51a86bd9e329" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/notifier/zipball/1c6c7a744483c939f0e75446446f51a86bd9e329", + "reference": "1c6c7a744483c939f0e75446446f51a86bd9e329", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "symfony/event-dispatcher": "<5.4", + "symfony/event-dispatcher-contracts": "<2.5", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4" + }, + "require-dev": { + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\": "" + }, + "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": "Sends notifications via one or more channels (email, SMS, ...)", + "homepage": "https://symfony.com", + "keywords": [ + "notification", + "notifier" + ], + "support": { + "source": "https://github.com/symfony/notifier/tree/v6.4.3" + }, + "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": "2024-01-23T14:51:35+00:00" + }, { "name": "symfony/options-resolver", "version": "v6.4.0", @@ -14452,84 +14530,6 @@ ], "time": "2023-11-06T17:08:13+00:00" }, - { - "name": "symfony/notifier", - "version": "v6.4.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/notifier.git", - "reference": "1c6c7a744483c939f0e75446446f51a86bd9e329" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/notifier/zipball/1c6c7a744483c939f0e75446446f51a86bd9e329", - "reference": "1c6c7a744483c939f0e75446446f51a86bd9e329", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/log": "^1|^2|^3" - }, - "conflict": { - "symfony/event-dispatcher": "<5.4", - "symfony/event-dispatcher-contracts": "<2.5", - "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<5.4" - }, - "require-dev": { - "symfony/event-dispatcher-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^2.5|^3", - "symfony/http-foundation": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Notifier\\": "" - }, - "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": "Sends notifications via one or more channels (email, SMS, ...)", - "homepage": "https://symfony.com", - "keywords": [ - "notification", - "notifier" - ], - "support": { - "source": "https://github.com/symfony/notifier/tree/v6.4.3" - }, - "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": "2024-01-23T14:51:35+00:00" - }, { "name": "symfony/phpunit-bridge", "version": "v6.4.6", diff --git a/config/services_test.yaml b/config/services_test.yaml index 24d0e89bd..6cc7ab93a 100644 --- a/config/services_test.yaml +++ b/config/services_test.yaml @@ -1,2 +1,12 @@ services: + _defaults: + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + scheb_two_factor.security.totp_authenticator: '@App\Security\TotpAuthenticatorMock' + + Draw\Bundle\TesterBundle\PHPUnit\Extension\DeleteTemporaryEntity\TemporaryEntityCleanerInterface: '@Draw\Bundle\TesterBundle\PHPUnit\Extension\DeleteTemporaryEntity\BaseTemporaryEntityCleaner' + + Draw\Bundle\TesterBundle\PHPUnit\Extension\DeleteTemporaryEntity\BaseTemporaryEntityCleaner: + class: Draw\Bundle\TesterBundle\PHPUnit\Extension\DeleteTemporaryEntity\BaseTemporaryEntityCleaner + public: true diff --git a/packages/application/Tests/Configuration/DoctrineConfigurationRegistryTest.php b/packages/application/Tests/Configuration/DoctrineConfigurationRegistryTest.php index e58256c37..1b919df6e 100644 --- a/packages/application/Tests/Configuration/DoctrineConfigurationRegistryTest.php +++ b/packages/application/Tests/Configuration/DoctrineConfigurationRegistryTest.php @@ -4,7 +4,6 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\UnitOfWork; -use Doctrine\Persistence\ManagerRegistry; use Draw\Component\Application\Configuration\DoctrineConfigurationRegistry; use Draw\Component\Application\Configuration\Entity\Config; use Draw\Component\Core\Reflection\ReflectionAccessor; @@ -41,73 +40,7 @@ public static function setUpBeforeClass(): void protected function setUp(): void { - $managerRegistry = new class(self::$entityManager) implements ManagerRegistry { - public function __construct(private EntityManagerInterface $entityManager) - { - } - - public function getDefaultConnectionName() - { - return 'default'; - } - - public function getConnection($name = null) - { - return $this->entityManager->getConnection(); - } - - public function getConnections() - { - return ['default' => $this->getConnection()]; - } - - public function getConnectionNames() - { - return ['default' => 'default']; - } - - public function getDefaultManagerName() - { - return 'default'; - } - - public function getManager($name = null) - { - return $this->entityManager; - } - - public function getManagers() - { - return ['default' => $this->entityManager]; - } - - public function resetManager($name = null) - { - return $this->entityManager; - } - - public function getAliasNamespace($alias) - { - return $alias; - } - - public function getManagerNames() - { - return ['default' => 'manager.default']; - } - - public function getRepository($persistentObject, $persistentManagerName = null) - { - return $this->entityManager->getRepository($persistentObject); - } - - public function getManagerForClass($class) - { - return $this->entityManager; - } - }; - - $this->object = new DoctrineConfigurationRegistry($managerRegistry); + $this->object = new DoctrineConfigurationRegistry(static::createRegistry(self::$entityManager)); } public function testConstruct(): void diff --git a/packages/sonata-import-bundle/Admin/ColumnAdmin.php b/packages/sonata-import-bundle/Admin/ColumnAdmin.php index 713ba21fa..102dcf1ac 100644 --- a/packages/sonata-import-bundle/Admin/ColumnAdmin.php +++ b/packages/sonata-import-bundle/Admin/ColumnAdmin.php @@ -2,8 +2,8 @@ namespace Draw\Bundle\SonataImportBundle\Admin; -use Draw\Bundle\SonataImportBundle\Column\MappedToOptionBuilderAggregator; use Draw\Bundle\SonataImportBundle\Entity\Column; +use Draw\Bundle\SonataImportBundle\Import\Importer; use Sonata\AdminBundle\Admin\AbstractAdmin; use Sonata\AdminBundle\Form\FormMapper; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; @@ -25,7 +25,7 @@ class ColumnAdmin extends AbstractAdmin { public function __construct( - private MappedToOptionBuilderAggregator $mappedToOptionBuilderAggregator + private Importer $importer ) { parent::__construct(); } @@ -64,7 +64,7 @@ protected function configureFormFields(FormMapper $form): void private function loadMappedToOptions(Column $column): array { - $options = $this->mappedToOptionBuilderAggregator->getOptions($column); + $options = $this->importer->getOptions($column); $result = []; // Iterate over each element in the original array diff --git a/packages/sonata-import-bundle/Admin/ImportAdmin.php b/packages/sonata-import-bundle/Admin/ImportAdmin.php index 168af6a4c..848cce533 100644 --- a/packages/sonata-import-bundle/Admin/ImportAdmin.php +++ b/packages/sonata-import-bundle/Admin/ImportAdmin.php @@ -2,13 +2,9 @@ namespace Draw\Bundle\SonataImportBundle\Admin; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\NonUniqueResultException; -use Doctrine\Persistence\ManagerRegistry; -use Draw\Bundle\SonataImportBundle\Column\ColumnFactory; use Draw\Bundle\SonataImportBundle\Controller\ImportController; use Draw\Bundle\SonataImportBundle\Entity\Import; -use Draw\Bundle\SonataImportBundle\Event\AttributeImportEvent; +use Draw\Bundle\SonataImportBundle\Import\Importer; use Sonata\AdminBundle\Admin\AbstractAdmin; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\AdminBundle\Form\FormMapper; @@ -19,8 +15,6 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\HttpFoundation\File\UploadedFile; -use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; #[AutoconfigureTag( 'sonata.admin', @@ -36,9 +30,7 @@ class ImportAdmin extends AbstractAdmin { public function __construct( - private ColumnFactory $columnFactory, - private EventDispatcherInterface $eventDispatcher, - private ManagerRegistry $managerRegistry, + private Importer $importer, #[Autowire('%draw.sonata_import.classes%')] private array $importableClassList ) { @@ -68,167 +60,13 @@ protected function prePersist($object): void protected function postUpdate($object): void { if (Import::STATE_VALIDATION === $object->getState()) { - if ($this->processImport($object)) { + if ($this->importer->processImport($object)) { $object->setState(Import::STATE_PROCESSED); $this->getModelManager()->update($object); } } } - private function processImport(Import $import): bool - { - $flashBag = $this->getRequest()->getSession()->getFlashBag(); - - $file = tempnam(sys_get_temp_dir(), 'csv_'); - file_put_contents($file, $import->getFileContent()); - register_shutdown_function('unlink', $file); - $handle = fopen($file, 'r+'); - $headers = fgetcsv($handle); - - $identifierHeaderName = $import->getIdentifierHeaderName(); - $columnMapping = $import->getColumnMapping(); - $line = 1; - $saved = 0; - $accessor = PropertyAccess::createPropertyAccessor(); - - $identifierColumns = $import->getIdentifierColumns(); - - $identifierHeaderNames = []; - foreach ($identifierColumns as $column) { - $identifierHeaderNames[$column->getHeaderName()] = $column->getMappedTo(); - } - - while (($row = fgetcsv($handle)) !== false) { - ++$line; - $data = array_combine($headers, $row); - $id = $data[$identifierHeaderName]; - $criteria = []; - foreach ($identifierHeaderNames as $headerName => $mappedTo) { - $criteria[$mappedTo] = $data[$headerName]; - } - - $model = $this->findOne($import->getEntityClass(), $criteria, $import->getInsertWhenNotFound()); - - if (null === $model) { - $flashBag->add( - 'sonata_flash_error', - 'Skipped Id ['.implode(', ', $criteria).'] cannot be found at line ['.$line.']. Make sure you are using unique id value.' - ); - continue; - } - - try { - foreach ($columnMapping as $headerName => $column) { - $value = $data[$headerName]; - if ($column->getIsDate()) { - $value = new \DateTime($value); - } - - $this->eventDispatcher->dispatch($event = new AttributeImportEvent($model, $column, $value)); - - if ($event->isPropagationStopped()) { - continue; - } - - $accessor->setValue($model, $column->getMappedTo(), $value); - } - } catch (\Throwable $exception) { - $flashBag->add( - 'sonata_flash_error', - 'Skipped Id ['.$id.'] at line ['.$line.']. Error: '.$exception->getMessage() - ); - continue; - } - - ++$saved; - } - - try { - $this->managerRegistry->getManagerForClass($import->getEntityClass())->flush(); - - $flashBag->add( - 'sonata_flash_success', - 'Entity saved: '.$saved - ); - - return true; - } catch (\Throwable $error) { - $flashBag->add( - 'sonata_flash_error', - 'Error saving data:'.$error->getMessage() - ); - - return false; - } - } - - /** - * The criteria can define path with dot for separator. - * - * @param array $criteria - */ - private function findOne(string $class, array $criteria, bool $create): ?object - { - $manager = $this->managerRegistry->getManagerForClass($class); - - \assert($manager instanceof EntityManagerInterface); - - $parameters = []; - - /** @var array> $relationsCriteria */ - $relationsCriteria = []; - - foreach ($criteria as $key => $value) { - if (1 === substr_count($key, '.')) { - [$object, $field] = explode('.', $key); - $relationsCriteria[$object][$field] = $value; - } else { - $parameters[$key] = $value; - } - } - - $classMetadata = $manager->getClassMetadata($class); - - foreach ($relationsCriteria as $relationName => $objectCriteria) { - $objectClass = $classMetadata->getAssociationTargetClass($relationName); - - $relatedObject = $this->managerRegistry - ->getRepository($objectClass) - ->findOneBy($objectCriteria); - - if (!$relatedObject) { - return null; - } - - $parameters[$relationName] = $relatedObject; - } - - $objects = $manager->getRepository($class)->findBy($parameters); - - if (\count($objects) > 1) { - throw new NonUniqueResultException(); - } - - if (1 === \count($objects)) { - return $objects[0]; - } - - if (!$create) { - return null; - } - - $accessor = PropertyAccess::createPropertyAccessor(); - - $object = new $class(); - foreach ($parameters as $key => $value) { - $accessor->setValue($object, $key, $value); - } - - $manager->persist($object); - - return $object; - } - private function processFileUpload(Import $import, ?UploadedFile $file = null): void { $flashBag = $this->getRequest()->getSession()->getFlashBag(); @@ -239,27 +77,8 @@ private function processFileUpload(Import $import, ?UploadedFile $file = null): return; } - $import->setFileContent(file_get_contents($file->getRealPath())); - - $handle = fopen($file->getRealPath(), 'r'); - - $headers = fgetcsv($handle); - $samples = []; - for ($i = 0; $i < 10; ++$i) { - $row = fgetcsv($handle); - if (!$row) { - break; - } - $samples[] = $row; - } - - $this->columnFactory->buildColumns( - $import, - $headers, - $samples - ); - - $import->setState(Import::STATE_CONFIGURATION); + $this->importer + ->buildFromFile($import, $file); } public function configureListFields(ListMapper $list): void diff --git a/packages/sonata-import-bundle/Column/BaseColumnExtractor.php b/packages/sonata-import-bundle/Column/BaseColumnExtractor.php new file mode 100644 index 000000000..7f20870de --- /dev/null +++ b/packages/sonata-import-bundle/Column/BaseColumnExtractor.php @@ -0,0 +1,28 @@ +getColumn(); - - if (!str_contains($column->getMappedTo(), '.')) { - return; - } - - $class = $column->getImport()->getEntityClass(); - - $metadata = $this->managerRegistry->getManagerForClass($class)->getClassMetadata($class); - - [$relation, $field] = explode('.', $column->getMappedTo()); - - if (!isset($metadata->associationMappings[$relation])) { - return; - } - - $targetEntityClass = $metadata->associationMappings[$relation]['targetEntity']; - - $targetEntity = $this->managerRegistry->getRepository($targetEntityClass)->findOneBy([$field => $event->getValue()]); - - if (null === $targetEntity) { - return; - } - - $event->getEntity()->{'set'.ucfirst($relation)}($targetEntity); - - $event->stopPropagation(); - } - + #[\Override] public function getOptions(Column $column, array $options): array { $class = $column->getImport()->getEntityClass(); @@ -58,10 +26,6 @@ public function getOptions(Column $column, array $options): array return $options; } - foreach ($metadata->fieldNames as $fieldName) { - $options[] = $fieldName; - } - foreach ($metadata->associationMappings as $name => $associationMapping) { if (!($associationMapping['type'] & ClassMetadataInfo::TO_ONE)) { continue; @@ -86,4 +50,36 @@ public function getOptions(Column $column, array $options): array return $options; } + + #[\Override] + public function assign(object $object, Column $column, mixed $value): bool + { + if (!\in_array($column->getMappedTo(), $this->getOptions($column, []))) { + return false; + } + + $class = $column->getImport()->getEntityClass(); + + $classMetadata = $this->managerRegistry + ->getManagerForClass($class) + ->getClassMetadata($class); + + \assert($classMetadata instanceof ClassMetadata); + + [$relation, $field] = explode('.', $column->getMappedTo()); + + $targetEntityClass = $classMetadata->associationMappings[$relation]['targetEntity']; + + $targetEntity = $this->managerRegistry + ->getRepository($targetEntityClass) + ->findOneBy([$field => $value]); + + if (null === $targetEntity) { + return false; + } + + $object->{'set'.ucfirst($relation)}($targetEntity); + + return true; + } } diff --git a/packages/sonata-import-bundle/Column/Bridge/Doctrine/Extractor/DoctrineFieldColumnExtractor.php b/packages/sonata-import-bundle/Column/Bridge/Doctrine/Extractor/DoctrineFieldColumnExtractor.php new file mode 100644 index 000000000..241f70903 --- /dev/null +++ b/packages/sonata-import-bundle/Column/Bridge/Doctrine/Extractor/DoctrineFieldColumnExtractor.php @@ -0,0 +1,66 @@ +getImport()->getEntityClass(); + + $metadata = $this->managerRegistry->getManagerForClass($class)->getClassMetadata($class); + + if (!$metadata instanceof ClassMetadata) { + return $options; + } + + return array_merge( + $options, + array_values($metadata->fieldNames) + ); + } + + #[\Override] + public function extractDefaultValue(Column $column, array $samples): ?Column + { + if (!\in_array($column->getHeaderName(), $this->getOptions($column, []))) { + return null; + } + + $class = $column->getImport()->getEntityClass(); + $headerName = $column->getHeaderName(); + + $classMetadata = $this->managerRegistry + ->getManagerForClass($class) + ->getClassMetadata($class); + + \assert($classMetadata instanceof ClassMetadata); + + $fieldMapping = $classMetadata->fieldMappings[$headerName]; + + $columnInfo = (new Column()) + ->setMappedTo($headerName) + ->setIsDate(str_starts_with($fieldMapping['type'], 'date')); + + if ($fieldMapping['id'] ?? false) { + $columnInfo->setIsIdentifier(true); + } + + if (null === $column->getIsIdentifier() && ($fieldMapping['unique'] ?? false)) { + $columnInfo->setIsIdentifier(true); + } + + return $columnInfo; + } +} diff --git a/packages/sonata-import-bundle/Column/MappedToOptionBuilder/DoctrineTranslationMappedToOptionBuilder.php b/packages/sonata-import-bundle/Column/Bridge/KnpDoctrineBehaviors/Extractor/DoctrineTranslationColumnExtractor.php similarity index 70% rename from packages/sonata-import-bundle/Column/MappedToOptionBuilder/DoctrineTranslationMappedToOptionBuilder.php rename to packages/sonata-import-bundle/Column/Bridge/KnpDoctrineBehaviors/Extractor/DoctrineTranslationColumnExtractor.php index a844c177c..1753513e5 100644 --- a/packages/sonata-import-bundle/Column/MappedToOptionBuilder/DoctrineTranslationMappedToOptionBuilder.php +++ b/packages/sonata-import-bundle/Column/Bridge/KnpDoctrineBehaviors/Extractor/DoctrineTranslationColumnExtractor.php @@ -1,15 +1,14 @@ getColumn(); - - if (!str_starts_with($column->getMappedTo(), 'translation#')) { - return; - } - - $model = $event->getEntity(); - - if (!$model instanceof TranslatableInterface) { - return; - } - - $fieldInfo = explode('#', $column->getMappedTo())[1]; - - [$locale, $fieldName] = explode('.', $fieldInfo); - - $model->translate($locale, false)->{'set'.ucfirst($fieldName)}($event->getValue()); - - $event->stopPropagation(); - } - + #[\Override] public function getOptions(Column $column, array $options): array { $class = $column->getImport()->getEntityClass(); @@ -83,4 +59,24 @@ public function getOptions(Column $column, array $options): array return $options; } + + #[\Override] + public function assign(object $object, Column $column, mixed $value): bool + { + if (!\in_array($column->getMappedTo(), $this->getOptions($column, []))) { + return false; + } + + if (!$object instanceof TranslatableInterface) { + return false; + } + + $fieldInfo = explode('#', $column->getMappedTo())[1]; + + [$locale, $fieldName] = explode('.', $fieldInfo); + + $object->translate($locale, false)->{'set'.ucfirst($fieldName)}($value); + + return true; + } } diff --git a/packages/sonata-import-bundle/Column/ColumnBuilder/ColumnBuilderInterface.php b/packages/sonata-import-bundle/Column/ColumnBuilder/ColumnBuilderInterface.php deleted file mode 100644 index f09bc4813..000000000 --- a/packages/sonata-import-bundle/Column/ColumnBuilder/ColumnBuilderInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -getImport()->getEntityClass(); - $headerName = $column->getHeaderName(); - $manager = $this->managerRegistry->getManagerForClass($class); - $metadata = $manager->getClassMetadata($class); - - if (!$metadata instanceof ClassMetadata) { - return null; - } - - foreach ($metadata->associationMappings as $associationName => $associationMapping) { - if (!isset($associationMapping['joinColumns'][0]['name'])) { - continue; - } - - $columnName = $associationMapping['joinColumns'][0]['name']; - if ($columnName === $headerName) { - return (new Column()) - ->setMappedTo($associationName.'.'.$headerName) - ->setIsIdentifier(false) - ->setIsDate(false); - } - } - - return null; - } -} diff --git a/packages/sonata-import-bundle/Column/ColumnBuilder/Doctrine/DoctrineAssociationFieldColumnBuilder.php b/packages/sonata-import-bundle/Column/ColumnBuilder/Doctrine/DoctrineAssociationFieldColumnBuilder.php deleted file mode 100644 index 64ec0af7b..000000000 --- a/packages/sonata-import-bundle/Column/ColumnBuilder/Doctrine/DoctrineAssociationFieldColumnBuilder.php +++ /dev/null @@ -1,38 +0,0 @@ -getImport()->getEntityClass(); - $headerName = $column->getHeaderName(); - $manager = $this->managerRegistry->getManagerForClass($class); - $metadata = $manager->getClassMetadata($class); - - if (!$metadata instanceof ClassMetadata) { - return null; - } - - $associationMapping = $metadata->associationMappings[$headerName] ?? null; - - if (!$associationMapping) { - return null; - } - - return (new Column()) - ->setMappedTo($headerName.'.'.$associationMapping['joinColumns'][0]['referencedColumnName']) - ->setIsIdentifier(false) - ->setIsDate(false); - } -} diff --git a/packages/sonata-import-bundle/Column/ColumnBuilder/Doctrine/DoctrineAssociationPathColumnBuilder.php b/packages/sonata-import-bundle/Column/ColumnBuilder/Doctrine/DoctrineAssociationPathColumnBuilder.php deleted file mode 100644 index aef12c3ca..000000000 --- a/packages/sonata-import-bundle/Column/ColumnBuilder/Doctrine/DoctrineAssociationPathColumnBuilder.php +++ /dev/null @@ -1,64 +0,0 @@ -getImport()->getEntityClass(); - $headerName = $column->getHeaderName(); - $manager = $this->managerRegistry->getManagerForClass($class); - $metadata = $manager->getClassMetadata($class); - - if (!$metadata instanceof ClassMetadata) { - return null; - } - - if (1 !== substr_count($headerName, '.')) { - return null; - } - - [$relation, $fieldName] = explode('.', $headerName); - - $associationMapping = $metadata->associationMappings[$relation] ?? null; - - if (!$associationMapping) { - return null; - } - - $targetClass = $associationMapping['targetEntity']; - - $fieldMapping = $this->getFieldMapping($targetClass, $fieldName); - - if (!$fieldMapping) { - return null; - } - - return (new Column()) - ->setMappedTo($relation.'.'.$fieldName) - ->setIsIdentifier($fieldMapping['id'] ?? false) - ->setIsDate(str_starts_with($fieldMapping['type'], 'date')); - } - - private function getFieldMapping(string $class, string $name): ?array - { - $manager = $this->managerRegistry->getManagerForClass($class); - $metadata = $manager->getClassMetadata($class); - - if (!$metadata instanceof ClassMetadata) { - return null; - } - - return $metadata->fieldMappings[$name] ?? null; - } -} diff --git a/packages/sonata-import-bundle/Column/ColumnBuilder/Doctrine/DoctrineFieldColumnBuilder.php b/packages/sonata-import-bundle/Column/ColumnBuilder/Doctrine/DoctrineFieldColumnBuilder.php deleted file mode 100644 index 0f16cd864..000000000 --- a/packages/sonata-import-bundle/Column/ColumnBuilder/Doctrine/DoctrineFieldColumnBuilder.php +++ /dev/null @@ -1,47 +0,0 @@ -getImport()->getEntityClass(); - $headerName = $column->getHeaderName(); - $manager = $this->managerRegistry->getManagerForClass($class); - $metadata = $manager->getClassMetadata($class); - - if (!$metadata instanceof ClassMetadata) { - return null; - } - - $fieldMapping = $metadata->fieldMappings[$headerName] ?? null; - - if (!$fieldMapping) { - return null; - } - - $columnInfo = (new Column()) - ->setMappedTo($headerName) - ->setIsDate(str_starts_with($fieldMapping['type'], 'date')); - - if ($fieldMapping['id'] ?? false) { - $columnInfo->setIsIdentifier(true); - } - - if (null === $column->getIsIdentifier() && ($fieldMapping['unique'] ?? false)) { - $columnInfo->setIsIdentifier(true); - } - - return $columnInfo; - } -} diff --git a/packages/sonata-import-bundle/Column/ColumnBuilder/MappedToOptionColumnBuilder.php b/packages/sonata-import-bundle/Column/ColumnBuilder/MappedToOptionColumnBuilder.php deleted file mode 100644 index e3cb4d80a..000000000 --- a/packages/sonata-import-bundle/Column/ColumnBuilder/MappedToOptionColumnBuilder.php +++ /dev/null @@ -1,34 +0,0 @@ -getMappedTo()) { - return null; - } - - if (\in_array($column->getHeaderName(), $this->mappedToOptionBuilderAggregator->getOptions($column))) { - return (new Column()) - ->setMappedTo($column->getHeaderName()); - } - - return null; - } -} diff --git a/packages/sonata-import-bundle/Column/ColumnBuilder/NamedBaseIdentifierColumnBuilder.php b/packages/sonata-import-bundle/Column/ColumnBuilder/NamedBaseIdentifierColumnBuilder.php deleted file mode 100644 index 13b72b840..000000000 --- a/packages/sonata-import-bundle/Column/ColumnBuilder/NamedBaseIdentifierColumnBuilder.php +++ /dev/null @@ -1,21 +0,0 @@ -getHeaderName(); - if (!\in_array(mb_strtolower($headerName), self::$names, true)) { - return null; - } - - return (new Column()) - ->setIsIdentifier(true); - } -} diff --git a/packages/sonata-import-bundle/Column/ColumnBuilder/ReflectionColumnBuilder.php b/packages/sonata-import-bundle/Column/ColumnBuilder/ReflectionColumnBuilder.php deleted file mode 100644 index da98a729c..000000000 --- a/packages/sonata-import-bundle/Column/ColumnBuilder/ReflectionColumnBuilder.php +++ /dev/null @@ -1,75 +0,0 @@ -getImport()->getEntityClass(); - - $headerName = $column->getHeaderName(); - - $reflectionClass = new \ReflectionClass($class); - - $method = 'set'.$headerName; - - if (!$reflectionClass->hasMethod($method)) { - return null; - } - - $reflectionMethod = $reflectionClass->getMethod($method); - - $parameters = $reflectionMethod->getParameters(); - - // No parameter cannot be a proper setter - if (0 === \count($parameters)) { - return null; - } - - $parameter = array_shift($parameters); - // More than one parameter and default value is not available it's not a proper setter - foreach ($parameters as $parameter) { - if (!$parameter->isDefaultValueAvailable()) { - return null; - } - } - - $columnInfo = (new Column()) - ->setMappedTo($headerName); - - $type = $parameter->getType(); - - if ($type instanceof \ReflectionNamedType) { - if ($this->isDate($type)) { - $columnInfo->setIsDate(true); - } - - return $columnInfo; - } - - if ($type instanceof \ReflectionUnionType) { - foreach ($type->getTypes() as $type) { - if ($this->isDate($type)) { - $columnInfo->setIsDate(true); - } - } - - return $columnInfo; - } - - return $columnInfo; - } - - private function isDate(\ReflectionNamedType $type): bool - { - $class = $type->getName(); - if (class_exists($class) && is_subclass_of($class, \DateTimeInterface::class)) { - return true; - } - - return false; - } -} diff --git a/packages/sonata-import-bundle/Column/ColumnExtractorInterface.php b/packages/sonata-import-bundle/Column/ColumnExtractorInterface.php new file mode 100644 index 000000000..d4c3abf43 --- /dev/null +++ b/packages/sonata-import-bundle/Column/ColumnExtractorInterface.php @@ -0,0 +1,38 @@ + the options to be used for the mapped to field + */ + public function getOptions(Column $column, array $options): array; + + /** + * Extract default value for the column. By returning a new column, you can override the current column value. + * + * If you don't want to override the current column value, return null. + * + * You should only handle columns that match your criteria. + * + * If you just want to assign the mappedTo value return null since it will be done automatically + * if the header is the same as the mappedTo value. + */ + public function extractDefaultValue(Column $column, array $samples): ?Column; + + /** + * If the assignation is just calling a setter on the object, you can return false, it will be done automatically. + * + * @return bool True if the value was assigned, false otherwise + */ + public function assign(object $object, Column $column, mixed $value): bool; +} diff --git a/packages/sonata-import-bundle/Column/ColumnFactory.php b/packages/sonata-import-bundle/Column/ColumnFactory.php index 44180e822..4c09d554a 100644 --- a/packages/sonata-import-bundle/Column/ColumnFactory.php +++ b/packages/sonata-import-bundle/Column/ColumnFactory.php @@ -2,7 +2,6 @@ namespace Draw\Bundle\SonataImportBundle\Column; -use Draw\Bundle\SonataImportBundle\Column\ColumnBuilder\ColumnBuilderInterface; use Draw\Bundle\SonataImportBundle\Entity\Column; use Draw\Bundle\SonataImportBundle\Entity\Import; use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; @@ -11,10 +10,10 @@ class ColumnFactory { public function __construct( /** - * @param iterable $columnBuilders + * @param iterable $columnExtractors */ - #[TaggedIterator(ColumnBuilderInterface::class)] - private iterable $columnBuilders = [] + #[TaggedIterator(ColumnExtractorInterface::class)] + private iterable $columnExtractors = [] ) { } @@ -43,8 +42,8 @@ public function buildColumns(Import $import, array $headers, array $samples): vo $columnSamples[] = $columnSample; } - foreach ($this->columnBuilders as $columnBuilder) { - $columnInfo = $columnBuilder->extract(clone $column, $columnSamples); + foreach ($this->columnExtractors as $columnBuilder) { + $columnInfo = $columnBuilder->extractDefaultValue(clone $column, $columnSamples); if ($columnInfo) { $this->assign($columnInfo, $column); } diff --git a/packages/sonata-import-bundle/Column/Extractor/ExactMatchColumnExtractor.php b/packages/sonata-import-bundle/Column/Extractor/ExactMatchColumnExtractor.php new file mode 100644 index 000000000..ec4d20f99 --- /dev/null +++ b/packages/sonata-import-bundle/Column/Extractor/ExactMatchColumnExtractor.php @@ -0,0 +1,36 @@ +getMappedTo()) { + return null; + } + + if (\in_array($column->getHeaderName(), $this->importer->getOptions($column))) { + return (new Column()) + ->setMappedTo($column->getHeaderName()); + } + + return null; + } +} diff --git a/packages/sonata-import-bundle/Column/Extractor/PropertyPathColumnExtractor.php b/packages/sonata-import-bundle/Column/Extractor/PropertyPathColumnExtractor.php new file mode 100644 index 000000000..ef42e38c0 --- /dev/null +++ b/packages/sonata-import-bundle/Column/Extractor/PropertyPathColumnExtractor.php @@ -0,0 +1,29 @@ +propertyAccessor ??= PropertyAccess::createPropertyAccessor(); + } + + #[\Override] + public function assign(object $object, Column $column, mixed $value): bool + { + $this->propertyAccessor->setValue($object, $column->getMappedTo(), $value); + + return true; + } +} diff --git a/packages/sonata-import-bundle/Column/Extractor/SetterMethodReflectionColumnExtractor.php b/packages/sonata-import-bundle/Column/Extractor/SetterMethodReflectionColumnExtractor.php new file mode 100644 index 000000000..da0e7c5e3 --- /dev/null +++ b/packages/sonata-import-bundle/Column/Extractor/SetterMethodReflectionColumnExtractor.php @@ -0,0 +1,119 @@ +getImport()->getEntityClass(); + + $reflectionClass = new \ReflectionClass($class); + + foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) { + if (!$this->isValidMethod($reflectionMethod)) { + continue; + } + + $name = $reflectionMethod->getName(); + + $options[] = lcfirst(substr($name, 3)); + } + + return $options; + } + + #[\Override] + public function extractDefaultValue(Column $column, array $samples): ?Column + { + $class = $column->getImport()->getEntityClass(); + + $headerName = $column->getHeaderName(); + + $reflectionClass = new \ReflectionClass($class); + + $method = 'set'.$headerName; + + if (!$reflectionClass->hasMethod($method)) { + return null; + } + + $reflectionMethod = $reflectionClass->getMethod($method); + + if (!$this->isValidMethod($reflectionMethod)) { + return null; + } + + $parameters = $reflectionMethod->getParameters(); + + $parameter = array_shift($parameters); + + $columnInfo = (new Column()) + ->setMappedTo($headerName); + + if ($this->isDate($parameter)) { + $columnInfo->setIsDate(true); + } + + return $columnInfo; + } + + private function isValidMethod(\ReflectionMethod $reflectionMethod): bool + { + $name = $reflectionMethod->getName(); + + if (!str_starts_with($name, 'set')) { + return false; + } + + if ($reflectionMethod->isStatic()) { + return false; + } + + if (0 === \count($parameters = $reflectionMethod->getParameters())) { + return false; + } + + $parameter = array_shift($parameters); + + \assert($parameter instanceof \ReflectionParameter); + + $type = $parameter->getType(); + + if (null !== $type && !$this->isDate($parameter)) { + return false; + } + + // More than one parameter and default value is not available it's not a proper setter + foreach ($parameters as $parameter) { + if (!$parameter->isDefaultValueAvailable()) { + return false; + } + } + + return true; + } + + private function isDate(\ReflectionParameter $reflectionParameter): bool + { + $type = $reflectionParameter->getType(); + + if (!$type instanceof \ReflectionNamedType) { + return false; + } + + $class = $type->getName(); + if ( + (class_exists($class) || interface_exists($class)) + && is_a($class, \DateTimeInterface::class, true) + ) { + return true; + } + + return false; + } +} diff --git a/packages/sonata-import-bundle/Column/MappedToOptionBuilder/MappedToOptionBuilderInterface.php b/packages/sonata-import-bundle/Column/MappedToOptionBuilder/MappedToOptionBuilderInterface.php deleted file mode 100644 index 2dcc7430f..000000000 --- a/packages/sonata-import-bundle/Column/MappedToOptionBuilder/MappedToOptionBuilderInterface.php +++ /dev/null @@ -1,17 +0,0 @@ - the options to be used for the mapped to field - */ - public function getOptions(Column $column, array $options): array; -} diff --git a/packages/sonata-import-bundle/Column/MappedToOptionBuilderAggregator.php b/packages/sonata-import-bundle/Column/MappedToOptionBuilderAggregator.php deleted file mode 100644 index bd7507bcf..000000000 --- a/packages/sonata-import-bundle/Column/MappedToOptionBuilderAggregator.php +++ /dev/null @@ -1,33 +0,0 @@ - - */ - #[TaggedIterator(MappedToOptionBuilderInterface::class)] - private iterable $mappedToOptionBuilders - ) { - } - - public function getOptions(Column $column): array - { - $options = []; - - foreach ($this->mappedToOptionBuilders as $mappedToOptionBuilder) { - $options = $mappedToOptionBuilder->getOptions( - $column, - $options - ); - } - - return $options; - } -} diff --git a/packages/sonata-import-bundle/DependencyInjection/DrawSonataImportExtension.php b/packages/sonata-import-bundle/DependencyInjection/DrawSonataImportExtension.php index d8b90e743..b6cdbac01 100644 --- a/packages/sonata-import-bundle/DependencyInjection/DrawSonataImportExtension.php +++ b/packages/sonata-import-bundle/DependencyInjection/DrawSonataImportExtension.php @@ -2,8 +2,8 @@ namespace Draw\Bundle\SonataImportBundle\DependencyInjection; -use Draw\Bundle\SonataImportBundle\Column\ColumnBuilder\ColumnBuilderInterface; -use Draw\Bundle\SonataImportBundle\Column\MappedToOptionBuilder\DoctrineTranslationMappedToOptionBuilder; +use Draw\Bundle\SonataImportBundle\Column\Bridge\KnpDoctrineBehaviors\Extractor\DoctrineTranslationColumnExtractor; +use Draw\Bundle\SonataImportBundle\Column\ColumnExtractorInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader; @@ -16,7 +16,7 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yaml'); $container - ->registerForAutoconfiguration(ColumnBuilderInterface::class) + ->registerForAutoconfiguration(ColumnExtractorInterface::class) ->addTag('draw.sonata_import.extractor'); $container->setParameter('draw.sonata_import.classes', $mergedConfig['classes']); @@ -27,13 +27,13 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container protected function loadDoctrineTranslationHandler(array $config, ContainerBuilder $container): void { if (!$this->isConfigEnabled($container, $config)) { - $container->removeDefinition(DoctrineTranslationMappedToOptionBuilder::class); + $container->removeDefinition(DoctrineTranslationColumnExtractor::class); return; } $container - ->getDefinition(DoctrineTranslationMappedToOptionBuilder::class) + ->getDefinition(DoctrineTranslationColumnExtractor::class) ->setArgument('$supportedLocales', $config['supported_locales']); } } diff --git a/packages/sonata-import-bundle/Event/AttributeImportEvent.php b/packages/sonata-import-bundle/Event/AttributeImportEvent.php deleted file mode 100644 index f6376c7c3..000000000 --- a/packages/sonata-import-bundle/Event/AttributeImportEvent.php +++ /dev/null @@ -1,34 +0,0 @@ -entity; - } - - public function getColumn(): Column - { - return $this->column; - } - - public function getValue(): mixed - { - return $this->value; - } -} diff --git a/packages/sonata-import-bundle/Import/Importer.php b/packages/sonata-import-bundle/Import/Importer.php new file mode 100644 index 000000000..92ad7f7c7 --- /dev/null +++ b/packages/sonata-import-bundle/Import/Importer.php @@ -0,0 +1,251 @@ + + */ + #[TaggedIterator(ColumnExtractorInterface::class)] + private iterable $columnsExtractors, + private ManagerRegistry $managerRegistry, + private ColumnFactory $columnFactory, + private NotifierInterface $notifier + ) { + } + + public function getOptions(Column $column): array + { + $options = []; + + foreach ($this->columnsExtractors as $mappedToOptionBuilder) { + + $options = $mappedToOptionBuilder->getOptions( + $column, + $options + ); + } + + return $options; + } + + public function buildFromFile(Import $import, \SplFileInfo $file): void + { + $import->setFileContent(file_get_contents($file->getRealPath())); + + $handle = fopen($file->getRealPath(), 'r'); + + $headers = fgetcsv($handle); + $samples = []; + for ($i = 0; $i < 10; ++$i) { + $row = fgetcsv($handle); + if (!$row) { + break; + } + $samples[] = $row; + } + + $this->columnFactory->buildColumns( + $import, + $headers, + $samples + ); + + $import->setState(Import::STATE_CONFIGURATION); + } + + public function processImport(Import $import): bool + { + $file = tempnam(sys_get_temp_dir(), 'csv_'); + file_put_contents($file, $import->getFileContent()); + register_shutdown_function('unlink', $file); + $handle = fopen($file, 'r+'); + $headers = fgetcsv($handle); + + $identifierHeaderName = $import->getIdentifierHeaderName(); + $columnMapping = $import->getColumnMapping(); + $line = 1; + $saved = 0; + + $identifierColumns = $import->getIdentifierColumns(); + + $identifierHeaderNames = []; + foreach ($identifierColumns as $column) { + $identifierHeaderNames[$column->getHeaderName()] = $column->getMappedTo(); + } + + while (($row = fgetcsv($handle)) !== false) { + ++$line; + $data = array_combine($headers, $row); + $id = $data[$identifierHeaderName]; + $criteria = []; + foreach ($identifierHeaderNames as $headerName => $mappedTo) { + $criteria[$mappedTo] = $data[$headerName]; + } + + $model = $this->findOne($import->getEntityClass(), $criteria, $import->getInsertWhenNotFound()); + + if (null === $model) { + $this->notifier + ->send( + SonataNotification::error( + sprintf( + 'Skipped Id [%s] cannot be found at line [%s]. Make sure you are using unique id value.', + implode(', ', $criteria), + $line + ) + ) + ); + + continue; + } + + try { + foreach ($columnMapping as $headerName => $column) { + $this->assignValue( + $model, + $column, + $data[$headerName] + ); + } + } catch (\Throwable $exception) { + $this->notifier + ->send( + SonataNotification::error( + sprintf( + 'Skipped Id [%s] at line [%s]. Error: %s', + $id, + $line, + $exception->getMessage() + ) + ) + ); + continue; + } + + ++$saved; + } + + try { + $this->managerRegistry->getManagerForClass($import->getEntityClass())->flush(); + + $this->notifier + ->send( + SonataNotification::success( + sprintf( + 'Entity saved: %s', + $saved + ) + ) + ); + + return true; + } catch (\Throwable $error) { + $this->notifier + ->send( + SonataNotification::error( + sprintf( + 'Error saving data: %s', + $error->getMessage() + ) + ) + ); + + return false; + } + } + + private function assignValue(object $object, Column $column, mixed $value): void + { + if ($column->getIsDate()) { + $value = new \DateTime($value); + } + + foreach ($this->columnsExtractors as $columnExtractor) { + if ($columnExtractor->assign($object, $column, $value)) { + return; + } + } + } + + /** + * The criteria can define path with dot for separator. + * + * @param array $criteria + */ + private function findOne(string $class, array $criteria, bool $create): ?object + { + $manager = $this->managerRegistry->getManagerForClass($class); + + \assert($manager instanceof EntityManagerInterface); + + $parameters = []; + + /** @var array> $relationsCriteria */ + $relationsCriteria = []; + + foreach ($criteria as $key => $value) { + if (1 === substr_count($key, '.')) { + [$object, $field] = explode('.', $key); + $relationsCriteria[$object][$field] = $value; + } else { + $parameters[$key] = $value; + } + } + + $classMetadata = $manager->getClassMetadata($class); + + foreach ($relationsCriteria as $relationName => $objectCriteria) { + $objectClass = $classMetadata->getAssociationTargetClass($relationName); + + $relatedObject = $this->managerRegistry + ->getRepository($objectClass) + ->findOneBy($objectCriteria); + + if (!$relatedObject) { + return null; + } + + $parameters[$relationName] = $relatedObject; + } + + $objects = $manager->getRepository($class)->findBy($parameters); + + if (\count($objects) > 1) { + throw new NonUniqueResultException(); + } + + if (1 === \count($objects)) { + return $objects[0]; + } + + if (!$create) { + return null; + } + + $accessor = PropertyAccess::createPropertyAccessor(); + + $object = new $class(); + foreach ($parameters as $key => $value) { + $accessor->setValue($object, $key, $value); + } + + $manager->persist($object); + + return $object; + } +} diff --git a/packages/sonata-import-bundle/Tests/Column/Bridge/Doctrine/Extractor/DoctrineAssociationColumnExtractorTest.php b/packages/sonata-import-bundle/Tests/Column/Bridge/Doctrine/Extractor/DoctrineAssociationColumnExtractorTest.php new file mode 100644 index 000000000..b9879cd2a --- /dev/null +++ b/packages/sonata-import-bundle/Tests/Column/Bridge/Doctrine/Extractor/DoctrineAssociationColumnExtractorTest.php @@ -0,0 +1,108 @@ +object = new DoctrineAssociationColumnExtractor( + static::createRegistry( + $this->entityManager = static::setUpMySqlWithAttributeDriver([ + \dirname((new \ReflectionClass(Column::class))->getFileName()), + ]) + ) + ); + } + + public function testConstruct(): void + { + static::assertInstanceOf( + ColumnExtractorInterface::class, + $this->object + ); + } + + public function testGetDefaultPriority(): void + { + static::assertSame( + 0, + $this->object::getDefaultPriority() + ); + } + + public function testGetOptions(): void + { + static::assertSame( + [ + 'kept', + 'import.id', + ], + $this->object->getOptions( + $this->createColumn(), + ['kept'] + ) + ); + } + + public function testExtractDefaultValue(): void + { + $columnInfo = $this->object->extractDefaultValue( + $this->createColumn() + ->setHeaderName('import.id'), + [] + ); + + static::assertNull($columnInfo); + } + + public function testAssign(): void + { + $import = (new Import()) + ->setEntityClass(\stdClass::class); + + $this->entityManager->persist($import); + $this->entityManager->flush(); + + $column = $this->createColumn() + ->setMappedTo('import.id'); + + $object = new Column(); + + static::assertTrue( + $this->object->assign( + $object, + $column, + $import->getId(), + ) + ); + + static::assertSame( + $import, + $object->getImport() + ); + } + + private function createColumn(): Column + { + return (new Column()) + ->setImport( + (new Import()) + ->setEntityClass(Column::class) + ); + } +} diff --git a/packages/sonata-import-bundle/Tests/Column/Bridge/Doctrine/Extractor/DoctrineFieldColumnExtractorTest.php b/packages/sonata-import-bundle/Tests/Column/Bridge/Doctrine/Extractor/DoctrineFieldColumnExtractorTest.php new file mode 100644 index 000000000..36d5e180f --- /dev/null +++ b/packages/sonata-import-bundle/Tests/Column/Bridge/Doctrine/Extractor/DoctrineFieldColumnExtractorTest.php @@ -0,0 +1,147 @@ +object = new DoctrineFieldColumnExtractor( + static::createRegistry( + static::setUpMySqlWithAttributeDriver([ + \dirname((new \ReflectionClass(Column::class))->getFileName()), + ]) + ) + ); + } + + public function testConstruct(): void + { + static::assertInstanceOf( + ColumnExtractorInterface::class, + $this->object + ); + } + + public function testGetDefaultPriority(): void + { + static::assertSame( + 0, + $this->object::getDefaultPriority() + ); + } + + public function testGetOptions(): void + { + static::assertSame( + [ + 'kept', + 'id', + 'headerName', + 'sample', + 'isIdentifier', + 'isIgnored', + 'mappedTo', + 'isDate', + 'createdAt', + 'updatedAt', + ], + $this->object->getOptions( + $this->createColumn(), + ['kept'] + ) + ); + } + + public function testExtractDefaultValueSimple(): void + { + $columnInfo = $this->object->extractDefaultValue( + $this->createColumn() + ->setHeaderName('headerName'), + [] + ); + + static::assertNotNull($columnInfo); + static::assertSame( + 'headerName', + $columnInfo->getMappedTo() + ); + static::assertFalse( + $columnInfo->getIsDate() + ); + static::assertNull( + $columnInfo->getIsIdentifier() + ); + static::assertNull( + $columnInfo->getIsIgnored() + ); + } + + public function testExtractDefaultValueDate(): void + { + $columnInfo = $this->object->extractDefaultValue( + $this->createColumn() + ->setHeaderName('createdAt'), + [] + ); + + static::assertNotNull($columnInfo); + static::assertSame( + 'createdAt', + $columnInfo->getMappedTo() + ); + static::assertTrue( + $columnInfo->getIsDate() + ); + static::assertNull( + $columnInfo->getIsIdentifier() + ); + static::assertNull( + $columnInfo->getIsIgnored() + ); + } + + public function testExtractDefaultValueIdentifier(): void + { + $columnInfo = $this->object->extractDefaultValue( + $this->createColumn() + ->setHeaderName('id'), + [] + ); + + static::assertNotNull($columnInfo); + static::assertSame( + 'id', + $columnInfo->getMappedTo() + ); + static::assertFalse( + $columnInfo->getIsDate() + ); + static::assertTrue( + $columnInfo->getIsIdentifier() + ); + static::assertNull( + $columnInfo->getIsIgnored() + ); + } + + private function createColumn(): Column + { + return (new Column()) + ->setImport( + (new Import()) + ->setEntityClass(Column::class) + ); + } +} diff --git a/packages/sonata-import-bundle/Tests/Column/Bridge/KnpDoctrineBehaviors/Extractor/DoctrineTranslationColumnExtractorTest.php b/packages/sonata-import-bundle/Tests/Column/Bridge/KnpDoctrineBehaviors/Extractor/DoctrineTranslationColumnExtractorTest.php new file mode 100644 index 000000000..3b7e6abab --- /dev/null +++ b/packages/sonata-import-bundle/Tests/Column/Bridge/KnpDoctrineBehaviors/Extractor/DoctrineTranslationColumnExtractorTest.php @@ -0,0 +1,94 @@ +object = new DoctrineTranslationColumnExtractor( + static::createRegistry( + static::setUpMySqlWithAttributeDriver( + [__DIR__.'/Fixtures'] + ) + ), + ['en', 'fr'] + ); + } + + public function testConstruct(): void + { + static::assertInstanceOf( + ColumnExtractorInterface::class, + $this->object + ); + } + + public function testGetDefaultPriority(): void + { + static::assertSame( + 0, + $this->object::getDefaultPriority() + ); + } + + public function testGetOptions(): void + { + static::assertSame( + [ + 'kept', + 'translation#en.label', + 'translation#fr.label', + ], + $this->object->getOptions( + $this->createColumn(), + ['kept'] + ) + ); + } + + public function testAssign(): void + { + $object = new TranslatableEntity(); + + static::assertTrue( + $this->object + ->assign( + $object, + $this->createColumn() + ->setMappedTo('translation#fr.label'), + 'test' + ) + ); + + static::assertSame( + 'test', + $object->translate('fr', false)->getLabel() + ); + + static::assertNull( + $object->translate('en', false)->getLabel() + ); + } + + private function createColumn(): Column + { + return (new Column()) + ->setImport( + (new Import()) + ->setEntityClass(TranslatableEntity::class) + ); + } +} diff --git a/packages/sonata-import-bundle/Tests/Column/Bridge/KnpDoctrineBehaviors/Extractor/Fixtures/TranslatableEntity.php b/packages/sonata-import-bundle/Tests/Column/Bridge/KnpDoctrineBehaviors/Extractor/Fixtures/TranslatableEntity.php new file mode 100644 index 000000000..38d75b8c2 --- /dev/null +++ b/packages/sonata-import-bundle/Tests/Column/Bridge/KnpDoctrineBehaviors/Extractor/Fixtures/TranslatableEntity.php @@ -0,0 +1,32 @@ +id; + } + + public function setId(?int $id): self + { + $this->id = $id; + + return $this; + } +} diff --git a/packages/sonata-import-bundle/Tests/Column/Bridge/KnpDoctrineBehaviors/Extractor/Fixtures/TranslatableEntityTranslation.php b/packages/sonata-import-bundle/Tests/Column/Bridge/KnpDoctrineBehaviors/Extractor/Fixtures/TranslatableEntityTranslation.php new file mode 100644 index 000000000..de795d841 --- /dev/null +++ b/packages/sonata-import-bundle/Tests/Column/Bridge/KnpDoctrineBehaviors/Extractor/Fixtures/TranslatableEntityTranslation.php @@ -0,0 +1,47 @@ +id; + } + + public function setId(?int $id): self + { + $this->id = $id; + + return $this; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(?string $label): self + { + $this->label = $label; + + return $this; + } +} diff --git a/packages/sonata-import-bundle/Tests/Column/Extractor/ExactMatchColumnExtractorTest.php b/packages/sonata-import-bundle/Tests/Column/Extractor/ExactMatchColumnExtractorTest.php new file mode 100644 index 000000000..017307e53 --- /dev/null +++ b/packages/sonata-import-bundle/Tests/Column/Extractor/ExactMatchColumnExtractorTest.php @@ -0,0 +1,122 @@ +object = new ExactMatchColumnExtractor( + $this->importer = $this->createMock(Importer::class) + ); + } + + public function testConstruct(): void + { + static::assertInstanceOf( + ColumnExtractorInterface::class, + $this->object + ); + } + + public function testGetDefaultPriority(): void + { + static::assertSame( + -1000, + $this->object::getDefaultPriority() + ); + } + + public function testGetOptions(): void + { + static::assertSame( + ['test'], + $this->object->getOptions( + new Column(), + ['test'] + ) + ); + } + + public function testExtractDefaultValueAlreadySet(): void + { + $this->importer + ->expects(static::never()) + ->method('getOptions'); + + static::assertNull( + $this->object->extractDefaultValue( + (new Column()) + ->setHeaderName('headerName') + ->setMappedTo('mappedTo'), + ['sample1', 'sample2'] + ) + ); + } + + public function testExtractDefaultValueNotInOptions(): void + { + $this->importer + ->expects(static::once()) + ->method('getOptions') + ->willReturn(['headerName1', 'headerName2']); + + static::assertNull( + $this->object->extractDefaultValue( + (new Column()) + ->setHeaderName('headerName'), + ['sample3', 'sample4'] + ) + ); + } + + public function testExtractDefaultValueInOptions(): void + { + $this->importer + ->expects(static::once()) + ->method('getOptions') + ->willReturn(['headerName']); + + $column = (new Column()) + ->setHeaderName('headerName'); + + $column = $this->object->extractDefaultValue( + $column, + ['sample5', 'sample6'] + ); + + static::assertSame( + 'headerName', + $column->getMappedTo() + ); + + static::assertNull( + $column->getHeaderName() + ); + + static::assertNull( + $column->getIsIdentifier() + ); + + static::assertNull( + $column->getIsIgnored() + ); + + static::assertNull( + $column->getIsDate() + ); + } +} diff --git a/packages/sonata-import-bundle/Tests/Column/Extractor/PropertyPathColumnExtractorTest.php b/packages/sonata-import-bundle/Tests/Column/Extractor/PropertyPathColumnExtractorTest.php new file mode 100644 index 000000000..8c7743f4a --- /dev/null +++ b/packages/sonata-import-bundle/Tests/Column/Extractor/PropertyPathColumnExtractorTest.php @@ -0,0 +1,75 @@ +object = new PropertyPathColumnExtractor(); + } + + public function testConstruct(): void + { + static::assertInstanceOf( + ColumnExtractorInterface::class, + $this->object + ); + } + + public function testGetDefaultPriority(): void + { + static::assertSame( + -1000, + $this->object::getDefaultPriority() + ); + } + + public function testGetOptions(): void + { + static::assertSame( + ['test'], + $this->object->getOptions( + new Column(), + ['test'] + ) + ); + } + + public function testExtractDefaultValue(): void + { + static::assertNull( + $this->object->extractDefaultValue( + (new Column()) + ->setMappedTo('test'), + [] + ) + ); + } + + public function testAssign(): void + { + $object = new class() { + public string $test; + }; + + $this->object->assign( + $object, + (new Column()) + ->setMappedTo('test'), + $value = 'value' + ); + + static::assertSame( + $value, + $object->test + ); + } +} diff --git a/packages/sonata-import-bundle/Tests/Column/Extractor/SetterMethodReflectionColumnExtractorTest.php b/packages/sonata-import-bundle/Tests/Column/Extractor/SetterMethodReflectionColumnExtractorTest.php new file mode 100644 index 000000000..8c4b36ceb --- /dev/null +++ b/packages/sonata-import-bundle/Tests/Column/Extractor/SetterMethodReflectionColumnExtractorTest.php @@ -0,0 +1,171 @@ +object = new SetterMethodReflectionColumnExtractor(); + } + + public function testConstruct(): void + { + static::assertInstanceOf( + ColumnExtractorInterface::class, + $this->object + ); + } + + public function testGetDefaultPriority(): void + { + static::assertSame( + 0, + $this->object::getDefaultPriority() + ); + } + + public function testGetOptions(): void + { + static::assertSame( + [ + 'kept', + 'test', + 'date', + 'dateUnion', + ], + $this->object->getOptions( + $this->createColumn(), + ['kept'] + ) + ); + } + + public function testExtractDefaultValueSimple(): void + { + $columnInfo = $this->object->extractDefaultValue( + $this->createColumn() + ->setHeaderName('test'), + [] + ); + + static::assertNotNull($columnInfo); + static::assertSame( + 'test', + $columnInfo->getMappedTo() + ); + static::assertNull( + $columnInfo->getIsIdentifier() + ); + static::assertNull( + $columnInfo->getIsDate() + ); + static::assertNull( + $columnInfo->getIsIgnored() + ); + } + + public function testExtractDefaultValueDate(): void + { + $columnInfo = $this->object->extractDefaultValue( + $this->createColumn() + ->setHeaderName('date'), + [] + ); + + static::assertNotNull($columnInfo); + static::assertSame( + 'date', + $columnInfo->getMappedTo() + ); + static::assertNull( + $columnInfo->getIsIdentifier() + ); + static::assertTrue( + $columnInfo->getIsDate() + ); + static::assertNull( + $columnInfo->getIsIgnored() + ); + } + + public function testExtractDefaultValueDateUnion(): void + { + $columnInfo = $this->object->extractDefaultValue( + $this->createColumn() + ->setHeaderName('dateUnion'), + [] + ); + + static::assertNotNull($columnInfo); + static::assertSame( + 'dateUnion', + $columnInfo->getMappedTo() + ); + static::assertNull( + $columnInfo->getIsIdentifier() + ); + static::assertTrue( + $columnInfo->getIsDate() + ); + static::assertNull( + $columnInfo->getIsIgnored() + ); + } + + private function createColumn(): Column + { + return (new Column()) + ->setImport( + (new Import()) + ->setEntityClass(SetterClassStub::class) + ); + } +} + +class SetterClassStub +{ + private function setPrivate(string $test): void + { + } + + protected function setProtected(string $test): void + { + // This is only to prevent phpstan to complain about unused method + $this->setPrivate($test); + } + + public static function setStatic(string $test): void + { + } + + public function setTest(string $test): void + { + } + + public function setNoParameter(): void + { + } + + public function setMultipleParameters(string $test, string $test2): void + { + } + + public function setDate(\DateTimeInterface $dateTime): void + { + } + + public function setDateUnion(\DateTimeInterface|string $dateTime): void + { + } +} diff --git a/packages/sonata-import-bundle/Tests/ColumnFactoryTest.php b/packages/sonata-import-bundle/Tests/ColumnFactoryTest.php index 17b1bffa3..b2148fae4 100644 --- a/packages/sonata-import-bundle/Tests/ColumnFactoryTest.php +++ b/packages/sonata-import-bundle/Tests/ColumnFactoryTest.php @@ -2,9 +2,9 @@ namespace Draw\Bundle\SonataImportBundle\Tests; -use Draw\Bundle\SonataImportBundle\Column\ColumnBuilder\NamedBaseIdentifierColumnBuilder; -use Draw\Bundle\SonataImportBundle\Column\ColumnBuilder\ReflectionColumnBuilder; use Draw\Bundle\SonataImportBundle\Column\ColumnFactory; +use Draw\Bundle\SonataImportBundle\Column\Extractor\PropertyPathColumnExtractor; +use Draw\Bundle\SonataImportBundle\Column\Extractor\SetterMethodReflectionColumnExtractor; use Draw\Bundle\SonataImportBundle\Entity\Column; use Draw\Bundle\SonataImportBundle\Entity\Import; use PHPUnit\Framework\Attributes\CoversClass; @@ -12,8 +12,8 @@ #[ CoversClass(ColumnFactory::class), - CoversClass(NamedBaseIdentifierColumnBuilder::class), - CoversClass(ReflectionColumnBuilder::class), + CoversClass(SetterMethodReflectionColumnExtractor::class), + CoversClass(PropertyPathColumnExtractor::class), ] class ColumnFactoryTest extends TestCase { @@ -23,39 +23,12 @@ protected function setUp(): void { $this->columnFactory = new ColumnFactory( [ - new NamedBaseIdentifierColumnBuilder(), - new ReflectionColumnBuilder(), + new SetterMethodReflectionColumnExtractor(), + new PropertyPathColumnExtractor(), ] ); } - public function testGenerateColumnsIdentifier(): void - { - $import = (new Import()) - ->setEntityClass(Import::class); - - $this->columnFactory - ->buildColumns( - $import, - ['id'], - [[12]] - ); - - $columns = $import->getColumns()->toArray(); - - static::assertCount(1, $columns); - - $column = $columns[0]; - - static::assertInstanceOf(Column::class, $column); - - static::assertSame('id', $column->getHeaderName()); - static::assertSame('id', $column->getMappedTo()); - static::assertTrue($column->getIsIdentifier()); - static::assertFalse($column->getIsIgnored()); - static::assertFalse($column->getIsDate()); - } - public function testGenerateColumnsDate(): void { $import = (new Import()) diff --git a/packages/sonata-import-bundle/composer.json b/packages/sonata-import-bundle/composer.json index 4dfd5d26c..b56d9de1c 100644 --- a/packages/sonata-import-bundle/composer.json +++ b/packages/sonata-import-bundle/composer.json @@ -12,12 +12,16 @@ ], "require": { "php": ">=8.1", + "draw/sonata-extra-bundle": "^0.11", "sonata-project/admin-bundle": "^4.8", "sonata-project/doctrine-orm-admin-bundle": "^4.2", "symfony/event-dispatcher": "^6.4", + "symfony/notifier": "^6.4", "symfony/validator": "^6.4" }, "require-dev": { + "draw/tester": "^0.11", + "knplabs/doctrine-behaviors": "*", "phpunit/phpunit": "^10.0" }, "minimum-stability": "dev", diff --git a/packages/tester-bundle/PHPUnit/Extension/DeleteTemporaryEntity/BaseTemporaryEntityCleaner.php b/packages/tester-bundle/PHPUnit/Extension/DeleteTemporaryEntity/BaseTemporaryEntityCleaner.php new file mode 100644 index 000000000..cdd204b57 --- /dev/null +++ b/packages/tester-bundle/PHPUnit/Extension/DeleteTemporaryEntity/BaseTemporaryEntityCleaner.php @@ -0,0 +1,37 @@ + $object) { + $class = $object::class; + $metadata = $this->entityManager->getClassMetadata($class); + $id = $metadata->getIdentifierValues($object); + + if (empty($id)) { + // This entity has no identifier, we can't delete it. + // It can happen if we delete the entity manually in a test and use this flow as a fallback. + continue; + } + + if ($object = $this->entityManager->find($class, $metadata->getIdentifierValues($object))) { + $this->entityManager->remove($object); + } + + unset(self::$temporaryEntities[$index]); + } + + $this->entityManager->flush(); + } +} diff --git a/packages/tester/DoctrineOrmTrait.php b/packages/tester/DoctrineOrmTrait.php index b0c62ab42..5d951ed34 100644 --- a/packages/tester/DoctrineOrmTrait.php +++ b/packages/tester/DoctrineOrmTrait.php @@ -6,6 +6,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\ORMSetup; use Doctrine\ORM\Tools\Console\ConsoleRunner; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; @@ -48,4 +49,75 @@ protected static function setUpMySqlWithAttributeDriver( return $entityManager; } + + protected static function createRegistry( + EntityManagerInterface $entityManager + ): ManagerRegistry { + return new class($entityManager) implements ManagerRegistry { + public function __construct(private EntityManagerInterface $entityManager) + { + } + + public function getDefaultConnectionName(): string + { + return 'default'; + } + + public function getConnection($name = null) + { + return $this->entityManager->getConnection(); + } + + public function getConnections() + { + return ['default' => $this->getConnection()]; + } + + public function getConnectionNames() + { + return ['default' => 'default']; + } + + public function getDefaultManagerName() + { + return 'default'; + } + + public function getManager($name = null) + { + return $this->entityManager; + } + + public function getManagers() + { + return ['default' => $this->entityManager]; + } + + public function resetManager($name = null) + { + return $this->entityManager; + } + + public function getAliasNamespace($alias) + { + return $alias; + } + + public function getManagerNames() + { + return ['default' => 'manager.default']; + } + + public function getRepository($persistentObject, $persistentManagerName = null) + { + return $this->entityManager->getRepository($persistentObject); + } + + public function getManagerForClass($class) + { + return $this->entityManager; + } + }; + + } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5b27a7e00..35b71ef37 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -39,9 +39,9 @@ + - diff --git a/tests/SonataImportBundle/ImporterTest.php b/tests/SonataImportBundle/ImporterTest.php new file mode 100644 index 000000000..9178e2299 --- /dev/null +++ b/tests/SonataImportBundle/ImporterTest.php @@ -0,0 +1,86 @@ +setEntityClass(Tag::class) + ->setInsertWhenNotFound(false); + + $this->importer->buildFromFile( + $import, + new \SplFileInfo($fileName) + ); + + $this->importer + ->processImport($import); + + $tag = $this->entityManager->getRepository(Tag::class)->findOneBy(['name' => $name]); + + static::assertNull( + $tag, + 'Should not have been created because insertWhenNotFound is false' + ); + + $import->setInsertWhenNotFound(true); + + $this->importer + ->processImport($import); + + $tag = $this->entityManager->getRepository(Tag::class)->findOneBy(['name' => $name]); + + static::assertInstanceOf( + Tag::class, + $tag + ); + + BaseTemporaryEntityCleaner::$temporaryEntities[] = $tag; + + static::assertSame( + $name, + $tag->getName() + ); + + static::assertTrue( + $tag->getActive() + ); + + static::assertSame( + 'testEn', + $tag->translate('en')->getLabel() + ); + + static::assertSame( + 'testFr', + $tag->translate('fr')->getLabel() + ); + } +}