From ccb94c8a139c5e76ae85221f484d3ebdd90b32b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 19 Sep 2023 23:52:11 +0200 Subject: [PATCH] Fixed problem that all properties in snake_case style were considered readOnly by API Platform --- src/Entity/Base/TimestampTrait.php | 3 + .../SnakeCasePropertyAccessExtractor.php | 71 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/Services/SnakeCasePropertyAccessExtractor.php diff --git a/src/Entity/Base/TimestampTrait.php b/src/Entity/Base/TimestampTrait.php index 93e58cb7..e6fac441 100644 --- a/src/Entity/Base/TimestampTrait.php +++ b/src/Entity/Base/TimestampTrait.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Entity\Base; +use ApiPlatform\Metadata\ApiProperty; use Doctrine\DBAL\Types\Types; use DateTime; use Doctrine\ORM\Mapping as ORM; @@ -36,6 +37,7 @@ trait TimestampTrait * @var \DateTimeInterface|null the date when this element was modified the last time */ #[Groups(['extended', 'full'])] + #[ApiProperty(writable: false)] #[ORM\Column(name: 'last_modified', type: Types::DATETIME_MUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])] protected ?\DateTimeInterface $lastModified = null; @@ -43,6 +45,7 @@ trait TimestampTrait * @var \DateTimeInterface|null the date when this element was created */ #[Groups(['extended', 'full'])] + #[ApiProperty(writable: false)] #[ORM\Column(name: 'datetime_added', type: Types::DATETIME_MUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])] protected ?\DateTimeInterface $addedDate = null; diff --git a/src/Services/SnakeCasePropertyAccessExtractor.php b/src/Services/SnakeCasePropertyAccessExtractor.php new file mode 100644 index 00000000..bfe82c63 --- /dev/null +++ b/src/Services/SnakeCasePropertyAccessExtractor.php @@ -0,0 +1,71 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services; + +use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; + +/** + * Workaround for using snake_case properties with ReflectionExtractor until this PR is merged: + * https://github.com/symfony/symfony/pull/51697 + */ +#[AsTaggedItem('property_info.access_extractor', priority: 0)] +class SnakeCasePropertyAccessExtractor implements PropertyAccessExtractorInterface +{ + + public function __construct(#[Autowire(service: 'property_info.reflection_extractor')] + private readonly PropertyAccessExtractorInterface $reflectionExtractor) + { + //$this->reflectionExtractor = new ReflectionExtractor(); + } + + public function isReadable(string $class, string $property, array $context = []) + { + //Null means skip this extractor + return null; + } + + /** + * Camelizes a given string. + */ + private function camelize(string $string): string + { + return str_replace(' ', '', ucwords(str_replace('_', ' ', $string))); + } + + + public function isWritable(string $class, string $property, array $context = []) + { + //Check writeablity using a camelized property name + $isWriteable = $this->reflectionExtractor->isWritable($class, $this->camelize($property), $context); + //If we found a writeable property that way, return true + if ($isWriteable === true) { + return true; + } + + //Otherwise skip this extractor + return null; + } +} \ No newline at end of file