diff --git a/Classes/Domain/Import/Entity/InvalidDataException.php b/Classes/Domain/Import/Entity/InvalidDataException.php new file mode 100644 index 0000000..bb7ff8a --- /dev/null +++ b/Classes/Domain/Import/Entity/InvalidDataException.php @@ -0,0 +1,30 @@ + + * + * 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\Import\Entity; + +use Exception; + +final class InvalidDataException extends Exception +{ +} diff --git a/Classes/Domain/Import/Entity/Properties/OpeningHour.php b/Classes/Domain/Import/Entity/Properties/OpeningHour.php index 99b5d60..045fab6 100644 --- a/Classes/Domain/Import/Entity/Properties/OpeningHour.php +++ b/Classes/Domain/Import/Entity/Properties/OpeningHour.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace WerkraumMedia\ThueCat\Domain\Import\Entity\Properties; use DateTimeImmutable; +use WerkraumMedia\ThueCat\Domain\Import\Entity\InvalidDataException; class OpeningHour { @@ -31,9 +32,9 @@ class OpeningHour protected ?DateTimeImmutable $validThrough = null; - protected DateTimeImmutable $opens; + protected ?DateTimeImmutable $opens = null; - protected DateTimeImmutable $closes; + protected ?DateTimeImmutable $closes = null; /** * @var string[] @@ -52,11 +53,19 @@ class OpeningHour public function getOpens(): DateTimeImmutable { + if ($this->opens === null) { + throw new InvalidDataException('Opens was empty for opening hour.'); + } + return $this->opens; } public function getCloses(): DateTimeImmutable { + if ($this->closes === null) { + throw new InvalidDataException('Closes was empty for opening hour.'); + } + return $this->closes; } diff --git a/Classes/Domain/Import/Typo3Converter/GeneralConverter.php b/Classes/Domain/Import/Typo3Converter/GeneralConverter.php index b34efd0..9571588 100644 --- a/Classes/Domain/Import/Typo3Converter/GeneralConverter.php +++ b/Classes/Domain/Import/Typo3Converter/GeneralConverter.php @@ -29,6 +29,7 @@ use TYPO3\CMS\Core\Log\LogManager; use TYPO3\CMS\Extbase\Persistence\QueryResultInterface; use WerkraumMedia\ThueCat\Domain\Import\Entity\AccessibilitySpecification; use WerkraumMedia\ThueCat\Domain\Import\Entity\Base; +use WerkraumMedia\ThueCat\Domain\Import\Entity\InvalidDataException; use WerkraumMedia\ThueCat\Domain\Import\Entity\MapsToType; use WerkraumMedia\ThueCat\Domain\Import\Entity\MediaObject; use WerkraumMedia\ThueCat\Domain\Import\Entity\Minimum; @@ -378,13 +379,21 @@ class GeneralConverter implements Converter $data = []; foreach ($openingHours as $openingHour) { - $data[] = array_filter([ - 'opens' => $openingHour->getOpens()->format('H:i:s'), - 'closes' => $openingHour->getCloses()->format('H:i:s'), - 'from' => $openingHour->getValidFrom() ?? '', - 'through' => $openingHour->getValidThrough() ?? '', - 'daysOfWeek' => $openingHour->getDaysOfWeek(), - ]); + try { + $data[] = array_filter([ + 'opens' => $openingHour->getOpens()->format('H:i:s'), + 'closes' => $openingHour->getCloses()->format('H:i:s'), + 'from' => $openingHour->getValidFrom() ?? '', + 'through' => $openingHour->getValidThrough() ?? '', + 'daysOfWeek' => $openingHour->getDaysOfWeek(), + ]); + } catch (InvalidDataException $e) { + $this->logger->error('Could not import opening hour due to type error: {errorMessage}', [ + 'errorMessage' => $e->getMessage(), + 'openingHour' => var_export($openingHour, true), + ]); + continue; + } } return json_encode($data, JSON_THROW_ON_ERROR) ?: ''; diff --git a/Documentation/Changelog/3.0.1.rst b/Documentation/Changelog/3.0.1.rst index dc6a53c..7876fd0 100644 --- a/Documentation/Changelog/3.0.1.rst +++ b/Documentation/Changelog/3.0.1.rst @@ -17,6 +17,10 @@ Fixes * Add missing dependency to `typo3/cms-install`. As this provides the upgrade wizard feature. +* Handle broken opening hours. + Those are skipped during import and written as error to TYPO3 logs. + That way entries with broken opening hours can still be imported. + Tasks ----- diff --git a/Tests/Functional/AbstractImportTestCase.php b/Tests/Functional/AbstractImportTestCase.php index a5c0b4a..b2615fa 100644 --- a/Tests/Functional/AbstractImportTestCase.php +++ b/Tests/Functional/AbstractImportTestCase.php @@ -108,7 +108,12 @@ abstract class AbstractImportTestCase extends \TYPO3\TestingFramework\Core\Funct { return [ self::getInstancePath() . '/typo3temp/var/log/typo3_0493d91d8e.log', - self::getInstancePath() . '/typo3temp/var/log/typo3_error_0493d91d8e.log', + $this->getErrorLogFile(), ]; } + + protected function getErrorLogFile(): string + { + return self::getInstancePath() . '/typo3temp/var/log/typo3_error_0493d91d8e.log'; + } } diff --git a/Tests/Functional/Fixtures/Import/Guzzle/thuecat.org/resources/attraction-with-broken-opening-hour.json b/Tests/Functional/Fixtures/Import/Guzzle/thuecat.org/resources/attraction-with-broken-opening-hour.json new file mode 100644 index 0000000..a7036aa --- /dev/null +++ b/Tests/Functional/Fixtures/Import/Guzzle/thuecat.org/resources/attraction-with-broken-opening-hour.json @@ -0,0 +1,97 @@ +{ + "@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/attraction-with-broken-opening-hour", + "@type": [ + "schema:Place", + "schema:Thing", + "schema:TouristAttraction", + "ttgds:PointOfInterest" + ], + "schema:name": { + "@language": "de", + "@value": "One specialOpeningHoursSpecification misses opens and closes, leading to type error" + }, + "schema:availableLanguage": [ + { + "@type": "thuecat:Language", + "@value": "thuecat:German" + }, + { + "@type": "thuecat:Language", + "@value": "thuecat:English" + } + ], + "schema:specialOpeningHoursSpecification": [ + { + "@id": "genid-1dccaefc0e06401c93068c8081b7ea8c294892-b11", + "@type": [ + "schema:Thing", + "schema:Intangible", + "schema:StructuredValue", + "schema:OpeningHoursSpecification" + ], + "schema:dayOfWeek": { + "@type": "schema:DayOfWeek", + "@value": "schema:Thursday" + }, + "schema:validThrough": { + "@type": "schema:Date", + "@value": "2024-09-19" + }, + "schema:validFrom": { + "@type": "schema:Date", + "@value": "2024-09-19" + } + }, + { + "@id": "genid-1dccaefc0e06401c93068c8081b7ea8c294892-b12", + "@type": [ + "schema:Thing", + "schema:Intangible", + "schema:StructuredValue", + "schema:OpeningHoursSpecification" + ], + "schema:opens": { + "@type": "schema:Time", + "@value": "09:30:00" + }, + "schema:closes": { + "@type": "schema:Time", + "@value": "18:00:00" + }, + "schema:dayOfWeek": { + "@type": "schema:DayOfWeek", + "@value": "schema:Thursday" + }, + "schema:validThrough": { + "@type": "schema:Date", + "@value": "2024-09-19" + }, + "schema:validFrom": { + "@type": "schema:Date", + "@value": "2024-09-19" + } + } + ], + "thuecat:contentResponsible": { + "@id": "https://thuecat.org/resources/018132452787-ngbe" + } + } + ] +} diff --git a/Tests/Functional/Fixtures/Import/ImportsWithBrokenOpeningHour.php b/Tests/Functional/Fixtures/Import/ImportsWithBrokenOpeningHour.php new file mode 100644 index 0000000..bc2a348 --- /dev/null +++ b/Tests/Functional/Fixtures/Import/ImportsWithBrokenOpeningHour.php @@ -0,0 +1,65 @@ + [ + 0 => [ + 'uid' => '1', + 'pid' => '0', + 'tstamp' => '1613400587', + 'crdate' => '1613400558', + 'doktype' => PageRepository::DOKTYPE_DEFAULT, + 'title' => 'Rootpage', + 'is_siteroot' => '1', + ], + 1 => [ + 'uid' => '10', + 'pid' => '1', + 'tstamp' => '1613400587', + 'crdate' => '1613400558', + 'doktype' => PageRepository::DOKTYPE_SYSFOLDER, + 'title' => 'Storage folder', + ], + ], + 'tx_thuecat_import_configuration' => [ + 0 => [ + 'uid' => '1', + 'pid' => '0', + 'tstamp' => '1613400587', + 'crdate' => '1613400558', + 'disable' => '0', + 'title' => 'Tourist Attraction', + 'type' => 'static', + 'configuration' => ' + + + + + + 10 + + + + + + + + https://thuecat.org/resources/attraction-with-broken-opening-hour + + + + 0 + + + + + + + + ', + ], + ], +]; diff --git a/Tests/Functional/ImportTest.php b/Tests/Functional/ImportTest.php index b5afffb..4b142d4 100644 --- a/Tests/Functional/ImportTest.php +++ b/Tests/Functional/ImportTest.php @@ -359,6 +359,32 @@ class ImportTest extends AbstractImportTestCase $this->assertPHPDataSet(__DIR__ . '/Assertions/Import/ImportsTouristAttractionWithSingleSlogan.php'); } + #[Test] + public function importsWithBrokenOpeningHour(): void + { + $this->importPHPDataSet(__DIR__ . '/Fixtures/Import/ImportsWithBrokenOpeningHour.php'); + + GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/attraction-with-broken-opening-hour.json'); + GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/018132452787-ngbe.json'); + + $this->importConfiguration(); + + $records = $this->getAllRecords('tx_thuecat_tourist_attraction'); + self::assertCount(1, $this->getAllRecords('tx_thuecat_tourist_attraction')); + $specialOpeningHours = json_decode($records[0]['special_opening_hours'], true, 512, JSON_THROW_ON_ERROR); + self::assertIsArray($specialOpeningHours); + self::assertCount(1, $specialOpeningHours); + + $this->expectErrors = true; + $loggedErrors = file_get_contents($this->getErrorLogFile()); + self::assertIsString($loggedErrors); + self::assertStringContainsString( + 'Could not import opening hour due to type error: Opens was empty for opening hour.', + $loggedErrors + ); + self::assertStringContainsString('\'closes\' => NULL,', $loggedErrors); + } + private function importConfiguration(): void { $configuration = $this->get(ImportConfigurationRepository::class)->findByUid(1);