diff --git a/CHANGELOG.md b/CHANGELOG.md index e42c8667..13b23546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - all rules initialize `Type` lazily (performance optimization) - `ArrayOfRule` - check during metadata parsing that default value is an array when `mergeDefaults` is enabled +- `FieldContext`, `MappedObjectContext` + - initializes `Type` lazily (performance optimization) +- `ArrayShapeType` + - `getFields()` always returns the same instances ## [0.2.0](https://github.com/orisai/object-mapper/compare/0.1.0...0.2.0) - 2024-06-22 diff --git a/src/Context/FieldContext.php b/src/Context/FieldContext.php index 1c39f035..974ab99e 100644 --- a/src/Context/FieldContext.php +++ b/src/Context/FieldContext.php @@ -2,6 +2,7 @@ namespace Orisai\ObjectMapper\Context; +use Closure; use Orisai\ObjectMapper\Meta\MetaLoader; use Orisai\ObjectMapper\Meta\Shared\DefaultValueMeta; use Orisai\ObjectMapper\Processing\Options; @@ -13,7 +14,10 @@ final class FieldContext extends BaseFieldContext { - private Type $type; + /** @var Closure(): Type */ + private Closure $typeCreator; + + private ?Type $type = null; private DefaultValueMeta $default; @@ -23,6 +27,7 @@ final class FieldContext extends BaseFieldContext private ReflectionProperty $property; /** + * @param Closure(): Type $typeCreator * @param int|string $fieldName */ public function __construct( @@ -30,7 +35,7 @@ public function __construct( RuleManager $ruleManager, Processor $processor, Options $options, - Type $type, + Closure $typeCreator, DefaultValueMeta $default, bool $initializeObjects, $fieldName, @@ -38,7 +43,7 @@ public function __construct( ) { parent::__construct($metaLoader, $ruleManager, $processor, $options, $initializeObjects); - $this->type = $type; + $this->typeCreator = $typeCreator; $this->default = $default; $this->fieldName = $fieldName; $this->property = $property; @@ -46,7 +51,14 @@ public function __construct( public function getType(): Type { - return $this->type; + if ($this->type !== null) { + return $this->type; + } + + $type = ($this->typeCreator)(); + unset($this->typeCreator); + + return $this->type = $type; } public function hasDefaultValue(): bool diff --git a/src/Context/MappedObjectContext.php b/src/Context/MappedObjectContext.php index 903b6698..eb88e54e 100644 --- a/src/Context/MappedObjectContext.php +++ b/src/Context/MappedObjectContext.php @@ -2,6 +2,7 @@ namespace Orisai\ObjectMapper\Context; +use Closure; use Orisai\ObjectMapper\Meta\MetaLoader; use Orisai\ObjectMapper\Processing\Options; use Orisai\ObjectMapper\Processing\Processor; @@ -11,24 +12,37 @@ final class MappedObjectContext extends BaseFieldContext { - private MappedObjectType $type; + /** @var Closure(): MappedObjectType */ + private Closure $typeCreator; + private ?MappedObjectType $type = null; + + /** + * @param Closure(): MappedObjectType $typeCreator + */ public function __construct( MetaLoader $metaLoader, RuleManager $ruleManager, Processor $processor, Options $options, - MappedObjectType $type, + Closure $typeCreator, bool $initializeObjects ) { parent::__construct($metaLoader, $ruleManager, $processor, $options, $initializeObjects); - $this->type = $type; + $this->typeCreator = $typeCreator; } public function getType(): MappedObjectType { - return $this->type; + if ($this->type !== null) { + return $this->type; + } + + $type = ($this->typeCreator)(); + unset($this->typeCreator); + + return $this->type = $type; } } diff --git a/src/Processing/DefaultProcessor.php b/src/Processing/DefaultProcessor.php index 8bf7c6df..9a4d686b 100644 --- a/src/Processing/DefaultProcessor.php +++ b/src/Processing/DefaultProcessor.php @@ -2,6 +2,7 @@ namespace Orisai\ObjectMapper\Processing; +use Closure; use Nette\Utils\Helpers; use Orisai\Exceptions\Logic\InvalidState; use Orisai\ObjectMapper\Args\Args; @@ -29,6 +30,7 @@ use Orisai\ObjectMapper\Rules\RuleManager; use Orisai\ObjectMapper\Types\MappedObjectType; use Orisai\ObjectMapper\Types\MessageType; +use Orisai\ObjectMapper\Types\Type; use ReflectionProperty; use function array_diff; use function array_key_exists; @@ -109,11 +111,11 @@ private function processBase($data, string $class, ?Options $options, bool $init { $options ??= new Options(); $options = $options->withProcessedClass($class); - $type = $this->createMappedObjectType($class, $options); + $typeCreator = fn (): MappedObjectType => $this->createMappedObjectType($class, $options); $meta = $this->metaCache[$class] ??= $this->metaLoader->load($class); $holder = $this->createHolder($class, $meta->getClass()); - $mappedObjectContext = $this->createMappedObjectContext($options, $type, $initializeObjects); + $mappedObjectContext = $this->createMappedObjectContext($options, $typeCreator, $initializeObjects); $callContext = $this->createProcessorRunContext($class, $meta, $holder); $processedData = $this->processData($data, $mappedObjectContext, $callContext); @@ -181,9 +183,12 @@ private function createMappedObjectType(string $class, Options $options): Mapped ); } + /** + * @param Closure(): MappedObjectType $typeCreator + */ private function createMappedObjectContext( Options $options, - MappedObjectType $type, + Closure $typeCreator, bool $initializeObjects ): MappedObjectContext { @@ -192,7 +197,7 @@ private function createMappedObjectContext( $this->ruleManager, $this, $options, - $type, + $typeCreator, $initializeObjects, ); } @@ -466,13 +471,14 @@ private function createFieldContext( ): FieldContext { $parentType = $mappedObjectContext->getType(); + $typeCreator = static fn (): Type => $parentType->getField($fieldName); return new FieldContext( $this->metaLoader, $this->ruleManager, $this, $mappedObjectContext->getOptions()->createClone(), - $parentType->getFields()[$fieldName], + $typeCreator, $meta->getDefault(), $mappedObjectContext->shouldInitializeObjects(), $fieldName, @@ -710,8 +716,9 @@ public function processSkippedFields( $skippedFieldsContext = $this->skippedMap->getSkippedFieldsContext($object); $type = $skippedFieldsContext->getType(); + $typeCreator = static fn (): MappedObjectType => $type; $options ??= $skippedFieldsContext->getOptions(); - $mappedObjectContext = $this->createMappedObjectContext($options, $type, true); + $mappedObjectContext = $this->createMappedObjectContext($options, $typeCreator, true); $skippedFields = $skippedFieldsContext->getSkippedFields(); $meta = $this->metaLoader->load($class); diff --git a/src/Tester/TesterDependencies.php b/src/Tester/TesterDependencies.php index 046337cc..001eb6ea 100644 --- a/src/Tester/TesterDependencies.php +++ b/src/Tester/TesterDependencies.php @@ -14,6 +14,7 @@ use Orisai\ObjectMapper\Processing\Processor; use Orisai\ObjectMapper\Rules\DefaultRuleManager; use Orisai\ObjectMapper\Types\MessageType; +use Orisai\ObjectMapper\Types\Type; use ReflectionProperty; final class TesterDependencies @@ -82,7 +83,7 @@ public function createFieldContext( $this->ruleManager, $this->processor, $options !== null ? $options->createClone() : new Options(), - new MessageType('test'), + static fn (): Type => new MessageType('test'), $defaultValueMeta ?? DefaultValueMeta::fromNothing(), $initializeObjects, 'test', diff --git a/src/Types/ArrayShapeType.php b/src/Types/ArrayShapeType.php index 3d59e87a..0e69a76d 100644 --- a/src/Types/ArrayShapeType.php +++ b/src/Types/ArrayShapeType.php @@ -3,6 +3,7 @@ namespace Orisai\ObjectMapper\Types; use Closure; +use Orisai\Exceptions\Logic\InvalidState; use Orisai\ObjectMapper\Exception\WithTypeAndValue; class ArrayShapeType implements Type @@ -37,6 +38,28 @@ public function overwriteInvalidField($field, WithTypeAndValue $typeAndValue): v $this->invalidFields[$field] = $typeAndValue; } + /** + * @param int|string $field + * + * @internal + */ + public function getField($field): Type + { + $type = $this->fields[$field] ?? null; + + if ($type === null) { + throw InvalidState::create() + ->withMessage("Cannot get field '$field' because it was never set."); + } + + if ($type instanceof Closure) { + $type = $type(); + $this->fields[$field] = $type; + } + + return $type; + } + /** * @return array */ @@ -46,6 +69,7 @@ public function getFields(): array foreach ($this->fields as $field => $type) { if ($type instanceof Closure) { $type = $type(); + $this->fields[$field] = $type; } $fields[$field] = $type; diff --git a/tests/Unit/Types/ArrayShapeTypeTest.php b/tests/Unit/Types/ArrayShapeTypeTest.php index 6cc50615..7cb8bf05 100644 --- a/tests/Unit/Types/ArrayShapeTypeTest.php +++ b/tests/Unit/Types/ArrayShapeTypeTest.php @@ -2,10 +2,13 @@ namespace Tests\Orisai\ObjectMapper\Unit\Types; +use Orisai\Exceptions\Logic\InvalidState; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; use Orisai\ObjectMapper\Processing\Value; use Orisai\ObjectMapper\Types\ArrayShapeType; use Orisai\ObjectMapper\Types\MessageType; +use Orisai\ObjectMapper\Types\SimpleValueType; +use Orisai\ObjectMapper\Types\Type; use PHPUnit\Framework\TestCase; final class ArrayShapeTypeTest extends TestCase @@ -103,19 +106,36 @@ public function testFields(): void public function testFieldFromClosure(): void { $type = new ArrayShapeType(); - $type->addField('field', static fn (): MessageType => new MessageType('test')); + $type->addField('field', static fn (): Type => new MessageType('test')); + $type->addField('field2', static fn (): Type => new SimpleValueType('string')); $expectedFields = [ - 'field' => new MessageType('test'), + 'field' => $t1 = new MessageType('test'), + 'field2' => $t2 = new SimpleValueType('string'), ]; + self::assertEquals($t1, $type->getField('field')); + self::assertSame($type->getField('field'), $type->getField('field')); + $fields = $type->getFields(); self::assertEquals($expectedFields, $fields); self::assertNotSame($expectedFields, $fields); $fields2 = $type->getFields(); self::assertEquals($fields, $fields2); - self::assertNotSame($fields, $fields2); + self::assertSame($fields, $fields2); + + self::assertEquals($t2, $type->getField('field2')); + } + + public function testGetUnknownField(): void + { + $type = new ArrayShapeType(); + + $this->expectException(InvalidState::class); + $this->expectExceptionMessage("Cannot get field 'unknown' because it was never set."); + + $type->getField('unknown'); } }