diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..9514205 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,154 @@ +name: Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + FLOW_FOLDER: ../flow-base-distribution + # The neos/metadata 3.x API this package targets is not released yet, so both jobs pull the + # `feature/extensible-metadata-properties` branch and alias it to the version the package requires. + NEOS_METADATA_PACKAGE_BRANCH: 'dev-feature/extensible-metadata-properties' + PACKAGE_FOLDER: metadata-extractor + +jobs: + codestyle: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php-versions: ['8.4'] + + steps: + - uses: actions/checkout@v5 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: exif, mbstring + + - name: Cache dependencies + uses: actions/cache@v5 + with: + path: ~/.composer/cache + key: dependencies-composer-${{ hashFiles('composer.json') }} + + - name: Install dependencies + run: composer require --no-interaction --no-progress "neos/metadata:${NEOS_METADATA_PACKAGE_BRANCH} as 3.0.0" + + - name: PHPStan + run: php vendor/bin/phpstan analyse -c phpstan.ci.neon --no-progress --memory-limit=1G + + php-unit-tests: + env: + FLOW_CONTEXT: Testing + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php-versions: ['8.4'] + flow-versions: ['8.4'] + + steps: + - uses: actions/checkout@v5 + + - name: Set package branch name + run: echo "PACKAGE_TARGET_VERSION=${GITHUB_BASE_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV + working-directory: . + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: exif, mbstring, xml, json, zlib, iconv, intl, pdo_sqlite, mysql + coverage: xdebug #optional + ini-values: opcache.fast_shutdown=0 + + - name: Cache dependencies + uses: actions/cache@v5 + with: + path: ~/.composer/cache + key: dependencies-composer-${{ hashFiles('composer.json') }} + + - name: Prepare Flow distribution + run: | + git clone https://github.com/neos/flow-base-distribution.git -b ${{ matrix.flow-versions }} ${FLOW_FOLDER} + cd ${FLOW_FOLDER} + composer require --no-update --dev --no-interaction phpunit/phpunit:"^11.0" + + git -C ../${{ env.PACKAGE_FOLDER }} checkout -b build + composer config repositories.package '{ "type": "path", "url": "../${{ env.PACKAGE_FOLDER }}", "options": { "symlink": false } }' + composer require --no-update --no-interaction neos/metadata-extractor:"dev-build as dev-${PACKAGE_TARGET_VERSION}" + # required for the resolution of the not yet released neos/metadata 3.x + composer require --no-update --no-interaction neos/metadata:"${NEOS_METADATA_PACKAGE_BRANCH} as 3.0.0" + + - name: Composer Install + run: | + cd ${FLOW_FOLDER} + composer install --no-interaction --no-progress + + - name: Run Unit tests + run: | + cd ${FLOW_FOLDER} + bin/phpunit --colors -c Build/BuildEssentials/PhpUnit/UnitTests.xml Packages/Application/Neos.MetaData.Extractor/Tests/Unit/ + + php-functional-tests: + env: + FLOW_CONTEXT: Testing + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php-versions: ['8.4'] + flow-versions: ['8.4'] + + steps: + - uses: actions/checkout@v5 + + - name: Set package branch name + run: echo "PACKAGE_TARGET_VERSION=${GITHUB_BASE_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV + working-directory: . + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: exif, mbstring, xml, json, zlib, iconv, intl, pdo_sqlite, mysql + coverage: xdebug #optional + ini-values: opcache.fast_shutdown=0 + + - name: Cache dependencies + uses: actions/cache@v5 + with: + path: ~/.composer/cache + key: dependencies-composer-${{ hashFiles('composer.json') }} + + - name: Prepare Flow distribution + run: | + git clone https://github.com/neos/flow-base-distribution.git -b ${{ matrix.flow-versions }} ${FLOW_FOLDER} + cd ${FLOW_FOLDER} + composer require --no-update --dev --no-interaction phpunit/phpunit:"^11.0" + + git -C ../${{ env.PACKAGE_FOLDER }} checkout -b build + composer config repositories.package '{ "type": "path", "url": "../${{ env.PACKAGE_FOLDER }}", "options": { "symlink": false } }' + composer require --no-update --no-interaction neos/metadata-extractor:"dev-build as dev-${PACKAGE_TARGET_VERSION}" + # required for the resolution of the not yet released neos/metadata 3.x + composer require --no-update --no-interaction neos/metadata:"${NEOS_METADATA_PACKAGE_BRANCH} as 3.0.0" + + - name: Composer Install + run: | + cd ${FLOW_FOLDER} + composer install --no-interaction --no-progress + + - name: Run Functional tests + run: | + cd ${FLOW_FOLDER} + bin/phpunit --colors -c Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/Neos.MetaData.Extractor/Tests/Functional/ \ No newline at end of file diff --git a/.styleci.yml b/.styleci.yml deleted file mode 100644 index 844ab2d..0000000 --- a/.styleci.yml +++ /dev/null @@ -1,95 +0,0 @@ -preset: psr2 - -risky: true - -enabled: - - alpha_ordered_imports - - array_indentation - - binary_operator_spaces - - combine_consecutive_issets - - combine_consecutive_unsets - - comment_to_phpdoc - - compact_nullable_typehint - - concat_with_spaces - - fully_qualified_strict_types - - function_typehint_space - - hash_to_slash_comment - - include - - is_null - - linebreak_after_opening_tag - - lowercase_cast - - magic_constant_casing - - method_chaining_indentation - - method_separation - - modernize_types_casting - - multiline_comment_opening_closing - - native_function_casing - - newline_before_semicolons_chained - - no_alias_functions - - no_alternative_syntax - - no_blank_lines_after_class_opening - - no_blank_lines_after_phpdoc - - no_blank_lines_after_return - - no_blank_lines_after_throw - - no_blank_lines_before_namespace - - no_blank_lines_between_imports - - no_empty_comment - - no_empty_phpdoc - - no_empty_statement - - no_extra_block_blank_lines - - no_homoglyph_names - - no_leading_import_slash - - no_leading_namespace_whitespace - - no_multiline_whitespace_around_double_arrow - - no_null_property_initialization - - no_php4_constructor - - no_short_bool_cast - - no_singleline_whitespace_before_semicolons - - no_spaces_inside_offset - - no_spaces_outside_offset - - no_superfluous_elseif - - no_trailing_comma_in_singleline_array - - no_unneeded_curly_braces - - no_unneeded_final_method - - no_unused_imports - - no_useless_else - - no_useless_return - - no_whitespace_before_comma_in_array - - no_whitespace_in_blank_line - - non_printable_character - - normalize_index_brace - - object_operator_without_whitespace - - phpdoc_add_missing_param_annotation - - phpdoc_annotation_without_dot - - phpdoc_indent - - phpdoc_no_access - - phpdoc_no_package - - phpdoc_no_useless_inheritdoc - - phpdoc_return_self_reference - - phpdoc_scalar - - phpdoc_single_line_var_spacing - - phpdoc_trim - - phpdoc_type_to_var - - phpdoc_types - - post_increment - - print_to_echo - - property_separation - - self_accessor - - semicolon_after_instruction - - short_array_syntax - - short_list_syntax - - short_scalar_cast - - simplified_null_return - - single_quote - - space_after_semicolon - - standardize_increment - - standardize_not_equals - - strict_comparison - - ternary_operator_spaces - - ternary_to_null_coalescing - - trailing_comma_in_multiline_array - - trim_array_spaces - - unalign_double_arrow - - unalign_equals - - unary_operator_spaces - - whitespace_after_comma_in_array diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b7f8b3e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: php -matrix: - include: - - php: 7.2 -sudo: false -before_install: - - export NEOS_TARGET_VERSION=5.0 - - cd .. - - git clone https://github.com/neos/neos-base-distribution.git -b ${NEOS_TARGET_VERSION} - - cd neos-base-distribution - - composer require --no-update --no-interaction neos/metadata-extractor -install: - - composer install --no-interaction - - cd .. - - rm -rf neos-base-distribution/Packages/Application/Neos.MetaData.Extractor - - mv metadata-extractor neos-base-distribution/Packages/Application/Neos.MetaData.Extractor - - cd neos-base-distribution -script: - - bin/phpunit --colors -c Build/BuildEssentials/PhpUnit/UnitTests.xml Packages/Application/Neos.MetaData.Extractor/Tests/Unit - - bin/phpunit --colors --stop-on-failure -c Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/Neos.MetaData.Extractor/Tests/Functional diff --git a/Classes/Command/MetaDataCommandController.php b/Classes/Command/MetaDataCommandController.php index 68ea734..bdc4616 100644 --- a/Classes/Command/MetaDataCommandController.php +++ b/Classes/Command/MetaDataCommandController.php @@ -1,4 +1,6 @@ assetRepository->findAllIterator(); $assetCount = $this->assetRepository->countAll(); + if ($assetCount === 0) { + $this->output->outputLine('No assets found.'); + return; + } + $this->output->progressStart($assetCount); + /** @phpstan-ignore-next-line */ foreach ($this->assetRepository->iterate($iterator) as $asset) { /** @var Asset $asset */ - try { - $this->extractionManager->extractMetaData($asset); - } catch (ExtractorException $exception) { - $this->output->outputLine(' ' . $exception->getMessage()); - } + $this->extractionManager->extractMetaData($asset); $this->output->progressAdvance(1); diff --git a/Classes/Converter/CoordinatesConverter.php b/Classes/Converter/CoordinatesConverter.php index 7eb2dc2..cd4359d 100644 --- a/Classes/Converter/CoordinatesConverter.php +++ b/Classes/Converter/CoordinatesConverter.php @@ -1,4 +1,6 @@ + */ +final class ExtractedMetaData implements IteratorAggregate +{ + /** + * @var array + */ + private array $values = []; + + public function set(string $propertyName, string|int|bool $value): void + { + $this->values[$propertyName] = $value; + } + + public function get(string $propertyName): string|int|bool|null + { + return $this->values[$propertyName] ?? null; + } + + public function has(string $propertyName): bool + { + return array_key_exists($propertyName, $this->values); + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->values; + } + + public function getIterator(): Traversable + { + yield from $this->values; + } +} diff --git a/Classes/Domain/ExtractionManager.php b/Classes/Domain/ExtractionManager.php index c10d0b3..eaaae55 100644 --- a/Classes/Domain/ExtractionManager.php +++ b/Classes/Domain/ExtractionManager.php @@ -1,4 +1,6 @@ getTags() as $tagObject) { - /** @var Tag $tagObject */ - $tags[] = $tagObject->getLabel(); - } - - $collections = []; - foreach ($asset->getAssetCollections() as $collectionObject) { - /** @var AssetCollection $collectionObject */ - $collections[] = $collectionObject->getTitle(); - } - - $properties = [ - 'Caption' => $asset->getCaption(), - 'Identifier' => $asset->getIdentifier(), - 'Title' => $asset->getTitle(), - 'FileName' => $asset->getResource()->getFilename(), - 'Collections' => $collections, - 'Tags' => $tags, - 'AssetObject' => $asset, - ]; - if (method_exists($asset, 'getCopyrightNotice')) { - $properties['CopyrightNotice'] = $asset->getCopyrightNotice(); - } - $assetDto = new Dto\Asset($properties); - $metaDataCollection->set('asset', $assetDto); - } - - /** - * @param FlowResource $flowResource * @return string[] Class names + * @throws \ReflectionException + * @throws ClassLoadingForReflectionFailedException + * @throws InvalidClassException */ protected function findSuitableExtractorAdaptersForResource(FlowResource $flowResource) : array { @@ -95,36 +53,33 @@ protected function findSuitableExtractorAdaptersForResource(FlowResource $flowRe ExtractorInterface::class ); - $suitableAdapterClasses = \array_filter( + return \array_filter( $extractorAdapters, - function ($extractorAdapterClass) use ($flowResource) { + static function ($extractorAdapterClass) use ($flowResource) { /** @var ExtractorInterface $extractorAdapterClass */ return $extractorAdapterClass::isSuitableFor($flowResource); } ); - - return $suitableAdapterClasses; } /** - * @param Asset $asset - * @return MetaDataCollection - * @throws ExtractorException - * @throws UnknownObjectException + * Extracts all metadata of the given asset and returns it as a collection of named, scalar values + * (e.g. {@code exif.Model}, {@code iptc.Title}). + * + * The extracted values are additionally persisted for every property that is defined in the meta data + * configuration, see {@see self::persistExtractedValues()}. + * + * Extractors that throw an {@see ExtractorException} are skipped, all others are processed. */ - public function extractMetaData(Asset $asset) : MetaDataCollection + public function extractMetaData(Asset $asset) : ExtractedMetaData { if ($asset instanceof ImageVariant) { $asset = $asset->getOriginalAsset(); } $flowResource = $asset->getResource(); - if ($flowResource === null) { - throw new ExtractorException('Resource of Asset "' . $asset->getTitle() . '"" not found.', 1484060541); - } - $metaDataCollection = new MetaDataCollection(); - $this->buildAssetMetaData($asset, $metaDataCollection); + $metaData = new ExtractedMetaData(); $suitableAdapterClasses = $this->findSuitableExtractorAdaptersForResource($flowResource); foreach ($suitableAdapterClasses as $suitableAdapterClass) { @@ -132,15 +87,38 @@ public function extractMetaData(Asset $asset) : MetaDataCollection /** @noinspection PhpUnhandledExceptionInspection */ $suitableAdapter = $this->objectManager->get($suitableAdapterClass); try { - $suitableAdapter->extractMetaData($flowResource, $metaDataCollection); - } catch (ExtractorException $exception) { - //Extractor is theoretically suitable but failed to extract meta data + $adapterData = $suitableAdapter->extractMetaData($flowResource); + foreach ($adapterData as $propertyName => $propertyValue) { + $metaData->set($propertyName, $propertyValue); + } + } catch (ExtractorException) { + // Extractor is theoretically suitable but failed to extract metadata continue; } } - $this->metaDataManager->updateMetaDataForAsset($asset, $metaDataCollection); + $this->persistExtractedValues($asset, $metaData); + + return $metaData; + } + + /** + * Persists the extracted values for all properties that are defined in the metadata configuration. + * + * Values for properties that are not defined are ignored, so that extraction stays flexible without + * writing arbitrary data. + */ + protected function persistExtractedValues(Asset $asset, ExtractedMetaData $metaData): void + { + $assetReference = MetaDataAssetReference::create($asset->getAssetSourceIdentifier(), $asset->getIdentifier()); + $propertyDefinitions = $this->metaDataManager->getPropertyDefinitions(); - return $metaDataCollection; + foreach ($metaData as $propertyName => $propertyValue) { + $metaDataPropertyName = MetaDataPropertyName::fromString($propertyName); + if (!$propertyDefinitions->include($metaDataPropertyName)) { + continue; + } + $this->metaDataManager->setMetaDataPropertyValue($assetReference, $metaDataPropertyName, $propertyValue); + } } } diff --git a/Classes/Domain/Extractor/AbstractExtractor.php b/Classes/Domain/Extractor/AbstractExtractor.php index cce2cc8..575340b 100644 --- a/Classes/Domain/Extractor/AbstractExtractor.php +++ b/Classes/Domain/Extractor/AbstractExtractor.php @@ -1,4 +1,6 @@ getMediaType(); @@ -37,4 +36,33 @@ public static function isSuitableFor(PersistentResource $resource) : bool return false; } + + /** + * Converts an extracted value into its scalar representation for further use. + * + * Dates are normalized to an ISO date string, arrays to their JSON representation. Floats are kept + * as strings so that their representation (e.g. "240" instead of "240.0") stays stable. + * @throws \JsonException + * @throws \InvalidArgumentException + */ + protected static function convertValueForMetadata(mixed $value): string|int|bool + { + if ($value instanceof \DateTimeInterface) { + return $value->format('Y-m-d H:i:s'); + } + if (\is_array($value)) { + return \json_encode($value, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); + } + if (\is_float($value)) { + return (string)$value; + } + if (\is_string($value) || \is_int($value) || \is_bool($value)) { + return $value; + } + + throw new \InvalidArgumentException( + 'Extracted metadata value of type "' . \get_debug_type($value) . '" cannot be converted to a scalar.', + 1484061567 + ); + } } diff --git a/Classes/Domain/Extractor/ExifExtractor.php b/Classes/Domain/Extractor/ExifExtractor.php index af47cf7..d9a7efb 100644 --- a/Classes/Domain/Extractor/ExifExtractor.php +++ b/Classes/Domain/Extractor/ExifExtractor.php @@ -1,4 +1,6 @@ */ - protected static $deprecatedOrUnmappedProperties = [ + protected static array $deprecatedOrUnmappedProperties = [ 'GPSVersion' => 'GPSVersionID', 'ISOSpeedRatings' => 'PhotographicSensitivity', 'UndefinedTag:0x8830' => 'SensitivityType', @@ -66,7 +67,7 @@ class ExifExtractor extends AbstractExtractor /** * @var string[] */ - protected static $rationalProperties = [ + protected static array $rationalProperties = [ 'Acceleration', 'ApertureValue', 'BrightnessValue', @@ -105,7 +106,7 @@ class ExifExtractor extends AbstractExtractor /** * @var string[] */ - protected static $rationalArrayProperties = [ + protected static array $rationalArrayProperties = [ 'GPSDestLatitude', 'GPSDestLongitude', 'GPSLatitude', @@ -121,7 +122,7 @@ class ExifExtractor extends AbstractExtractor /** * @var string[] */ - protected static $gpsProperties = [ + protected static array $gpsProperties = [ 'GPSDestLatitude', 'GPSDestLongitude', 'GPSLatitude', @@ -129,18 +130,18 @@ class ExifExtractor extends AbstractExtractor ]; /** - * @var string[] + * @var array */ - protected static $subSecondProperties = [ + protected static array $subSecondProperties = [ 'SubSecTime' => 'DateTime', 'SubSecTimeDigitized' => 'DateTimeDigitized', 'SubSecTimeOriginal' => 'DateTimeOriginal', ]; /** - * @var string[] + * @var array */ - protected static $timeOffsetProperties = [ + protected static array $timeOffsetProperties = [ 'OffsetTime' => 'DateTime', 'OffsetTimeDigitized' => 'DateTimeDigitized', 'OffsetTimeOriginal' => 'DateTimeOriginal', @@ -149,7 +150,7 @@ class ExifExtractor extends AbstractExtractor /** * @inheritdoc */ - public function extractMetaData(FlowResource $resource, MetaDataCollection $metaDataCollection) + public function extractMetaData(FlowResource $resource): ExtractedMetaData { try { $exifData = @\exif_read_data($resource->createTemporaryLocalCopy(), 'EXIF'); @@ -230,7 +231,8 @@ public function extractMetaData(FlowResource $resource, MetaDataCollection $meta } foreach (static::$subSecondProperties as $subSecondProperty => $dateTimeProperty) { - if (isset($exifData[$subSecondProperty], $exifData[$dateTimeProperty])) { + if (isset($exifData[$subSecondProperty], $exifData[$dateTimeProperty]) + && $exifData[$dateTimeProperty] instanceof \DateTimeInterface) { $exifData[$dateTimeProperty] = \DateTime::createFromFormat( 'Y-m-d H:i:s.u', $exifData[$dateTimeProperty]->format('Y-m-d H:i:s.') . $exifData[$subSecondProperty] @@ -240,7 +242,8 @@ public function extractMetaData(FlowResource $resource, MetaDataCollection $meta } foreach (static::$timeOffsetProperties as $timeOffsetProperty => $dateTimeProperty) { - if (isset($exifData[$timeOffsetProperty], $exifData[$dateTimeProperty])) { + if (isset($exifData[$timeOffsetProperty], $exifData[$dateTimeProperty]) + && $exifData[$dateTimeProperty] instanceof \DateTimeInterface) { $exifData[$dateTimeProperty] = \DateTime::createFromFormat( 'Y-m-d H:i:s.uP', $exifData[$dateTimeProperty]->format('Y-m-d H:i:s.u') . $exifData[$timeOffsetProperty] @@ -281,6 +284,14 @@ public function extractMetaData(FlowResource $resource, MetaDataCollection $meta } } - $metaDataCollection->set('exif', new Dto\Exif($exifData)); + $metaData = new ExtractedMetaData(); + foreach ($exifData as $property => $value) { + if ($value === null || $value === '' || $value === []) { + continue; + } + $metaData->set('exif.' . $property, self::convertValueForMetadata($value)); + } + + return $metaData; } } diff --git a/Classes/Domain/Extractor/ExtractorInterface.php b/Classes/Domain/Extractor/ExtractorInterface.php index 6f98aa9..788ab17 100644 --- a/Classes/Domain/Extractor/ExtractorInterface.php +++ b/Classes/Domain/Extractor/ExtractorInterface.php @@ -12,22 +12,21 @@ */ use Neos\Flow\ResourceManagement\PersistentResource; -use Neos\MetaData\Domain\Collection\MetaDataCollection; +use Neos\MetaData\Extractor\Domain\Dto\ExtractedMetaData; use Neos\MetaData\Extractor\Exception\ExtractorException; interface ExtractorInterface { - /** - * @param PersistentResource $resource - * @return bool - */ public static function isSuitableFor(PersistentResource $resource) : bool; /** - * @param PersistentResource $resource - * @param MetaDataCollection $metaDataCollection - * @return void + * Extracts the metadata of the given resource as a collection of named, scalar property values. + * + * Property names are prefixed with the extractor type (e.g. {@code exif.Model}, {@code iptc.Title}). + * Values must be scalar (string, integer or boolean): dates are normalized to an ISO date string, + * arrays to their JSON representation. + * * @throws ExtractorException */ - public function extractMetaData(PersistentResource $resource, MetaDataCollection $metaDataCollection); + public function extractMetaData(PersistentResource $resource): ExtractedMetaData; } diff --git a/Classes/Domain/Extractor/IptcIimExtractor.php b/Classes/Domain/Extractor/IptcIimExtractor.php index 400197b..660cfe1 100644 --- a/Classes/Domain/Extractor/IptcIimExtractor.php +++ b/Classes/Domain/Extractor/IptcIimExtractor.php @@ -1,4 +1,6 @@ Iptc\Iim::CITY, 'Contact' => Iptc\Iim::CONTACT, 'CopyrightNotice' => Iptc\Iim::COPYRIGHT_NOTICE, @@ -74,7 +75,7 @@ class IptcIimExtractor extends AbstractExtractor /** * @var string[][] */ - protected static $dateTimeMapping = [ + protected static array $dateTimeMapping = [ 'CreationDate' => [ 'date' => Iptc\Iim::DATE_CREATED, 'time' => Iptc\Iim::TIME_CREATED, @@ -89,7 +90,7 @@ class IptcIimExtractor extends AbstractExtractor /** * @inheritdoc */ - public function extractMetaData(FlowResource $resource, MetaDataCollection $metaDataCollection) + public function extractMetaData(FlowResource $resource): ExtractedMetaData { try { \getimagesize($resource->createTemporaryLocalCopy(), $fileInfo); @@ -113,26 +114,34 @@ public function extractMetaData(FlowResource $resource, MetaDataCollection $meta $iim = new Iptc\Iim($iimData); - $iptcData = []; - - foreach (static::$mapping as $iptcProperty => $iimProperty) { - $iptcData[$iptcProperty] = $iim->getProperty($iimProperty); - } + $iptcData = array_map(static function ($iimProperty) use ($iim) { + return $iim->getProperty($iimProperty); + }, static::$mapping); foreach (static::$dateTimeMapping as $iptcProperty => $iimProperties) { $dateString = $iim->getProperty($iimProperties['date']); - if (!empty($dateString)) { + if (\is_string($dateString) && $dateString !== '') { + $timeString = $iim->getProperty($iimProperties['time']); $iptcData[$iptcProperty] = DateConverter::convertIso8601DateAndTimeString( $dateString, - $iim->getProperty($iimProperties['time']) + \is_string($timeString) ? $timeString : null ); } } //caring for deprecated (supplemental) category /** @var string[] $categories */ - $categories = $iim->getProperty(Iptc\Iim::SUPPLEMENTAL_CATEGORY); - $categories[] = $iim->getProperty(Iptc\Iim::CATEGORY); + $categories = []; + $supplementalCategories = $iim->getProperty(Iptc\Iim::SUPPLEMENTAL_CATEGORY); + if (\is_array($supplementalCategories)) { + $categories = $supplementalCategories; + } else { + $categories[] = $supplementalCategories; + } + $category = $iim->getProperty(Iptc\Iim::CATEGORY); + if (\is_string($category)) { + $categories[] = $category; + } $subjectCodesFromCategories = []; $deprecatedCategories = []; foreach ($categories as $category) { @@ -148,14 +157,24 @@ public function extractMetaData(FlowResource $resource, MetaDataCollection $meta if (!empty($subjectCodesFromCategories)) { if (!isset($iptcData['SubjectCodes'])) { $iptcData['SubjectCodes'] = $subjectCodesFromCategories; - } else { + } elseif (\is_array($iptcData['SubjectCodes'])) { $iptcData['SubjectCodes'] = \array_merge($iptcData['SubjectCodes'], $subjectCodesFromCategories); + } else { + $iptcData['SubjectCodes'] = \array_merge([$iptcData['SubjectCodes']], $subjectCodesFromCategories); } } if (!empty($deprecatedCategories)) { $iptcData['DeprecatedCategories'] = $deprecatedCategories; } - $metaDataCollection->set('iptc', new Dto\Iptc($iptcData)); + $metaData = new ExtractedMetaData(); + foreach ($iptcData as $property => $value) { + if ($value === '' || $value === []) { + continue; + } + $metaData->set('iptc.' . $property, self::convertValueForMetadata($value)); + } + + return $metaData; } } diff --git a/Classes/Package.php b/Classes/Package.php index 57c71cd..f960ccd 100644 --- a/Classes/Package.php +++ b/Classes/Package.php @@ -42,12 +42,7 @@ function (ConfigurationManager $configurationManager) use ($packageKey, $dispatc ); } - /** - * @param SignalSlotDispatcher $dispatcher - * - * @return void - */ - protected static function connectMetaDataExtraction(SignalSlotDispatcher $dispatcher) + protected static function connectMetaDataExtraction(SignalSlotDispatcher $dispatcher): void { $dispatcher->connect(AssetService::class, 'assetCreated', ExtractionManager::class, 'extractMetaData'); $dispatcher->connect(AssetService::class, 'assetUpdated', ExtractionManager::class, 'extractMetaData'); diff --git a/Classes/Specifications/Exif.php b/Classes/Specifications/Exif.php index 27ef2ce..a1cd9c9 100644 --- a/Classes/Specifications/Exif.php +++ b/Classes/Specifications/Exif.php @@ -1,4 +1,6 @@ 'ImageWidth', 0x101 => 'ImageLength', @@ -128,7 +130,7 @@ class Exif /** * @var string[] */ - public static $gpsIfd = [ + public static array $gpsIfd = [ // GPS Attribute Information 0x0 => 'GPSVersionID', 0x1 => 'GPSLatitudeRef', @@ -167,7 +169,7 @@ class Exif /** * @var string[][] */ - public static $valueInterpretationMap = [ + public static array $valueInterpretationMap = [ 'Compression' => [ 1 => 'uncompressed', 6 => 'JPEG compression (thumbnails only)', @@ -455,20 +457,15 @@ class Exif ], ]; - /** - * @param string $property - * @param mixed $value - * @return string|mixed - */ - public static function interpretValue(string $property, $value) + public static function interpretValue(string $property, mixed $value): mixed { switch ($property) { case 'DateTime': case 'DateTimeOriginal': case 'DateTimeDigitized': - return \DateTime::createFromFormat('Y:m:d H:i:s', $value); + return \is_string($value) ? \DateTime::createFromFormat('Y:m:d H:i:s', $value) : $value; case 'YCbCrSubSampling': - if (isset($value[0], $value[1]) && $value[0] === 2) { + if (\is_array($value) && isset($value[0], $value[1]) && $value[0] === 2) { switch ($value[1]) { case 1: return 'YCbCr4:2:2'; @@ -489,14 +486,22 @@ public static function interpretValue(string $property, $value) 5 => 'G', 6 => 'B', ]; - foreach (\unpack('C*', $value) as $singleValue) { - if (isset($componentsConfigurationInterpretations[$singleValue])) { - $interpretedValue .= $componentsConfigurationInterpretations[$singleValue]; + if (\is_string($value)) { + $unpackedValues = \unpack('C*', $value); + if ($unpackedValues !== false) { + foreach ($unpackedValues as $singleValue) { + if (isset($componentsConfigurationInterpretations[$singleValue])) { + $interpretedValue .= $componentsConfigurationInterpretations[$singleValue]; + } + } } } return $interpretedValue; case 'Flash': + if (!\is_int($value)) { + return $value; + } $interpretedValue = ''; $flashFired = $value & 0b1; $flashReturn = ($value >> 1) & 0b11; diff --git a/Classes/Specifications/Iptc/Iim.php b/Classes/Specifications/Iptc/Iim.php index 8e472c5..9604bec 100644 --- a/Classes/Specifications/Iptc/Iim.php +++ b/Classes/Specifications/Iptc/Iim.php @@ -1,4 +1,6 @@ > */ - protected $properties; + protected array $properties; /** - * IptcIim constructor. - * - * @param mixed[] $properties + * @param array> $properties */ public function __construct(array $properties) { @@ -1257,7 +1260,7 @@ public function __construct(array $properties) if (!\array_key_exists(self::CODED_CHARACTER_SET, $properties)) { \array_walk_recursive( $properties, - function (&$element) { + static function (&$element) { if (\is_string($element) && \mb_detect_encoding($element, 'UTF-8', true) === false) { $element = \mb_convert_encoding($element, 'UTF-8', 'ISO-8859-1'); } @@ -1267,11 +1270,7 @@ function (&$element) { $this->properties = $properties; } - /** - * @param string $category - * @return bool|string - */ - public static function convertCategoryToSubjectCode(string $category) + public static function convertCategoryToSubjectCode(string $category): string|bool { $mapping = [ 'ACE' => '01000000', @@ -1297,10 +1296,9 @@ public static function convertCategoryToSubjectCode(string $category) } /** - * @param string $property - * @return string|string[] + * @return string|list */ - public function getProperty(string $property) + public function getProperty(string $property): string|array { if (\array_key_exists($property, $this->properties)) { return \in_array($property, self::$repeatable, false) diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index b076c3e..fd71295 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -3,3 +3,118 @@ Neos: Extractor: realtimeExtraction: enabled: true + + metaDataProperties: + # Properties filled from the EXIF / IPTC data of an asset. + # Values are stored as strings; array-valued tags are stored as their JSON representation, dates as + # an ISO date string ('Y-m-d H:i:s'). + 'exif.ApertureValue': &propertyConfig + type: string + globalScope: true + ui: + label: i18n + inspector: + editorOptions: + readonly: true + 'exif.Artist': *propertyConfig + 'exif.BodySerialNumber': *propertyConfig + 'exif.CameraOwnerName': *propertyConfig + 'exif.ColorSpace': *propertyConfig + 'exif.ComponentsConfiguration': *propertyConfig + 'exif.Compression': *propertyConfig + 'exif.Copyright': *propertyConfig + 'exif.CustomRendered': *propertyConfig + 'exif.DateTime': *propertyConfig + 'exif.DateTimeDigitized': *propertyConfig + 'exif.DateTimeOriginal': *propertyConfig + 'exif.DigitalZoomRatio': *propertyConfig + 'exif.ExifVersion': *propertyConfig + 'exif.ExposureBiasValue': *propertyConfig + 'exif.ExposureIndex': *propertyConfig + 'exif.ExposureMode': *propertyConfig + 'exif.ExposureProgram': *propertyConfig + 'exif.ExposureTime': *propertyConfig + 'exif.FNumber': *propertyConfig + 'exif.Flash': *propertyConfig + 'exif.FlashpixVersion': *propertyConfig + 'exif.FocalLength': *propertyConfig + 'exif.FocalLengthIn35mmFilm': *propertyConfig + 'exif.FocalPlaneResolutionUnit': *propertyConfig + 'exif.FocalPlaneXResolution': *propertyConfig + 'exif.FocalPlaneYResolution': *propertyConfig + 'exif.GainControl': *propertyConfig + 'exif.Gamma': *propertyConfig + 'exif.GPSAltitude': *propertyConfig + 'exif.GPSDestBearing': *propertyConfig + 'exif.GPSDestDistance': *propertyConfig + 'exif.GPSDestLatitude': *propertyConfig + 'exif.GPSDestLongitude': *propertyConfig + 'exif.GPSDOP': *propertyConfig + 'exif.GPSHPositioningError': *propertyConfig + 'exif.GPSImgDirection': *propertyConfig + 'exif.GPSLatitude': *propertyConfig + 'exif.GPSLongitude': *propertyConfig + 'exif.GPSSpeed': *propertyConfig + 'exif.GPSTrack': *propertyConfig + 'exif.GPSVersionID': *propertyConfig + 'exif.Humidity': *propertyConfig + 'exif.ImageDescription': *propertyConfig + 'exif.ImageUniqueID': *propertyConfig + 'exif.LensMake': *propertyConfig + 'exif.LensModel': *propertyConfig + 'exif.LensSerialNumber': *propertyConfig + 'exif.LensSpecification': *propertyConfig + 'exif.LightSource': *propertyConfig + 'exif.Make': *propertyConfig + 'exif.MaxApertureValue': *propertyConfig + 'exif.MeteringMode': *propertyConfig + 'exif.Model': *propertyConfig + 'exif.OffsetTime': *propertyConfig + 'exif.OffsetTimeDigitized': *propertyConfig + 'exif.OffsetTimeOriginal': *propertyConfig + 'exif.Orientation': *propertyConfig + 'exif.PhotometricInterpretation': *propertyConfig + 'exif.PhotographicSensitivity': *propertyConfig + 'exif.PixelXDimension': *propertyConfig + 'exif.PixelYDimension': *propertyConfig + 'exif.Pressure': *propertyConfig + 'exif.RecommendedExposureIndex': *propertyConfig + 'exif.ResolutionUnit': *propertyConfig + 'exif.SceneCaptureType': *propertyConfig + 'exif.SensingMethod': *propertyConfig + 'exif.SensitivityType': *propertyConfig + 'exif.ShutterSpeedValue': *propertyConfig + 'exif.Software': *propertyConfig + 'exif.StandardOutputSensitivity': *propertyConfig + 'exif.SubjectDistance': *propertyConfig + 'exif.Temperature': *propertyConfig + 'exif.UserComment': *propertyConfig + 'exif.WaterDepth': *propertyConfig + 'exif.WhiteBalance': *propertyConfig + 'exif.XResolution': *propertyConfig + 'exif.YResolution': *propertyConfig + 'exif.Acceleration': *propertyConfig + 'exif.CameraElevationAngle': *propertyConfig + 'iptc.City': *propertyConfig + 'iptc.Contact': *propertyConfig + 'iptc.CopyrightNotice': *propertyConfig + 'iptc.Country': *propertyConfig + 'iptc.CountryCode': *propertyConfig + 'iptc.CreationDate': *propertyConfig + 'iptc.Creator': *propertyConfig + 'iptc.CreatorTitle': *propertyConfig + 'iptc.CreditLine': *propertyConfig + 'iptc.DeprecatedCategories': *propertyConfig + 'iptc.Description': *propertyConfig + 'iptc.DescriptionWriter': *propertyConfig + 'iptc.DigitalCreationDate': *propertyConfig + 'iptc.Headline': *propertyConfig + 'iptc.Instructions': *propertyConfig + 'iptc.IntellectualGenres': *propertyConfig + 'iptc.JobId': *propertyConfig + 'iptc.Keywords': *propertyConfig + 'iptc.Source': *propertyConfig + 'iptc.State': *propertyConfig + 'iptc.SubjectCodes': *propertyConfig + 'iptc.Sublocation': *propertyConfig + 'iptc.Title': *propertyConfig diff --git a/Readme.md b/Readme.md index 24e8875..d44599a 100644 --- a/Readme.md +++ b/Readme.md @@ -6,7 +6,7 @@ # Neos.MetaData.Extractor Package -This package handles extraction of meta data from assets. +This package handles extraction of metadata from assets. ## Installation @@ -16,10 +16,10 @@ Install using composer: Some related packages are: -- [`neos/metadata`](https://github.com/neos/metadata): Provides provides data types and interfaces +- [`neos/metadata`](https://github.com/neos/metadata): Provides data types and interfaces (and is automatically installed with this package) - [`neos/metadata-contentrepositoryadapter`](https://github.com/neos/metadata-contentrepositoryadapter): - Handles the mapping of meta data DTOs to the Neos Content Repository + Handles the mapping of metadata DTOs to the Neos Content Repository ### Requirements @@ -28,7 +28,7 @@ is available in PHP by default.) ## Configuration -This package provides realtime meta data extraction. This can be switched off, if needed, using: +This package provides realtime metadata extraction. This can be switched off, if needed, using: ```yaml Neos: @@ -40,7 +40,7 @@ Neos: ## Usage -The package extracts IPTC and EXIF meta data from assets using a CLI command: +The package extracts IPTC and EXIF metadata from assets using a CLI command: ./flow metadata:extract diff --git a/Resources/Private/Translations/de/Main.xlf b/Resources/Private/Translations/de/Main.xlf new file mode 100644 index 0000000..a5e3a2e --- /dev/null +++ b/Resources/Private/Translations/de/Main.xlf @@ -0,0 +1,316 @@ + + + + + + Beschleunigung + + + Blendenwert + + + Künstler + + + Seriennummer des Gehäuses + + + Kameraneigungswinkel + + + Name des Kamerabesitzers + + + Farbraum + + + Komponentenkonfiguration + + + Kompression + + + Copyright + + + Individuell bearbeitet + + + Datum/Uhrzeit + + + Digitalisierungsdatum + + + Aufnahmedatum + + + Digitaler Zoomfaktor + + + Exif-Version + + + Belichtungskorrektur + + + Belichtungsindex + + + Belichtungsmodus + + + Belichtungsprogramm + + + Belichtungszeit + + + Blendenzahl + + + Blitz + + + FlashPix-Version + + + Brennweite + + + Brennweite (35mm-Äquivalent) + + + Auflösungseinheit der Fokusebene + + + Horizontale Auflösung der Fokusebene + + + Vertikale Auflösung der Fokusebene + + + Verstärkungsregelung + + + Gammawert + + + GPS-Höhe + + + GPS-Peilung des Ziels + + + GPS-Entfernung zum Ziel + + + GPS-Breitengrad des Ziels + + + GPS-Längengrad des Ziels + + + GPS-Präzision + + + GPS-Horizontalpositionsfehler + + + GPS-Bildrichtung + + + GPS-Breitengrad + + + GPS-Längengrad + + + GPS-Geschwindigkeit + + + GPS-Kurs + + + GPS-Versions-ID + + + Luftfeuchtigkeit + + + Bildbeschreibung + + + Eindeutige Bild-ID + + + Objektivhersteller + + + Objektivmodell + + + Objektiv-Seriennummer + + + Objektivspezifikation + + + Lichtquelle + + + Kamerahersteller + + + Maximale Blendenzahl + + + Messmethode + + + Kameramodell + + + Zeitzonenversatz + + + Zeitzonenversatz (digitalisiert) + + + Zeitzonenversatz (Original) + + + Ausrichtung + + + Photometrische Interpretation + + + ISO-Empfindlichkeit + + + Bildbreite + + + Bildhöhe + + + Luftdruck + + + Empfohlener Belichtungsindex + + + Auflösungseinheit + + + Aufnahmeszene + + + Sensortyp + + + Empfindlichkeitstyp + + + Verschlusszeit + + + Software + + + Standard-Ausgabeempfindlichkeit + + + Objektabstand + + + Temperatur + + + Benutzerkommentar + + + Wassertiefe + + + Weißabgleich + + + Horizontale Auflösung + + + Vertikale Auflösung + + + Stadt + + + Kontakt + + + Urheberrechtshinweis + + + Land + + + Ländercode + + + Erstellungsdatum + + + Autor + + + Berufsbezeichnung des Autors + + + Quellenangabe + + + Kategorien + + + Beschreibung + + + Verfasser der Beschreibung + + + Digitales Erstellungsdatum + + + Überschrift + + + Anweisungen + + + Intellektuelle Genres + + + Auftrags-ID + + + Schlagwörter + + + Quelle + + + Bundesland + + + Themenschlüssel + + + Ort + + + Titel + + + + diff --git a/Resources/Private/Translations/en/Main.xlf b/Resources/Private/Translations/en/Main.xlf new file mode 100644 index 0000000..3b9364f --- /dev/null +++ b/Resources/Private/Translations/en/Main.xlf @@ -0,0 +1,316 @@ + + + + + + Acceleration + + + Aperture Value + + + Artist + + + Body Serial Number + + + Camera Elevation Angle + + + Camera Owner Name + + + Color Space + + + Components Configuration + + + Compression + + + Copyright + + + Custom Rendered + + + Date Time + + + Date Time Digitized + + + Date Time Original + + + Digital Zoom Ratio + + + Exif Version + + + Exposure Bias Value + + + Exposure Index + + + Exposure Mode + + + Exposure Program + + + Exposure Time + + + F-Number + + + Flash + + + FlashPix Version + + + Focal Length + + + Focal Length in 35mm Film + + + Focal Plane Resolution Unit + + + Focal Plane X Resolution + + + Focal Plane Y Resolution + + + Gain Control + + + Gamma + + + GPS Altitude + + + GPS Destination Bearing + + + GPS Destination Distance + + + GPS Destination Latitude + + + GPS Destination Longitude + + + GPS Degree of Precision + + + GPS Horizontal Positioning Error + + + GPS Image Direction + + + GPS Latitude + + + GPS Longitude + + + GPS Speed + + + GPS Track + + + GPS Version ID + + + Humidity + + + Image Description + + + Image Unique ID + + + Lens Manufacturer + + + Lens Model + + + Lens Serial Number + + + Lens Specification + + + Light Source + + + Camera Manufacturer + + + Maximum Aperture Value + + + Metering Mode + + + Camera Model + + + Offset Time + + + Offset Time Digitized + + + Offset Time Original + + + Orientation + + + Photometric Interpretation + + + ISO Speed + + + Image Width + + + Image Height + + + Pressure + + + Recommended Exposure Index + + + Resolution Unit + + + Scene Capture Type + + + Sensing Method + + + Sensitivity Type + + + Shutter Speed Value + + + Software + + + Standard Output Sensitivity + + + Subject Distance + + + Temperature + + + User Comment + + + Water Depth + + + White Balance + + + X Resolution + + + Y Resolution + + + City + + + Contact + + + Copyright Notice + + + Country + + + Country Code + + + Creation Date + + + Creator + + + Creator Title + + + Credit Line + + + Categories + + + Description + + + Description Writer + + + Digital Creation Date + + + Headline + + + Instructions + + + Intellectual Genres + + + Job ID + + + Keywords + + + Source + + + State + + + Subject Codes + + + Sublocation + + + Title + + + + diff --git a/Tests/Functional/AbstractExtractorTest.php b/Tests/Functional/AbstractExtractorTest.php deleted file mode 100644 index 60c03d4..0000000 --- a/Tests/Functional/AbstractExtractorTest.php +++ /dev/null @@ -1,73 +0,0 @@ -resourceManager = $this->objectManager->get(ResourceManager::class); - $this->testAsset = $this->buildTestAsset(); - } - - /** - * @return Asset - */ - protected function buildTestAsset() : Asset - { - $testImagePath = Files::concatenatePaths([__DIR__, 'Fixtures/Resources/Lighthouse.jpg']); - $this->assertFileExists($testImagePath); - - $resource = $this->resourceManager->importResource($testImagePath); - - return new Asset($resource); - } - - /** - * @param AbstractMetaDataDto $dto - * @param mixed[] $expectedDtoData - * @return void - */ - protected function assertDtoGettersReturnData(AbstractMetaDataDto $dto, array $expectedDtoData) - { - foreach ($expectedDtoData as $key => $value) { - $getter = 'get' . $key; - $this->assertEquals($value, $dto->$getter(), \sprintf('Value of %s does not match expected.', $key)); - } - } -} diff --git a/Tests/Functional/AbstractExtractorTestCase.php b/Tests/Functional/AbstractExtractorTestCase.php new file mode 100644 index 0000000..643dccd --- /dev/null +++ b/Tests/Functional/AbstractExtractorTestCase.php @@ -0,0 +1,146 @@ +createMetaDataValueTable(); + + /** @var ResourceManager $resourceManager */ + $resourceManager = $this->objectManager->get(ResourceManager::class); + $this->resourceManager = $resourceManager; + $this->testAsset = $this->buildTestAsset(); + // the metadata storage references assets by their identifier, so the asset must be persisted first + $this->persistenceManager->persistAll(); + + /** @var MetaDataManager $metaDataManager */ + $metaDataManager = $this->objectManager->get(MetaDataManager::class); + $this->metaDataManager = $metaDataManager; + } + + /** + * Creates the table the MetaDataManager writes to. + * + * The table is not mapped as an entity, so the functional test schema, which is derived from entity + * metadata only, does not contain it. As with the MetaData storage adapter tests, the foreign key of + * the Doctrine migration is omitted on purpose - the migration cannot be run here. + * + * TODO: This should be solved in a better way in the metadata package so we can either mock the metadatamanager or can rely on the tables to exist + * + * @see \Neos\Flow\Persistence\Doctrine\Migrations\Version20260415145934 + */ + protected function createMetaDataValueTable(): void + { + /** @var EntityManagerInterface $entityManager */ + $entityManager = $this->objectManager->get(EntityManagerInterface::class); + $connection = $entityManager->getConnection(); + if (!$connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { + $this->markTestSkipped('The metadata storage adapter requires MySQL or MariaDB'); + } + $connection->executeStatement('CREATE TABLE IF NOT EXISTS neos_metadata_value ( + `asset_source_id` VARCHAR(255) DEFAULT NULL, + `asset_id` VARCHAR(40) DEFAULT NULL, + `property_name` VARCHAR(40) NOT NULL, + `property_value` TEXT NOT NULL, + `dimension_hash` VARCHAR(250) NOT NULL, + UNIQUE INDEX idx_unique (`asset_source_id`, `asset_id`, `property_name`, `dimension_hash`) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + } + + protected function buildTestAsset(): Asset + { + $testImagePath = Files::concatenatePaths([__DIR__, 'Fixtures/Resources/Lighthouse.jpg']); + $this->assertFileExists($testImagePath); + + $resource = $this->resourceManager->importResource($testImagePath); + + return new Asset($resource); + } + + /** + * Asserts that the extracted data contains the given property values. + * + * @param array $expectedExtractedData + */ + protected function assertExtractedData(array $expectedExtractedData, ExtractedMetaData $extractedData): void + { + foreach ($expectedExtractedData as $propertyName => $expectedValue) { + $this->assertTrue( + $extractedData->has($propertyName), + \sprintf('Extracted value for %s is missing.', $propertyName) + ); + $this->assertEquals( + $expectedValue, + $extractedData->get($propertyName), + \sprintf('Value of %s does not match expected.', $propertyName) + ); + } + } + + /** + * The stored value of the given property of the test asset, as it is resolved by the MetaDataManager. + */ + protected function getStoredMetaDataPropertyValue(string $propertyName): string|int|bool|null + { + return $this->metaDataManager->getMetaDataPropertyValue( + $this->testAssetMetaDataReference(), + MetaDataPropertyName::fromString($propertyName) + )->value; + } + + protected function testAssetMetaDataReference(): MetaDataAssetReference + { + return MetaDataAssetReference::create( + $this->testAsset->getAssetSourceIdentifier(), + $this->testAsset->getIdentifier() + ); + } +} diff --git a/Tests/Functional/Domain/Extractor/ExifExtractorTest.php b/Tests/Functional/Domain/Extractor/ExifExtractorTest.php index 5f2a2db..3ad9125 100644 --- a/Tests/Functional/Domain/Extractor/ExifExtractorTest.php +++ b/Tests/Functional/Domain/Extractor/ExifExtractorTest.php @@ -11,12 +11,10 @@ * source code. */ -use Neos\MetaData\Domain\Collection\MetaDataCollection; -use Neos\MetaData\Domain\Dto; use Neos\MetaData\Extractor\Domain\Extractor\ExifExtractor; -use Neos\MetaData\Extractor\Tests\Functional\AbstractExtractorTest; +use Neos\MetaData\Extractor\Tests\Functional\AbstractExtractorTestCase; -class ExifExtractorTest extends AbstractExtractorTest +class ExifExtractorTest extends AbstractExtractorTestCase { /** * @var ExifExtractor @@ -26,7 +24,7 @@ class ExifExtractorTest extends AbstractExtractorTest /** * @inheritDoc */ - public function setUp() + public function setUp(): void { parent::setUp(); @@ -36,149 +34,56 @@ public function setUp() /** * @test */ - public function extractMetaData() + public function extractMetaData(): void { - $metaDataCollection = new MetaDataCollection(); - - $this->exifExtractor->extractMetaData($this->testAsset->getResource(), $metaDataCollection); - - $exifDto = $metaDataCollection->get('exif'); - - $this->assertInstanceOf(Dto\Exif::class, $exifDto); + $extractedData = $this->exifExtractor->extractMetaData($this->testAsset->getResource()); $expectedExifData = [ - 'ApertureValue' => 6.0, - 'Artist' => 'Daniel Lienert', - 'BodySerialNumber' => '1330801819', - 'CameraOwnerName' => 'Daniel Lienert', - 'ColorSpace' => 'sRGB', - 'Copyright' => '© Daniel Lienert', - 'CustomRendered' => 'Normal process', - 'DateTime' => \DateTime::createFromFormat('YmdHis', '20160407070947'), - 'DateTimeDigitized' => \DateTime::createFromFormat('YmdHisu', '20130917235911260000'), - 'DateTimeOriginal' => \DateTime::createFromFormat('YmdHis', '20130918105911'), - 'ExifVersion' => '0230', - 'ExposureBiasValue' => 0.0, - 'ExposureMode' => 'Manual exposure', - 'ExposureProgram' => 'Manual', - 'ExposureTime' => 0.0015625, - 'Flash' => 'Flash did not fire. No strobe return detection function. Compulsory flash suppression.' + 'exif.ApertureValue' => 6.0, + 'exif.Artist' => 'Daniel Lienert', + 'exif.BodySerialNumber' => '1330801819', + 'exif.CameraOwnerName' => 'Daniel Lienert', + 'exif.ColorSpace' => 'sRGB', + 'exif.Copyright' => '© Daniel Lienert', + 'exif.CustomRendered' => 'Normal process', + 'exif.DateTime' => '2016-04-07 07:09:47', + 'exif.DateTimeDigitized' => '2013-09-17 23:59:11', + 'exif.DateTimeOriginal' => '2013-09-18 10:59:11', + 'exif.ExifVersion' => '0230', + 'exif.ExposureBiasValue' => 0.0, + 'exif.ExposureMode' => 'Manual exposure', + 'exif.ExposureProgram' => 'Manual', + 'exif.ExposureTime' => 0.0015625, + 'exif.Flash' => 'Flash did not fire. No strobe return detection function. Compulsory flash suppression.' . ' Flash function present. No red-eye reduction mode or unknown.', - 'FNumber' => 8.0, - 'FocalLength' => 24, - 'FocalPlaneResolutionUnit' => 'inches', - 'FocalPlaneXResolution' => 3849.2117888965, - 'FocalPlaneYResolution' => 3908.1419624217, - 'GPSAltitude' => 17.8953, - 'GPSImgDirection' => 180.0, - 'GPSLatitude' => -46.659787, - 'GPSLongitude' => 168.84703, - 'GPSVersionID' => '2.2.0.0', - 'ImageDescription' => 'Waipapa Point Lighthouse with the sea in the background and bush in the foreground.', - 'LensModel' => 'EF24-105mm f/4L IS USM', - 'LensSpecification' => [24.0, 105.0, 0.0, 0.0], - 'Make' => 'Canon', - 'MaxApertureValue' => 4.0, - 'MeteringMode' => 'Pattern', - 'Model' => 'Canon EOS 5D Mark II', - 'PhotographicSensitivity' => 100, - 'ResolutionUnit' => 'inches', - 'SceneCaptureType' => 'Standard', - 'ShutterSpeedValue' => 9.321928, - 'Software' => 'Adobe Photoshop Lightroom 6.3 (Macintosh)', - 'UserComment' => 'Great weather', - 'WhiteBalance' => 'Auto white balance', - 'XResolution' => 240, - 'YResolution' => 240, - // no data present for the following - 'ImageWidth' => 0, - 'ImageLength' => 0, - 'BitsPerSample' => [0, 0, 0], - 'Compression' => '', - 'PhotometricInterpretation' => '', - 'Orientation' => '', - 'SamplesPerPixel' => 0, - 'PlanarConfiguration' => '', - 'YCbCrSubSampling' => '', - 'YCbCrPositioning' => '', - 'StripOffsets' => [], - 'RowsPerStrip' => 0, - 'StripByteCounts' => [], - 'JPEGInterchangeFormat' => 0, - 'JPEGInterchangeFormatLength' => 0, - 'TransferFunction' => [], - 'WhitePoint' => [], - 'PrimaryChromaticities' => [], - 'YCbCrCoefficients' => [], - 'ReferenceBlackWhite' => [], - 'FlashpixVersion' => '', - 'Gamma' => 0.0, - 'ComponentsConfiguration' => '', - 'CompressedBitsPerPixel' => 0.0, - 'PixelXDimension' => 0, - 'PixelYDimension' => 0, - 'MakerNote' => '', - 'RelatedSoundFile' => '', - 'SpectralSensitivity' => '', - 'OECF' => [], - 'SensitivityType' => '', - 'StandardOutputSensitivity' => 0, - 'RecommendedExposureIndex' => 0, - 'ISOSpeed' => 0, - 'ISOSpeedLatitudeyyy' => 0, - 'ISOSpeedLatitudezzz' => 0, - 'BrightnessValue' => 0.0, - 'SubjectDistance' => 0, - 'LightSource' => '', - 'SubjectArea' => [0, 0], - 'FlashEnergy' => 0.0, - 'SpatialFrequencyResponse' => [], - 'SubjectLocation' => [0, 0], - 'ExposureIndex' => 0.0, - 'SensingMethod' => '', - 'FileSource' => '', - 'SceneType' => '', - 'CFAPattern' => [], - 'DigitalZoomRatio' => 0.0, - 'FocalLengthIn35mmFilm' => 0, - 'GainControl' => '', - 'Contrast' => '', - 'Saturation' => '', - 'Sharpness' => '', - 'DeviceSettingDescription' => [], - 'SubjectDistanceRange' => '', - 'Temperature' => 0.0, - 'Humidity' => 0.0, - 'Pressure' => 0.0, - 'WaterDepth' => 0.0, - 'Acceleration' => 0.0, - 'CameraElevationAngle' => 0.0, - 'ImageUniqueID' => '', - 'LensMake' => '', - 'LensSerialNumber' => '', - 'GPSSatellites' => '', - 'GPSStatus' => '', - 'GPSMeasureMode' => '', - 'GPSDOP' => 0.0, - 'GPSSpeedRef' => '', - 'GPSSpeed' => 0.0, - 'GPSTrackRef' => '', - 'GPSTrack' => 0.0, - 'GPSImgDirectionRef' => '', - 'GPSMapDatum' => '', - 'GPSDestLatitude' => 0.0, - 'GPSDestLongitude' => 0.0, - 'GPSDestBearingRef' => '', - 'GPSDestBearing' => 0.0, - 'GPSDestDistanceRef' => '', - 'GPSDestDistance' => 0.0, - 'GPSProcessingMethod' => '', - 'GPSAreaInformation' => '', - 'GPSDateTimeStamp' => null, - 'GPSDifferential' => '', - 'GPSHPositioningError' => 0.0, + 'exif.FNumber' => 8.0, + 'exif.FocalLength' => 24, + 'exif.FocalPlaneResolutionUnit' => 'inches', + 'exif.FocalPlaneXResolution' => 3849.2117888965, + 'exif.FocalPlaneYResolution' => 3908.1419624217, + 'exif.GPSAltitude' => 17.8953, + 'exif.GPSImgDirection' => 180.0, + 'exif.GPSLatitude' => -46.659787, + 'exif.GPSLongitude' => 168.84703, + 'exif.GPSVersionID' => '2.2.0.0', + 'exif.ImageDescription' => 'Waipapa Point Lighthouse with the sea in the background and bush in the foreground.', + 'exif.LensModel' => 'EF24-105mm f/4L IS USM', + 'exif.LensSpecification' => '[24,105,0,0]', + 'exif.Make' => 'Canon', + 'exif.MaxApertureValue' => 4.0, + 'exif.MeteringMode' => 'Pattern', + 'exif.Model' => 'Canon EOS 5D Mark II', + 'exif.PhotographicSensitivity' => 100, + 'exif.ResolutionUnit' => 'inches', + 'exif.SceneCaptureType' => 'Standard', + 'exif.ShutterSpeedValue' => 9.321928, + 'exif.Software' => 'Adobe Photoshop Lightroom 6.3 (Macintosh)', + 'exif.UserComment' => 'Great weather', + 'exif.WhiteBalance' => 'Auto white balance', + 'exif.XResolution' => 240, + 'exif.YResolution' => 240, ]; - $this->assertDtoGettersReturnData($exifDto, $expectedExifData); + $this->assertExtractedData($expectedExifData, $extractedData); } } diff --git a/Tests/Functional/Domain/Extractor/ExtractionManagerTest.php b/Tests/Functional/Domain/Extractor/ExtractionManagerTest.php index ee9e7d7..99e1617 100644 --- a/Tests/Functional/Domain/Extractor/ExtractionManagerTest.php +++ b/Tests/Functional/Domain/Extractor/ExtractionManagerTest.php @@ -11,10 +11,11 @@ * source code. */ +use Neos\MetaData\Domain\Dto\MetaDataPropertyName; use Neos\MetaData\Extractor\Domain\ExtractionManager; -use Neos\MetaData\Extractor\Tests\Functional\AbstractExtractorTest; +use Neos\MetaData\Extractor\Tests\Functional\AbstractExtractorTestCase; -class ExtractionManagerTest extends AbstractExtractorTest +class ExtractionManagerTest extends AbstractExtractorTestCase { /** * @var ExtractionManager @@ -24,18 +25,47 @@ class ExtractionManagerTest extends AbstractExtractorTest /** * @inheritDoc */ - public function setUp() + public function setUp(): void { parent::setUp(); - $this->extractionManager = $this->objectManager->get(ExtractionManager::class); + /** @var ExtractionManager $extractionManager */ + $extractionManager = $this->objectManager->get(ExtractionManager::class); + $this->extractionManager = $extractionManager; } /** * @test */ - public function instanceCreated() + public function instanceCreated(): void { $this->assertInstanceOf(ExtractionManager::class, $this->extractionManager); } + + /** + * @test + */ + public function extractMetaData(): void + { + $extractedData = $this->extractionManager->extractMetaData($this->testAsset); + + $this->assertSame('Canon EOS 5D Mark II', $extractedData->get('exif.Model')); + $this->assertSame('Otara', $extractedData->get('iptc.City')); + + // the values of the defined properties are persisted and can be read back via the MetaDataManager + $this->assertSame('Canon EOS 5D Mark II', $this->getStoredMetaDataPropertyValue('exif.Model')); + $this->assertSame('Daniel Lienert', $this->getStoredMetaDataPropertyValue('exif.Artist')); + $this->assertSame('© Daniel Lienert', $this->getStoredMetaDataPropertyValue('iptc.CopyrightNotice')); + $this->assertSame( + '["Beste","Leuchtturm","Neu Seeland","Neuseeland","New Zealand"]', + $this->getStoredMetaDataPropertyValue('iptc.Keywords') + ); + + // only properties that are defined in the meta data configuration are persisted + $this->assertTrue($extractedData->has('exif.MimeType')); + $this->assertSame('image/jpeg', $extractedData->get('exif.MimeType')); + $this->assertFalse( + $this->metaDataManager->getPropertyDefinitions()->include(MetaDataPropertyName::fromString('exif.MimeType')) + ); + } } diff --git a/Tests/Functional/Domain/Extractor/IptcIimExtractorTest.php b/Tests/Functional/Domain/Extractor/IptcIimExtractorTest.php index fca4ae8..6b4abbc 100644 --- a/Tests/Functional/Domain/Extractor/IptcIimExtractorTest.php +++ b/Tests/Functional/Domain/Extractor/IptcIimExtractorTest.php @@ -11,12 +11,10 @@ * source code. */ -use Neos\MetaData\Domain\Collection\MetaDataCollection; -use Neos\MetaData\Domain\Dto; use Neos\MetaData\Extractor\Domain\Extractor\IptcIimExtractor; -use Neos\MetaData\Extractor\Tests\Functional\AbstractExtractorTest; +use Neos\MetaData\Extractor\Tests\Functional\AbstractExtractorTestCase; -class IptcIimExtractorTest extends AbstractExtractorTest +class IptcIimExtractorTest extends AbstractExtractorTestCase { /** * @var IptcIimExtractor @@ -26,7 +24,7 @@ class IptcIimExtractorTest extends AbstractExtractorTest /** * @inheritDoc */ - public function setUp() + public function setUp(): void { parent::setUp(); @@ -36,47 +34,33 @@ public function setUp() /** * @test */ - public function extractMetaData() + public function extractMetaData(): void { - $metaDataCollection = new MetaDataCollection(); - - $this->iptcIimExtractor->extractMetaData($this->testAsset->getResource(), $metaDataCollection); - $iptcDto = $metaDataCollection->get('iptc'); - - $this->assertInstanceOf(Dto\Iptc::class, $iptcDto); + $extractedData = $this->iptcIimExtractor->extractMetaData($this->testAsset->getResource()); $expectedIptcData = [ - 'City' => 'Otara', - 'Contact' => [], - 'CopyrightNotice' => '© Daniel Lienert', - 'Country' => 'Newzealand', - 'CountryCode' => 'NZ', - 'CreationDate' => \DateTime::createFromFormat('YmdHisO', '20130918105911+0000'), - 'Creator' => ['Daniel Lienert'], - 'CreatorTitle' => ['Informatiker'], - 'CreditLine' => 'by-nc', - 'DeprecatedCategories' => ['Nature', 'Lig'], - 'Description' => 'Waipapa Point Lighthouse with the sea in the background and bush in the foreground.', - 'DescriptionWriter' => ['Daniel Lienert'], - 'DigitalCreationDate' => \DateTime::createFromFormat('YmdHisO', '20130917235911+0000'), - 'Headline' => 'Waipapa Point Lighthouse', - 'Instructions' => 'None - it knows what to do', - 'IntellectualGenres' => [], - 'JobId' => 'Shines in the night', - 'Keywords' => [ - 'Beste', - 'Leuchtturm', - 'Neu Seeland', - 'Neuseeland', - 'New Zealand', - ], - 'Source' => 'Camera', - 'State' => 'Southland', - 'SubjectCodes' => [], - 'Sublocation' => 'Waipapa Point Lighthouse', - 'Title' => 'Waipapa Point Leuchtturm', + 'iptc.City' => 'Otara', + 'iptc.CopyrightNotice' => '© Daniel Lienert', + 'iptc.Country' => 'Newzealand', + 'iptc.CountryCode' => 'NZ', + 'iptc.CreationDate' => '2013-09-18 10:59:11', + 'iptc.Creator' => '["Daniel Lienert"]', + 'iptc.CreatorTitle' => '["Informatiker"]', + 'iptc.CreditLine' => 'by-nc', + 'iptc.DeprecatedCategories' => '["Nature","Lig"]', + 'iptc.Description' => 'Waipapa Point Lighthouse with the sea in the background and bush in the foreground.', + 'iptc.DescriptionWriter' => '["Daniel Lienert"]', + 'iptc.DigitalCreationDate' => '2013-09-17 23:59:11', + 'iptc.Headline' => 'Waipapa Point Lighthouse', + 'iptc.Instructions' => 'None - it knows what to do', + 'iptc.JobId' => 'Shines in the night', + 'iptc.Keywords' => '["Beste","Leuchtturm","Neu Seeland","Neuseeland","New Zealand"]', + 'iptc.Source' => 'Camera', + 'iptc.State' => 'Southland', + 'iptc.Sublocation' => 'Waipapa Point Lighthouse', + 'iptc.Title' => 'Waipapa Point Leuchtturm', ]; - $this->assertDtoGettersReturnData($iptcDto, $expectedIptcData); + $this->assertExtractedData($expectedIptcData, $extractedData); } } diff --git a/Tests/Unit/Domain/Extractor/Converter/CoordinatesConverterTest.php b/Tests/Unit/Domain/Extractor/Converter/CoordinatesConverterTest.php index ced69c5..42adb3f 100644 --- a/Tests/Unit/Domain/Extractor/Converter/CoordinatesConverterTest.php +++ b/Tests/Unit/Domain/Extractor/Converter/CoordinatesConverterTest.php @@ -19,7 +19,7 @@ class CoordinatesConverterTest extends UnitTestCase /** * @return mixed[][] */ - public function gpsDataProvider() + public static function gpsDataProvider() { return [ 'latitude' => [ diff --git a/Tests/Unit/Domain/Extractor/Converter/DateConverterTest.php b/Tests/Unit/Domain/Extractor/Converter/DateConverterTest.php index 1ea8eaa..b53163e 100644 --- a/Tests/Unit/Domain/Extractor/Converter/DateConverterTest.php +++ b/Tests/Unit/Domain/Extractor/Converter/DateConverterTest.php @@ -19,7 +19,7 @@ class DateConverterTest extends UnitTestCase /** * @return mixed[][] */ - public function gpsTimeStampDataProvider() : array + public static function gpsTimeStampDataProvider() : array { return [ [ @@ -52,7 +52,7 @@ public function convertGpsDateAndTime(string $dateStamp, array $timeStamp, \Date /** * @return mixed[][] */ - public function iso8601DateAndTimeDataProvider() : array + public static function iso8601DateAndTimeDataProvider() : array { return [ 'dateStringOnly' => [ @@ -78,7 +78,7 @@ public function iso8601DateAndTimeDataProvider() : array * @dataProvider iso8601DateAndTimeDataProvider * * @param string $dateString - * @param string|null $timeString + * @param string $timeString * @param \DateTime $expected * @return void */ diff --git a/Tests/Unit/Domain/Extractor/Converter/NumberConverterTest.php b/Tests/Unit/Domain/Extractor/Converter/NumberConverterTest.php index 9ebd70f..97eb263 100644 --- a/Tests/Unit/Domain/Extractor/Converter/NumberConverterTest.php +++ b/Tests/Unit/Domain/Extractor/Converter/NumberConverterTest.php @@ -19,7 +19,7 @@ class NumberConverterTest extends UnitTestCase /** * @return mixed[][] */ - public function rationalDataProvider() : array + public static function rationalDataProvider() : array { return [ 'correctRational' => [ diff --git a/composer.json b/composer.json index c3d2f9f..30ef159 100644 --- a/composer.json +++ b/composer.json @@ -1,111 +1,143 @@ { - "name": "neos/metadata-extractor", - "description": "Extracts meta data from files into a neos/metadata data structure", - "keywords": [ - "exif", - "extractor", - "flow", - "iptc", - "iptc iim", - "iptciim", - "meta data", - "metadata", - "neos" - ], - "type": "neos-package", - "license": "MIT", - "require": { - "php": "^7.1 || ^8.0", - "ext-exif": "*", - "ext-mbstring": "*", - - "neos/flow": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || dev-master", - "neos/media": "^3.0 || ^4.0 || ^5.0 || ^7.0 || ^8.0 || dev-master", - "neos/metadata": "^2.0 || dev-master", - "neos/utility-mediatypes": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || dev-master" - }, - "autoload": { - "psr-4": { - "Neos\\MetaData\\Extractor\\": "Classes" - } - }, - "extra": { - "neos": { - "package-key": "Neos.MetaData.Extractor" + "name": "neos/metadata-extractor", + "description": "Extracts meta data from files into a neos/metadata data structure", + "keywords": [ + "exif", + "extractor", + "flow", + "iptc", + "iptc iim", + "iptciim", + "meta data", + "metadata", + "neos" + ], + "type": "neos-package", + "license": "MIT", + "require": { + "php": "^8.4", + "ext-exif": "*", + "ext-mbstring": "*", + "neos/flow": "^8.3", + "neos/media": "^8.3", + "neos/metadata": "^3.0", + "neos/utility-mediatypes": "^8.3" + }, + "autoload": { + "psr-4": { + "Neos\\MetaData\\Extractor\\": "Classes" + } + }, + "autoload-dev": { + "psr-4": { + "Neos\\MetaData\\Extractor\\Tests\\": "Tests" + } + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "phpstan/phpstan": "^1.12" }, - "applied-flow-migrations": [ - "TYPO3.FLOW3-201201261636", - "TYPO3.Fluid-201205031303", - "TYPO3.FLOW3-201205292145", - "TYPO3.FLOW3-201206271128", - "TYPO3.FLOW3-201209201112", - "TYPO3.Flow-201209251426", - "TYPO3.Flow-201211151101", - "TYPO3.Flow-201212051340", - "TYPO3.TypoScript-130516234520", - "TYPO3.TypoScript-130516235550", - "TYPO3.TYPO3CR-130523180140", - "TYPO3.Neos.NodeTypes-201309111655", - "TYPO3.Flow-201310031523", - "TYPO3.Flow-201405111147", - "TYPO3.Neos-201407061038", - "TYPO3.Neos-201409071922", - "TYPO3.TYPO3CR-140911160326", - "TYPO3.Neos-201410010000", - "TYPO3.TYPO3CR-141101082142", - "TYPO3.Neos-20141113115300", - "TYPO3.Fluid-20141113120800", - "TYPO3.Flow-20141113121400", - "TYPO3.Fluid-20141121091700", - "TYPO3.Neos-20141218134700", - "TYPO3.Fluid-20150214130800", - "TYPO3.Neos-20150303231600", - "TYPO3.TYPO3CR-20150510103823", - "TYPO3.Flow-20151113161300", - "TYPO3.Form-20160601101500", - "TYPO3.Flow-20161115140400", - "TYPO3.Flow-20161115140430", - "Neos.Flow-20161124204700", - "Neos.Flow-20161124204701", - "Neos.Twitter.Bootstrap-20161124204912", - "Neos.Form-20161124205254", - "Neos.Flow-20161124224015", - "Neos.Party-20161124225257", - "Neos.Eel-20161124230101", - "Neos.Kickstart-20161124230102", - "Neos.Setup-20161124230842", - "Neos.Imagine-20161124231742", - "Neos.Media-20161124233100", - "Neos.NodeTypes-20161125002300", - "Neos.SiteKickstarter-20161125002311", - "Neos.Neos-20161125002322", - "Neos.ContentRepository-20161125012000", - "Neos.Fusion-20161125013710", - "Neos.Setup-20161125014759", - "Neos.SiteKickstarter-20161125095901", - "Neos.Fusion-20161125104701", - "Neos.NodeTypes-20161125104800", - "Neos.Neos-20161125104802", - "Neos.Kickstarter-20161125110814", - "Neos.Neos-20161125122412", - "Neos.Flow-20161125124112", - "TYPO3.FluidAdaptor-20161130112935", - "Neos.Fusion-20161201202543", - "Neos.Neos-20161201222211", - "Neos.Fusion-20161202215034", - "Neos.Fusion-20161219092345", - "Neos.ContentRepository-20161219093512", - "Neos.Media-20161219094126", - "Neos.Neos-20161219094403", - "Neos.Neos-20161219122512", - "Neos.Fusion-20161219130100", - "Neos.Neos-20161220163741", - "Neos.SwiftMailer-20161130105617", - "Neos.Neos-20170115114620", - "Neos.Fusion-20170120013047", - "Neos.Flow-20170125103800", - "Neos.Seo-20170127154600", - "Neos.Flow-20170127183102" - ] - } + "scripts": { + "test": "../../../bin/phpunit --enforce-time-limit --bootstrap ../../Libraries/autoload.php --testdox Tests", + "test:ci": "phpunit --enforce-time-limit --bootstrap vendor/autoload.php --testdox Tests", + "codestyle": "phpstan analyse --autoload-file ../../Libraries/autoload.php -c phpstan.neon" + }, + "config": { + "allow-plugins": { + "neos/composer-plugin": true + } + }, + "extra": { + "neos": { + "package-key": "Neos.MetaData.Extractor" + }, + "applied-flow-migrations": [ + "TYPO3.FLOW3-201201261636", + "TYPO3.Fluid-201205031303", + "TYPO3.FLOW3-201205292145", + "TYPO3.FLOW3-201206271128", + "TYPO3.FLOW3-201209201112", + "TYPO3.Flow-201209251426", + "TYPO3.Flow-201211151101", + "TYPO3.Flow-201212051340", + "TYPO3.TypoScript-130516234520", + "TYPO3.TypoScript-130516235550", + "TYPO3.TYPO3CR-130523180140", + "TYPO3.Neos.NodeTypes-201309111655", + "TYPO3.Flow-201310031523", + "TYPO3.Flow-201405111147", + "TYPO3.Neos-201407061038", + "TYPO3.Neos-201409071922", + "TYPO3.TYPO3CR-140911160326", + "TYPO3.Neos-201410010000", + "TYPO3.TYPO3CR-141101082142", + "TYPO3.Neos-20141113115300", + "TYPO3.Fluid-20141113120800", + "TYPO3.Flow-20141113121400", + "TYPO3.Fluid-20141121091700", + "TYPO3.Neos-20141218134700", + "TYPO3.Fluid-20150214130800", + "TYPO3.Neos-20150303231600", + "TYPO3.TYPO3CR-20150510103823", + "TYPO3.Flow-20151113161300", + "TYPO3.Form-20160601101500", + "TYPO3.Flow-20161115140400", + "TYPO3.Flow-20161115140430", + "Neos.Flow-20161124204700", + "Neos.Flow-20161124204701", + "Neos.Twitter.Bootstrap-20161124204912", + "Neos.Form-20161124205254", + "Neos.Flow-20161124224015", + "Neos.Party-20161124225257", + "Neos.Eel-20161124230101", + "Neos.Kickstart-20161124230102", + "Neos.Setup-20161124230842", + "Neos.Imagine-20161124231742", + "Neos.Media-20161124233100", + "Neos.NodeTypes-20161125002300", + "Neos.SiteKickstarter-20161125002311", + "Neos.Neos-20161125002322", + "Neos.ContentRepository-20161125012000", + "Neos.Fusion-20161125013710", + "Neos.Setup-20161125014759", + "Neos.SiteKickstarter-20161125095901", + "Neos.Fusion-20161125104701", + "Neos.NodeTypes-20161125104800", + "Neos.Neos-20161125104802", + "Neos.Kickstarter-20161125110814", + "Neos.Neos-20161125122412", + "Neos.Flow-20161125124112", + "TYPO3.FluidAdaptor-20161130112935", + "Neos.Fusion-20161201202543", + "Neos.Neos-20161201222211", + "Neos.Fusion-20161202215034", + "Neos.Fusion-20161219092345", + "Neos.ContentRepository-20161219093512", + "Neos.Media-20161219094126", + "Neos.Neos-20161219094403", + "Neos.Neos-20161219122512", + "Neos.Fusion-20161219130100", + "Neos.Neos-20161220163741", + "Neos.SwiftMailer-20161130105617", + "Neos.Neos-20170115114620", + "Neos.Fusion-20170120013047", + "Neos.Flow-20170125103800", + "Neos.Seo-20170127154600", + "Neos.Flow-20170127183102", + "Neos.Flow-20180415105700", + "Neos.Neos-20180907103800", + "Neos.Neos.Ui-20190319094900", + "Neos.Flow-20190425144900", + "Neos.Flow-20190515215000", + "Neos.Flow-20200813181400", + "Neos.Flow-20201003165200", + "Neos.Flow-20201109224100", + "Neos.Flow-20201205172733", + "Neos.Flow-20201207104500", + "Neos.Neos-20220318111600", + "Neos.Flow-20220318174300", + "Neos.Fusion-20220326120900", + "Neos.Fusion-20251006080506" + ] + } } diff --git a/phpstan.ci.neon b/phpstan.ci.neon new file mode 100644 index 0000000..e44ba17 --- /dev/null +++ b/phpstan.ci.neon @@ -0,0 +1,9 @@ +parameters: + level: max + phpVersion: 80400 + paths: + - Classes + - Tests + scanDirectories: + - Packages/Framework/Neos.Flow/Tests + - Packages/Application/Neos.Media/Tests diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..be2843c --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,9 @@ +parameters: + level: max + phpVersion: 80400 + paths: + - Classes + - Tests + scanDirectories: + - ../../Framework/Neos.Flow/Tests + - ../../Application/Neos.Media/Tests