From 723ea3b512edfe4619ac0541344606aeba32ff0d Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Thu, 15 Dec 2022 12:41:35 +0100 Subject: [PATCH] Skip import of none mappable objects (#100) The schema is very flexible and some values are not validated upfront. This will result in many objects which we currently can not map. This resulted in an exception breaking the rest of the import. We now handle the broken mapping and skip those objects with proper logging. This allows to continue with import and report and debug those objects in order to improve the mapping step by step. Relates: #10198 --- .../Import/EntityMapper/MappingException.php | 5 + Classes/Domain/Import/Importer.php | 34 ++- Classes/Domain/Import/Importer/SaveData.php | 4 +- .../Typo3Converter/GeneralConverter.php | 25 ++- Classes/Domain/Model/Backend/ImportLog.php | 19 +- .../Domain/Model/Backend/ImportLogEntry.php | 91 ++------ .../Backend/ImportLogEntry/MappingError.php | 76 +++++++ .../Backend/ImportLogEntry/SavingEntity.php | 129 ++++++++++++ .../Backend/ImportLogRepository.php | 18 +- Configuration/Extbase/Persistence/Classes.php | 12 ++ Configuration/TCA/tx_thuecat_import_log.php | 6 +- .../TCA/tx_thuecat_import_log_entry.php | 42 +++- Documentation/Changelog/1.3.0.rst | 5 + Resources/Private/Language/locallang_tca.xlf | 12 ++ .../resources/mapping-exception.json | 195 ++++++++++++++++++ ...owingRecordsInCaseOfAnMappingException.csv | 18 ++ ...owingRecordsInCaseOfAnMappingException.xml | 99 +++++++++ ...ecordsInCaseOfAnMappingExceptionOldPhp.csv | 18 ++ Tests/Functional/ImportTest.php | 37 ++++ .../Typo3Converter/GeneralConverterTest.php | 14 +- ext_tables.sql | 4 +- phpstan-baseline.neon | 4 +- 22 files changed, 743 insertions(+), 124 deletions(-) create mode 100644 Classes/Domain/Model/Backend/ImportLogEntry/MappingError.php create mode 100644 Classes/Domain/Model/Backend/ImportLogEntry/SavingEntity.php create mode 100644 Tests/Functional/Fixtures/Import/Guzzle/thuecat.org/resources/mapping-exception.json create mode 100644 Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingException.csv create mode 100644 Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingException.xml create mode 100644 Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingExceptionOldPhp.csv diff --git a/Classes/Domain/Import/EntityMapper/MappingException.php b/Classes/Domain/Import/EntityMapper/MappingException.php index d7237c3..035860a 100644 --- a/Classes/Domain/Import/EntityMapper/MappingException.php +++ b/Classes/Domain/Import/EntityMapper/MappingException.php @@ -48,4 +48,9 @@ class MappingException extends \Exception $this->jsonLD = $jsonLD; $this->targetClassName = $targetClassName; } + + public function getUrl(): string + { + return $this->jsonLD['@id'] ?? 'unknown'; + } } diff --git a/Classes/Domain/Import/Importer.php b/Classes/Domain/Import/Importer.php index 43acae6..fd3f0d4 100644 --- a/Classes/Domain/Import/Importer.php +++ b/Classes/Domain/Import/Importer.php @@ -27,6 +27,7 @@ use TYPO3\CMS\Core\Log\LogManager; use TYPO3\CMS\Core\Log\Logger; use WerkraumMedia\ThueCat\Domain\Import\EntityMapper\EntityRegistry; use WerkraumMedia\ThueCat\Domain\Import\EntityMapper\JsonDecode; +use WerkraumMedia\ThueCat\Domain\Import\EntityMapper\MappingException; use WerkraumMedia\ThueCat\Domain\Import\Entity\MapsToType; use WerkraumMedia\ThueCat\Domain\Import\Importer\Converter; use WerkraumMedia\ThueCat\Domain\Import\Importer\FetchData; @@ -36,6 +37,7 @@ use WerkraumMedia\ThueCat\Domain\Import\Model\EntityCollection; use WerkraumMedia\ThueCat\Domain\Import\UrlProvider\Registry as UrlProviderRegistry; use WerkraumMedia\ThueCat\Domain\Import\UrlProvider\UrlProvider; use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLog; +use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\MappingError; use WerkraumMedia\ThueCat\Domain\Repository\Backend\ImportLogRepository; class Importer @@ -180,17 +182,29 @@ class Importer foreach ($this->languages->getAvailable($this->import->getConfiguration()) as $language) { $this->logger->info('Process entity for language.', ['language' => $language, 'targetEntity' => $targetEntity]); - $mappedEntity = $this->entityMapper->mapDataToEntity( - $jsonEntity, - $targetEntity, - [ - JsonDecode::ACTIVE_LANGUAGE => $language, - ] - ); - if (!$mappedEntity instanceof MapsToType) { - $this->logger->alert('Mapping did not result in an MapsToType instance.', ['class' => get_class($mappedEntity)]); + try { + $mappedEntity = $this->entityMapper->mapDataToEntity( + $jsonEntity, + $targetEntity, + [ + JsonDecode::ACTIVE_LANGUAGE => $language, + ] + ); + } catch (MappingException $e) { + $this->logger->error('Could not map data to entity.', [ + 'url' => $e->getUrl(), + 'language' => $language, + 'mappingError' => $e->getMessage(), + ]); + $this->import->getLog()->addEntry(new MappingError($e)); continue; } + + if (!$mappedEntity instanceof MapsToType) { + $this->logger->error('Mapping did not result in an MapsToType instance.', ['class' => get_class($mappedEntity)]); + continue; + } + $convertedEntity = $this->converter->convert( $mappedEntity, $this->import->getConfiguration(), @@ -198,7 +212,7 @@ class Importer ); if ($convertedEntity === null) { - $this->logger->alert('Could not convert entity.', ['language' => $language, 'targetEntity' => $targetEntity]); + $this->logger->error('Could not convert entity.', ['language' => $language, 'targetEntity' => $targetEntity]); continue; } $entities->add($convertedEntity); diff --git a/Classes/Domain/Import/Importer/SaveData.php b/Classes/Domain/Import/Importer/SaveData.php index 8def6ec..ac12613 100644 --- a/Classes/Domain/Import/Importer/SaveData.php +++ b/Classes/Domain/Import/Importer/SaveData.php @@ -28,7 +28,7 @@ use TYPO3\CMS\Core\DataHandling\DataHandler; use WerkraumMedia\ThueCat\Domain\Import\Model\Entity; use WerkraumMedia\ThueCat\Domain\Import\Model\EntityCollection; use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLog; -use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry; +use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\SavingEntity; class SaveData { @@ -65,7 +65,7 @@ class SaveData $this->updateEntities($entityCollection); foreach ($entityCollection->getEntities() as $entity) { - $log->addEntry(new ImportLogEntry($entity, $this->errorLog)); + $log->addEntry(new SavingEntity($entity, $this->errorLog)); } } diff --git a/Classes/Domain/Import/Typo3Converter/GeneralConverter.php b/Classes/Domain/Import/Typo3Converter/GeneralConverter.php index 1fef58d..40613cb 100644 --- a/Classes/Domain/Import/Typo3Converter/GeneralConverter.php +++ b/Classes/Domain/Import/Typo3Converter/GeneralConverter.php @@ -23,8 +23,8 @@ declare(strict_types=1); namespace WerkraumMedia\ThueCat\Domain\Import\Typo3Converter; -use Psr\Log\LoggerAwareInterface; -use Psr\Log\LoggerAwareTrait; +use TYPO3\CMS\Core\Log\LogManager; +use TYPO3\CMS\Core\Log\Logger; use TYPO3\CMS\Extbase\Persistence\QueryResultInterface; use WerkraumMedia\ThueCat\Domain\Import\Entity\AccessibilitySpecification; use WerkraumMedia\ThueCat\Domain\Import\Entity\Base; @@ -50,10 +50,8 @@ use WerkraumMedia\ThueCat\Domain\Repository\Backend\OrganisationRepository; use WerkraumMedia\ThueCat\Domain\Repository\Backend\ParkingFacilityRepository; use WerkraumMedia\ThueCat\Domain\Repository\Backend\TownRepository; -class GeneralConverter implements Converter, LoggerAwareInterface +class GeneralConverter implements Converter { - use LoggerAwareTrait; - /** * @var ResolveForeignReference */ @@ -89,6 +87,11 @@ class GeneralConverter implements Converter, LoggerAwareInterface */ private $nameExtractor; + /** + * @var Logger + */ + private $logger; + /** * @var ImportConfiguration */ @@ -113,7 +116,8 @@ class GeneralConverter implements Converter, LoggerAwareInterface OrganisationRepository $organisationRepository, TownRepository $townRepository, ParkingFacilityRepository $parkingFacilityRepository, - NameExtractor $nameExtractor + NameExtractor $nameExtractor, + LogManager $logManager ) { $this->resolveForeignReference = $resolveForeignReference; $this->importer = $importer; @@ -122,6 +126,7 @@ class GeneralConverter implements Converter, LoggerAwareInterface $this->townRepository = $townRepository; $this->parkingFacilityRepository = $parkingFacilityRepository; $this->nameExtractor = $nameExtractor; + $this->logger = $logManager->getLogger(__CLASS__); } public function convert( @@ -165,14 +170,14 @@ class GeneralConverter implements Converter, LoggerAwareInterface $tableName = $this->getTableNameByEntityClass(get_class($entity)); if (!$entity instanceof Minimum) { - $this->logger->debug('Skipped conversion of entity, got unexpected type', [ + $this->logger->info('Skipped conversion of entity, got unexpected type', [ 'expectedType' => Minimum::class, 'actualType' => get_class($entity), ]); return false; } if ($entity->hasName() === false) { - $this->logger->debug('Skipped conversion of entity, had no name', [ + $this->logger->info('Skipped conversion of entity, had no name', [ 'remoteId' => $entity->getId(), ]); return false; @@ -186,7 +191,7 @@ class GeneralConverter implements Converter, LoggerAwareInterface $languageUid > 0 && isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField']) === false ) { - $this->logger->debug('Skipped conversion of entity, table does not support translations', [ + $this->logger->info('Skipped conversion of entity, table does not support translations', [ 'remoteId' => $entity->getId(), 'requestedLanguage' => $language, 'resolvedLanguageUid' => $languageUid, @@ -196,7 +201,7 @@ class GeneralConverter implements Converter, LoggerAwareInterface } if ($tableName !== 'tx_thuecat_organisation' && $this->getManagerUid($entity) === '') { - $this->logger->debug('Skipped conversion of entity, is not an organisation and no manager was available', [ + $this->logger->info('Skipped conversion of entity, is not an organisation and no manager was available', [ 'remoteId' => $entity->getId(), ]); return false; diff --git a/Classes/Domain/Model/Backend/ImportLog.php b/Classes/Domain/Model/Backend/ImportLog.php index fd35105..ca094b8 100644 --- a/Classes/Domain/Model/Backend/ImportLog.php +++ b/Classes/Domain/Model/Backend/ImportLog.php @@ -25,6 +25,7 @@ namespace WerkraumMedia\ThueCat\Domain\Model\Backend; use TYPO3\CMS\Extbase\DomainObject\AbstractEntity as Typo3AbstractEntity; use TYPO3\CMS\Extbase\Persistence\ObjectStorage; +use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\SavingEntity; class ImportLog extends Typo3AbstractEntity { @@ -88,7 +89,11 @@ class ImportLog extends Typo3AbstractEntity foreach ($this->getEntries() as $entry) { if ($entry->hasErrors()) { - $errors = array_merge($errors, $entry->getErrors()); + $entryErrors = array_map(function (string $error) use ($entry) { + return 'Resource: ' . $entry->getRemoteId() . ' Error: ' . $error; + }, $entry->getErrors()); + + $errors = array_merge($errors, $entryErrors); $errors = array_unique($errors); } } @@ -111,7 +116,7 @@ class ImportLog extends Typo3AbstractEntity { $summary = []; - foreach ($this->getEntries() as $entry) { + foreach ($this->getSavingEntries() as $entry) { if (isset($summary[$entry->getRecordDatabaseTableName()])) { ++$summary[$entry->getRecordDatabaseTableName()]; continue; @@ -139,4 +144,14 @@ class ImportLog extends Typo3AbstractEntity $this->addEntry($entry); } } + + /** + * @return SavingEntity[] + */ + private function getSavingEntries(): array + { + return array_filter($this->logEntries->getArray(), function (ImportLogEntry $entry) { + return $entry instanceof SavingEntity; + }); + } } diff --git a/Classes/Domain/Model/Backend/ImportLogEntry.php b/Classes/Domain/Model/Backend/ImportLogEntry.php index 2412c3b..bc5952b 100644 --- a/Classes/Domain/Model/Backend/ImportLogEntry.php +++ b/Classes/Domain/Model/Backend/ImportLogEntry.php @@ -23,90 +23,25 @@ declare(strict_types=1); namespace WerkraumMedia\ThueCat\Domain\Model\Backend; -use TYPO3\CMS\Extbase\DomainObject\AbstractEntity as Typo3AbstractEntity; -use WerkraumMedia\ThueCat\Domain\Import\Model\Entity; +use TYPO3\CMS\Extbase\DomainObject\AbstractEntity; -class ImportLogEntry extends Typo3AbstractEntity +abstract class ImportLogEntry extends AbstractEntity { - /** - * @var string - */ - protected $remoteId = ''; + abstract public function getRemoteId(): string; + + abstract public function getErrors(): array; + + abstract public function hasErrors(): bool; /** - * @var bool + * The type as defined within TCA. */ - protected $insertion = false; + abstract public function getType(): string; /** - * @var int + * Return an column -> value array used for insertion into the database. + * Only return special for the concrete instance, or empty array. + * Defaults inherited by this class are already handled. */ - protected $recordUid = 0; - - /** - * @var int - */ - protected $recordPid = 0; - - /** - * @var string - */ - protected $tableName = ''; - - /** - * @var string - */ - protected $errors = ''; - - /** - * @var string[] - */ - protected $errorsAsArray = []; - - public function __construct( - Entity $entity, - array $dataHandlerErrorLog - ) { - $this->remoteId = $entity->getRemoteId(); - $this->insertion = $entity->wasCreated(); - $this->recordUid = $entity->getTypo3Uid(); - $this->recordPid = $entity->getTypo3StoragePid(); - $this->tableName = $entity->getTypo3DatabaseTableName(); - $this->errorsAsArray = $dataHandlerErrorLog; - } - - public function getRemoteId(): string - { - return $this->remoteId; - } - - public function wasInsertion(): bool - { - return $this->insertion; - } - - public function getRecordUid(): int - { - return $this->recordUid; - } - - public function getRecordDatabaseTableName(): string - { - return $this->tableName; - } - - public function getErrors(): array - { - if ($this->errorsAsArray === [] && $this->errors !== '') { - $this->errorsAsArray = json_decode($this->errors, true); - $this->errorsAsArray = array_unique($this->errorsAsArray); - } - - return $this->errorsAsArray; - } - - public function hasErrors(): bool - { - return $this->getErrors() !== []; - } + abstract public function getInsertion(): array; } diff --git a/Classes/Domain/Model/Backend/ImportLogEntry/MappingError.php b/Classes/Domain/Model/Backend/ImportLogEntry/MappingError.php new file mode 100644 index 0000000..12db1d3 --- /dev/null +++ b/Classes/Domain/Model/Backend/ImportLogEntry/MappingError.php @@ -0,0 +1,76 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +namespace WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry; + +use WerkraumMedia\ThueCat\Domain\Import\EntityMapper\MappingException; +use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry; + +class MappingError extends ImportLogEntry +{ + /** + * @var string + */ + protected $remoteId = ''; + + /** + * @var string + */ + protected $errors = ''; + + public function __construct( + MappingException $exception + ) { + $this->remoteId = $exception->getUrl(); + $this->errors = json_encode([$exception->getMessage()]) ?: ''; + } + + public function getRemoteId(): string + { + return $this->remoteId; + } + + public function getErrors(): array + { + $errors = json_decode($this->errors, true); + if (is_array($errors) === false) { + throw new \Exception('Could not parse errors.', 1671097690); + } + return $errors; + } + + public function hasErrors(): bool + { + return true; + } + + public function getType(): string + { + return 'mappingError'; + } + + public function getInsertion(): array + { + return []; + } +} diff --git a/Classes/Domain/Model/Backend/ImportLogEntry/SavingEntity.php b/Classes/Domain/Model/Backend/ImportLogEntry/SavingEntity.php new file mode 100644 index 0000000..4b61597 --- /dev/null +++ b/Classes/Domain/Model/Backend/ImportLogEntry/SavingEntity.php @@ -0,0 +1,129 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +namespace WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry; + +use WerkraumMedia\ThueCat\Domain\Import\Model\Entity; +use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry; + +class SavingEntity extends ImportLogEntry +{ + /** + * @var string + */ + protected $remoteId = ''; + + /** + * @var bool + */ + protected $insertion = false; + + /** + * @var int + */ + protected $recordUid = 0; + + /** + * @var int + */ + protected $recordPid = 0; + + /** + * @var string + */ + protected $tableName = ''; + + /** + * @var string + */ + protected $errors = ''; + + /** + * @var string[] + */ + protected $errorsAsArray = []; + + public function __construct( + Entity $entity, + array $dataHandlerErrorLog + ) { + $this->remoteId = $entity->getRemoteId(); + $this->insertion = $entity->wasCreated(); + $this->recordUid = $entity->getTypo3Uid(); + $this->recordPid = $entity->getTypo3StoragePid(); + $this->tableName = $entity->getTypo3DatabaseTableName(); + $this->errorsAsArray = $dataHandlerErrorLog; + } + + public function getRemoteId(): string + { + return $this->remoteId; + } + + public function wasInsertion(): bool + { + return $this->insertion; + } + + public function getRecordUid(): int + { + return $this->recordUid; + } + + public function getRecordDatabaseTableName(): string + { + return $this->tableName; + } + + public function getErrors(): array + { + if ($this->errorsAsArray === [] && $this->errors !== '') { + $errorsAsArray = json_decode($this->errors, true); + if (is_array($errorsAsArray) === false) { + throw new \Exception('Could not parse errors.', 1671097690); + } + $this->errorsAsArray = array_unique($errorsAsArray); + } + + return $this->errorsAsArray; + } + + public function hasErrors(): bool + { + return $this->getErrors() !== []; + } + + public function getType(): string + { + return 'savingEntity'; + } + + public function getInsertion(): array + { + return [ + 'insertion' => (int) $this->wasInsertion(), + 'record_uid' => $this->getRecordUid(), + 'table_name' => $this->getRecordDatabaseTableName(), + ]; + } +} diff --git a/Classes/Domain/Repository/Backend/ImportLogRepository.php b/Classes/Domain/Repository/Backend/ImportLogRepository.php index 61e25c6..a4fa8e9 100644 --- a/Classes/Domain/Repository/Backend/ImportLogRepository.php +++ b/Classes/Domain/Repository/Backend/ImportLogRepository.php @@ -77,14 +77,16 @@ class ImportLogRepository extends Repository foreach ($log->getEntries() as $entry) { $number++; - $entries['NEW' . $number] = [ - 'pid' => 0, - 'import_log' => 'NEW0', - 'insertion' => (int) $entry->wasInsertion(), - 'record_uid' => $entry->getRecordUid(), - 'table_name' => $entry->getRecordDatabaseTableName(), - 'errors' => json_encode($entry->getErrors()), - ]; + $entries['NEW' . $number] = array_merge( + $entry->getInsertion(), + [ + 'pid' => 0, + 'import_log' => 'NEW0', + 'type' => $entry->getType(), + 'remote_id' => $entry->getRemoteId(), + 'errors' => json_encode($entry->getErrors()), + ] + ); } return $entries; diff --git a/Configuration/Extbase/Persistence/Classes.php b/Configuration/Extbase/Persistence/Classes.php index 209d03f..33481e4 100644 --- a/Configuration/Extbase/Persistence/Classes.php +++ b/Configuration/Extbase/Persistence/Classes.php @@ -22,6 +22,18 @@ return [ ], \WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry::class => [ 'tableName' => 'tx_thuecat_import_log_entry', + 'subclasses' => [ + 'savingEntity' => \WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\SavingEntity::class, + 'mappingError' => \WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\MappingError::class, + ], + ], + \WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\SavingEntity::class => [ + 'tableName' => 'tx_thuecat_import_log_entry', + 'recordType' => 'savingEntity', + ], + \WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\MappingError::class => [ + 'tableName' => 'tx_thuecat_import_log_entry', + 'recordType' => 'mappingError', ], \WerkraumMedia\ThueCat\Domain\Model\Frontend\TouristAttraction::class => [ 'tableName' => 'tx_thuecat_tourist_attraction', diff --git a/Configuration/TCA/tx_thuecat_import_log.php b/Configuration/TCA/tx_thuecat_import_log.php index 52e25f2..c679555 100644 --- a/Configuration/TCA/tx_thuecat_import_log.php +++ b/Configuration/TCA/tx_thuecat_import_log.php @@ -9,8 +9,10 @@ return (static function (string $extensionKey, string $tableName) { return [ 'ctrl' => [ 'label' => 'crdate', + 'label_alt' => 'configuration', + 'label_alt_force' => true, 'iconfile' => \WerkraumMedia\ThueCat\Extension::getIconPath() . $tableName . '.svg', - 'default_sortby' => 'crdate', + 'default_sortby' => 'crdate desc', 'tstamp' => 'tstamp', 'crdate' => 'crdate', 'cruser_id' => 'cruser_id', @@ -51,7 +53,7 @@ return (static function (string $extensionKey, string $tableName) { ], 'types' => [ '0' => [ - 'showitem' => 'crdate, log_entries, configuration', + 'showitem' => 'crdate, configuration, log_entries', ], ], ]; diff --git a/Configuration/TCA/tx_thuecat_import_log_entry.php b/Configuration/TCA/tx_thuecat_import_log_entry.php index a65c7d2..3edf288 100644 --- a/Configuration/TCA/tx_thuecat_import_log_entry.php +++ b/Configuration/TCA/tx_thuecat_import_log_entry.php @@ -8,10 +8,11 @@ return (static function (string $extensionKey, string $tableName) { return [ 'ctrl' => [ - 'label' => 'table_name', - 'label_alt' => 'record_uid', + 'label' => 'type', + 'label_alt' => 'remote_id, table_name, record_uid', 'label_alt_force' => true, 'iconfile' => \WerkraumMedia\ThueCat\Extension::getIconPath() . $tableName . '.svg', + 'type' => 'type', 'default_sortby' => 'crdate', 'tstamp' => 'tstamp', 'crdate' => 'crdate', @@ -24,6 +25,30 @@ return (static function (string $extensionKey, string $tableName) { 'hideTable' => true, ], 'columns' => [ + 'type' => [ + 'label' => $languagePath . '.type', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + [ + $languagePath . '.type.savingEntity', + 'savingEntity', + ], + [ + $languagePath . '.type.mappingError', + 'mappingError', + ], + ], + ], + ], + 'remote_id' => [ + 'label' => $languagePath . '.remote_id', + 'config' => [ + 'type' => 'input', + 'readOnly' => true, + ], + ], 'insertion' => [ 'label' => $languagePath . '.insertion', 'config' => [ @@ -80,9 +105,18 @@ return (static function (string $extensionKey, string $tableName) { ], ], ], + 'palettes' => [ + 'always' => [ + 'label' => $languagePath . '.palette.always', + 'showitem' => 'type, remote_id, import_log, crdate', + ], + ], 'types' => [ - '0' => [ - 'showitem' => 'table_name, record_uid, insertion, errors, import_log, crdate', + 'savingEntity' => [ + 'showitem' => '--palette--;;always, table_name, record_uid, insertion, errors', + ], + 'mappingError' => [ + 'showitem' => '--palette--;;always, errors', ], ], ]; diff --git a/Documentation/Changelog/1.3.0.rst b/Documentation/Changelog/1.3.0.rst index 603a8d0..76f067a 100644 --- a/Documentation/Changelog/1.3.0.rst +++ b/Documentation/Changelog/1.3.0.rst @@ -17,6 +17,11 @@ Features This allows to provide a single entity, e.g. a Town that has multiple ``schema:containsPlace`` entries. Each of them will be imported. +* Import will no longer break on mapping issues. + Those will be logged and are available within the existing backend module. + This allows to skip some objects which can not be handled yet. + The log can be used to open issues. We then can improve the mapping. + * Import author of media. This allows to either render the license author or the author. * Filter and sort opening hours. diff --git a/Resources/Private/Language/locallang_tca.xlf b/Resources/Private/Language/locallang_tca.xlf index c5d5c49..51052fe 100644 --- a/Resources/Private/Language/locallang_tca.xlf +++ b/Resources/Private/Language/locallang_tca.xlf @@ -265,6 +265,18 @@ Import Log Entry + + Type + + + Saved Entity + + + Mapping Error + + + Remote ID (URL) + Was inserted or updated diff --git a/Tests/Functional/Fixtures/Import/Guzzle/thuecat.org/resources/mapping-exception.json b/Tests/Functional/Fixtures/Import/Guzzle/thuecat.org/resources/mapping-exception.json new file mode 100644 index 0000000..68d2436 --- /dev/null +++ b/Tests/Functional/Fixtures/Import/Guzzle/thuecat.org/resources/mapping-exception.json @@ -0,0 +1,195 @@ +{ + "@context": { + "cdb": "https://thuecat.org/ontology/cdb/1.0/", + "dachkg": "https://thuecat.org/ontology/dachkg/1.0/", + "dbo": "http://dbpedia.org/ontology/", + "dsv": "http://ontologies.sti-innsbruck.at/dsv/", + "foaf": "http://xmlns.com/foaf/0.1/", + "owl": "http://www.w3.org/2002/07/owl#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "schema": "http://schema.org/", + "sh": "http://www.w3.org/ns/shacl#", + "thuecat": "https://thuecat.org/ontology/thuecat/1.0/", + "ttgds": "https://thuecat.org/ontology/ttgds/1.0/", + "xsd": "http://www.w3.org/2001/XMLSchema#" + }, + "@graph": [ + { + "@id": "https://thuecat.org/resources/mapping-exception", + "@type": [ + "schema:Place", + "schema:CivicStructure", + "schema:PlaceOfWorship", + "schema:Thing", + "schema:Museum", + "schema:TouristAttraction", + "schema:Synagogue", + "ttgds:PointOfInterest", + "thuecat:Building", + "thuecat:ReligiousBuilding", + "thuecat:CultureHistoricalMuseum" + ], + "schema:address": { + "@id": "genid-28b33237f71b41e3ad54a99e1da769b9-b0", + "@type": [ + "schema:Intangible", + "schema:PostalAddress", + "schema:StructuredValue", + "schema:Thing", + "schema:ContactPoint" + ], + "schema:addressCountry": { + "@type": "thuecat:AddressCountry", + "@value": "thuecat:Germany" + }, + "schema:addressLocality": { + "@language": "de", + "@value": "Erfurt" + }, + "schema:addressRegion": { + "@type": "thuecat:AddressFederalState", + "@value": "thuecat:Thuringia" + }, + "schema:email": { + "@language": "de", + "@value": "altesynagoge@erfurt.de" + }, + "schema:faxNumber": { + "@language": "de", + "@value": "+49 361 6551669" + }, + "schema:postalCode": { + "@language": "de", + "@value": "99084" + }, + "schema:streetAddress": { + "@language": "de", + "@value": "Waagegasse 8" + }, + "schema:telephone": { + "@language": "de", + "@value": "+49 361 6551520" + }, + "thuecat:typOfAddress": { + "@type": "thuecat:TypOfAddress", + "@value": "thuecat:HouseAddress" + } + }, + "schema:availableLanguage": [ + { + "@type": "thuecat:Language", + "@value": "thuecat:German" + }, + { + "@type": "thuecat:Language", + "@value": "thuecat:English" + }, + { + "@type": "thuecat:Language", + "@value": "thuecat:French" + } + ], + "schema:description": [ + { + "@language": "fr", + "@value": "La vieille synagogue (datant des ann\u00e9es 1100) est la synagogue la plus vieille d\u2019Europe totalement conserv\u00e9e, dans laquelle est expos\u00e9 un tr\u00e9sor datant des 13/14\u00e8mes si\u00e8cles avec une alliance juive unique et des \u00e9critures h\u00e9bra\u00efques (datant des 12\u00e8me, 13\u00e8me et 14\u00e8mes si\u00e8cles). Apr\u00e8s la red\u00e9couverte du Mikw\u00e9, Erfurt abrite des t\u00e9moins uniques et fascinants d\u2019une communaut\u00e9 juive m\u00e9di\u00e9vale. " + }, + { + "@language": "en", + "@value": "The Old Synagogue is one of very few preserved medieval synagogues in Europe. Thanks to the extensive preservation of the original structure, it has a special place in the history of art and architecture and is among the most impressive and highly rated architectural monuments in Erfurt and Thuringia. The synagogue was constructed during the Middle Ages on the \"via regia\", one of the major European trade routes, at the heart of the historical old quarter very close to the Merchants Bridge and the town hall. Many parts of the structure still remain today, including all four thick outer walls, the Roman\u00adesque gemel window, the Gothic rose window and the entrance to the synagogue room. " + }, + { + "@language": "de", + "@value": "Beispiel Beschreibung" + }, + { + "@id": "genid-28b33237f71b41e3ad54a99e1da769b9-b1", + "@type": [ + "thuecat:Html" + ], + "schema:value": { + "@language": "de", + "@value": "Mit der Alten Synagoge weist Erfurt die \u00e4lteste bis zum Dach erhaltene Synagoge in Mitteleuropa vor. Hier waren bis Ende der 90er Jahre nur die Spitzen zweier Giebel sichtbar, welche aus einem Gewirr von Anbauten herausragten. Nach dem Abriss einiger Bauten ringsum konnte ein Bauforscher klar vier Bauphasen der Synagoge unterscheiden, dessen \u00e4lteste um 1100 zu datieren ist. \n\nDer Bau von 1270, mit der heute sichtbaren Westfassade samt Ma\u00dfwerkrosette, wurde nach Norden erweitert. Brandspuren am Mauerwerk verweisen auf einen Vorg\u00e4ngerbau, der wahrscheinlich einem Pogrom zum Opfer fiel. Die Synagoge diente bis 1349 als Gotteshaus. In diesem Jahr l\u00f6schte ein barbarisches Pestpogrom die erste j\u00fcdische Gemeinde Erfurts aus. Die Stadt verkaufte das Geb\u00e4ude an einen H\u00e4ndler, der es zum Speicher umbauen lie\u00df. Dabei wurde der hohe Raum von Balkendecken unterteilt, ein breiterer Eingang an Stelle des Thoraschreins geschaffen und die Synagoge unterkellert. Im Erdgeschoss zeugen noch einige Spuren von der Erstnutzung, wie bspw. ein Lichtergesims. \n\nDas Erdgeschoss mit der wuchtigen gotischen Balkendecke und der Keller werden ebenso wie das Obergeschoss, welches von der Festkultur des 19. Jahrhunderts zeugt, museal genutzt. Wer heute den Saal betritt, der f\u00fchlt sich in die vergangene Welt von Tango und Foxtrott unter Gouvernantenaufsicht zur\u00fcckversetzt. Schablonenmalerei sowie einige Tapetenreste schm\u00fccken die W\u00e4nde. \nIm Erdgeschoss wird die Baugeschichte thematisiert, der Keller ist dem Erfurter Schatz aus M\u00fcnzen, Gef\u00e4\u00dfen, gotischem Schmuck und dem j\u00fcdischen Hochzeitsring vorbehalten. \n\nIm Saal zeigt das Haus eine Sammlung von hebr\u00e4ischen Handschriften, welche der Erfurter Gemeinde geh\u00f6rten. Diese Hebraica werden heute in der Staatsbibliothek Berlin aufbewahrt. Abwechselnd k\u00f6nnen sie in Erfurt als Original oder Faksimile bestaunt werden. \n\nMit der Alten Synagoge und einer 2007 an der Kr\u00e4merbr\u00fccke gefundenen Mikwe aus der Gotik, deren wissenschaftliche Erforschung noch andauert, kann Erfurt einmalige und faszinierende Zeugnisse der noch wenig bekannten Geschichte einer mittelalterlichen Gemeinde vorweisen." + } + }, + { + "@id": "genid-28b33237f71b41e3ad54a99e1da769b9-b2", + "@type": [ + "thuecat:Html" + ], + "schema:value": { + "@language": "fr", + "@value": "La vieille synagogue (datant des ann\u00e9es 1100) est la synagogue la plus vieille d\u2019Europe totalement conserv\u00e9e, dans laquelle est expos\u00e9 un tr\u00e9sor datant des 13/14\u00e8mes si\u00e8cles avec une alliance juive unique et des \u00e9critures h\u00e9bra\u00efques (datant des 12\u00e8me, 13\u00e8me et 14\u00e8mes si\u00e8cles). Apr\u00e8s la red\u00e9couverte du Mikw\u00e9, Erfurt abrite des t\u00e9moins uniques et fascinants d\u2019une communaut\u00e9 juive m\u00e9di\u00e9vale. " + } + }, + { + "@id": "genid-28b33237f71b41e3ad54a99e1da769b9-b3", + "@type": [ + "thuecat:Html" + ], + "schema:value": { + "@language": "en", + "@value": "The Old Synagogue is one of very few preserved medieval synagogues in Europe. Thanks to the extensive preservation of the original structure, it has a special place in the history of art and architecture and is among the most impressive and highly rated architectural monuments in Erfurt and Thuringia. The synagogue was constructed during the Middle Ages on the \"via regia\", one of the major European trade routes, at the heart of the historical old quarter very close to the Merchants Bridge and the town hall. Many parts of the structure still remain today, including all four thick outer walls, the Roman\u00adesque gemel window, the Gothic rose window and the entrance to the synagogue room. " + } + } + ], + "schema:identifier": { + "@type": "schema:URL", + "@value": "https://www.thueringen-entdecken.de/urlaub-hotel-reisen/alte-synagoge-erfurt-115157.html" + }, + "schema:name": [ + { + "@language": "de", + "@value": "Alte Synagoge" + }, + { + "@language": "fr", + "@value": "La vieille synagogue" + }, + { + "@language": "en", + "@value": "Old Synagogue" + } + ], + "schema:url": { + "@type": "schema:URL", + "@value": "http://www.alte-synagoge.erfurt.de" + }, + "thuecat:contentResponsible": { + "@id": "https://thuecat.org/resources/018132452787-ngbe" + }, + "schema:openingHoursSpecification": { + "@id": "genid-28b33237f71b41e3ad54a99e1da769b9-b13", + "@type": [ + "schema:Intangible", + "schema:StructuredValue", + "schema:Thing", + "schema:OpeningHoursSpecification" + ], + "schema:closes": { + "@type": "schema:Time", + "@value": "18:00: 00" + }, + "schema:dayOfWeek": [ + { + "@type": "schema:DayOfWeek", + "@value": "schema:Wednesday" + } + ], + "schema:opens": { + "@type": "schema:Time", + "@value": "10:00:00" + }, + "schema:validFrom": { + "@type": "schema:Date", + "@value": "2021-03-01" + }, + "schema:validThrough": { + "@type": "schema:Date", + "@value": "2099-12-31" + } + } + } + ] +} diff --git a/Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingException.csv b/Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingException.csv new file mode 100644 index 0000000..b8d795c --- /dev/null +++ b/Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingException.csv @@ -0,0 +1,18 @@ +"tx_thuecat_tourist_attraction",,,,,,,, +,"uid","pid","sys_language_uid","remote_id","title",,, +,1,10,0,"https://thuecat.org/resources/165868194223-zmqf","Alte Synagoge",,, +,2,10,1,"https://thuecat.org/resources/165868194223-zmqf","Old Synagogue",,, +,3,10,2,"https://thuecat.org/resources/165868194223-zmqf","La vieille synagogue",,, +"tx_thuecat_import_log",,,,,,,, +,"uid","pid","configuration","log_entries",,,, +,1,0,1,0,,,, +"tx_thuecat_import_log_entry",,,,,,,, +,"uid","pid","type","import_log","record_uid","table_name","insertion","errors" +,1,0,"mappingError",1,0,,0,"[""Could not map incoming JSON-LD to target object: Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character""]" +,2,0,"mappingError",1,0,,0,"[""Could not map incoming JSON-LD to target object: Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character""]" +,3,0,"mappingError",1,0,,0,"[""Could not map incoming JSON-LD to target object: Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character""]" +,4,0,"savingEntity",1,1,"tx_thuecat_organisation",1,"[]" +,5,0,"savingEntity",1,1,"tx_thuecat_town",0,"[]" +,6,0,"savingEntity",1,1,"tx_thuecat_tourist_attraction",1,"[]" +,7,0,"savingEntity",1,2,"tx_thuecat_tourist_attraction",0,"[]" +,8,0,"savingEntity",1,3,"tx_thuecat_tourist_attraction",0,"[]" diff --git a/Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingException.xml b/Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingException.xml new file mode 100644 index 0000000..c78bae8 --- /dev/null +++ b/Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingException.xml @@ -0,0 +1,99 @@ + + + + 1 + 0 + 1613400587 + 1613400558 + 1 + 4 + Rootpage + 1 + + + 10 + 1 + 1613400587 + 1613400558 + 1 + 254 + Storage folder + + + + 1 + 0 + English + en-us-gb + en + + + + 2 + 0 + French + fr + fr + + + + 1 + 0 + 1613400587 + 1613400558 + 1 + 0 + Import With Exceptions + static + + + + + + + 10 + + + + + + + + https://thuecat.org/resources/mapping-exception + + + + 0 + + + + + + https://thuecat.org/resources/165868194223-zmqf + + + + 0 + + + + + + + + ]]> + + + + 1 + 10 + 1613401129 + 1613401129 + 1 + 0 + https://thuecat.org/resources/043064193523-jcyt + 1 + 0 + Erfurt + + diff --git a/Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingExceptionOldPhp.csv b/Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingExceptionOldPhp.csv new file mode 100644 index 0000000..3de3703 --- /dev/null +++ b/Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingExceptionOldPhp.csv @@ -0,0 +1,18 @@ +"tx_thuecat_tourist_attraction",,,,,,,, +,"uid","pid","sys_language_uid","remote_id","title",,, +,1,10,0,"https://thuecat.org/resources/165868194223-zmqf","Alte Synagoge",,, +,2,10,1,"https://thuecat.org/resources/165868194223-zmqf","Old Synagogue",,, +,3,10,2,"https://thuecat.org/resources/165868194223-zmqf","La vieille synagogue",,, +"tx_thuecat_import_log",,,,,,,, +,"uid","pid","configuration","log_entries",,,, +,1,0,1,0,,,, +"tx_thuecat_import_log_entry",,,,,,,, +,"uid","pid","type","import_log","record_uid","table_name","insertion","errors" +,1,0,"mappingError",1,0,,0,"[""Could not map incoming JSON-LD to target object: DateTimeImmutable::__construct(): Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character""]" +,2,0,"mappingError",1,0,,0,"[""Could not map incoming JSON-LD to target object: DateTimeImmutable::__construct(): Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character""]" +,3,0,"mappingError",1,0,,0,"[""Could not map incoming JSON-LD to target object: DateTimeImmutable::__construct(): Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character""]" +,4,0,"savingEntity",1,1,"tx_thuecat_organisation",1,"[]" +,5,0,"savingEntity",1,1,"tx_thuecat_town",0,"[]" +,6,0,"savingEntity",1,1,"tx_thuecat_tourist_attraction",1,"[]" +,7,0,"savingEntity",1,2,"tx_thuecat_tourist_attraction",0,"[]" +,8,0,"savingEntity",1,3,"tx_thuecat_tourist_attraction",0,"[]" diff --git a/Tests/Functional/ImportTest.php b/Tests/Functional/ImportTest.php index dd9f239..742be66 100644 --- a/Tests/Functional/ImportTest.php +++ b/Tests/Functional/ImportTest.php @@ -77,6 +77,17 @@ class ImportTest extends TestCase ]; protected $configurationToUseInTestInstance = [ + 'LOG' => [ + 'WerkraumMedia' => [ + 'writerConfiguration' => [ + \TYPO3\CMS\Core\Log\LogLevel::DEBUG => [ + \TYPO3\CMS\Core\Log\Writer\FileWriter::class => [ + 'logFileInfix' => 'debug', + ], + ], + ], + ], + ], 'EXTENSIONS' => [ 'thuecat' => [ 'apiKey' => null, @@ -343,6 +354,32 @@ class ImportTest extends TestCase $this->assertCSVDataSet('EXT:thuecat/Tests/Functional/Fixtures/Import/ImportsContainsPlace.csv'); } + /** + * @test + */ + public function importsFollowingRecordsInCaseOfAnMappingException(): void + { + $this->importDataSet(__DIR__ . '/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingException.xml'); + GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/mapping-exception.json'); + GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/165868194223-zmqf.json'); + GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/018132452787-ngbe.json'); + GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/043064193523-jcyt.json'); + GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/573211638937-gmqb.json'); + GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/497839263245-edbm.json'); + for ($i = 1; $i <= 9; $i++) { + GuzzleClientFaker::appendNotFoundResponse(); + } + + $configuration = $this->get(ImportConfigurationRepository::class)->findByUid(1); + $this->get(Importer::class)->importConfiguration($configuration); + + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $this->assertCSVDataSet('EXT:thuecat/Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingExceptionOldPhp.csv'); + } else { + $this->assertCSVDataSet('EXT:thuecat/Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingException.csv'); + } + } + /** * @test * @testdox Referencing the same thing multiple times only adds it once. diff --git a/Tests/Unit/Domain/Import/Typo3Converter/GeneralConverterTest.php b/Tests/Unit/Domain/Import/Typo3Converter/GeneralConverterTest.php index c109b75..699aec0 100644 --- a/Tests/Unit/Domain/Import/Typo3Converter/GeneralConverterTest.php +++ b/Tests/Unit/Domain/Import/Typo3Converter/GeneralConverterTest.php @@ -23,7 +23,8 @@ declare(strict_types=1); namespace WerkraumMedia\ThueCat\Tests\Unit\Domain\Import\Typo3Converter; -use Psr\Log\LoggerInterface; +use TYPO3\CMS\Core\Log\LogManager; +use TYPO3\CMS\Core\Log\Logger; use WerkraumMedia\ThueCat\Domain\Import\Entity\Properties\ForeignReference; use WerkraumMedia\ThueCat\Domain\Import\Entity\Town; use WerkraumMedia\ThueCat\Domain\Import\Importer; @@ -55,6 +56,7 @@ class GeneralConverterTest extends TestCase $townRepository = $this->createStub(TownRepository::class); $parkingFacilityRepository = $this->createStub(ParkingFacilityRepository::class); $nameExtractor = $this->createStub(NameExtractor::class); + $logManager = $this->createStub(LogManager::class); $subject = new GeneralConverter( $resolveForeignReference, @@ -63,7 +65,8 @@ class GeneralConverterTest extends TestCase $organisationRepository, $townRepository, $parkingFacilityRepository, - $nameExtractor + $nameExtractor, + $logManager ); self::assertInstanceOf( @@ -86,7 +89,8 @@ class GeneralConverterTest extends TestCase $townRepository = $this->createStub(TownRepository::class); $parkingFacilityRepository = $this->createStub(ParkingFacilityRepository::class); $nameExtractor = $this->createStub(NameExtractor::class); - $logger = $this->createStub(LoggerInterface::class); + $logManager = $this->createStub(LogManager::class); + $logManager->method('getLogger')->willReturn($this->createStub(Logger::class)); $subject = new GeneralConverter( $resolveForeignReference, @@ -95,9 +99,9 @@ class GeneralConverterTest extends TestCase $organisationRepository, $townRepository, $parkingFacilityRepository, - $nameExtractor + $nameExtractor, + $logManager ); - $subject->setLogger($logger); $contentResponsible = new ForeignReference(); $contentResponsible->setId('https://example.com/content-responsible'); diff --git a/ext_tables.sql b/ext_tables.sql index 196a66d..32c1f79 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -15,11 +15,13 @@ CREATE TABLE tx_thuecat_import_log ( ); CREATE TABLE tx_thuecat_import_log_entry ( + type varchar(255) DEFAULT 'savingEntity' NOT NULL, import_log int(11) unsigned DEFAULT '0' NOT NULL, + errors text, + remote_id varchar(255) DEFAULT '' NOT NULL, record_uid int(11) unsigned DEFAULT '0' NOT NULL, table_name varchar(255) DEFAULT '' NOT NULL, insertion TINYINT(1) unsigned DEFAULT '0' NOT NULL, - errors text, ); CREATE TABLE tx_thuecat_organisation ( diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 89d4597..723a06f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -326,11 +326,11 @@ parameters: - message: "#^Cannot call method findByUid\\(\\) on mixed\\.$#" - count: 11 + count: 12 path: Tests/Functional/ImportTest.php - message: "#^Cannot call method importConfiguration\\(\\) on mixed\\.$#" - count: 11 + count: 12 path: Tests/Functional/ImportTest.php