Commit 4269a602 authored by Chivy Lim's avatar Chivy Lim

Merge branch 'KIME-4583' into 'master'

Bugfixes and improvements

refs KIME-4583

See merge request !15
parents 0f755ee6 eb20ee60
...@@ -51,7 +51,7 @@ class SpreadsheetImportCommandController extends CommandController { ...@@ -51,7 +51,7 @@ class SpreadsheetImportCommandController extends CommandController {
public function importCommand() { public function importCommand() {
$currentImportingCount = $this->spreadsheetImportRepository->countByImportingStatus(SpreadsheetImport::IMPORTING_STATUS_IN_PROGRESS); $currentImportingCount = $this->spreadsheetImportRepository->countByImportingStatus(SpreadsheetImport::IMPORTING_STATUS_IN_PROGRESS);
if ($currentImportingCount > 0) { if ($currentImportingCount > 0) {
$this->outputFormatted('Previous spreadsheet import is still in progress.'); $this->outputLine('Previous spreadsheet import is still in progress.');
$this->quit(); $this->quit();
} }
/** @var SpreadsheetImport $spreadsheetImport */ /** @var SpreadsheetImport $spreadsheetImport */
...@@ -67,35 +67,65 @@ class SpreadsheetImportCommandController extends CommandController { ...@@ -67,35 +67,65 @@ class SpreadsheetImportCommandController extends CommandController {
try { try {
$this->spreadsheetImportService->import(); $this->spreadsheetImportService->import();
$spreadsheetImport->setImportingStatus(SpreadsheetImport::IMPORTING_STATUS_COMPLETED); $spreadsheetImport->setImportingStatus(SpreadsheetImport::IMPORTING_STATUS_COMPLETED);
$this->outputFormatted('Spreadsheet has been imported. %d inserted, %d updated, %d deleted, %d skipped', $this->outputLine('Spreadsheet has been imported. %d inserted, %d updated, %d deleted, %d skipped',
array($spreadsheetImport->getTotalInserted(), $spreadsheetImport->getTotalUpdated(), $spreadsheetImport->getTotalDeleted(), $spreadsheetImport->getTotalSkipped())); array($spreadsheetImport->getTotalInserted(), $spreadsheetImport->getTotalUpdated(), $spreadsheetImport->getTotalDeleted(), $spreadsheetImport->getTotalSkipped()));
} catch (Exception $e) { } catch (\Exception $e) {
$spreadsheetImport->setImportingStatus(SpreadsheetImport::IMPORTING_STATUS_FAILED); $spreadsheetImport->setImportingStatus(SpreadsheetImport::IMPORTING_STATUS_FAILED);
$this->outputFormatted('Spreadsheet import failed.'); $this->outputLine('Spreadsheet import failed.');
}
try {
$this->spreadsheetImportRepository->update($spreadsheetImport);
} catch (\Exception $e) {
$this->outputLine('Spreadsheet import status update error. It remains in progress until cleanup.');
} }
$this->spreadsheetImportRepository->update($spreadsheetImport);
} else { } else {
$this->outputFormatted('No spreadsheet import in queue.'); $this->outputFormatted('No spreadsheet import in queue.');
} }
} }
/** /**
* Cleanup previous spreadsheet imports. Threashold defined in settings. * Cleanup past and stalled imports.
*
* @param int $keepPastImportsThreasholdDays Overwrites the setting value
* @param int $maxExecutionThreasholdMinutes Overwrites the setting value
*/ */
public function cleanupCommand() { public function cleanupCommand($keepPastImportsThreasholdDays = -1, $maxExecutionThreasholdMinutes = -1) {
$cleanupImportsThreasholdDays = intval($this->settings['cleanupImportsThreasholdDays']); $keepPastImportsThreasholdDays = ($keepPastImportsThreasholdDays >= 0) ? $keepPastImportsThreasholdDays : intval($this->settings['keepPastImportsThreasholdDays']);
$cleanupFromDate = new \DateTime(); $maxExecutionThreasholdMinutes = ($maxExecutionThreasholdMinutes >= 0) ? $maxExecutionThreasholdMinutes : intval($this->settings['maxExecutionThreasholdMinutes']);
$cleanupFromDate->sub(new \DateInterval('P' . $cleanupImportsThreasholdDays . 'D')); $this->cleanupPastImports($keepPastImportsThreasholdDays);
$oldSpreadsheetImports = $this->spreadsheetImportRepository->findPreviousImportsBySpecificDate($cleanupFromDate); $this->cleanupStalledImports($maxExecutionThreasholdMinutes);
if ($oldSpreadsheetImports->count() > 0) { }
/** @var SpreadsheetImport $oldSpreadsheetImport */
foreach ($oldSpreadsheetImports as $oldSpreadsheetImport) { /**
$this->spreadsheetImportRepository->remove($oldSpreadsheetImport); * Delete past imports
} *
$this->outputLine('%d spreadsheet imports were removed.', array($oldSpreadsheetImports->count())); * @param int $keepPastImportsThreasholdDays
} else { */
$this->outputLine('There is no spreadsheet import in queue to remove.'); private function cleanupPastImports($keepPastImportsThreasholdDays) {
$cleanupFromDateTime = new \DateTime();
$cleanupFromDateTime->sub(new \DateInterval('P' . $keepPastImportsThreasholdDays . 'D'));
$spreadsheetImports = $this->spreadsheetImportRepository->findBySpecificDateTimeAndImportingStatus($cleanupFromDateTime);
/** @var SpreadsheetImport $spreadsheetImport */
foreach ($spreadsheetImports as $spreadsheetImport) {
$this->spreadsheetImportRepository->remove($spreadsheetImport);
} }
$this->outputLine('%d spreadsheet imports removed.', array($spreadsheetImports->count()));
} }
/**
* Set stalled imports to failed
*
* @param int $maxExecutionThreasholdMinutes
*/
private function cleanupStalledImports($maxExecutionThreasholdMinutes) {
$cleanupFromDateTime = new \DateTime();
$cleanupFromDateTime->sub(new \DateInterval('PT' . $maxExecutionThreasholdMinutes . 'M'));
$spreadsheetImports = $this->spreadsheetImportRepository->findBySpecificDateTimeAndImportingStatus($cleanupFromDateTime, SpreadsheetImport::IMPORTING_STATUS_IN_PROGRESS);
/** @var SpreadsheetImport $spreadsheetImport */
foreach ($spreadsheetImports as $spreadsheetImport) {
$spreadsheetImport->setImportingStatus(SpreadsheetImport::IMPORTING_STATUS_FAILED);
$this->spreadsheetImportRepository->update($spreadsheetImport);
}
$this->outputLine('%d spreadsheet imports set to failed.', array($spreadsheetImports->count()));
}
} }
...@@ -38,10 +38,19 @@ class SpreadsheetImport { ...@@ -38,10 +38,19 @@ class SpreadsheetImport {
protected $file; protected $file;
/** /**
* DateTime when import is scheduled to progress
*
* @var \DateTime * @var \DateTime
*/ */
protected $scheduleDate; protected $scheduleDate;
/**
* Actual DateTime when import is set to in progress
*
* @var \DateTime
*/
protected $progressDate;
/** /**
* @var string * @var string
* @ORM\Column(type="text") * @ORM\Column(type="text")
...@@ -100,6 +109,7 @@ class SpreadsheetImport { ...@@ -100,6 +109,7 @@ class SpreadsheetImport {
*/ */
public function __construct() { public function __construct() {
$this->scheduleDate = new \DateTime(); $this->scheduleDate = new \DateTime();
$this->progressDate = new \DateTime();
} }
/** /**
...@@ -142,6 +152,7 @@ class SpreadsheetImport { ...@@ -142,6 +152,7 @@ class SpreadsheetImport {
*/ */
public function setScheduleDate($scheduleDate) { public function setScheduleDate($scheduleDate) {
$this->scheduleDate = $scheduleDate; $this->scheduleDate = $scheduleDate;
$this->progressDate = $scheduleDate;
} }
/** /**
...@@ -166,7 +177,11 @@ class SpreadsheetImport { ...@@ -166,7 +177,11 @@ class SpreadsheetImport {
* @return array * @return array
*/ */
public function getArguments() { public function getArguments() {
return unserialize($this->arguments); $arguments = unserialize($this->arguments);
if (! is_array($arguments)) {
$arguments = array();
}
return $arguments;
} }
/** /**
...@@ -234,6 +249,9 @@ class SpreadsheetImport { ...@@ -234,6 +249,9 @@ class SpreadsheetImport {
*/ */
public function setImportingStatus($importingStatus) { public function setImportingStatus($importingStatus) {
$this->importingStatus = $importingStatus; $this->importingStatus = $importingStatus;
if ($importingStatus === self::IMPORTING_STATUS_IN_PROGRESS) {
$this->progressDate = new \DateTime();
}
} }
/** /**
......
...@@ -42,12 +42,16 @@ class SpreadsheetImportRepository extends Repository { ...@@ -42,12 +42,16 @@ class SpreadsheetImportRepository extends Repository {
/** /**
* @param \DateTime $dateTime * @param \DateTime $dateTime
* @param int $importingStatus
* *
* @return \TYPO3\Flow\Persistence\QueryResultInterface * @return \TYPO3\Flow\Persistence\QueryResultInterface
*/ */
public function findPreviousImportsBySpecificDate(\DateTime $dateTime) { public function findBySpecificDateTimeAndImportingStatus(\DateTime $dateTime, $importingStatus = -1) {
$query = $this->createQuery(); $query = $this->createQuery();
$constraint = $query->lessThanOrEqual('scheduleDate', $dateTime); $constraint = $query->lessThanOrEqual('progressDate', $dateTime);
if ($importingStatus >= 0) {
$constraint = $query->logicalAnd($constraint, $query->equals('importingStatus', $importingStatus));
}
return $query->matching($constraint)->execute(); return $query->matching($constraint)->execute();
} }
......
...@@ -122,7 +122,7 @@ class FrontendMappingService { ...@@ -122,7 +122,7 @@ class FrontendMappingService {
$previewObject = $this->spreadsheetImportService->getObjectByRow($record); $previewObject = $this->spreadsheetImportService->getObjectByRow($record);
$preview = array(); $preview = array();
$hasErrors = FALSE; $hasErrors = FALSE;
$objectValidator = $this->validatorResolver->getBaseValidatorConjunction($domain); $objectValidator = $this->validatorResolver->getBaseValidatorConjunction($domain, SpreadsheetImportService::VALIDATION_GROUPS);
$errors = $objectValidator->validate($previewObject)->getFlattenedErrors(); $errors = $objectValidator->validate($previewObject)->getFlattenedErrors();
foreach ($mapping as $property => $columnMapping) { foreach ($mapping as $property => $columnMapping) {
/** @var Mapping $mapping */ /** @var Mapping $mapping */
......
...@@ -12,22 +12,29 @@ namespace WE\SpreadsheetImport; ...@@ -12,22 +12,29 @@ namespace WE\SpreadsheetImport;
* */ * */
use TYPO3\Flow\Annotations as Flow; use TYPO3\Flow\Annotations as Flow;
use TYPO3\Flow\Persistence\QueryInterface;
use TYPO3\Flow\Persistence\RepositoryInterface; use TYPO3\Flow\Persistence\RepositoryInterface;
use WE\SpreadsheetImport\Annotations\Mapping; use WE\SpreadsheetImport\Annotations\Mapping;
use WE\SpreadsheetImport\Domain\Model\SpreadsheetImport; use WE\SpreadsheetImport\Domain\Model\SpreadsheetImport;
/** /**
* Service to handle the column mapping and the import itself
*
* @Flow\Scope("singleton") * @Flow\Scope("singleton")
*/ */
class SpreadsheetImportService { class SpreadsheetImportService {
const VALIDATION_GROUPS = array('Default', 'SpreadsheetImport');
/** /**
* Domain object containing the configuration the service works with
*
* @var SpreadsheetImport * @var SpreadsheetImport
*/ */
protected $spreadsheetImport; protected $spreadsheetImport;
/** /**
* Class name of the domain model to be imported
*
* @var string * @var string
*/ */
protected $domain; protected $domain;
...@@ -69,25 +76,59 @@ class SpreadsheetImportService { ...@@ -69,25 +76,59 @@ class SpreadsheetImportService {
protected $validatorResolver; protected $validatorResolver;
/** /**
* Inverse SpreadsheetImport mapping array * Inverse array of SpreadsheetDomain mapping array property. Always use the getter function, which will process the
* property in case it is not set.
* *
* @internal
* @see SpreadsheetImportService::getInverseMappingProperties()
* @var array * @var array
*/ */
private $inverseSpreadsheetImportMapping; private $inverseMappingProperties;
/** /**
* Identifier properties of SpreadsheetDomain mapping array. Always use the getter function, which will process the
* property in case it is not set.
*
* @internal
* @see SpreadsheetImportService::getMappingIdentifierProperties()
* @var array
*/
private $mappingIdentifierProperties;
/**
* Identifier properties of SpreadsheetDomain argument array. Always use the getter function, which will process the
* property in case it is not set.
*
* @internal
* @see SpreadsheetImportService::getArgumentIdentifierProperties()
* @var array
*/
private $argumentIdentifierProperties;
/**
* Initialize the service before usage with the object to be worked with
*
* @param \WE\SpreadsheetImport\Domain\Model\SpreadsheetImport $spreadsheetImport * @param \WE\SpreadsheetImport\Domain\Model\SpreadsheetImport $spreadsheetImport
* *
* @return $this * @return $this
*/ */
public function init(SpreadsheetImport $spreadsheetImport) { public function init(SpreadsheetImport $spreadsheetImport) {
$this->inverseMappingProperties = array();
$this->mappingIdentifierProperties = array();
$this->argumentIdentifierProperties = array();
$this->spreadsheetImport = $spreadsheetImport; $this->spreadsheetImport = $spreadsheetImport;
$this->domain = $this->settings[$spreadsheetImport->getContext()]['domain']; $context = $spreadsheetImport->getContext();
$this->domain = $this->settings[$context]['domain'];
return $this; return $this;
} }
/** /**
* Returns the annotation properties of the related domain model with the property name as key.
*
* Example:
* array(['name'] => :Mapping, ['id'] => :Mapping)
*
* @return array * @return array
*/ */
public function getAnnotationMappingProperties() { public function getAnnotationMappingProperties() {
...@@ -100,6 +141,8 @@ class SpreadsheetImportService { ...@@ -100,6 +141,8 @@ class SpreadsheetImportService {
} }
/** /**
* Returns the total records of the spreadsheet excluding the header row
*
* @return array * @return array
*/ */
public function getTotalRecords() { public function getTotalRecords() {
...@@ -110,6 +153,11 @@ class SpreadsheetImportService { ...@@ -110,6 +153,11 @@ class SpreadsheetImportService {
} }
/** /**
* Returns the spreadsheet column as key value pair together with the heading taken of the first row
*
* Example:
* array(['A'] => 'firstname', ['C'] => 'id')
*
* @return array * @return array
*/ */
public function getSpreadsheetColumns() { public function getSpreadsheetColumns() {
...@@ -130,6 +178,8 @@ class SpreadsheetImportService { ...@@ -130,6 +178,8 @@ class SpreadsheetImportService {
} }
/** /**
* Returns a completely mapped new domain object with values taken of the specified row in the spreadsheet.
*
* @param int $number * @param int $number
* *
* @return object * @return object
...@@ -145,6 +195,9 @@ class SpreadsheetImportService { ...@@ -145,6 +195,9 @@ class SpreadsheetImportService {
} }
/** /**
* Run the import. It inserts, updates and deletes dependent on the configuration of the initialized
* SpreadsheetImport object.
*
* @return void * @return void
*/ */
public function import() { public function import() {
...@@ -153,15 +206,14 @@ class SpreadsheetImportService { ...@@ -153,15 +206,14 @@ class SpreadsheetImportService {
$totalDeleted = 0; $totalDeleted = 0;
$processedObjectIds = array(); $processedObjectIds = array();
$objectRepository = $this->getDomainRepository(); $objectRepository = $this->getDomainRepository();
$objectValidator = $this->validatorResolver->getBaseValidatorConjunction($this->domain); $objectValidator = $this->validatorResolver->getBaseValidatorConjunction($this->domain, self::VALIDATION_GROUPS);
$identifierProperties = $this->getDomainMappingIdentifierProperties();
$sheet = $this->getFileActiveSheet(); $sheet = $this->getFileActiveSheet();
$persistRecordsChunkSize = intval($this->settings['persistRecordsChunkSize']); $persistRecordsChunkSize = intval($this->settings['persistRecordsChunkSize']);
$totalCount = 0; $totalCount = 0;
/** @var \PHPExcel_Worksheet_Row $row */ /** @var \PHPExcel_Worksheet_Row $row */
foreach ($sheet->getRowIterator(2) as $row) { foreach ($sheet->getRowIterator(2) as $row) {
$totalCount++; $totalCount++;
$object = $this->findObjectByIdentifierPropertiesPerRow($identifierProperties, $row); $object = $this->findExistingObjectByRow($row);
if (is_object($object)) { if (is_object($object)) {
$processedObjectIds[] = $this->persistenceManager->getIdentifierByObject($object); $processedObjectIds[] = $this->persistenceManager->getIdentifierByObject($object);
if ($this->spreadsheetImport->isUpdating()) { if ($this->spreadsheetImport->isUpdating()) {
...@@ -194,7 +246,7 @@ class SpreadsheetImportService { ...@@ -194,7 +246,7 @@ class SpreadsheetImportService {
} }
$deleteCount = 0; $deleteCount = 0;
if ($this->spreadsheetImport->isDeleting()) { if ($this->spreadsheetImport->isDeleting()) {
$notExistingObjects = $this->findObjectsByExcludedIds($processedObjectIds); $notExistingObjects = $this->findObjectsByArgumentsAndExcludedIds($processedObjectIds);
foreach ($notExistingObjects as $object) { foreach ($notExistingObjects as $object) {
$objectRepository->remove($object); $objectRepository->remove($object);
if (++$deleteCount % $persistRecordsChunkSize === 0) { if (++$deleteCount % $persistRecordsChunkSize === 0) {
...@@ -210,6 +262,8 @@ class SpreadsheetImportService { ...@@ -210,6 +262,8 @@ class SpreadsheetImportService {
} }
/** /**
* Return the active sheet of a spreadsheet. Other potential sheets are ignored on the import.
*
* @return \PHPExcel_Worksheet * @return \PHPExcel_Worksheet
*/ */
private function getFileActiveSheet() { private function getFileActiveSheet() {
...@@ -220,52 +274,30 @@ class SpreadsheetImportService { ...@@ -220,52 +274,30 @@ class SpreadsheetImportService {
} }
/** /**
* @return \TYPO3\Flow\Persistence\RepositoryInterface * Returns an existing object found or null for a specific row.
*/ *
private function getDomainRepository() {
$repositoryClassName = preg_replace(array('/\\\Model\\\/', '/$/'), array('\\Repository\\', 'Repository'), $this->domain);
/** @var RepositoryInterface $repository */
$repository = $this->objectManager->get($repositoryClassName);
return $repository;
}
/**
* @return array
*/
private function getDomainMappingIdentifierProperties() {
// TODO: Don't use the annotation properties but the SpreadsheetImport mapping since we store the Mapping object there as well
$domainMappingProperties = array();
$properties = $this->reflectionService->getPropertyNamesByAnnotation($this->domain, Mapping::class);
foreach ($properties as $property) {
/** @var Mapping $mapping */
$mapping = $this->reflectionService->getPropertyAnnotation($this->domain, $property, Mapping::class);
if ($mapping->identifier) {
$domainMappingProperties[$property] = $mapping;
}
}
return $domainMappingProperties;
}
/**
* @param array $identifierProperties
* @param \PHPExcel_Worksheet_Row $row * @param \PHPExcel_Worksheet_Row $row
* *
* @return null|object * @return null|object
*/ */
private function findObjectByIdentifierPropertiesPerRow(array $identifierProperties, \PHPExcel_Worksheet_Row $row) { private function findExistingObjectByRow(\PHPExcel_Worksheet_Row $row) {
$query = $this->getDomainRepository()->createQuery(); $query = $this->getDomainRepository()->createQuery();
$constraints = array(); $constraints = array();
$spreadsheetImportMapping = $this->spreadsheetImport->getMapping(); $mappingIdentifierProperties = $this->getMappingIdentifierProperties();
/** @var Mapping $mapping */ foreach ($mappingIdentifierProperties as $property => $columnMapping) {
foreach ($identifierProperties as $property => $mapping) { $column = $columnMapping['column'];
$column = $spreadsheetImportMapping[$property]['column']; /** @var Mapping $mapping */
$mapping = $columnMapping['mapping'];
$propertyName = $mapping->queryPropertyName ?: $property;
/** @var \PHPExcel_Worksheet_RowCellIterator $cellIterator */ /** @var \PHPExcel_Worksheet_RowCellIterator $cellIterator */
$cellIterator = $row->getCellIterator($column, $column); $cellIterator = $row->getCellIterator($column, $column);
$value = $cellIterator->current()->getValue(); $value = $cellIterator->current()->getValue();
$propertyName = $mapping->queryPropertyName ?: $property;
$constraints[] = $query->equals($propertyName, $value); $constraints[] = $query->equals($propertyName, $value);
} }
$this->mergeQueryConstraintsWithArgumentIdentifiers($query, $constraints); $argumentIdentifierProperties = $this->getArgumentIdentifierProperties();
foreach ($argumentIdentifierProperties as $property => $value) {
$constraints[] = $query->equals($property, $value);
}
if (!empty($constraints)) { if (!empty($constraints)) {
return $query->matching($query->logicalAnd($constraints))->execute()->getFirst(); return $query->matching($query->logicalAnd($constraints))->execute()->getFirst();
} else { } else {
...@@ -274,68 +306,64 @@ class SpreadsheetImportService { ...@@ -274,68 +306,64 @@ class SpreadsheetImportService {
} }
/** /**
* @param \TYPO3\Flow\Persistence\QueryInterface $query * @param array $identifiers
* @param array $constraints *
* @return \TYPO3\Flow\Persistence\QueryResultInterface
*/ */
private function mergeQueryConstraintsWithArgumentIdentifiers(QueryInterface $query, &$constraints) { private function findObjectsByArgumentsAndExcludedIds(array $identifiers) {
$contextArguments = $this->settings[$this->spreadsheetImport->getContext()]['arguments']; $query = $this->getDomainRepository()->createQuery();
if (is_array($contextArguments)) { $constraints[] = $query->logicalNot($query->in('Persistence_Object_Identifier', $identifiers));
foreach ($contextArguments as $contextArgument) { // Include all the arguments into the query to get a subset only
if (isset($contextArgument['identifier']) && $contextArgument['identifier'] == TRUE) { foreach ($this->spreadsheetImport->getArguments() as $name => $value) {
$name = $contextArgument['name']; $constraints[] = $query->equals($name, $value);
$arguments = $this->spreadsheetImport->getArguments();
if (array_key_exists($name, $arguments)) {
$value = $arguments[$name];
$constraints[] = $query->equals($name, $value);
}
}
}
} }
return $query->matching($query->logicalAnd($constraints))->execute();
} }
/** /**
* @param \TYPO3\Flow\Persistence\QueryInterface $query * @param object $object
* @param array $constraints * @param \PHPExcel_Worksheet_Row $row
*/ */
private function mergeQueryConstraintsWithArguments(QueryInterface $query, &$constraints) { private function setObjectPropertiesByRow($object, $row) {
/* Set the argument properties before the mapping properties, as mapping property setters might be dependent on
argument property values */
$this->setObjectArgumentProperties($object);
$this->setObjectMappingProperties($object, $row);
}
/**
* @param $object
*/
private function setObjectArgumentProperties($object) {
$contextArguments = $this->settings[$this->spreadsheetImport->getContext()]['arguments']; $contextArguments = $this->settings[$this->spreadsheetImport->getContext()]['arguments'];
if (is_array($contextArguments)) { if (is_array($contextArguments)) {
$arguments = $this->spreadsheetImport->getArguments();
foreach ($contextArguments as $contextArgument) { foreach ($contextArguments as $contextArgument) {
$name = $contextArgument['name']; $name = $contextArgument['name'];
$arguments = $this->spreadsheetImport->getArguments();
if (array_key_exists($name, $arguments)) { if (array_key_exists($name, $arguments)) {
$value = $arguments[$name]; if (array_key_exists('domain', $contextArgument)) {
$constraints[] = $query->equals($name, $value); $value = $this->propertyMapper->convert($arguments[$name], $contextArgument['domain']);
} else {
$value = $arguments[$name];
}
$setter = 'set' . ucfirst($name);
$object->$setter($value);
} }
} }
} }
} }
/**
* @param array $identifiers
*
* @return \TYPO3\Flow\Persistence\QueryResultInterface
*/
private function findObjectsByExcludedIds(array $identifiers) {
$query = $this->getDomainRepository()->createQuery();
$constraints[] = $query->logicalNot($query->in('Persistence_Object_Identifier', $identifiers));
$this->mergeQueryConstraintsWithArguments($query, $constraints);
return $query->matching($query->logicalAnd($constraints))->execute();
}
/** /**
* @param object $object * @param object $object
* @param \PHPExcel_Worksheet_Row $row * @param \PHPExcel_Worksheet_Row $row
*/ */
private function setObjectPropertiesByRow($object, $row) { private function setObjectMappingProperties($object, $row) {
// Set the arguments first as mapping property setters might be dependent on argument properties $inversedMappingProperties = $this->getInverseMappingProperties();
$this->setObjectArgumentProperties($object);
$inverseSpreadsheetImportMapping = $this->getInverseSpreadsheetImportMapping();
/** @var \PHPExcel_Cell $cell */ /** @var \PHPExcel_Cell $cell */
foreach ($row->getCellIterator() as $cell) { foreach ($row->getCellIterator() as $cell) {
$column = $cell->getColumn(); $column = $cell->getColumn();
if (array_key_exists($column, $inverseSpreadsheetImportMapping)) { if (array_key_exists($column, $inversedMappingProperties)) {
$properties = $inverseSpreadsheetImportMapping[$column]; $properties = $inversedMappingProperties[$column];
foreach ($properties as $propertyMapping) { foreach ($properties as $propertyMapping) {
$property = $propertyMapping['property']; $property = $propertyMapping['property'];
/** @var Mapping $mapping */ /** @var Mapping $mapping */
...@@ -349,40 +377,84 @@ class SpreadsheetImportService { ...@@ -349,40 +377,84 @@ class SpreadsheetImportService {
/** /**
* Return an inverse SpreadsheetImport mapping array. It flips the property and column attribute and returns it as a * Return an inverse SpreadsheetImport mapping array. It flips the property and column attribute and returns it as a
* 3-dim array instead of a 2-dim array. The reason for that is the case when the same column is assigned to multiple * 3-dim array instead of a 2-dim array. This is done for the case when the same column is assigned to multiple
* properties. * properties.
*
* Example:
* array(
* ['A'] => array(
* [0] => array(['property'] => 'name', ['mapping'] => :Mapping),
* [1] => array(['property'] => 'id', ['mapping'] => :Mapping)));
*
* @return array
*/ */
private function getInverseSpreadsheetImportMapping() { private function getInverseMappingProperties() {
if (empty($this->inverseSpreadsheetImportMapping)) { if (empty($this->inverseMappingProperties)) {
$this->inverseSpreadsheetImportMapping = array(); $this->inverseMappingProperties = array();
foreach ($this->spreadsheetImport->getMapping() as $property => $columnMapping) { foreach ($this->spreadsheetImport->getMapping() as $property => $columnMapping) {
$column = $columnMapping['column']; $column = $columnMapping['column'];
$propertyMapping = array('property' => $property, 'mapping' => $columnMapping['mapping']); $propertyMapping = array('property' => $property, 'mapping' => $columnMapping['mapping']);
$this->inverseSpreadsheetImportMapping[$column][] = $propertyMapping; $this->inverseMappingProperties[$column][] = $propertyMapping;
} }
} }
return $this->inverseSpreadsheetImportMapping; return $this->inverseMappingProperties;
} }
/** /**
* @param $object * Filters the SpreadsheetImport mappings elements for Mapping identifiers only.
*
* Example:
* array(['id'] => array(['column'] => 'A', ['mapping'] => :Mapping)));
*
* @return array
*/ */
private function setObjectArgumentProperties($object) { private function getMappingIdentifierProperties() {
$contextArguments = $this->settings[$this->spreadsheetImport->getContext()]['arguments']; if (empty($this->mappingIdentifierProperties)) {
if (is_array($contextArguments)) { foreach ($this->spreadsheetImport->getMapping() as $property => $columnMapping) {
$arguments = $this->spreadsheetImport->getArguments(); /** @var Mapping $mapping */
foreach ($contextArguments as $contextArgument) { $mapping = $columnMapping['mapping'];
$name = $contextArgument['name']; if ($mapping->identifier) {
if (array_key_exists($name, $arguments)) { $this->mappingIdentifierProperties[$property] = $columnMapping;
if (array_key_exists('domain', $contextArgument)) { }
$value = $this->propertyMapper->convert($arguments[$name], $contextArgument['domain']); }
} else { }
$value = $arguments[$name]; return $this->mappingIdentifierProperties;
}
/**
* Filters the SpreadsheetImport argument elements for identifier arguments only.
*
* Example:
* array(['id'] => '8df83181-6fb2-11e4-8b37-00505601050c');
*
* @return array
*/
private function getArgumentIdentifierProperties() {
if (empty($this->argumentIdentifierProperties)) {
$context = $this->spreadsheetImport->getContext();
$contextArguments = $this->settings[$context]['arguments'];
if (is_array($contextArguments)) {
foreach ($contextArguments as $contextArgument) {
if (isset($contextArgument['identifier']) && $contextArgument['identifier'] == TRUE) {
$name = $contextArgument['name'];
$arguments = $this->spreadsheetImport->getArguments();
if (array_key_exists($name, $arguments)) {
$this->argumentIdentifierProperties[$name] = $arguments[$name];
}
} }
$setter = 'set' . ucfirst($name);
$object->$setter($value);
} }
} }
} }
return $this->argumentIdentifierProperties;
}
/**
* @return \TYPO3\Flow\Persistence\RepositoryInterface
*/
private function getDomainRepository() {
$repositoryClassName = preg_replace(array('/\\\Model\\\/', '/$/'), array('\\Repository\\', 'Repository'), $this->domain);
/** @var RepositoryInterface $repository */
$repository = $this->objectManager->get($repositoryClassName);
return $repository;
} }
} }
WE: WE:
SpreadsheetImport: SpreadsheetImport:
cleanupImportsThreasholdDays: 365 keepPastImportsThreasholdDays: 365
maxExecutionThreasholdMinutes: 720
persistRecordsChunkSize: 100 persistRecordsChunkSize: 100
WE: WE:
SpreadsheetImport: SpreadsheetImport:
cleanupImportsThreasholdDays: 365 keepPastImportsThreasholdDays: 365
maxExecutionThreasholdMinutes: 720
persistRecordsChunkSize: 100 persistRecordsChunkSize: 100
default: default:
domain: WE\Sample\Domain\Model\User domain: WE\Sample\Domain\Model\User
......
WE: WE:
SpreadsheetImport: SpreadsheetImport:
testing: testing:
domain: WE\SpreadsheetImport\Tests\Functional\Fixtures\ImportTarget domain: WE\SpreadsheetImport\Tests\Functional\Fixtures\Domain\Model\ImportTarget
arguments: ~ arguments:
- { name: category, domain: 'WE\SpreadsheetImport\Tests\Functional\Fixtures\Domain\Model\ImportTargetCategory', identifier: TRUE }
- { name: comment, static: 'Sample import' }
<?php
namespace TYPO3\Flow\Persistence\Doctrine\Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Add progressdate property
*/
class Version20161103191621 extends AbstractMigration
{
/**
* @return string
*/
public function getDescription() {
return '';
}
/**
* @param Schema $schema
* @return void
*/
public function up(Schema $schema)
{
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on "mysql".');
$this->addSql('ALTER TABLE we_spreadsheetimport_domain_model_spreadsheetimport ADD progressdate DATETIME NOT NULL');
}
/**
* @param Schema $schema
* @return void
*/
public function down(Schema $schema)
{
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on "mysql".');
$this->addSql('ALTER TABLE we_spreadsheetimport_domain_model_spreadsheetimport DROP progressdate');
}
}
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
<f:form.hidden name="spreadsheetImport" value="{spreadsheetImport}"/> <f:form.hidden name="spreadsheetImport" value="{spreadsheetImport}"/>
<f:for each="{preview}" key="property" as="previewMapping"> <f:for each="{preview}" key="property" as="previewMapping">
<div> <div>
<label> <label class="{f:if(condition: previewMapping.error, then: 'error-text')}">
<f:if condition="{previewMapping.mapping.labelId}"> <f:if condition="{previewMapping.mapping.labelId}">
<f:then><f:translate id="{previewMapping.mapping.labelId}" />:</f:then> <f:then><f:translate id="{previewMapping.mapping.labelId}" />:</f:then>
<f:else>{property}:</f:else> <f:else>{property}:</f:else>
......
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
<source>Import Vorschau</source> <source>Import Vorschau</source>
</trans-unit> </trans-unit>
<trans-unit id="label.spreadsheet_import.preview.invalid_record"> <trans-unit id="label.spreadsheet_import.preview.invalid_record">
<source>Ungültige Daten. Dieser Record wird nicht übersprungen.</source> <source>Ungültige Daten. Dieser Record wird übersprungen.</source>
</trans-unit> </trans-unit>
<trans-unit id="label.spreadsheet_import.preview.record"> <trans-unit id="label.spreadsheet_import.preview.record">
<source>Zeile</source> <source>Zeile</source>
......
<?php
namespace WE\SpreadsheetImport\Tests\Functional\Fixtures\Domain\Model;
/* *
* This script belongs to the Flow package "SpreadsheetImport". *
* *
* It is free software; you can redistribute it and/or modify it under *
* the terms of the GNU Lesser General Public License, either version 3 *
* of the License, or (at your option) any later version. *
* *
* The TYPO3 project - inspiring people to share! *
* */
use TYPO3\Flow\Annotations as Flow;
use WE\SpreadsheetImport\Annotations as SpreadsheetImport;
/**
* @Flow\Entity
*/
class ImportTarget {
/**
* @var string
* @SpreadsheetImport\Mapping(identifier=true, setter="setRawId")
*/
protected $id;
/**
* @var string
* @SpreadsheetImport\Mapping
*/
protected $firstName;
/**
* @var string
* @SpreadsheetImport\Mapping
*/
protected $lastName;
/**
* @var string
* @SpreadsheetImport\Mapping
*/
protected $account;
/**
* @var ImportTargetCategory
*/
protected $category;
/**
* @var string
*/
protected $comment;
/**
* @return string
*/
public function getId() {
return $this->id;
}
/**
* @param string $id
*/
public function setId($id) {
$this->id = $id;
}
/**
* @param string $id
*/
public function setRawId($id) {
$this->id = sprintf('%05d', $id);
}
/**
* @return string
*/
public function getFirstName() {
return $this->firstName;
}
/**
* @param string $firstName
*/
public function setFirstName($firstName) {
$this->firstName = $firstName;
}
/**
* @return string
*/
public function getLastName() {
return $this->lastName;
}
/**
* @param string $lastName
*/
public function setLastName($lastName) {
$this->lastName = $lastName;
}
/**
* @return string
*/
public function getAccount() {
return $this->account;
}
/**
* @param string $account
*/
public function setAccount($account) {
$this->account = $account;
}
/**
* @return \WE\SpreadsheetImport\Tests\Functional\Fixtures\Domain\Model\ImportTargetCategory
*/
public function getCategory() {
return $this->category;
}
/**
* @param \WE\SpreadsheetImport\Tests\Functional\Fixtures\Domain\Model\ImportTargetCategory $category
*/
public function setCategory($category) {
$this->category = $category;
}
/**
* @return string
*/
public function getComment() {
return $this->comment;
}
/**
* @param string $comment
*/
public function setComment($comment) {
$this->comment = $comment;
}
}
<?php <?php
namespace WE\SpreadsheetImport\Tests\Functional\Fixtures; namespace WE\SpreadsheetImport\Tests\Functional\Fixtures\Domain\Model;
/* * /* *
* This script belongs to the Flow package "SpreadsheetImport". * * This script belongs to the Flow package "SpreadsheetImport". *
...@@ -13,22 +13,19 @@ namespace WE\SpreadsheetImport\Tests\Functional\Fixtures; ...@@ -13,22 +13,19 @@ namespace WE\SpreadsheetImport\Tests\Functional\Fixtures;
use TYPO3\Flow\Annotations as Flow; use TYPO3\Flow\Annotations as Flow;
use WE\SpreadsheetImport\Annotations as SpreadsheetImport; use WE\SpreadsheetImport\Annotations as SpreadsheetImport;
use WE\SpreadsheetImport\Domain\Model\SpreadsheetImportTargetInterface;
/** /**
* @Flow\Entity * @Flow\Entity
*/ */
class ImportTarget { class ImportTargetCategory {
/** /**
* @var string * @var string
* @SpreadsheetImport\Mapping(identifier=true, setter="setRawId")
*/ */
protected $id; protected $id;
/** /**
* @var string * @var string
* @SpreadsheetImport\Mapping
*/ */
protected $name; protected $name;
...@@ -46,13 +43,6 @@ class ImportTarget { ...@@ -46,13 +43,6 @@ class ImportTarget {
$this->id = $id; $this->id = $id;
} }
/**
* @param string $id
*/
public function setRawId($id) {
$this->id = sprintf('%05d', $id);
}
/** /**
* @return string * @return string
*/ */
...@@ -66,4 +56,5 @@ class ImportTarget { ...@@ -66,4 +56,5 @@ class ImportTarget {
public function setName($name) { public function setName($name) {
$this->name = $name; $this->name = $name;
} }
} }
<?php
namespace WE\SpreadsheetImport\Tests\Functional\Fixtures\Domain\Repository;
/* *
* This script belongs to the Flow package "SpreadsheetImport". *
* *
* It is free software; you can redistribute it and/or modify it under *
* the terms of the GNU Lesser General Public License, either version 3 *
* of the License, or (at your option) any later version. *
* *
* The TYPO3 project - inspiring people to share! *
* */
use TYPO3\Flow\Annotations as Flow;
use TYPO3\Flow\Persistence\Repository;
/**
* @Flow\Scope("singleton")
*/
class ImportTargetRepository extends Repository {
}
...@@ -17,7 +17,8 @@ use TYPO3\Flow\Tests\FunctionalTestCase; ...@@ -17,7 +17,8 @@ use TYPO3\Flow\Tests\FunctionalTestCase;
use WE\SpreadsheetImport\Annotations\Mapping; use WE\SpreadsheetImport\Annotations\Mapping;
use WE\SpreadsheetImport\Domain\Model\SpreadsheetImport; use WE\SpreadsheetImport\Domain\Model\SpreadsheetImport;
use WE\SpreadsheetImport\SpreadsheetImportService; use WE\SpreadsheetImport\SpreadsheetImportService;
use WE\SpreadsheetImport\Tests\Functional\Fixtures\ImportTarget; use WE\SpreadsheetImport\Tests\Functional\Fixtures\Domain\Model\ImportTarget;
use WE\SpreadsheetImport\Tests\Functional\Fixtures\Domain\Model\ImportTargetCategory;
class SpreadsheetImportServiceTest extends FunctionalTestCase { class SpreadsheetImportServiceTest extends FunctionalTestCase {
...@@ -41,19 +42,17 @@ class SpreadsheetImportServiceTest extends FunctionalTestCase { ...@@ -41,19 +42,17 @@ class SpreadsheetImportServiceTest extends FunctionalTestCase {
$reflectionService = $this->objectManager->get(ReflectionService::class); $reflectionService = $this->objectManager->get(ReflectionService::class);
$this->inject($this->spreadsheetImportService, 'reflectionService', $reflectionService); $this->inject($this->spreadsheetImportService, 'reflectionService', $reflectionService);
$this->initializeSpreadsheetMock();
} }
/** /**
* @return void * @return void
*/ */
public function tearDown() { public function tearDown() {
$persistenceManager = self::$bootstrap->getObjectManager()->get('TYPO3\Flow\Persistence\PersistenceManagerInterface'); $persistenceManager = $this->objectManager->get('TYPO3\Flow\Persistence\PersistenceManagerInterface');
if (is_callable(array($persistenceManager, 'tearDown'))) { if (is_callable(array($persistenceManager, 'tearDown'))) {
$persistenceManager->tearDown(); $persistenceManager->tearDown();
} }
self::$bootstrap->getObjectManager()->forgetInstance('TYPO3\Flow\Persistence\PersistenceManagerInterface'); $this->objectManager->forgetInstance('TYPO3\Flow\Persistence\PersistenceManagerInterface');
parent::tearDown(); parent::tearDown();
} }
...@@ -62,47 +61,75 @@ class SpreadsheetImportServiceTest extends FunctionalTestCase { ...@@ -62,47 +61,75 @@ class SpreadsheetImportServiceTest extends FunctionalTestCase {
$spreadsheetImport->setContext('testing'); $spreadsheetImport->setContext('testing');
$resource = $this->resourceManager->importResource(__DIR__ . '/Fixtures/Resources/sample.xlsx'); $resource = $this->resourceManager->importResource(__DIR__ . '/Fixtures/Resources/sample.xlsx');
$spreadsheetImport->setFile($resource); $spreadsheetImport->setFile($resource);
$idMapping = array('column' => 'C', 'mapping' => new Mapping());
$nameMapping = array('column' => 'A', 'mapping' => new Mapping());
$spreadsheetImport->setMapping(array('id' => $idMapping, 'name' => $nameMapping));
$this->spreadsheetImportService->init($spreadsheetImport); $this->spreadsheetImportService->init($spreadsheetImport);
return $spreadsheetImport;
}
private function initializeConfiguredSpreadsheetMock() {
$spreadsheetImport = $this->initializeSpreadsheetMock();
$annotationMappings = $this->spreadsheetImportService->getAnnotationMappingProperties();
$spreadsheetImport->setMapping(array(
'id' => array('column' => 'C', 'mapping' => $annotationMappings['id']),
'firstName' => array('column' => 'A', 'mapping' => $annotationMappings['firstName']),
'lastName' => array('column' => 'B', 'mapping' => $annotationMappings['lastName']),
'account' => array('column' => 'C', 'mapping' => $annotationMappings['account'])));
$spreadsheetImport->setArguments(array(
'category' => new ImportTargetCategory(), // Could also simply assign the UUID
'comment' => 'Sample import'
));
} }
/** /**
* @test * @test
*/ */
public function getMappingPropertiesReturnsPropertiesWithMappingAnnotation() { public function getAnnotationMappingPropertiesReturnsArrayMappingAnnotation() {
$this->initializeSpreadsheetMock();
$properties = $this->spreadsheetImportService->getAnnotationMappingProperties(); $properties = $this->spreadsheetImportService->getAnnotationMappingProperties();
$this->assertArrayHasKey('id', $properties); $this->assertArrayHasKey('id', $properties);
$this->assertArrayHasKey('name', $properties); $this->assertArrayHasKey('firstName', $properties);
/** @var Mapping $id */ /** @var Mapping $id */
$id = $properties['id']; $id = $properties['id'];
/** @var Mapping $name */ /** @var Mapping $firstName */
$name = $properties['name']; $firstName = $properties['firstName'];
$this->assertSame(TRUE, $id->identifier); $this->assertSame(TRUE, $id->identifier);
$this->assertSame(FALSE, $name->identifier); $this->assertSame(FALSE, $firstName->identifier);
}
/**
* @test
*/
public function getTotalRecordsRecordsNumberOfObjectsToImport() {
$this->initializeSpreadsheetMock();
$this->assertSame(2, $this->spreadsheetImportService->getTotalRecords());
} }
/** /**
* @test * @test
*/ */
public function getSpreadsheetColumnsReturnsColumnsWithHeadings() { public function getSpreadsheetColumnsReturnsColumnsWithHeadings() {
$this->initializeSpreadsheetMock();
$columns = $this->spreadsheetImportService->getSpreadsheetColumns(); $columns = $this->spreadsheetImportService->getSpreadsheetColumns();
$this->assertArrayHasKey('A', $columns); $this->assertArrayHasKey('A', $columns);
$this->assertArrayHasKey('B', $columns); $this->assertArrayHasKey('B', $columns);
$this->assertArrayHasKey('C', $columns); $this->assertArrayHasKey('C', $columns);
$this->assertContains('name', $columns); $this->assertSame('firstname', $columns['A']);
$this->assertContains('lastname', $columns); $this->assertSame('name', $columns['B']);
$this->assertContains('id', $columns); $this->assertSame('id', $columns['C']);
} }
/** /**
* @test * @test
*/ */
public function getObjectByRowOneReturnsImportTargetWithSetProperties() { public function getObjectByRowOneReturnsImportTargetWithSetProperties() {
$this->initializeConfiguredSpreadsheetMock();
/** @var ImportTarget $object */ /** @var ImportTarget $object */
$object = $this->spreadsheetImportService->getObjectByRow(1); $object = $this->spreadsheetImportService->getObjectByRow(1);
$this->assertEquals('00001', $object->getId()); $this->assertSame('00001', $object->getId());
$this->assertEquals('Hans', $object->getName()); $this->assertSame('Hans', $object->getFirstName());
$this->assertSame('Muster', $object->getLastName());
$this->assertSame('001', $object->getAccount());
$this->assertInstanceOf(ImportTargetCategory::class, $object->getCategory());
$this->assertSame('Sample import', $object->getComment());
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment