From 7421699adde57cbfdcc005e792f071b4021762cc Mon Sep 17 00:00:00 2001 From: Martin Gold Date: Tue, 29 Oct 2024 11:20:23 +0100 Subject: [PATCH 1/4] chore: pass PHPStan at level 9 --- composer.json | 1 + phpstan.neon.dist | 4 + .../RegisterDoctrineTypePass.php | 1 + .../WebfactoryPolyglotExtension.php | 3 + src/Doctrine/PersistentTranslatable.php | 37 +++++--- src/Doctrine/PolyglotListener.php | 22 +++-- .../SerializedTranslatableClassMetadata.php | 27 +++--- src/Doctrine/TranslatableClassMetadata.php | 90 +++++++++++++++---- src/Doctrine/TranslatableStringType.php | 8 ++ .../UninitializedPersistentTranslatable.php | 1 + src/Entity/BaseTranslation.php | 8 +- src/Exception/ShouldNotHappen.php | 16 ++++ src/Translatable.php | 13 +++ src/TranslatableChain.php | 26 +++++- .../Fixtures/Entity/TestEntityTranslation.php | 14 +-- 15 files changed, 210 insertions(+), 61 deletions(-) create mode 100644 phpstan.neon.dist create mode 100644 src/Exception/ShouldNotHappen.php diff --git a/composer.json b/composer.json index b018251..8168e9a 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "require-dev": { "doctrine/common": "^2.0|^3.1", "doctrine/doctrine-bundle": "^2.0", + "phpstan/phpstan": "^1.12", "phpunit/phpunit": "^9.6.18", "symfony/error-handler": "^6.4|^7.0", "symfony/framework-bundle": "^5.4|^6.4|^7.0", diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..ce4117b --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,4 @@ +parameters: + level: 9 + paths: + - src \ No newline at end of file diff --git a/src/DependencyInjection/RegisterDoctrineTypePass.php b/src/DependencyInjection/RegisterDoctrineTypePass.php index 794d5b8..968ea30 100644 --- a/src/DependencyInjection/RegisterDoctrineTypePass.php +++ b/src/DependencyInjection/RegisterDoctrineTypePass.php @@ -15,6 +15,7 @@ public function process(ContainerBuilder $container): void throw new RuntimeException('This bundle expects DoctrineBundle to be registered, it should provide the doctrine.dbal.connection_factory.types container parameter'); } + /** @var array $types */ $types = $container->getParameter('doctrine.dbal.connection_factory.types'); $types[TranslatableStringType::NAME] = ['class' => TranslatableStringType::class]; diff --git a/src/DependencyInjection/WebfactoryPolyglotExtension.php b/src/DependencyInjection/WebfactoryPolyglotExtension.php index ecc59e8..917e7fc 100644 --- a/src/DependencyInjection/WebfactoryPolyglotExtension.php +++ b/src/DependencyInjection/WebfactoryPolyglotExtension.php @@ -16,6 +16,9 @@ final class WebfactoryPolyglotExtension extends Extension { + /** + * @param list $configs + */ public function load(array $configs, ContainerBuilder $container): void { $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); diff --git a/src/Doctrine/PersistentTranslatable.php b/src/Doctrine/PersistentTranslatable.php index 309d303..7ce6b17 100644 --- a/src/Doctrine/PersistentTranslatable.php +++ b/src/Doctrine/PersistentTranslatable.php @@ -9,6 +9,7 @@ namespace Webfactory\Bundle\PolyglotBundle\Doctrine; +use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Selectable; use Doctrine\ORM\UnitOfWork; @@ -25,6 +26,9 @@ use Webfactory\Bundle\PolyglotBundle\TranslatableInterface; /** + * @template T + * @implements TranslatableInterface + * * This class implements `TranslatableInterface` for entities that are managed by * the entity manager. PolyglotListener will replace `Translatable` instances with * instances of this class as soon as a new entity is passed to EntityManager::persist(). @@ -61,17 +65,17 @@ final class PersistentTranslatable implements TranslatableInterface private LoggerInterface $logger; /** - * @param UnitOfWork $unitOfWork The UoW managing the entity that contains this PersistentTranslatable - * @param class-string $class The class of the entity containing this PersistentTranslatable instance - * @param object $entity The entity containing this PersistentTranslatable instance - * @param string $primaryLocale The locale for which the translated value will be persisted in the "main" entity - * @param DefaultLocaleProvider $defaultLocaleProvider DefaultLocaleProvider that provides the locale to use when no explicit locale is passed to e. g. translate() - * @param ReflectionProperty $translationProperty ReflectionProperty pointing to the field in the translations class that holds the translated value to use - * @param ReflectionProperty $translationCollection ReflectionProperty pointing to the collection in the main class that holds translation instances - * @param ReflectionClass $translationClass ReflectionClass for the class holding translated values - * @param ReflectionProperty $localeField ReflectionProperty pointing to the field in the translations class that holds a translation's locale - * @param ReflectionProperty $translationMapping ReflectionProperty pointing to the field in the translations class that refers back to the main entity (the owning side of the one-to-many translations collection). - * @param ReflectionProperty $translatedProperty ReflectionProperty pointing to the field in the main entity where this PersistentTranslatable instance will be used + * @param UnitOfWork $unitOfWork The UoW managing the entity that contains this PersistentTranslatable + * @param class-string $class The class of the entity containing this PersistentTranslatable instance + * @param object $entity The entity containing this PersistentTranslatable instance + * @param string $primaryLocale The locale for which the translated value will be persisted in the "main" entity + * @param DefaultLocaleProvider $defaultLocaleProvider DefaultLocaleProvider that provides the locale to use when no explicit locale is passed to e. g. translate() + * @param ReflectionProperty $translationProperty ReflectionProperty pointing to the field in the translations class that holds the translated value to use + * @param ReflectionProperty $translationCollection ReflectionProperty pointing to the collection in the main class that holds translation instances + * @param ReflectionClass $translationClass ReflectionClass for the class holding translated values + * @param ReflectionProperty $localeField ReflectionProperty pointing to the field in the translations class that holds a translation's locale + * @param ReflectionProperty $translationMapping ReflectionProperty pointing to the field in the translations class that refers back to the main entity (the owning side of the one-to-many translations collection). + * @param ReflectionProperty $translatedProperty ReflectionProperty pointing to the field in the main entity where this PersistentTranslatable instance will be used */ public function __construct( private readonly UnitOfWork $unitOfWork, @@ -150,7 +154,10 @@ private function createTranslationEntity(string $locale): object $this->localeField->setValue($entity, $locale); $this->translationMapping->setValue($entity, $this->entity); - $this->translationCollection->getValue($this->entity)->add($entity); + + /** @var Collection $collection */ + $collection = $this->translationCollection->getValue($this->entity); + $collection->add($entity); self::$_translations[$this->class][$this->oid][$locale] = $entity; $this->unitOfWork->persist($entity); @@ -241,13 +248,17 @@ private function isTranslationCached(string $locale): bool */ private function cacheTranslation(string $locale): void { - /** @var $translationsInAllLanguages Selectable */ + /** @var Selectable $translationsInAllLanguages */ $translationsInAllLanguages = $this->translationCollection->getValue($this->entity); $criteria = $this->createLocaleCriteria($locale); $translationsFilteredByLocale = $translationsInAllLanguages->matching($criteria); $translationInLocale = ($translationsFilteredByLocale->count() > 0) ? $translationsFilteredByLocale->first() : null; + if (is_bool($translationInLocale)) { + return; + } + self::$_translations[$this->class][$this->oid][$locale] = $translationInLocale; } diff --git a/src/Doctrine/PolyglotListener.php b/src/Doctrine/PolyglotListener.php index e01c5c2..236982b 100644 --- a/src/Doctrine/PolyglotListener.php +++ b/src/Doctrine/PolyglotListener.php @@ -10,10 +10,12 @@ namespace Webfactory\Bundle\PolyglotBundle\Doctrine; use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\PostFlushEventArgs; use Doctrine\ORM\Event\PreFlushEventArgs; use Doctrine\Persistence\Event\LifecycleEventArgs; use Doctrine\Persistence\Mapping\RuntimeReflectionService; +use Doctrine\Persistence\ObjectManager; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use WeakReference; @@ -39,28 +41,34 @@ final class PolyglotListener private array $translatedClasses = []; /** - * @var array + * @var array> */ private array $entitiesWithTranslatables = []; /** - * @var list + * @var list> */ private array $ejectedTranslatables = []; public function __construct( private readonly DefaultLocaleProvider $defaultLocaleProvider, - private readonly LoggerInterface $logger = null ?? new NullLogger(), + private readonly LoggerInterface $logger = new NullLogger(), private readonly RuntimeReflectionService $reflectionService = new RuntimeReflectionService(), ) { } + /** + * @param LifecycleEventArgs $event + */ public function postLoad(LifecycleEventArgs $event): void { // Called when the entity has been hydrated $this->injectPersistentTranslatables($event->getObjectManager(), $event->getObject()); } + /** + * @param LifecycleEventArgs $event + */ public function prePersist(LifecycleEventArgs $event): void { // Called when a new entity is passed to persist() for the first time @@ -109,7 +117,7 @@ public function postFlush(PostFlushEventArgs $event): void /** * @return list */ - private function getTranslationMetadatas(object $entity, EntityManager $em): array + private function getTranslationMetadatas(object $entity, EntityManagerInterface $em): array { $class = $entity::class; @@ -127,7 +135,10 @@ private function getTranslationMetadatas(object $entity, EntityManager $em): arr return $this->translatableClassMetadatasByClass[$class]; } - private function loadTranslationMetadataForClass($className, EntityManager $em): ?TranslatableClassMetadata + /** + * @param class-string $className + */ + private function loadTranslationMetadataForClass(string $className, EntityManagerInterface $em): ?TranslatableClassMetadata { // In memory cache if (isset($this->translatedClasses[$className])) { @@ -140,6 +151,7 @@ private function loadTranslationMetadataForClass($className, EntityManager $em): if ($cache?->hasItem($cacheKey)) { $item = $cache->getItem($cacheKey); + /** @var SerializedTranslatableClassMetadata|null $data */ $data = $item->get(); if (null === $data) { $this->translatedClasses[$className] = null; diff --git a/src/Doctrine/SerializedTranslatableClassMetadata.php b/src/Doctrine/SerializedTranslatableClassMetadata.php index fc3fdbf..92317d0 100644 --- a/src/Doctrine/SerializedTranslatableClassMetadata.php +++ b/src/Doctrine/SerializedTranslatableClassMetadata.php @@ -11,33 +11,40 @@ final class SerializedTranslatableClassMetadata { + /** + * @var class-string + */ public string $class; + + /** + * @var class-string + */ public string $translationClass; /** - * @var array + * @var array, 1: string}> */ - public array $translationFieldMapping = []; + public array $translationFieldMapping; /** - * @var array + * @var array, 1: string}> */ - public array $translatedProperties = []; + public array $translatedProperties; /** - * @var array{0: string, 1: string} + * @var array{0: class-string, 1: string} */ - public array $translationLocaleProperty = []; + public array $translationLocaleProperty; /** - * @var array{0: string, 1: string} + * @var array{0: class-string, 1: string} */ - public array $translationsCollectionProperty = []; + public array $translationsCollectionProperty; /** - * @var array{0: string, 1: string} + * @var array{0: class-string, 1: string} */ - public array $translationMappingProperty = []; + public array $translationMappingProperty; public string $primaryLocale; } diff --git a/src/Doctrine/TranslatableClassMetadata.php b/src/Doctrine/TranslatableClassMetadata.php index 41b56df..063ab45 100644 --- a/src/Doctrine/TranslatableClassMetadata.php +++ b/src/Doctrine/TranslatableClassMetadata.php @@ -12,12 +12,14 @@ use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataFactory; +use Doctrine\ORM\Mapping\InverseSideMapping; use Doctrine\Persistence\Mapping\RuntimeReflectionService; use Psr\Log\LoggerInterface; use ReflectionClass; use ReflectionProperty; use RuntimeException; use Webfactory\Bundle\PolyglotBundle\Attribute; +use Webfactory\Bundle\PolyglotBundle\Exception\ShouldNotHappen; use Webfactory\Bundle\PolyglotBundle\Locale\DefaultLocaleProvider; use Webfactory\Bundle\PolyglotBundle\Translatable; @@ -60,7 +62,7 @@ final class TranslatableClassMetadata private ?ReflectionProperty $translationLocaleProperty = null; /** - * Die Übersetzungs-Klasse. + * @var ReflectionClass|null $translationClass */ private ?ReflectionClass $translationClass = null; @@ -72,7 +74,7 @@ final class TranslatableClassMetadata private ?LoggerInterface $logger = null; /** - * @param class-string $class The FQCN for the entity class whose translatable fields are described by this + * @param class-string $class The FQCN for the entity class whose translatable fields are described by this * TranslatableClassMetadata instance. If the class has base entity classes (or mapped * superclasses), a separate instance of TranslatableClassMetadata will be used for * their fields. @@ -82,9 +84,12 @@ private function __construct( ) { } + /** + * @param class-string $class + */ public static function parseFromClass(string $class, ClassMetadataFactory $classMetadataFactory): ?self { - /** @var ClassMetadata $cm */ + /** @var ClassMetadata $cm */ $cm = $classMetadataFactory->getMetadataFor($class); $tm = new static($class); @@ -107,21 +112,47 @@ public function setLogger(?LoggerInterface $logger = null): void public function sleep(): SerializedTranslatableClassMetadata { + if ($this->translationClass === null) { + throw new ShouldNotHappen('translationClass cannot be null'); + } + + if ($this->primaryLocale === null) { + throw new ShouldNotHappen('primaryLocale cannot be null'); + } + + if ($this->translationLocaleProperty === null) { + throw new ShouldNotHappen('translationLocaleProperty cannot be null'); + } + + if ($this->translationMappingProperty === null) { + throw new ShouldNotHappen('translationMappingProperty cannot be null'); + } + + if ($this->translationsCollectionProperty === null) { + throw new ShouldNotHappen('translationsCollectionProperty cannot be null'); + } + $sleep = new SerializedTranslatableClassMetadata(); $sleep->class = $this->class; $sleep->primaryLocale = $this->primaryLocale; $sleep->translationClass = $this->translationClass->name; foreach ($this->translationFieldMapping as $fieldname => $property) { + // @see https://github.com/phpstan/phpstan/issues/11334 + // @phpstan-ignore assign.propertyType $sleep->translationFieldMapping[$fieldname] = [$property->class, $property->name]; } foreach ($this->translatedProperties as $fieldname => $property) { + // @phpstan-ignore assign.propertyType $sleep->translatedProperties[$fieldname] = [$property->class, $property->name]; } + // @phpstan-ignore assign.propertyType $sleep->translationLocaleProperty = [$this->translationLocaleProperty->class, $this->translationLocaleProperty->name]; + // @phpstan-ignore assign.propertyType $sleep->translationsCollectionProperty = [$this->translationsCollectionProperty->class, $this->translationsCollectionProperty->name]; + // @phpstan-ignore assign.propertyType $sleep->translationMappingProperty = [$this->translationMappingProperty->class, $this->translationMappingProperty->name]; return $sleep; @@ -133,12 +164,15 @@ public static function wakeup(SerializedTranslatableClassMetadata $data, Runtime $self->primaryLocale = $data->primaryLocale; $self->translationClass = $reflectionService->getClass($data->translationClass); + foreach ($data->translationFieldMapping as $fieldname => $property) { - $self->translationFieldMapping[$fieldname] = $reflectionService->getAccessibleProperty(...$property); + $self->translationFieldMapping[$fieldname] = $reflectionService->getAccessibleProperty(...$property) ?? + throw new ShouldNotHappen("Cannot get reflection on {$property[0]}::{$property[1]}"); } foreach ($data->translatedProperties as $fieldname => $property) { - $self->translatedProperties[$fieldname] = $reflectionService->getAccessibleProperty(...$property); + $self->translatedProperties[$fieldname] = $reflectionService->getAccessibleProperty(...$property) ?? + throw new ShouldNotHappen("Cannot get reflection on {$property[0]}::{$property[1]}"); } $self->translationsCollectionProperty = $reflectionService->getAccessibleProperty(...$data->translationsCollectionProperty); @@ -179,6 +213,9 @@ private function assertAttributesAreComplete(string $class): void } } + /** + * @param ClassMetadata $cm + */ private function findTranslatedProperties(ClassMetadata $cm, ClassMetadataFactory $classMetadataFactory): void { if (!$this->translationClass) { @@ -189,7 +226,7 @@ private function findTranslatedProperties(ClassMetadata $cm, ClassMetadataFactor $translationClassMetadata = $classMetadataFactory->getMetadataFor($this->translationClass->getName()); /* Iterate all properties of the class, not only those mapped by Doctrine */ - foreach ($cm->getReflectionClass()->getProperties() as $reflectionProperty) { + foreach ($cm->getReflectionClass()?->getProperties() ?? [] as $reflectionProperty) { $propertyName = $reflectionProperty->name; /* @@ -208,12 +245,19 @@ private function findTranslatedProperties(ClassMetadata $cm, ClassMetadataFactor } $attribute = $attributes[0]->newInstance(); - $this->translatedProperties[$propertyName] = $reflectionService->getAccessibleProperty($cm->name, $propertyName); + $this->translatedProperties[$propertyName] = $reflectionService->getAccessibleProperty($cm->name, $propertyName) ?? + throw new ShouldNotHappen("Cannot get reflection for {$cm->name}::{$propertyName}."); + $translationFieldname = $attribute->getTranslationFieldname() ?: $propertyName; - $this->translationFieldMapping[$propertyName] = $reflectionService->getAccessibleProperty($translationClassMetadata->name, $translationFieldname); + + $this->translationFieldMapping[$propertyName] = $reflectionService->getAccessibleProperty($translationClassMetadata->name, $translationFieldname) ?? + throw new ShouldNotHappen("Cannot get reflection for {$translationClassMetadata->name}::{$translationFieldname}."); } } + /** + * @param ClassMetadata $cm + */ private function findTranslationsCollection(ClassMetadata $cm, ClassMetadataFactory $classMetadataFactory): void { foreach ($cm->associationMappings as $fieldName => $mapping) { @@ -224,12 +268,16 @@ private function findTranslationsCollection(ClassMetadata $cm, ClassMetadataFact $reflectionProperty = $cm->getReflectionProperty($fieldName); - if ($reflectionProperty->getAttributes(Attribute\TranslationCollection::class)) { + if ($reflectionProperty?->getAttributes(Attribute\TranslationCollection::class) ?? false) { + if (!$mapping instanceof InverseSideMapping) { + return; + } + $this->translationsCollectionProperty = $reflectionProperty; - $translationEntityMetadata = $classMetadataFactory->getMetadataFor($mapping['targetEntity']); + $translationEntityMetadata = $classMetadataFactory->getMetadataFor($mapping->targetEntity); $this->translationClass = $translationEntityMetadata->getReflectionClass(); - $this->translationMappingProperty = $translationEntityMetadata->getReflectionProperty($mapping['mappedBy']); + $this->translationMappingProperty = $translationEntityMetadata->getReflectionProperty($mapping->mappedBy); $this->parseTranslationsEntity($translationEntityMetadata); return; @@ -237,6 +285,9 @@ private function findTranslationsCollection(ClassMetadata $cm, ClassMetadataFact } } + /** + * @param ClassMetadata $cm + */ private function findPrimaryLocale(ClassMetadata $cm): void { foreach (array_merge([$cm->name], $cm->parentClasses) as $class) { @@ -250,12 +301,15 @@ private function findPrimaryLocale(ClassMetadata $cm): void } } + /** + * @param ClassMetadata $cm + */ private function parseTranslationsEntity(ClassMetadata $cm): void { foreach ($cm->fieldMappings as $fieldName => $mapping) { $reflectionProperty = $cm->getReflectionProperty($fieldName); - if ($reflectionProperty->getAttributes(Attribute\Locale::class)) { + if ($reflectionProperty?->getAttributes(Attribute\Locale::class) ?? false) { $this->translationLocaleProperty = $reflectionProperty; return; @@ -274,13 +328,13 @@ public function injectNewPersistentTranslatables(object $entity, EntityManager $ $entityManager->getUnitOfWork(), $this->class, $entity, - $this->primaryLocale, + $this->primaryLocale ?? new ShouldNotHappen('primaryLocale cannot be null.'), $defaultLocaleProvider, $this->translationFieldMapping[$fieldName], - $this->translationsCollectionProperty, - $this->translationClass, - $this->translationLocaleProperty, - $this->translationMappingProperty, + $this->translationsCollectionProperty ?? throw new ShouldNotHappen('primaryLocale cannot be null.'), + $this->translationClass ?? throw new ShouldNotHappen('primaryLocale cannot be null.'), + $this->translationLocaleProperty ?? throw new ShouldNotHappen('primaryLocale cannot be null.'), + $this->translationMappingProperty ?? throw new ShouldNotHappen('primaryLocale cannot be null.'), $property, $this->logger ); @@ -289,7 +343,7 @@ public function injectNewPersistentTranslatables(object $entity, EntityManager $ } /** - * @return list + * @return list> */ public function ejectPersistentTranslatables(object $entity): array { diff --git a/src/Doctrine/TranslatableStringType.php b/src/Doctrine/TranslatableStringType.php index ce38328..59287cd 100644 --- a/src/Doctrine/TranslatableStringType.php +++ b/src/Doctrine/TranslatableStringType.php @@ -5,6 +5,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type; use RuntimeException; +use Webfactory\Bundle\PolyglotBundle\Exception\ShouldNotHappen; /** * Doctrine type to support mapping database string types (VARCHAR, TEXT etc.) @@ -15,6 +16,9 @@ class TranslatableStringType extends Type { public const NAME = 'translatable_string'; + /** + * @param array{options: array{use_text_column?: string}} $column + */ public function getSQLDeclaration(array $column, AbstractPlatform $platform): string { if ($column['options']['use_text_column'] ?? false) { @@ -49,6 +53,10 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?str public function convertToPHPValue($value, AbstractPlatform $platform): UninitializedPersistentTranslatable { + if (!is_string($value)) { + throw new ShouldNotHappen('Translated value is not string.'); + } + return new UninitializedPersistentTranslatable($value); } diff --git a/src/Doctrine/UninitializedPersistentTranslatable.php b/src/Doctrine/UninitializedPersistentTranslatable.php index 66270a5..6d4a530 100644 --- a/src/Doctrine/UninitializedPersistentTranslatable.php +++ b/src/Doctrine/UninitializedPersistentTranslatable.php @@ -7,6 +7,7 @@ /** * @psalm-internal Webfactory\Bundle\PolyglotBundle + * @implements TranslatableInterface */ final class UninitializedPersistentTranslatable implements TranslatableInterface { diff --git a/src/Entity/BaseTranslation.php b/src/Entity/BaseTranslation.php index 91b3efa..1ae54ae 100644 --- a/src/Entity/BaseTranslation.php +++ b/src/Entity/BaseTranslation.php @@ -28,22 +28,22 @@ class BaseTranslation #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] - protected $id; + protected mixed $id; /** * @ORM\Column */ #[Polyglot\Locale] #[ORM\Column] - protected $locale; + protected string|null $locale; /** * @ORM\JoinColumn(name="entity_id", referencedColumnName="id", nullable=false) */ #[ORM\JoinColumn(name: 'entity_id', referencedColumnName: 'id', nullable: false)] - protected $entity; + protected object $entity; - public function getLocale() + public function getLocale(): string|null { return $this->locale; } diff --git a/src/Exception/ShouldNotHappen.php b/src/Exception/ShouldNotHappen.php new file mode 100644 index 0000000..b42cc4f --- /dev/null +++ b/src/Exception/ShouldNotHappen.php @@ -0,0 +1,16 @@ + */ final class Translatable implements TranslatableInterface { @@ -46,6 +49,9 @@ final class Translatable implements TranslatableInterface */ private array $translations = []; + /** + * @param T $value + */ public function __construct( mixed $value = null, private string|DefaultLocaleProvider|null $defaultLocale = null, @@ -66,6 +72,9 @@ public function setDefaultLocale(string $locale): void $this->defaultLocale = $locale; } + /** + * @return T + */ public function translate(?string $locale = null): mixed { $locale = $locale ?: $this->getDefaultLocale(); @@ -73,6 +82,9 @@ public function translate(?string $locale = null): mixed return $this->translations[$locale] ?? null; } + /** + * @param T $value + */ public function setTranslation(mixed $value, ?string $locale = null): void { $locale = $locale ?: $this->getDefaultLocale(); @@ -92,6 +104,7 @@ public function __toString(): string /** * Copies translations from this object into the given one. + * @param TranslatableInterface $p */ public function copy(TranslatableInterface $p): void { diff --git a/src/TranslatableChain.php b/src/TranslatableChain.php index 0f624b5..09cec48 100644 --- a/src/TranslatableChain.php +++ b/src/TranslatableChain.php @@ -9,21 +9,32 @@ * * Goes through a list of `TranslatableInterface` instances and returns the first * (optionally, non-empty) translation found. Updates are passed to the primary translatable. + * + * @template T + * @implements TranslatableInterface */ final class TranslatableChain implements TranslatableInterface { /** - * @var list + * @var list> */ private array $translatables; + /** + * @param TranslatableInterface ...$translatables + * @return self + */ public static function firstNonEmpty(TranslatableInterface ...$translatables): self { return new self(function ($value) { - return null !== $value && '' !== trim($value); + return null !== $value && '' !== trim((string) $value); }, ...$translatables); } + /** + * @param TranslatableInterface ...$translatables + * @return self + */ public static function firstTranslation(TranslatableInterface ...$translatables): self { return new self(function ($value) { @@ -31,13 +42,20 @@ public static function firstTranslation(TranslatableInterface ...$translatables) }, ...$translatables); } + /** + * @param Closure(T): bool $comparator + * @param TranslatableInterface ...$translatables + */ private function __construct( private readonly Closure $comparator, TranslatableInterface ...$translatables, ) { - $this->translatables = $translatables; + $this->translatables = array_values($translatables); } + /** + * @return T|null + */ public function translate(?string $locale = null): mixed { $c = $this->comparator; @@ -69,6 +87,6 @@ public function isTranslatedInto(string $locale): bool public function __toString(): string { - return $this->translate(); + return (string) $this->translate(); } } diff --git a/tests/Fixtures/Entity/TestEntityTranslation.php b/tests/Fixtures/Entity/TestEntityTranslation.php index 20a9932..d222118 100644 --- a/tests/Fixtures/Entity/TestEntityTranslation.php +++ b/tests/Fixtures/Entity/TestEntityTranslation.php @@ -22,7 +22,7 @@ class TestEntityTranslation extends BaseTranslation * @var TestEntity */ #[ORM\ManyToOne(targetEntity: TestEntity::class, inversedBy: 'translations')] - protected $entity; + protected object $entity; /** * Contains the translation. @@ -32,15 +32,15 @@ class TestEntityTranslation extends BaseTranslation * @var string */ #[ORM\Column(type: 'string')] - protected $text; + protected string|null $text; - /** - * @param string|null $text - */ - public function __construct($locale = null, $text = null, ?TestEntity $entity = null) + public function __construct(string|null $locale = null, string|null $text = null, ?TestEntity $entity = null) { $this->locale = $locale; $this->text = $text; - $this->entity = $entity; + + if ($entity !== null) { + $this->entity = $entity; + } } } From e41b82d8287e2a4afda1ad2d23099a0987af8f25 Mon Sep 17 00:00:00 2001 From: Martin Gold Date: Tue, 29 Oct 2024 17:35:19 +0100 Subject: [PATCH 2/4] chore: pass PHPStan strict level --- composer.json | 1 + phpstan.neon.dist | 4 +++- .../WebfactoryPolyglotExtension.php | 7 ++++--- src/Doctrine/PersistentTranslatable.php | 19 +++++++++++-------- src/Doctrine/PolyglotListener.php | 7 ++++--- src/Doctrine/TranslatableClassMetadata.php | 12 ++++++------ src/Doctrine/TranslatableStringType.php | 6 ++---- src/Translatable.php | 10 +++++----- src/TranslatableChain.php | 5 +++++ src/TranslatableInterface.php | 2 +- 10 files changed, 42 insertions(+), 31 deletions(-) diff --git a/composer.json b/composer.json index 8168e9a..210cdb0 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "doctrine/common": "^2.0|^3.1", "doctrine/doctrine-bundle": "^2.0", "phpstan/phpstan": "^1.12", + "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpunit": "^9.6.18", "symfony/error-handler": "^6.4|^7.0", "symfony/framework-bundle": "^5.4|^6.4|^7.0", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index ce4117b..e5dcceb 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,4 +1,6 @@ parameters: level: 9 paths: - - src \ No newline at end of file + - src +includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon \ No newline at end of file diff --git a/src/DependencyInjection/WebfactoryPolyglotExtension.php b/src/DependencyInjection/WebfactoryPolyglotExtension.php index 917e7fc..b49dc77 100644 --- a/src/DependencyInjection/WebfactoryPolyglotExtension.php +++ b/src/DependencyInjection/WebfactoryPolyglotExtension.php @@ -16,15 +16,16 @@ final class WebfactoryPolyglotExtension extends Extension { - /** - * @param list $configs - */ public function load(array $configs, ContainerBuilder $container): void { $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.xml'); $m = ['defaultLocale' => 'de_DE']; + + /** + * @var array{defaultLocale: string} $c + */ foreach ($configs as $c) { $m = array_merge($m, $c); } diff --git a/src/Doctrine/PersistentTranslatable.php b/src/Doctrine/PersistentTranslatable.php index 7ce6b17..cff00b7 100644 --- a/src/Doctrine/PersistentTranslatable.php +++ b/src/Doctrine/PersistentTranslatable.php @@ -120,7 +120,7 @@ public function eject(): void $type = $this->translatedProperty->getType(); if ($type instanceof ReflectionNamedType && TranslatableInterface::class === $type->getName() && \is_string($value)) { - if (!$this->valueForEjection || $this->valueForEjection->getPrimaryValue() !== $value) { + if ($this->valueForEjection === null || $this->valueForEjection->getPrimaryValue() !== $value) { $this->valueForEjection = new UninitializedPersistentTranslatable($value); } $value = $this->valueForEjection; @@ -167,12 +167,13 @@ private function createTranslationEntity(string $locale): object public function setTranslation(mixed $value, ?string $locale = null): void { - $locale = $locale ?: $this->getDefaultLocale(); + $locale ??= $this->getDefaultLocale(); + if ($locale === $this->primaryLocale) { $this->setPrimaryValue($value); } else { $entity = $this->getTranslationEntity($locale); - if (!$entity) { + if ($entity === null) { $entity = $this->createTranslationEntity($locale); } $this->translationProperty->setValue($entity, $value); @@ -184,13 +185,15 @@ public function setTranslation(mixed $value, ?string $locale = null): void */ public function translate(?string $locale = null): mixed { - $locale = $locale ?: $this->getDefaultLocale(); + $locale ??= $this->getDefaultLocale(); + try { if ($locale === $this->primaryLocale) { return $this->primaryValue; } - if ($entity = $this->getTranslationEntity($locale)) { + $entity = $this->getTranslationEntity($locale); + if ($entity !== null) { $translated = $this->translationProperty->getValue($entity); if (null !== $translated) { return $translated; @@ -212,12 +215,12 @@ public function translate(?string $locale = null): mixed public function isTranslatedInto(string $locale): bool { if ($locale === $this->primaryLocale) { - return !empty($this->primaryValue); + return $this->primaryValue !== '' && $this->primaryValue !== null; } $entity = $this->getTranslationEntity($locale); - return $entity && null !== $this->translationProperty->getValue($entity); + return $entity !== null && $this->translationProperty->getValue($entity) !== null; } public function __toString(): string @@ -279,7 +282,7 @@ private function stringifyException(Throwable $e): string { $exceptionAsString = ''; while (null !== $e) { - if (!empty($exceptionAsString)) { + if ($exceptionAsString !== '') { $exceptionAsString .= \PHP_EOL.'Previous exception: '.\PHP_EOL; } $exceptionAsString .= \sprintf( diff --git a/src/Doctrine/PolyglotListener.php b/src/Doctrine/PolyglotListener.php index 236982b..4b3bb50 100644 --- a/src/Doctrine/PolyglotListener.php +++ b/src/Doctrine/PolyglotListener.php @@ -126,7 +126,8 @@ private function getTranslationMetadatas(object $entity, EntityManagerInterface $classMetadata = $em->getClassMetadata($class); foreach (array_merge([$classMetadata->name], $classMetadata->parentClasses) as $className) { - if ($tm = $this->loadTranslationMetadataForClass($className, $em)) { + $tm = $this->loadTranslationMetadataForClass($className, $em); + if ($tm !== null) { $this->translatableClassMetadatasByClass[$class][] = $tm; } } @@ -149,7 +150,7 @@ private function loadTranslationMetadataForClass(string $className, EntityManage $cache = $em->getConfiguration()->getMetadataCache(); $cacheKey = $this->getCacheKey($className); - if ($cache?->hasItem($cacheKey)) { + if ($cache !== null && $cache->hasItem($cacheKey)) { $item = $cache->getItem($cacheKey); /** @var SerializedTranslatableClassMetadata|null $data */ $data = $item->get(); @@ -175,7 +176,7 @@ private function loadTranslationMetadataForClass(string $className, EntityManage } // Save if cache driver available - if ($cache) { + if ($cache !== null) { $item = $cache->getItem($cacheKey); $item->set($meta?->sleep()); $cache->save($item); diff --git a/src/Doctrine/TranslatableClassMetadata.php b/src/Doctrine/TranslatableClassMetadata.php index 063ab45..c5d49bd 100644 --- a/src/Doctrine/TranslatableClassMetadata.php +++ b/src/Doctrine/TranslatableClassMetadata.php @@ -218,7 +218,7 @@ private function assertAttributesAreComplete(string $class): void */ private function findTranslatedProperties(ClassMetadata $cm, ClassMetadataFactory $classMetadataFactory): void { - if (!$this->translationClass) { + if ($this->translationClass === null) { return; } @@ -234,13 +234,13 @@ private function findTranslatedProperties(ClassMetadata $cm, ClassMetadataFactor already contains that declaration, we need not include it. */ $declaringClass = $reflectionProperty->getDeclaringClass()->name; - if ($declaringClass !== $cm->name && $cm->parentClasses && is_a($cm->parentClasses[0], $declaringClass, true)) { + if ($declaringClass !== $cm->name && $cm->parentClasses !== [] && is_a($cm->parentClasses[0], $declaringClass, true)) { continue; } $attributes = $reflectionProperty->getAttributes(Attribute\Translatable::class); - if (!$attributes) { + if ($attributes === []) { continue; } @@ -248,7 +248,7 @@ private function findTranslatedProperties(ClassMetadata $cm, ClassMetadataFactor $this->translatedProperties[$propertyName] = $reflectionService->getAccessibleProperty($cm->name, $propertyName) ?? throw new ShouldNotHappen("Cannot get reflection for {$cm->name}::{$propertyName}."); - $translationFieldname = $attribute->getTranslationFieldname() ?: $propertyName; + $translationFieldname = $attribute->getTranslationFieldname() ?? $propertyName; $this->translationFieldMapping[$propertyName] = $reflectionService->getAccessibleProperty($translationClassMetadata->name, $translationFieldname) ?? throw new ShouldNotHappen("Cannot get reflection for {$translationClassMetadata->name}::{$translationFieldname}."); @@ -268,7 +268,7 @@ private function findTranslationsCollection(ClassMetadata $cm, ClassMetadataFact $reflectionProperty = $cm->getReflectionProperty($fieldName); - if ($reflectionProperty?->getAttributes(Attribute\TranslationCollection::class) ?? false) { + if ($reflectionProperty !== null && $reflectionProperty->getAttributes(Attribute\TranslationCollection::class) !== []) { if (!$mapping instanceof InverseSideMapping) { return; } @@ -309,7 +309,7 @@ private function parseTranslationsEntity(ClassMetadata $cm): void foreach ($cm->fieldMappings as $fieldName => $mapping) { $reflectionProperty = $cm->getReflectionProperty($fieldName); - if ($reflectionProperty?->getAttributes(Attribute\Locale::class) ?? false) { + if ($reflectionProperty !== null && $reflectionProperty->getAttributes(Attribute\Locale::class) !== []) { $this->translationLocaleProperty = $reflectionProperty; return; diff --git a/src/Doctrine/TranslatableStringType.php b/src/Doctrine/TranslatableStringType.php index 59287cd..722cc26 100644 --- a/src/Doctrine/TranslatableStringType.php +++ b/src/Doctrine/TranslatableStringType.php @@ -16,15 +16,13 @@ class TranslatableStringType extends Type { public const NAME = 'translatable_string'; - /** - * @param array{options: array{use_text_column?: string}} $column - */ public function getSQLDeclaration(array $column, AbstractPlatform $platform): string { - if ($column['options']['use_text_column'] ?? false) { + if (isset($column['options']) && is_array($column['options']) && ($column['options']['use_text_column'] ?? false)) { return $platform->getClobTypeDeclarationSQL($column); } + // @phpstan-ignore function.alreadyNarrowedType if (method_exists($platform, 'getStringTypeDeclarationSQL')) { return $platform->getStringTypeDeclarationSQL($column); } else { diff --git a/src/Translatable.php b/src/Translatable.php index e9d8b02..61de4a2 100644 --- a/src/Translatable.php +++ b/src/Translatable.php @@ -45,7 +45,7 @@ final class Translatable implements TranslatableInterface /** * Maps locales to translations. * - * @var array + * @var array */ private array $translations = []; @@ -73,11 +73,11 @@ public function setDefaultLocale(string $locale): void } /** - * @return T + * @return T|null */ public function translate(?string $locale = null): mixed { - $locale = $locale ?: $this->getDefaultLocale(); + $locale ??= $this->getDefaultLocale(); return $this->translations[$locale] ?? null; } @@ -87,14 +87,14 @@ public function translate(?string $locale = null): mixed */ public function setTranslation(mixed $value, ?string $locale = null): void { - $locale = $locale ?: $this->getDefaultLocale(); + $locale ??= $this->getDefaultLocale(); $this->translations[$locale] = $value; } public function isTranslatedInto(string $locale): bool { - return isset($this->translations[$locale]) && !empty($this->translations[$locale]); + return isset($this->translations[$locale]) && (string) $this->translations[$locale] !== ''; } public function __toString(): string diff --git a/src/TranslatableChain.php b/src/TranslatableChain.php index 09cec48..e77cec3 100644 --- a/src/TranslatableChain.php +++ b/src/TranslatableChain.php @@ -61,6 +61,11 @@ public function translate(?string $locale = null): mixed $c = $this->comparator; foreach ($this->translatables as $translation) { $value = $translation->translate($locale); + + if (null === $value) { + continue; + } + if ($c($value)) { return $value; } diff --git a/src/TranslatableInterface.php b/src/TranslatableInterface.php index 8e7b191..ff58827 100644 --- a/src/TranslatableInterface.php +++ b/src/TranslatableInterface.php @@ -21,7 +21,7 @@ interface TranslatableInterface * * @param string|null $locale The target locale or null for the current locale. * - * @return TTranslatedValue The translation or null if not available. + * @return TTranslatedValue|null The translation or null if not available. */ public function translate(?string $locale = null): mixed; From 74212d1528bfe4fc47c98845745e9cdcb7da6d53 Mon Sep 17 00:00:00 2001 From: Martin Gold Date: Tue, 29 Oct 2024 17:46:50 +0100 Subject: [PATCH 3/4] chore: run PHPStan in CI --- .github/workflows/phpstan.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/phpstan.yml diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..cea2c31 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,28 @@ +# Update this by running +# curl https://gist.githubusercontent.com/mpdude/ca93a185bcbf56eb7e341632ad4f8263/raw/fix-cs-php.yml > .github/workflows/fix-cs-php.yml + +on: + push: + branches: ['master'] + pull_request: + branches: ['*'] + +name: Coding Standards + +jobs: + phpstan: + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@main + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + + - name: Install Composer dependencies + run: composer install --no-progress --prefer-dist + + - name: Run PHPStan + run: composer run phpstan \ No newline at end of file From 8f3745434ffd5f35ea479161925e314e0de84c50 Mon Sep 17 00:00:00 2001 From: martingold Date: Tue, 29 Oct 2024 17:49:52 +0000 Subject: [PATCH 4/4] Fix CS with PHP-CS-Fixer --- src/Doctrine/PersistentTranslatable.php | 14 ++++----- src/Doctrine/PolyglotListener.php | 7 ++--- src/Doctrine/TranslatableClassMetadata.php | 29 +++++++++---------- src/Doctrine/TranslatableStringType.php | 4 +-- src/Entity/BaseTranslation.php | 4 +-- src/Exception/ShouldNotHappen.php | 2 +- src/Translatable.php | 2 +- src/TranslatableChain.php | 6 ++-- .../Fixtures/Entity/TestEntityTranslation.php | 8 ++--- 9 files changed, 36 insertions(+), 40 deletions(-) diff --git a/src/Doctrine/PersistentTranslatable.php b/src/Doctrine/PersistentTranslatable.php index cff00b7..e3666b9 100644 --- a/src/Doctrine/PersistentTranslatable.php +++ b/src/Doctrine/PersistentTranslatable.php @@ -120,7 +120,7 @@ public function eject(): void $type = $this->translatedProperty->getType(); if ($type instanceof ReflectionNamedType && TranslatableInterface::class === $type->getName() && \is_string($value)) { - if ($this->valueForEjection === null || $this->valueForEjection->getPrimaryValue() !== $value) { + if (null === $this->valueForEjection || $this->valueForEjection->getPrimaryValue() !== $value) { $this->valueForEjection = new UninitializedPersistentTranslatable($value); } $value = $this->valueForEjection; @@ -173,7 +173,7 @@ public function setTranslation(mixed $value, ?string $locale = null): void $this->setPrimaryValue($value); } else { $entity = $this->getTranslationEntity($locale); - if ($entity === null) { + if (null === $entity) { $entity = $this->createTranslationEntity($locale); } $this->translationProperty->setValue($entity, $value); @@ -193,7 +193,7 @@ public function translate(?string $locale = null): mixed } $entity = $this->getTranslationEntity($locale); - if ($entity !== null) { + if (null !== $entity) { $translated = $this->translationProperty->getValue($entity); if (null !== $translated) { return $translated; @@ -215,12 +215,12 @@ public function translate(?string $locale = null): mixed public function isTranslatedInto(string $locale): bool { if ($locale === $this->primaryLocale) { - return $this->primaryValue !== '' && $this->primaryValue !== null; + return '' !== $this->primaryValue && null !== $this->primaryValue; } $entity = $this->getTranslationEntity($locale); - return $entity !== null && $this->translationProperty->getValue($entity) !== null; + return null !== $entity && null !== $this->translationProperty->getValue($entity); } public function __toString(): string @@ -258,7 +258,7 @@ private function cacheTranslation(string $locale): void $translationInLocale = ($translationsFilteredByLocale->count() > 0) ? $translationsFilteredByLocale->first() : null; - if (is_bool($translationInLocale)) { + if (\is_bool($translationInLocale)) { return; } @@ -282,7 +282,7 @@ private function stringifyException(Throwable $e): string { $exceptionAsString = ''; while (null !== $e) { - if ($exceptionAsString !== '') { + if ('' !== $exceptionAsString) { $exceptionAsString .= \PHP_EOL.'Previous exception: '.\PHP_EOL; } $exceptionAsString .= \sprintf( diff --git a/src/Doctrine/PolyglotListener.php b/src/Doctrine/PolyglotListener.php index 4b3bb50..fea14ac 100644 --- a/src/Doctrine/PolyglotListener.php +++ b/src/Doctrine/PolyglotListener.php @@ -15,7 +15,6 @@ use Doctrine\ORM\Event\PreFlushEventArgs; use Doctrine\Persistence\Event\LifecycleEventArgs; use Doctrine\Persistence\Mapping\RuntimeReflectionService; -use Doctrine\Persistence\ObjectManager; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use WeakReference; @@ -127,7 +126,7 @@ private function getTranslationMetadatas(object $entity, EntityManagerInterface foreach (array_merge([$classMetadata->name], $classMetadata->parentClasses) as $className) { $tm = $this->loadTranslationMetadataForClass($className, $em); - if ($tm !== null) { + if (null !== $tm) { $this->translatableClassMetadatasByClass[$class][] = $tm; } } @@ -150,7 +149,7 @@ private function loadTranslationMetadataForClass(string $className, EntityManage $cache = $em->getConfiguration()->getMetadataCache(); $cacheKey = $this->getCacheKey($className); - if ($cache !== null && $cache->hasItem($cacheKey)) { + if (null !== $cache && $cache->hasItem($cacheKey)) { $item = $cache->getItem($cacheKey); /** @var SerializedTranslatableClassMetadata|null $data */ $data = $item->get(); @@ -176,7 +175,7 @@ private function loadTranslationMetadataForClass(string $className, EntityManage } // Save if cache driver available - if ($cache !== null) { + if (null !== $cache) { $item = $cache->getItem($cacheKey); $item->set($meta?->sleep()); $cache->save($item); diff --git a/src/Doctrine/TranslatableClassMetadata.php b/src/Doctrine/TranslatableClassMetadata.php index c5d49bd..9b3bfe8 100644 --- a/src/Doctrine/TranslatableClassMetadata.php +++ b/src/Doctrine/TranslatableClassMetadata.php @@ -62,7 +62,7 @@ final class TranslatableClassMetadata private ?ReflectionProperty $translationLocaleProperty = null; /** - * @var ReflectionClass|null $translationClass + * @var ReflectionClass|null */ private ?ReflectionClass $translationClass = null; @@ -75,9 +75,9 @@ final class TranslatableClassMetadata /** * @param class-string $class The FQCN for the entity class whose translatable fields are described by this - * TranslatableClassMetadata instance. If the class has base entity classes (or mapped - * superclasses), a separate instance of TranslatableClassMetadata will be used for - * their fields. + * TranslatableClassMetadata instance. If the class has base entity classes (or mapped + * superclasses), a separate instance of TranslatableClassMetadata will be used for + * their fields. */ private function __construct( private readonly string $class @@ -112,23 +112,23 @@ public function setLogger(?LoggerInterface $logger = null): void public function sleep(): SerializedTranslatableClassMetadata { - if ($this->translationClass === null) { + if (null === $this->translationClass) { throw new ShouldNotHappen('translationClass cannot be null'); } - if ($this->primaryLocale === null) { + if (null === $this->primaryLocale) { throw new ShouldNotHappen('primaryLocale cannot be null'); } - if ($this->translationLocaleProperty === null) { + if (null === $this->translationLocaleProperty) { throw new ShouldNotHappen('translationLocaleProperty cannot be null'); } - if ($this->translationMappingProperty === null) { + if (null === $this->translationMappingProperty) { throw new ShouldNotHappen('translationMappingProperty cannot be null'); } - if ($this->translationsCollectionProperty === null) { + if (null === $this->translationsCollectionProperty) { throw new ShouldNotHappen('translationsCollectionProperty cannot be null'); } @@ -164,7 +164,6 @@ public static function wakeup(SerializedTranslatableClassMetadata $data, Runtime $self->primaryLocale = $data->primaryLocale; $self->translationClass = $reflectionService->getClass($data->translationClass); - foreach ($data->translationFieldMapping as $fieldname => $property) { $self->translationFieldMapping[$fieldname] = $reflectionService->getAccessibleProperty(...$property) ?? throw new ShouldNotHappen("Cannot get reflection on {$property[0]}::{$property[1]}"); @@ -218,7 +217,7 @@ private function assertAttributesAreComplete(string $class): void */ private function findTranslatedProperties(ClassMetadata $cm, ClassMetadataFactory $classMetadataFactory): void { - if ($this->translationClass === null) { + if (null === $this->translationClass) { return; } @@ -234,13 +233,13 @@ private function findTranslatedProperties(ClassMetadata $cm, ClassMetadataFactor already contains that declaration, we need not include it. */ $declaringClass = $reflectionProperty->getDeclaringClass()->name; - if ($declaringClass !== $cm->name && $cm->parentClasses !== [] && is_a($cm->parentClasses[0], $declaringClass, true)) { + if ($declaringClass !== $cm->name && [] !== $cm->parentClasses && is_a($cm->parentClasses[0], $declaringClass, true)) { continue; } $attributes = $reflectionProperty->getAttributes(Attribute\Translatable::class); - if ($attributes === []) { + if ([] === $attributes) { continue; } @@ -268,7 +267,7 @@ private function findTranslationsCollection(ClassMetadata $cm, ClassMetadataFact $reflectionProperty = $cm->getReflectionProperty($fieldName); - if ($reflectionProperty !== null && $reflectionProperty->getAttributes(Attribute\TranslationCollection::class) !== []) { + if (null !== $reflectionProperty && [] !== $reflectionProperty->getAttributes(Attribute\TranslationCollection::class)) { if (!$mapping instanceof InverseSideMapping) { return; } @@ -309,7 +308,7 @@ private function parseTranslationsEntity(ClassMetadata $cm): void foreach ($cm->fieldMappings as $fieldName => $mapping) { $reflectionProperty = $cm->getReflectionProperty($fieldName); - if ($reflectionProperty !== null && $reflectionProperty->getAttributes(Attribute\Locale::class) !== []) { + if (null !== $reflectionProperty && [] !== $reflectionProperty->getAttributes(Attribute\Locale::class)) { $this->translationLocaleProperty = $reflectionProperty; return; diff --git a/src/Doctrine/TranslatableStringType.php b/src/Doctrine/TranslatableStringType.php index 722cc26..624b220 100644 --- a/src/Doctrine/TranslatableStringType.php +++ b/src/Doctrine/TranslatableStringType.php @@ -18,7 +18,7 @@ class TranslatableStringType extends Type public function getSQLDeclaration(array $column, AbstractPlatform $platform): string { - if (isset($column['options']) && is_array($column['options']) && ($column['options']['use_text_column'] ?? false)) { + if (isset($column['options']) && \is_array($column['options']) && ($column['options']['use_text_column'] ?? false)) { return $platform->getClobTypeDeclarationSQL($column); } @@ -51,7 +51,7 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?str public function convertToPHPValue($value, AbstractPlatform $platform): UninitializedPersistentTranslatable { - if (!is_string($value)) { + if (!\is_string($value)) { throw new ShouldNotHappen('Translated value is not string.'); } diff --git a/src/Entity/BaseTranslation.php b/src/Entity/BaseTranslation.php index 1ae54ae..bf00708 100644 --- a/src/Entity/BaseTranslation.php +++ b/src/Entity/BaseTranslation.php @@ -35,7 +35,7 @@ class BaseTranslation */ #[Polyglot\Locale] #[ORM\Column] - protected string|null $locale; + protected ?string $locale; /** * @ORM\JoinColumn(name="entity_id", referencedColumnName="id", nullable=false) @@ -43,7 +43,7 @@ class BaseTranslation #[ORM\JoinColumn(name: 'entity_id', referencedColumnName: 'id', nullable: false)] protected object $entity; - public function getLocale(): string|null + public function getLocale(): ?string { return $this->locale; } diff --git a/src/Exception/ShouldNotHappen.php b/src/Exception/ShouldNotHappen.php index b42cc4f..bed1d2a 100644 --- a/src/Exception/ShouldNotHappen.php +++ b/src/Exception/ShouldNotHappen.php @@ -9,7 +9,7 @@ class ShouldNotHappen extends Exception { - public function __construct(string $message, Throwable|null $previous = null) + public function __construct(string $message, ?Throwable $previous = null) { parent::__construct($message, 0, $previous); } diff --git a/src/Translatable.php b/src/Translatable.php index 61de4a2..007f6c8 100644 --- a/src/Translatable.php +++ b/src/Translatable.php @@ -94,7 +94,7 @@ public function setTranslation(mixed $value, ?string $locale = null): void public function isTranslatedInto(string $locale): bool { - return isset($this->translations[$locale]) && (string) $this->translations[$locale] !== ''; + return isset($this->translations[$locale]) && '' !== (string) $this->translations[$locale]; } public function __toString(): string diff --git a/src/TranslatableChain.php b/src/TranslatableChain.php index e77cec3..affb7cb 100644 --- a/src/TranslatableChain.php +++ b/src/TranslatableChain.php @@ -21,7 +21,7 @@ final class TranslatableChain implements TranslatableInterface private array $translatables; /** - * @param TranslatableInterface ...$translatables + * @param TranslatableInterface ...$translatables * @return self */ public static function firstNonEmpty(TranslatableInterface ...$translatables): self @@ -32,7 +32,7 @@ public static function firstNonEmpty(TranslatableInterface ...$translatables): s } /** - * @param TranslatableInterface ...$translatables + * @param TranslatableInterface ...$translatables * @return self */ public static function firstTranslation(TranslatableInterface ...$translatables): self @@ -43,7 +43,7 @@ public static function firstTranslation(TranslatableInterface ...$translatables) } /** - * @param Closure(T): bool $comparator + * @param Closure(T): bool $comparator * @param TranslatableInterface ...$translatables */ private function __construct( diff --git a/tests/Fixtures/Entity/TestEntityTranslation.php b/tests/Fixtures/Entity/TestEntityTranslation.php index d222118..4384786 100644 --- a/tests/Fixtures/Entity/TestEntityTranslation.php +++ b/tests/Fixtures/Entity/TestEntityTranslation.php @@ -28,18 +28,16 @@ class TestEntityTranslation extends BaseTranslation * Contains the translation. * * Must be protected to be usable when this class is used as base for a mock. - * - * @var string */ #[ORM\Column(type: 'string')] - protected string|null $text; + protected ?string $text; - public function __construct(string|null $locale = null, string|null $text = null, ?TestEntity $entity = null) + public function __construct(?string $locale = null, ?string $text = null, ?TestEntity $entity = null) { $this->locale = $locale; $this->text = $text; - if ($entity !== null) { + if (null !== $entity) { $this->entity = $entity; } }