diff --git a/CHANGELOG.md b/CHANGELOG.md
index b029c14..e935a80 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
* perf: Optimize `guessTypeFromVariable`
* perf: Optimize `Context` & `ObjectCache`
* perf: Change 'simpletypes' cache to array.
+* perf: Use our `PropertyAccessLite` instead of Symfony's.
## 0.5.14
diff --git a/config/services.php b/config/services.php
index 61a2632..8a93a1f 100644
--- a/config/services.php
+++ b/config/services.php
@@ -23,6 +23,7 @@
use Rekalogika\Mapper\MethodMapper\ClassMethodTransformer;
use Rekalogika\Mapper\MethodMapper\SubMapper;
use Rekalogika\Mapper\ObjectCache\ObjectCacheFactory;
+use Rekalogika\Mapper\PropertyAccessLite\PropertyAccessLite;
use Rekalogika\Mapper\Transformer\ArrayToObjectTransformer;
use Rekalogika\Mapper\Transformer\CopyTransformer;
use Rekalogika\Mapper\Transformer\DateTimeTransformer;
@@ -138,7 +139,7 @@
$services
->set(ObjectToObjectTransformer::class)
->args([
- '$propertyAccessor' => service('property_accessor'),
+ '$propertyAccessor' => service('rekalogika.mapper.property_access_lite'),
'$typeResolver' => service('rekalogika.mapper.type_resolver'),
'$objectMappingResolver' => service('rekalogika.mapper.object_mapping_resolver'),
])
@@ -174,6 +175,11 @@
])
->tag('kernel.cache_warmer');
+ # propertyaccess lite
+
+ $services
+ ->set('rekalogika.mapper.property_access_lite', PropertyAccessLite::class);
+
# type resolver
$services
diff --git a/psalm.xml b/psalm.xml
index c297242..1a7513e 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -46,5 +46,10 @@
+
+
+
+
+
diff --git a/src/MapperFactory/MapperFactory.php b/src/MapperFactory/MapperFactory.php
index 0d9ac00..d4b7977 100644
--- a/src/MapperFactory/MapperFactory.php
+++ b/src/MapperFactory/MapperFactory.php
@@ -27,6 +27,7 @@
use Rekalogika\Mapper\MethodMapper\SubMapper;
use Rekalogika\Mapper\ObjectCache\ObjectCacheFactory;
use Rekalogika\Mapper\ObjectCache\ObjectCacheFactoryInterface;
+use Rekalogika\Mapper\PropertyAccessLite\PropertyAccessLite;
use Rekalogika\Mapper\Transformer\ArrayToObjectTransformer;
use Rekalogika\Mapper\Transformer\Contracts\TransformerInterface;
use Rekalogika\Mapper\Transformer\CopyTransformer;
@@ -49,8 +50,6 @@
use Rekalogika\Mapper\TypeResolver\TypeResolverInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Console\Application;
-use Symfony\Component\PropertyAccess\PropertyAccess;
-use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
@@ -58,7 +57,9 @@
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
+use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
+use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer;
@@ -114,7 +115,7 @@ public function __construct(
private array $additionalTransformers = [],
private ?ReflectionExtractor $reflectionExtractor = null,
private ?PhpStanExtractor $phpStanExtractor = null,
- private ?PropertyAccessor $propertyAccessor = null,
+ private ?PropertyAccessorInterface $propertyAccessor = null,
private ?NormalizerInterface $normalizer = null,
private ?DenormalizerInterface $denormalizer = null,
?CacheItemPoolInterface $propertyInfoExtractorCache = null,
@@ -188,7 +189,7 @@ private function getPropertyInfoExtractor(): PropertyInfoExtractorInterface&Prop
private function getConcretePropertyAccessor(): PropertyAccessorInterface
{
if (null === $this->propertyAccessor) {
- $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
+ $this->propertyAccessor = new PropertyAccessLite();
}
return $this->propertyAccessor;
@@ -258,9 +259,9 @@ protected function getObjectToObjectTransformer(): TransformerInterface
{
if (null === $this->objectToObjectTransformer) {
$this->objectToObjectTransformer = new ObjectToObjectTransformer(
- $this->getPropertyAccessor(),
- $this->getTypeResolver(),
- $this->getObjectMappingResolver(),
+ propertyAccessor: $this->getPropertyAccessor(),
+ typeResolver: $this->getTypeResolver(),
+ objectMappingResolver: $this->getObjectMappingResolver(),
);
}
diff --git a/src/PropertyAccessLite/PropertyAccessLite.php b/src/PropertyAccessLite/PropertyAccessLite.php
new file mode 100644
index 0000000..24b518f
--- /dev/null
+++ b/src/PropertyAccessLite/PropertyAccessLite.php
@@ -0,0 +1,146 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE file
+ * that was distributed with this source code.
+ */
+
+namespace Rekalogika\Mapper\PropertyAccessLite;
+
+use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
+use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
+use Symfony\Component\PropertyAccess\PropertyPathInterface;
+
+final class PropertyAccessLite implements PropertyAccessorInterface
+{
+ /**
+ * @param object|array $objectOrArray
+ */
+ public function getValue(
+ object|array $objectOrArray,
+ string|PropertyPathInterface $propertyPath,
+ ): mixed {
+ assert(is_string($propertyPath));
+ assert(\is_object($objectOrArray));
+
+ $getter = 'get' . ucfirst($propertyPath);
+
+ try {
+ /** @psalm-suppress MixedMethodCall */
+ return $objectOrArray->{$getter}();
+ } catch (\Throwable $e) {
+ if (!\str_starts_with($e->getMessage(), 'Call to undefined method')) {
+ if (\str_starts_with($e->getMessage(), 'Typed property')) {
+ throw new UninitializedPropertyException(\sprintf(
+ 'Property "%s" is not initialized in object "%s"',
+ $propertyPath,
+ \get_class($objectOrArray),
+ ), 0, $e);
+ }
+
+ throw $e;
+ }
+ }
+
+ try {
+ return $objectOrArray->{$propertyPath};
+ // @phpstan-ignore-next-line
+ } catch (\Throwable $e) {
+ if (\str_starts_with($e->getMessage(), 'Typed property')) {
+ throw new UninitializedPropertyException(\sprintf(
+ 'Property "%s" is not initialized in object "%s"',
+ $propertyPath,
+ \get_class($objectOrArray),
+ ), 0, $e);
+ } elseif (\str_starts_with($e->getMessage(), 'Cannot access private property')) {
+ throw new NoSuchPropertyException(\sprintf(
+ 'Property "%s" is not public in object "%s"',
+ $propertyPath,
+ \get_class($objectOrArray),
+ ), 0, $e);
+ } elseif (\str_starts_with($e->getMessage(), 'Undefined property')) {
+ throw new NoSuchPropertyException(\sprintf(
+ 'Property "%s" is not defined in object "%s"',
+ $propertyPath,
+ \get_class($objectOrArray),
+ ), 0, $e);
+ }
+
+ throw $e;
+ }
+ }
+
+ /**
+ * @param object|array $objectOrArray
+ */
+ public function setValue(
+ object|array &$objectOrArray,
+ string|PropertyPathInterface $propertyPath,
+ mixed $value
+ ): void {
+ assert(is_string($propertyPath));
+ assert(\is_object($objectOrArray));
+
+ $setter = 'set' . ucfirst($propertyPath);
+
+ try {
+ /** @psalm-suppress MixedMethodCall */
+ $objectOrArray->{$setter}($value);
+ return;
+ } catch (\Throwable $e) {
+ if (!\str_starts_with($e->getMessage(), 'Call to undefined method')) {
+ throw $e;
+ }
+ }
+
+ try {
+ if (!\property_exists($objectOrArray, $propertyPath)) {
+ throw new NoSuchPropertyException(\sprintf(
+ 'Property "%s" is not defined in object "%s"',
+ $propertyPath,
+ \get_class($objectOrArray),
+ ));
+ }
+ $objectOrArray->{$propertyPath} = $value;
+ } catch (NoSuchPropertyException $e) {
+ throw $e;
+ } catch (\Throwable $e) {
+ if (\str_starts_with($e->getMessage(), 'Cannot access private property')) {
+ throw new NoSuchPropertyException(\sprintf(
+ 'Property "%s" is not public in object "%s"',
+ $propertyPath,
+ \get_class($objectOrArray),
+ ), 0, $e);
+ }
+
+ throw $e;
+ }
+ }
+
+ /**
+ * @param object|array $objectOrArray
+ */
+ public function isWritable(
+ object|array $objectOrArray,
+ string|PropertyPathInterface $propertyPath
+ ): bool {
+ throw new \RuntimeException('Not implemented');
+ }
+
+ /**
+ * @param object|array $objectOrArray
+ */
+ public function isReadable(
+ object|array $objectOrArray,
+ string|PropertyPathInterface $propertyPath
+ ): bool {
+ throw new \RuntimeException('Not implemented');
+ }
+}
diff --git a/tests/Fixtures/AccessMethods/ObjectWithVariousAccessMethods.php b/tests/Fixtures/AccessMethods/ObjectWithVariousAccessMethods.php
new file mode 100644
index 0000000..d7de27c
--- /dev/null
+++ b/tests/Fixtures/AccessMethods/ObjectWithVariousAccessMethods.php
@@ -0,0 +1,59 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE file
+ * that was distributed with this source code.
+ */
+
+namespace Rekalogika\Mapper\Tests\Fixtures\AccessMethods;
+
+class ObjectWithVariousAccessMethods
+{
+ private string $privatePropertyWithGetterSetter = 'privateProperty';
+ // @phpstan-ignore-next-line
+ private string $privatePropertyWithoutGetterSetter = 'privatePropertyWithoutGetterSetter';
+ public string $publicPropertyWithGetterSetter = 'publicProperty';
+ public string $publicPropertyWithoutGetterSetter = 'publicPropertyWithoutGetterSetter';
+
+
+ public bool $publicPropertySetterAccessed = false;
+ public bool $publicPropertyGetterAccessed = false;
+
+ public string $unsetPublicProperty;
+
+ // @phpstan-ignore-next-line
+ private string $unsetPrivatePropertyWithGetter;
+
+ public function getPrivateProperty(): string
+ {
+ return $this->privatePropertyWithGetterSetter;
+ }
+
+ public function setPrivateProperty(string $privateProperty): void
+ {
+ $this->privatePropertyWithGetterSetter = $privateProperty;
+ }
+
+ public function getPublicProperty(): string
+ {
+ $this->publicPropertyGetterAccessed = true;
+ return $this->publicPropertyWithGetterSetter;
+ }
+
+ public function setPublicProperty(string $publicProperty): void
+ {
+ $this->publicPropertySetterAccessed = true;
+ $this->publicPropertyWithGetterSetter = $publicProperty;
+ }
+
+ public function getUnsetPrivatePropertyWithGetter(): string
+ {
+ return $this->unsetPrivatePropertyWithGetter;
+ }
+}
diff --git a/tests/UnitTest/Transformer/PropertyAccessLiteTest.php b/tests/UnitTest/Transformer/PropertyAccessLiteTest.php
new file mode 100644
index 0000000..92d2046
--- /dev/null
+++ b/tests/UnitTest/Transformer/PropertyAccessLiteTest.php
@@ -0,0 +1,101 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE file
+ * that was distributed with this source code.
+ */
+
+namespace Rekalogika\Mapper\Tests\UnitTest\Transformer;
+
+use PHPUnit\Framework\TestCase;
+use Rekalogika\Mapper\PropertyAccessLite\PropertyAccessLite;
+use Rekalogika\Mapper\Tests\Fixtures\AccessMethods\ObjectWithVariousAccessMethods;
+use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
+use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
+
+class PropertyAccessLiteTest extends TestCase
+{
+ public function testGetterSetter(): void
+ {
+ $accessor = new PropertyAccessLite();
+ $object = new ObjectWithVariousAccessMethods();
+ $object2 = $object;
+
+ $accessor->setValue($object, 'publicProperty', 'foo');
+ $accessor->setValue($object, 'privateProperty', 'foo');
+
+ $this->assertSame('foo', $accessor->getValue($object, 'publicProperty'));
+ $this->assertSame('foo', $accessor->getValue($object, 'privateProperty'));
+
+ $this->assertTrue($object2->publicPropertySetterAccessed);
+ $this->assertTrue($object2->publicPropertyGetterAccessed);
+ }
+
+ public function testGetPrivateProperty(): void
+ {
+ $accessor = new PropertyAccessLite();
+ $object = new ObjectWithVariousAccessMethods();
+
+ $this->expectException(NoSuchPropertyException::class);
+
+ $accessor->getValue($object, 'privatePropertyWithoutGetterSetter');
+ }
+
+ public function testSetPrivateProperty(): void
+ {
+ $accessor = new PropertyAccessLite();
+ $object = new ObjectWithVariousAccessMethods();
+
+ $this->expectException(NoSuchPropertyException::class);
+
+ $accessor->setValue($object, 'privatePropertyWithoutGetterSetter', 'foo');
+ }
+
+ public function testPublic(): void
+ {
+ $accessor = new PropertyAccessLite();
+ $object = new ObjectWithVariousAccessMethods();
+
+ $accessor->setValue($object, 'publicPropertyWithoutGetterSetter', 'foo');
+
+ $this->assertSame('foo', $accessor->getValue($object, 'publicPropertyWithoutGetterSetter'));
+ }
+
+ public function testGetUnsetPublicProperty(): void
+ {
+ $accessor = new PropertyAccessLite();
+ $object = new ObjectWithVariousAccessMethods();
+ $this->expectException(UninitializedPropertyException::class);
+ $accessor->getValue($object, 'unsetPublicProperty');
+ }
+
+ public function testGetUnsetGetter(): void
+ {
+ $accessor = new PropertyAccessLite();
+ $object = new ObjectWithVariousAccessMethods();
+ $this->expectException(UninitializedPropertyException::class);
+ $accessor->getValue($object, 'unsetPrivatePropertyWithGetter');
+ }
+
+ public function testGetMissingProperty(): void
+ {
+ $accessor = new PropertyAccessLite();
+ $object = new ObjectWithVariousAccessMethods();
+ $this->expectException(NoSuchPropertyException::class);
+ $accessor->getValue($object, 'missingProperty');
+ }
+
+ public function testSetMissingProperty(): void
+ {
+ $accessor = new PropertyAccessLite();
+ $object = new ObjectWithVariousAccessMethods();
+ $this->expectException(NoSuchPropertyException::class);
+ $accessor->setValue($object, 'missingProperty', 'foo');
+ }
+}