diff --git a/Annotation/ImportExclude.php b/Annotation/ImportExclude.php index a99ce7c..5a69f2f 100644 --- a/Annotation/ImportExclude.php +++ b/Annotation/ImportExclude.php @@ -1,6 +1,6 @@ formFactory->create(ImportFormType::class, null, ['field_choices' => $fieldChoices]); - $form->get('fields')->setData(array_fill_keys((array)$headers, null)); + $form->get('fields')->setData(array_fill_keys((array) $headers, null)); $form->handleRequest($request); $rows = $this->reader->getRows($this->container->getParameter('avro_csv.sample_count')); @@ -129,7 +129,7 @@ public function mappingAction(Request $request, $alias) [ 'form' => $form->createView(), 'alias' => $alias, - 'headers' => array_combine((array)$headers, (array)$fileHeaders), + 'headers' => array_combine((array) $headers, (array) $fileHeaders), 'headersJson' => json_encode( $this->caseConverter->toTitleCase($fileHeaders), JSON_FORCE_OBJECT @@ -152,7 +152,7 @@ public function mappingAction(Request $request, $alias) * Previews the uploaded csv and allows the user to map the fields. * * @param Request $request The request - * @param string $alias The objects alias + * @param string $alias The objects alias * * @return Response */ @@ -184,7 +184,7 @@ public function processAction(Request $request, $alias) $request->getSession()->getFlashBag()->set( 'success', - $this->importer->getImportCount() . ' items imported. ' . $this->importer->getImportErrors() . ' errors.' + $this->importer->getImportCount().' items imported. '.$this->importer->getImportErrors().' errors.' ); } else { $request->getSession()->getFlashBag()->set('error', 'Import failed. Please try again.'); diff --git a/DependencyInjection/AvroCsvExtension.php b/DependencyInjection/AvroCsvExtension.php index 0519cb1..3df9a1d 100644 --- a/DependencyInjection/AvroCsvExtension.php +++ b/DependencyInjection/AvroCsvExtension.php @@ -1,6 +1,6 @@ + */ +class AssociationFieldEvent extends Event +{ + protected $object; + protected $associationMapping; + protected $row; + protected $fields; + protected $headers; + protected $index; + + /** + * @param object $object + * @param array $associationMapping + * @param array $row + * @param array $fields + * @param array $headers + * @param int $index + */ + public function __construct(object $object, array $associationMapping, array $row, array $fields, array $headers, int $index) + { + $this->object = $object; + $this->associationMapping = $associationMapping; + $this->row = $row; + $this->fields = $fields; + $this->headers = $headers; + $this->index = $index; + } + + /** + * @return object + */ + public function getObject(): object + { + return $this->object; + } + + /** + * Get field association mapping. + * + * @return array + */ + public function getAssociationMapping(): array + { + return $this->associationMapping; + } + + /** + * Get field row. + * + * @return array + */ + public function getRow(): array + { + return $this->row; + } + + /** + * Get mapped fields. + * + * @return array + */ + public function getFields(): array + { + return $this->fields; + } + + /** + * Get CSV headers. + * + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * Get the current field index. + * + * @return int + */ + public function getIndex(): int + { + return $this->index; + } +} diff --git a/Event/CustomFieldEvent.php b/Event/CustomFieldEvent.php new file mode 100644 index 0000000..8c44847 --- /dev/null +++ b/Event/CustomFieldEvent.php @@ -0,0 +1,102 @@ + + */ +class CustomFieldEvent extends Event +{ + protected $object; + protected $reflectionClass; + protected $row; + protected $fields; + protected $headers; + protected $index; + + /** + * @param object $object + * @param ReflectionClass $reflectionClass + * @param array $row + * @param array $fields + * @param array $headers + * @param int $index + */ + public function __construct(object $object, ReflectionClass $reflectionClass, array $row, array $fields, array $headers, int $index) + { + $this->object = $object; + $this->reflectionClass = $reflectionClass; + $this->row = $row; + $this->fields = $fields; + $this->headers = $headers; + $this->index = $index; + } + + /** + * @return object + */ + public function getObject(): object + { + return $this->object; + } + + /** + * Get object reflection class. + * + * @return ReflectionClass + */ + public function getReflectionClass(): ReflectionClass + { + return $this->reflectionClass; + } + + /** + * Get field row. + * + * @return array + */ + public function getRow(): array + { + return $this->row; + } + + /** + * Get mapped fields. + * + * @return array + */ + public function getFields(): array + { + return $this->fields; + } + + /** + * Get CSV headers. + * + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * Get the current field index. + * + * @return int + */ + public function getIndex(): int + { + return $this->index; + } +} diff --git a/Event/ExportEvent.php b/Event/ExportEvent.php index cb10fd1..ad4418e 100644 --- a/Event/ExportEvent.php +++ b/Event/ExportEvent.php @@ -1,6 +1,6 @@ exporter; } @@ -43,7 +43,7 @@ public function getExporter() * * @return QueryBuilder */ - public function getQueryBuilder() + public function getQueryBuilder(): QueryBuilder { return $this->exporter->getQueryBuilder(); } diff --git a/Event/ExportedEvent.php b/Event/ExportedEvent.php index 481a456..8ca7acf 100644 --- a/Event/ExportedEvent.php +++ b/Event/ExportedEvent.php @@ -1,6 +1,6 @@ content = $content; } @@ -31,7 +31,7 @@ public function __construct($content) * * @return string */ - public function getContent() + public function getContent(): string { return $this->content; } diff --git a/Event/RowAddedEvent.php b/Event/RowAddedEvent.php index 9aea5d1..0735049 100644 --- a/Event/RowAddedEvent.php +++ b/Event/RowAddedEvent.php @@ -1,6 +1,6 @@ object = $object; $this->row = $row; @@ -33,9 +33,9 @@ public function __construct($object, array $row, array $fields) } /** - * @param \stdClass|null $object + * @param object|null $object */ - public function setObject($object) + public function setObject(?object $object): void { $this->object = $object; } @@ -43,9 +43,9 @@ public function setObject($object) /** * Get the doctrine object. * - * @return \stdClass|null + * @return object|null */ - public function getObject() + public function getObject(): ?object { return $this->object; } @@ -55,7 +55,7 @@ public function getObject() * * @return array */ - public function getRow() + public function getRow(): array { return $this->row; } @@ -65,7 +65,7 @@ public function getRow() * * @return array */ - public function getFields() + public function getFields(): array { return $this->fields; } diff --git a/Event/RowErrorEvent.php b/Event/RowErrorEvent.php index b59274c..e63fbed 100644 --- a/Event/RowErrorEvent.php +++ b/Event/RowErrorEvent.php @@ -1,6 +1,6 @@ row; } @@ -44,7 +44,7 @@ public function getRow() * * @return array */ - public function getFields() + public function getFields(): array { return $this->fields; } diff --git a/Export/Doctrine/ORM/Exporter.php b/Export/Doctrine/ORM/Exporter.php index ed02205..ab9f3d8 100644 --- a/Export/Doctrine/ORM/Exporter.php +++ b/Export/Doctrine/ORM/Exporter.php @@ -1,6 +1,6 @@ reader->open($file, $delimiter); $this->class = $class; @@ -110,11 +112,9 @@ public function init($file, $class, $delimiter = ',', $headerFormat = 'title') * * @param array $fields The fields to persist * - * @return true if successful - * * @throws MappingException */ - public function import($fields) + public function import($fields): void { $fields = array_unique($this->caseConverter->toPascalCase($fields)); while ($row = $this->reader->getRow()) { @@ -131,8 +131,6 @@ public function import($fields) } // one last flush to make sure no persisted objects get left behind $this->objectManager->flush(); - - return true; } /** @@ -157,35 +155,35 @@ public function toFormFieldName($input) } /** - * Generate a hash string suitable as form field name. - * - * @param string $input + * Get import count. * - * @return string + * @return int */ - private function convertToFormFieldName($input) + public function getImportCount(): int { - return sha1($input); + return $this->importCount; } /** - * Get import count. + * Get import errors. * * @return int */ - public function getImportCount() + public function getImportErrors(): int { - return $this->importCount; + return $this->importErrors; } /** - * Get import errors. + * Generate a hash string suitable as form field name. * - * @return int + * @param string $input + * + * @return string */ - public function getImportErrors() + private function convertToFormFieldName($input): string { - return $this->importErrors; + return sha1($input); } /** @@ -195,63 +193,25 @@ public function getImportErrors() * @param array $fields An array of the fields to import * @param bool $andFlush Flush the ObjectManager * - * @return bool - * * @throws MappingException + * + * @return bool */ - private function addRow($row, $fields, $andFlush = true) + private function addRow($row, $fields, $andFlush = true): bool { // Create new entity $entity = new $this->class(); - if (in_array('Id', $fields)) { - $key = array_search('Id', $fields); - if ($this->metadata->hasField('legacyId')) { - $entity->setLegacyId($row[$key]); - } - unset($fields[$key]); - } - // loop through fields and set to row value + // Loop through fields and set to row value foreach ($fields as $k => $v) { if ($this->metadata->hasField(lcfirst($v))) { - $entity->{'set'.$fields[$k]}($row[$k]); + $entity->{'set'.$v}($row[$k]); } elseif ($this->metadata->hasAssociation(lcfirst($v))) { - $association = $this->metadata->getAssociationMapping(lcfirst($v)); - switch ($association['type']) { - case '1': // oneToOne - //Todo: - break; - case '2': // manyToOne - break; - // still needs work - $joinColumnId = $association['joinColumns'][0]['name']; - $legacyId = $row[array_search($this->caseConverter->toCamelCase($joinColumnId), $this->headers)]; - if ($legacyId) { - try { - $criteria = ['legacyId' => $legacyId]; - if ($this->useOwner) { - $criteria['owner'] = $this->owner->getId(); - } - - $associationClass = new \ReflectionClass($association['targetEntity']); - if ($associationClass->hasProperty('legacyId')) { - $relation = $this->objectManager->getRepository($association['targetEntity'])->findOneBy($criteria); - if ($relation) { - $entity->{'set'.ucfirst($association['fieldName'])}($relation); - } - } - } catch (\Exception $e) { - // legacyId does not exist - // fail silently - } - } - break; - case '4': // oneToMany - //TODO: - break; - case '8': // manyToMany - //TODO: - break; - } + // Let implementors handle associations to allow complex cases + $event = new AssociationFieldEvent($entity, $this->metadata->getAssociationMapping(lcfirst($v)), $row, $fields, $this->headers, $k); + $this->dispatcher->dispatch(AvroCsvEvents::ASSOCIATION_FIELD, $event); + } elseif ($this->metadata->getReflectionClass()->hasProperty(lcfirst($v))) { + $event = new CustomFieldEvent($entity, $this->metadata->getReflectionClass(), $row, $fields, $this->headers, $k); + $this->dispatcher->dispatch(AvroCsvEvents::CUSTOM_FIELD, $event); } } // Allow RowAddedEvent listeners to nullify objects (i.e. when invalid) diff --git a/Import/ImporterInterface.php b/Import/ImporterInterface.php index 15986f4..3b512bd 100644 --- a/Import/ImporterInterface.php +++ b/Import/ImporterInterface.php @@ -1,6 +1,6 @@ em = $em; + } + + public static function getSubscribedEvents() + { + return [ + AvroCsvEvents::ASSOCIATION_FIELD => 'importAssociation', + ]; + } + + /** + * Set the objects createdBy field + * + * @param AssociationFieldEvent $event + */ + public function importAssociation(AssociationFieldEvent $event) + { + $association = $event->getAssociationMapping(); + switch ($association['type']) { + case ClassMetadataInfo::ONE_TO_ONE: + case ClassMetadataInfo::MANY_TO_ONE: + $relation = $this->em->getRepository($association['targetEntity'])->findOneBy( + [ + 'name' => $event->getRow()[$event->getIndex()], + ] + ); + if ($relation) { + $event->getObject()->{'set'.ucfirst($association['fieldName'])}($relation); + } + break; + } + } +} +``` + Customizing each row -------------------- @@ -247,7 +313,6 @@ Register your controller or use your already setup autowiring To Do: ------ -- Allow association mapping - Finish mongodb support Acknowledgements diff --git a/Tests/Doctrine/ImporterTest.php b/Tests/Doctrine/ImporterTest.php index 2ee408c..ac8fcf5 100644 --- a/Tests/Doctrine/ImporterTest.php +++ b/Tests/Doctrine/ImporterTest.php @@ -1,12 +1,21 @@ createMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); - $metadata - ->expects($this->any()) - ->method('hasField') - ->willReturn(true); - $metadataFactory = $this->createMock('Doctrine\Common\Persistence\Mapping\ClassMetadataFactory'); - $metadataFactory - ->expects($this->any()) - ->method('getMetadataFor') - ->willReturn($metadata); - $objectManager = $this->createMock('Doctrine\Common\Persistence\ObjectManager'); - $objectManager - ->expects($this->any()) - ->method('getMetadataFactory') - ->willReturn($metadataFactory); - $dispatcher = $this->createMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); - - $class = 'Avro\CsvBundle\Tests\TestEntity'; + $objectManager = $this->createMock(ObjectManager::class); + $dispatcher = $this->createMock(EventDispatcherInterface::class); + + $class = TestEntity::class; $this->importer = new Importer($reader, $dispatcher, $caseConverter, $objectManager, 5); - $this->importer->init(__DIR__.'/../import.csv', $class, ',', 'title'); + $this->importer->init(__DIR__.'/../import.csv', $class); } /** diff --git a/Tests/Export/Doctrine/ORM/ExporterTest.php b/Tests/Export/Doctrine/ORM/ExporterTest.php index fe2f93b..f81509e 100644 --- a/Tests/Export/Doctrine/ORM/ExporterTest.php +++ b/Tests/Export/Doctrine/ORM/ExporterTest.php @@ -1,6 +1,6 @@ getMockBuilder('Doctrine\ORM\AbstractQuery') - ->disableOriginalConstructor() - ->setMethods(['iterate', 'HYDRATE_ARRAY', 'getSQL', '_doExecute']) - ->getMock(); - $query->expects($this->any()) + $query = $this->getMockForAbstractClass(AbstractQuery::class, [], '', false, true, true, ['iterate', 'HYDRATE_ARRAY', 'getSQL', '_doExecute']); + $query ->method('iterate') ->willReturn([0 => [0 => ['row 1' => 'val\'1', 'row 2' => 'val,2', 'row 3' => 'val"3']]]); - $queryBuilder = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + $queryBuilder = $this->getMockBuilder(QueryBuilder::class) ->disableOriginalConstructor() ->setMethods(['select', 'from', 'getQuery']) ->getMock(); - $queryBuilder->expects($this->any()) + $queryBuilder ->method('select') ->willReturn($queryBuilder); - $queryBuilder->expects($this->any()) + $queryBuilder ->method('from') ->willReturn($queryBuilder); - $queryBuilder->expects($this->any()) + $queryBuilder ->method('from') ->willReturn($queryBuilder); - $queryBuilder->expects($this->any()) + $queryBuilder ->method('getQuery') ->willReturn($query); - $entityManager = $this->getMockBuilder('Doctrine\ORM\EntityManager') + $entityManager = $this->getMockBuilder(EntityManager::class) ->disableOriginalConstructor() ->setMethods(['createQueryBuilder']) ->getMock(); - $entityManager->expects($this->any()) + $entityManager ->method('createQueryBuilder') ->willReturn($queryBuilder); @@ -61,8 +62,8 @@ public function setUp() */ public function testInit() { - $this->exporter->init('Avro\CsvBundle\Tests\TestEntity'); - $this->assertTrue($this->exporter->getQueryBuilder() instanceof QueryBuilder); + $this->exporter->init(TestEntity::class); + $this->assertInstanceOf(QueryBuilder::class, $this->exporter->getQueryBuilder()); } /** @@ -71,7 +72,7 @@ public function testInit() public function testArrayToCsv() { $this->assertEquals( - '"val\'1","val,2","val""3"' . "\n", + '"val\'1","val,2","val""3"'."\n", $this->exporter->arrayToCsv(['val\'1', 'val,2', 'val"3']) ); } @@ -86,7 +87,7 @@ public function testGetContent() $expected .= '"val\'1","val,2","val""3"'; $expected .= "\n"; - $this->exporter->init('Avro\CsvBundle\Tests\TestEntity'); + $this->exporter->init(TestEntity::class); $this->assertEquals( $expected, $this->exporter->getContent() diff --git a/Tests/Import/ImporterTest.php b/Tests/Import/ImporterTest.php index d38afdb..12e80f4 100644 --- a/Tests/Import/ImporterTest.php +++ b/Tests/Import/ImporterTest.php @@ -1,6 +1,6 @@ fields = $fields; + $assocs = ['assoc']; + $customs = ['assoc']; + $this->fields = array_merge($fields, $assocs, $customs); $caseConverter = new CaseConverter(); $reader = new Reader(); - $metadata = $this->getMockForAbstractClass('Doctrine\Common\Persistence\Mapping\ClassMetadata', ['hasField']); - $metadata->expects($this->any()) + $metadata = $this->createMock(ClassMetadataInfo::class); + $metadata ->method('hasField') - ->will( - $this->returnCallback( - function ($value) use ($fields) { - return in_array($value, $fields); - } - ) + ->willReturnCallback( + static function ($value) use ($fields) { + return in_array($value, $fields, true); + } + ); + $metadata + ->method('hasAssociation') + ->willReturnCallback( + static function ($value) use ($assocs) { + return in_array($value, $assocs, true); + } ); - $objectManager = $this->createMock('Doctrine\Common\Persistence\ObjectManager'); - $objectManager->expects($this->any()) + $metadata + ->method('getAssociationMapping') + ->willReturn([]); + $objectManager = $this->createMock(ObjectManager::class); + $objectManager ->method('getClassMetadata') ->willReturn($metadata); - $dispatcher = $this->createMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); - $dispatcher->expects($this->any()) + $dispatcher = $this->createMock(EventDispatcherInterface::class); + $dispatcher ->method('dispatch') ->willReturn('true'); $this->importer = new Importer($reader, $dispatcher, $caseConverter, $objectManager, 5); - - $this->importer->init(__DIR__.'/../import.csv', 'Avro\CsvBundle\Tests\TestEntity', ',', 'title'); + $this->importer->init(__DIR__.'/../import.csv', TestEntity::class); } /** @@ -63,11 +77,7 @@ function ($value) use ($fields) { */ public function testImport() { - $this->assertEquals( - true, - $this->importer->import($this->fields) - ); - + $this->importer->import($this->fields); $this->assertEquals( 3, $this->importer->getImportCount() diff --git a/Tests/TestEntity.php b/Tests/TestEntity.php index 7a92c6f..6d2da0c 100644 --- a/Tests/TestEntity.php +++ b/Tests/TestEntity.php @@ -1,5 +1,10 @@ id; @@ -39,4 +48,24 @@ public function setField2($field2) { $this->field2 = $field2; } + + public function getAssoc() + { + return $this->assoc; + } + + public function setAssoc($assoc): void + { + $this->assoc = $assoc; + } + + public function getCustom() + { + return $this->custom; + } + + public function setCustom($custom): void + { + $this->custom = $custom; + } } diff --git a/Tests/Util/FieldRetrieverTest.php b/Tests/Util/FieldRetrieverTest.php index 30bf966..a7b407c 100644 --- a/Tests/Util/FieldRetrieverTest.php +++ b/Tests/Util/FieldRetrieverTest.php @@ -1,6 +1,6 @@ fieldRetriever = new FieldRetriever($annotationReader, $caseConverter); - $this->class = 'Avro\CsvBundle\Tests\TestEntity'; + $this->class = TestEntity::class; } public function testGetFields() @@ -39,6 +41,8 @@ public function testGetFields() '1' => 'Id', '2' => 'Field1', '3' => 'Field2', + '4' => 'Assoc', + '5' => 'Custom', ] ); } @@ -52,6 +56,8 @@ public function testGetFieldsAsCamelCase() '1' => 'id', '2' => 'field1', '3' => 'field2', + '4' => 'assoc', + '5' => 'custom', ] ); } @@ -65,6 +71,8 @@ public function testGetFieldsAndCopyKeys() 'id' => 'id', 'field1' => 'field1', 'field2' => 'field2', + 'assoc' => 'assoc', + 'custom' => 'custom', ] ); } diff --git a/Tests/Util/ReaderTest.php b/Tests/Util/ReaderTest.php index 86d4582..f097e13 100644 --- a/Tests/Util/ReaderTest.php +++ b/Tests/Util/ReaderTest.php @@ -1,6 +1,6 @@