From 97a317ba3349586cc1f05eb92453aa807e1abfa1 Mon Sep 17 00:00:00 2001 From: Sylvain Fabre Date: Fri, 13 Feb 2026 15:54:18 +0100 Subject: [PATCH 1/6] Upgrade PHPStan to level 8, fix type issues, apply Rector fixes Co-Authored-By: Claude Opus 4.6 --- phpstan-baseline.neon | 9 ++++++- phpstan.neon.dist | 1 - src/Entity/Log.php | 2 +- src/Exception/UnsupportObjectException.php | 28 +++++++++++----------- src/Factory/LogDataFactory.php | 13 +++++++--- src/Serializer/LogSerializer.php | 8 +++---- tests/Factory/LogDataFactoryTest.php | 3 +++ tests/Functional/Entity/Address.php | 1 + tests/Functional/Entity/FunctionalLog.php | 1 + tests/Functional/Entity/Post.php | 4 +++- tests/Functional/Entity/Tag.php | 1 + tests/Serializer/LogSerializerTest.php | 5 +++- tests/Subscriber/LoggerSubscriberTest.php | 2 +- 13 files changed, 51 insertions(+), 27 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e76475b..b494d8d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,6 +1,13 @@ parameters: ignoreErrors: - - message: "#^Throwing checked exception InvalidArgumentException in yielding method is denied as it gets thrown upon Generator iteration$#" + message: '#^Call to an undefined method object\:\:getId\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Serializer/LogSerializer.php + + - + message: '#^Throwing checked exception InvalidArgumentException in yielding method is denied as it gets thrown upon Generator iteration$#' + identifier: shipmonk.checkedExceptionInYieldingMethod count: 2 path: tests/Serializer/LogSerializerTest.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 381c1b2..c65ef82 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,4 @@ parameters: - level: 5 paths: - src/ - tests/ diff --git a/src/Entity/Log.php b/src/Entity/Log.php index df4d3a8..cb46968 100755 --- a/src/Entity/Log.php +++ b/src/Entity/Log.php @@ -23,7 +23,7 @@ public function __construct( ) { $this->entityClass = $entity::class; $this->entityColumn = $entityColumn; - $this->entityOldValue = mb_substr($entityOldValue, 0, Log::MAX_STRING_LENGTH); + $this->entityOldValue = $entityOldValue !== null ? mb_substr($entityOldValue, 0, Log::MAX_STRING_LENGTH) : null; $this->requestTrace = $requestTrace; $this->createdAt = new DateTimeImmutable(); } diff --git a/src/Exception/UnsupportObjectException.php b/src/Exception/UnsupportObjectException.php index 2c472f5..1e5c696 100644 --- a/src/Exception/UnsupportObjectException.php +++ b/src/Exception/UnsupportObjectException.php @@ -1,14 +1,14 @@ - $includedEntities + * @param list $excludedEntities + */ public function __construct( private readonly LogSerializer $formatter, private readonly array $includedEntities, @@ -54,7 +58,7 @@ public function createFromEvent(OnFlushEventArgs $eventArgs): iterable } } - private function isLoggable($entity): bool + private function isLoggable(object $entity): bool { if ($this->isSubClassFromList($entity, $this->excludedEntities)) { return false; @@ -63,7 +67,10 @@ private function isLoggable($entity): bool return [] === $this->includedEntities || $this->isSubClassFromList($entity, $this->includedEntities); } - private function isSubClassFromList($entity, array $classes): bool + /** + * @param list $classes + */ + private function isSubClassFromList(object $entity, array $classes): bool { foreach ($classes as $class) { if (is_a($entity, $class)) { @@ -75,7 +82,7 @@ private function isSubClassFromList($entity, array $classes): bool } /** @return LogData[] */ - private function getLogsForEntityFields($entity, EntityManagerInterface $entityManager): iterable + private function getLogsForEntityFields(object $entity, EntityManagerInterface $entityManager): iterable { $unitOfWork = $entityManager->getUnitOfWork(); diff --git a/src/Serializer/LogSerializer.php b/src/Serializer/LogSerializer.php index 0f5db0d..bc8ec05 100755 --- a/src/Serializer/LogSerializer.php +++ b/src/Serializer/LogSerializer.php @@ -47,12 +47,12 @@ public function formatEntity(EntityManagerInterface $entityManager, object $enti } } - return json_encode($data, JSON_PRETTY_PRINT); + return (string) json_encode($data, JSON_PRETTY_PRINT); } - public function formatValueAsString($value): string + public function formatValueAsString(mixed $value): string { - return json_encode($this->formatValue($value)); + return (string) json_encode($this->formatValue($value)); } /** @@ -71,7 +71,7 @@ private function formatField(object $entity, string $field): mixed /** * Returns a formatted value depending on the given value's type. */ - private function formatValue($value): mixed + private function formatValue(mixed $value): mixed { return match (gettype($value)) { 'string' => mb_substr($value, 0, Log::MAX_STRING_LENGTH), diff --git a/tests/Factory/LogDataFactoryTest.php b/tests/Factory/LogDataFactoryTest.php index 95d39e6..8e4c0ba 100644 --- a/tests/Factory/LogDataFactoryTest.php +++ b/tests/Factory/LogDataFactoryTest.php @@ -19,6 +19,7 @@ class LogDataFactoryTest extends KernelTestCase public function testExcludedEntityIsIgnored(): void { $em = self::getContainer()->get(EntityManagerInterface::class); + self::assertInstanceOf(EntityManagerInterface::class, $em); $em->persist(new Author()); $em->getUnitOfWork()->computeChangeSets(); @@ -31,6 +32,7 @@ public function testExcludedEntityIsIgnored(): void public function testNewEntityIsLogged(): void { $em = self::getContainer()->get(EntityManagerInterface::class); + self::assertInstanceOf(EntityManagerInterface::class, $em); $em->persist($createdAuthor = new Author()); $em->getUnitOfWork()->computeChangeSets(); @@ -94,6 +96,7 @@ public function testUpdatedEntityIsLogged(): void private function mockEntityManager(): array { $emReal = self::getContainer()->get(EntityManagerInterface::class); + self::assertInstanceOf(EntityManagerInterface::class, $emReal); $em = $this->createMock(EntityManagerInterface::class); $unitOfWork = $this->createMock(UnitOfWork::class); diff --git a/tests/Functional/Entity/Address.php b/tests/Functional/Entity/Address.php index ab35594..e19515d 100755 --- a/tests/Functional/Entity/Address.php +++ b/tests/Functional/Entity/Address.php @@ -9,6 +9,7 @@ #[ORM\Entity] class Address extends AbstractEntity { + /** @phpstan-ignore property.unusedType */ private ?string $streetName = null; public function __construct() diff --git a/tests/Functional/Entity/FunctionalLog.php b/tests/Functional/Entity/FunctionalLog.php index c41f356..d3805bb 100755 --- a/tests/Functional/Entity/FunctionalLog.php +++ b/tests/Functional/Entity/FunctionalLog.php @@ -22,6 +22,7 @@ public function __construct( string $requestTrace, ) { $this->id = Uuid::uuid1(); + /** @phpstan-ignore method.notFound */ $this->entityId = (string) $entity->getId(); parent::__construct( $entity, diff --git a/tests/Functional/Entity/Post.php b/tests/Functional/Entity/Post.php index d21fcd9..da9c897 100755 --- a/tests/Functional/Entity/Post.php +++ b/tests/Functional/Entity/Post.php @@ -21,7 +21,7 @@ public function __construct( } #[Assert\NotBlank] - protected $title; + protected ?string $title = null; #[ORM\ManyToOne(targetEntity: Author::class, inversedBy: 'posts')] protected Author $author; @@ -31,9 +31,11 @@ public function getAuthor(): Author return $this->author; } + /** @var ArrayCollection */ #[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'posts')] protected ArrayCollection $tags; + /** @return Collection */ public function getTags(): Collection { return $this->tags; diff --git a/tests/Functional/Entity/Tag.php b/tests/Functional/Entity/Tag.php index 04cd666..7e589ec 100755 --- a/tests/Functional/Entity/Tag.php +++ b/tests/Functional/Entity/Tag.php @@ -16,6 +16,7 @@ public function __construct() $this->posts = new ArrayCollection(); } + /** @var ArrayCollection */ #[ORM\ManyToMany(Post::class, 'tags')] protected ArrayCollection $posts; diff --git a/tests/Serializer/LogSerializerTest.php b/tests/Serializer/LogSerializerTest.php index 2caa74b..0db8f62 100755 --- a/tests/Serializer/LogSerializerTest.php +++ b/tests/Serializer/LogSerializerTest.php @@ -34,8 +34,10 @@ public function testFormatEntity(): void $post->addTag($tag); $entityManager = self::getContainer()->get(EntityManagerInterface::class); + self::assertInstanceOf(EntityManagerInterface::class, $entityManager); $formatter = self::getContainer()->get(LogSerializer::class); + self::assertInstanceOf(LogSerializer::class, $formatter); self::assertSame( json_encode(array_merge( $this->helperFormatEntity($author), @@ -64,6 +66,7 @@ public function testFormatEntity(): void ); } + /** @return array */ public function helperFormatEntity(AbstractEntity $entity): array { return [ @@ -76,7 +79,7 @@ public function helperFormatEntity(AbstractEntity $entity): array /** * @dataProvider providerFormatValueAsString */ - public function testFormatValueAsStringWorks($value, $formatted): void + public function testFormatValueAsStringWorks(mixed $value, string $formatted): void { $formatter = new LogSerializer(); self::assertSame($formatted, $formatter->formatValueAsString($value)); diff --git a/tests/Subscriber/LoggerSubscriberTest.php b/tests/Subscriber/LoggerSubscriberTest.php index a64405b..17a91dc 100755 --- a/tests/Subscriber/LoggerSubscriberTest.php +++ b/tests/Subscriber/LoggerSubscriberTest.php @@ -66,7 +66,7 @@ public function testSubscriberPersistsLogs(): void $subscriber = new LoggerSubscriber( $logFactory, $logDataFactory, - realpath(__DIR__ . '/../..') + (string) realpath(__DIR__ . '/../..') ); $subscriber->onFlush($event); From e5970eb98fadc6f4e96adfc803e79c9a9beb64c3 Mon Sep 17 00:00:00 2001 From: Sylvain Fabre Date: Fri, 13 Feb 2026 17:31:44 +0100 Subject: [PATCH 2/6] Fix CRLF line endings to LF Co-Authored-By: Claude Opus 4.6 --- src/Exception/UnsupportObjectException.php | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Exception/UnsupportObjectException.php b/src/Exception/UnsupportObjectException.php index 1e5c696..257a254 100644 --- a/src/Exception/UnsupportObjectException.php +++ b/src/Exception/UnsupportObjectException.php @@ -1,14 +1,14 @@ - Date: Sun, 22 Mar 2026 10:42:00 +0100 Subject: [PATCH 3/6] Remove @phpstan-ignore annotations: fix types properly - Add setStreetName() setter to Address so ?string type is not unused - Type FunctionalLog constructor with AbstractEntity and assert in LogFactory Co-Authored-By: Claude Sonnet 4.6 --- tests/Functional/Entity/Address.php | 3 +-- tests/Functional/Entity/FunctionalLog.php | 3 +-- tests/Functional/Service/LogFactory.php | 2 ++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Functional/Entity/Address.php b/tests/Functional/Entity/Address.php index e19515d..b6d8c94 100755 --- a/tests/Functional/Entity/Address.php +++ b/tests/Functional/Entity/Address.php @@ -9,7 +9,6 @@ #[ORM\Entity] class Address extends AbstractEntity { - /** @phpstan-ignore property.unusedType */ private ?string $streetName = null; public function __construct() @@ -22,7 +21,7 @@ public function getStreetName(): ?string return $this->streetName; } - public function setStreetName(?string $streetName): self + public function setStreetName(string $streetName): self { $this->streetName = $streetName; diff --git a/tests/Functional/Entity/FunctionalLog.php b/tests/Functional/Entity/FunctionalLog.php index d3805bb..4ed95e7 100755 --- a/tests/Functional/Entity/FunctionalLog.php +++ b/tests/Functional/Entity/FunctionalLog.php @@ -16,13 +16,12 @@ class FunctionalLog extends Log protected UuidInterface $id; public function __construct( - object $entity, + AbstractEntity $entity, string $entityColumn, ?string $entityOldValue, string $requestTrace, ) { $this->id = Uuid::uuid1(); - /** @phpstan-ignore method.notFound */ $this->entityId = (string) $entity->getId(); parent::__construct( $entity, diff --git a/tests/Functional/Service/LogFactory.php b/tests/Functional/Service/LogFactory.php index bd2e7a8..5824214 100755 --- a/tests/Functional/Service/LogFactory.php +++ b/tests/Functional/Service/LogFactory.php @@ -6,6 +6,7 @@ use AssoConnect\LogBundle\Entity\Log; use AssoConnect\LogBundle\Factory\LogFactoryInterface; +use AssoConnect\LogBundle\Tests\Functional\Entity\AbstractEntity; use AssoConnect\LogBundle\Tests\Functional\Entity\FunctionalLog; class LogFactory implements LogFactoryInterface @@ -16,6 +17,7 @@ public function createLogFromEntity( ?string $entityOldValue, string $requestTrace, ): Log { + assert($entity instanceof AbstractEntity); return new FunctionalLog( $entity, $entityColumn, From 9e1c4d1df5f5f4a6d4437b006520b584dce1c3a4 Mon Sep 17 00:00:00 2001 From: Sylvain Fabre Date: Sun, 22 Mar 2026 10:48:59 +0100 Subject: [PATCH 4/6] ci: retrigger From 23349dea9e36090069fcdcd10746c1f3c9e61705 Mon Sep 17 00:00:00 2001 From: Sylvain Fabre Date: Sun, 22 Mar 2026 11:00:28 +0100 Subject: [PATCH 5/6] Use Safe\json_encode and Safe\realpath instead of casting Co-Authored-By: Claude Sonnet 4.6 --- composer.json | 3 ++- src/Serializer/LogSerializer.php | 6 ++++-- tests/Subscriber/LoggerSubscriberTest.php | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index f00efdc..591b40f 100755 --- a/composer.json +++ b/composer.json @@ -48,7 +48,8 @@ "moneyphp/money": "^3.2|^4.0", "ext-json": "*", "ramsey/uuid": "^4.3", - "ramsey/uuid-doctrine": "^1.4" + "ramsey/uuid-doctrine": "^1.4", + "thecodingmachine/safe": "^3.4" }, "config": { "allow-plugins": { diff --git a/src/Serializer/LogSerializer.php b/src/Serializer/LogSerializer.php index bc8ec05..ba808db 100755 --- a/src/Serializer/LogSerializer.php +++ b/src/Serializer/LogSerializer.php @@ -12,6 +12,8 @@ use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessor; +use function Safe\json_encode; + class LogSerializer { // Maximum number of associations to log in order to avoid column oversize @@ -47,12 +49,12 @@ public function formatEntity(EntityManagerInterface $entityManager, object $enti } } - return (string) json_encode($data, JSON_PRETTY_PRINT); + return json_encode($data, JSON_PRETTY_PRINT); } public function formatValueAsString(mixed $value): string { - return (string) json_encode($this->formatValue($value)); + return json_encode($this->formatValue($value)); } /** diff --git a/tests/Subscriber/LoggerSubscriberTest.php b/tests/Subscriber/LoggerSubscriberTest.php index 17a91dc..7eaf833 100755 --- a/tests/Subscriber/LoggerSubscriberTest.php +++ b/tests/Subscriber/LoggerSubscriberTest.php @@ -18,6 +18,8 @@ use Doctrine\ORM\UnitOfWork; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use function Safe\realpath; + class LoggerSubscriberTest extends KernelTestCase { public function testEventSubscriptionAsAnAttribute(): void @@ -66,7 +68,7 @@ public function testSubscriberPersistsLogs(): void $subscriber = new LoggerSubscriber( $logFactory, $logDataFactory, - (string) realpath(__DIR__ . '/../..') + realpath(__DIR__ . '/../..') ); $subscriber->onFlush($event); From 13c313d00f22186961253d4fc525d3b696f48f85 Mon Sep 17 00:00:00 2001 From: Sylvain Fabre Date: Sun, 22 Mar 2026 13:57:23 +0100 Subject: [PATCH 6/6] Rename UnsupportObjectException to UnsupportedObjectException Co-Authored-By: Claude Sonnet 4.6 --- ...portObjectException.php => UnsupportedObjectException.php} | 2 +- src/Serializer/LogSerializer.php | 4 ++-- tests/Serializer/LogSerializerTest.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/Exception/{UnsupportObjectException.php => UnsupportedObjectException.php} (84%) diff --git a/src/Exception/UnsupportObjectException.php b/src/Exception/UnsupportedObjectException.php similarity index 84% rename from src/Exception/UnsupportObjectException.php rename to src/Exception/UnsupportedObjectException.php index 257a254..b60413f 100644 --- a/src/Exception/UnsupportObjectException.php +++ b/src/Exception/UnsupportedObjectException.php @@ -4,7 +4,7 @@ namespace AssoConnect\LogBundle\Exception; -class UnsupportObjectException extends \DomainException +class UnsupportedObjectException extends \DomainException { public function __construct(object $object, int $code = 0, ?\Throwable $previous = null) { diff --git a/src/Serializer/LogSerializer.php b/src/Serializer/LogSerializer.php index ba808db..7fce797 100755 --- a/src/Serializer/LogSerializer.php +++ b/src/Serializer/LogSerializer.php @@ -5,7 +5,7 @@ namespace AssoConnect\LogBundle\Serializer; use AssoConnect\LogBundle\Entity\Log; -use AssoConnect\LogBundle\Exception\UnsupportObjectException; +use AssoConnect\LogBundle\Exception\UnsupportedObjectException; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\EntityManagerInterface; use Money\Money; @@ -124,6 +124,6 @@ private function formatObject(object $value): mixed return $value->__toString(); } - throw new UnsupportObjectException($value); + throw new UnsupportedObjectException($value); } } diff --git a/tests/Serializer/LogSerializerTest.php b/tests/Serializer/LogSerializerTest.php index 0db8f62..85d0e59 100755 --- a/tests/Serializer/LogSerializerTest.php +++ b/tests/Serializer/LogSerializerTest.php @@ -5,7 +5,7 @@ namespace AssoConnect\LogBundle\Tests\Serializer; use AssoConnect\LogBundle\Entity\Log; -use AssoConnect\LogBundle\Exception\UnsupportObjectException; +use AssoConnect\LogBundle\Exception\UnsupportedObjectException; use AssoConnect\LogBundle\Serializer\LogSerializer; use AssoConnect\LogBundle\Tests\Functional\Entity\AbstractEntity; use AssoConnect\LogBundle\Tests\Functional\Entity\Author; @@ -132,7 +132,7 @@ public function testUnsupportedObjectThrowsAnException(): void { $formatter = new LogSerializer(); - $this->expectException(UnsupportObjectException::class); + $this->expectException(UnsupportedObjectException::class); $formatter->formatValueAsString(new ObjectWithoutId()); }