From b9801478bc0cab77c19a8d5d8c22748c4c311279 Mon Sep 17 00:00:00 2001 From: jerodev Date: Tue, 9 May 2023 13:29:41 +0200 Subject: [PATCH 01/25] DataTypeFactory --- .editorconfig | 9 + .github/workflows/run-tests.yml | 16 +- src/Attributes/PostMapping.php | 16 -- src/Exceptions/ClassNotFoundException.php | 13 -- .../ConstructorParameterMissingException.php | 13 -- .../UnexpectedNullValueException.php | 13 -- src/Exceptions/UnexpectedTokenException.php | 13 ++ src/Mapper.php | 111 +--------- src/MapsItself.php | 8 - src/Models/ClassBluePrint.php | 95 -------- src/Models/DataType.php | 115 ---------- src/Models/MapperOptions.php | 21 -- src/Models/MethodParameter.php | 40 ---- src/Models/PropertyBluePrint.php | 47 ---- src/ObjectMapper.php | 108 --------- src/Printers/BluePrinter.php | 113 ---------- .../ClassName/ClassImportResolver.php | 56 ----- src/Printers/ClassName/ClassNameResolver.php | 10 - .../ClassName/ParentNamespaceResolver.php | 22 -- .../ClassName/RootNamespaceResolver.php | 18 -- src/Printers/ClassNamePrinter.php | 53 ----- .../Property/PhpDocPropertyResolver.php | 43 ---- .../Property/PropertyTypeResolver.php | 15 -- .../Property/TypedPropertyResolver.php | 58 ----- src/Printers/PropertyPrinter.php | 87 -------- src/Types/DataType.php | 29 +++ src/Types/DataTypeCollection.php | 21 ++ src/Types/DataTypeFactory.php | 127 +++++++++++ tests/MapperTest.php | 123 ----------- tests/Models/DataTypeTest.php | 53 ----- tests/ObjectMapperTest.php | 209 ------------------ tests/TestClasses/ArrayConstructorClass.php | 14 -- tests/TestClasses/CallingClass.php | 9 - tests/TestClasses/ConstructorClass.php | 17 -- tests/TestClasses/PostMappingClass.php | 16 -- tests/TestClasses/RecursiveClass.php | 9 - tests/TestClasses/SimpleClass.php | 17 -- tests/TestClasses/StatusEnum.php | 9 - tests/TestClasses/UnionTypesClass.php | 9 - tests/Types/DataTypeFactoryTest.php | 102 +++++++++ 40 files changed, 322 insertions(+), 1555 deletions(-) create mode 100644 .editorconfig delete mode 100644 src/Attributes/PostMapping.php delete mode 100644 src/Exceptions/ClassNotFoundException.php delete mode 100644 src/Exceptions/ConstructorParameterMissingException.php delete mode 100644 src/Exceptions/UnexpectedNullValueException.php create mode 100644 src/Exceptions/UnexpectedTokenException.php delete mode 100644 src/MapsItself.php delete mode 100644 src/Models/ClassBluePrint.php delete mode 100644 src/Models/DataType.php delete mode 100644 src/Models/MapperOptions.php delete mode 100644 src/Models/MethodParameter.php delete mode 100644 src/Models/PropertyBluePrint.php delete mode 100644 src/ObjectMapper.php delete mode 100644 src/Printers/BluePrinter.php delete mode 100644 src/Printers/ClassName/ClassImportResolver.php delete mode 100644 src/Printers/ClassName/ClassNameResolver.php delete mode 100644 src/Printers/ClassName/ParentNamespaceResolver.php delete mode 100644 src/Printers/ClassName/RootNamespaceResolver.php delete mode 100644 src/Printers/ClassNamePrinter.php delete mode 100644 src/Printers/Property/PhpDocPropertyResolver.php delete mode 100644 src/Printers/Property/PropertyTypeResolver.php delete mode 100644 src/Printers/Property/TypedPropertyResolver.php delete mode 100644 src/Printers/PropertyPrinter.php create mode 100644 src/Types/DataType.php create mode 100644 src/Types/DataTypeCollection.php create mode 100644 src/Types/DataTypeFactory.php delete mode 100644 tests/MapperTest.php delete mode 100644 tests/Models/DataTypeTest.php delete mode 100644 tests/ObjectMapperTest.php delete mode 100644 tests/TestClasses/ArrayConstructorClass.php delete mode 100644 tests/TestClasses/CallingClass.php delete mode 100644 tests/TestClasses/ConstructorClass.php delete mode 100644 tests/TestClasses/PostMappingClass.php delete mode 100644 tests/TestClasses/RecursiveClass.php delete mode 100644 tests/TestClasses/SimpleClass.php delete mode 100644 tests/TestClasses/StatusEnum.php delete mode 100644 tests/TestClasses/UnionTypesClass.php create mode 100644 tests/Types/DataTypeFactoryTest.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fcdf61e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index acc6ba3..ca8c373 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -16,13 +16,15 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ~/.composer/cache/files - key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + uses: actions/cache@v3 + with:with: + path: | + .phpstan-cache + vendor + key: composer-${{ hashFiles('composer.json') }} - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -33,10 +35,10 @@ jobs: tools: composer:v2 - name: Install dependencies - run: composer update --prefer-stable --prefer-dist --no-interaction + run: composer update --no-interaction - name: Code Styles run: vendor/bin/phpcs -p - name: Execute tests - run: vendor/bin/phpunit \ No newline at end of file + run: vendor/bin/phpunit diff --git a/src/Attributes/PostMapping.php b/src/Attributes/PostMapping.php deleted file mode 100644 index 213a07d..0000000 --- a/src/Attributes/PostMapping.php +++ /dev/null @@ -1,16 +0,0 @@ -mapperOptions = $mapperOptions ?? new MapperOptions(); - $this->objectMapper = new ObjectMapper( - $this, - new BluePrinter() - ); + public function __construct() + { + $this->dataTypeFactory = new DataTypeFactory(); } + /** * Map anything! * @@ -31,93 +24,13 @@ public function __construct( */ public function map($type, $data) { - if ($type === 'null') { - return null; - } - if (\is_string($type)) { - $type = DataType::parse($type); - } - - if ($data === null) { - if ($type->isNullable() || $this->mapperOptions->strictNullMapping === false) { - return null; - } else { - throw new UnexpectedNullValueException(); - } - } - - if ($type->isArray() && \is_array($data)) { - return $this->mapArray($type, $data); - } - - if ($type->isNativeType()) { - return $this->mapNative($type, $data); - } - - if (empty($data) && $type->isNullable()) { - return null; + return $this->map( + $this->dataTypeFactory->fromString($type), + $data, + ); } - return $this->objectMapper->map($type->getType(), $data); - } - - /** - * Map an array of a certain type. - * - * @param DataType $type - * @param array $data - * @return array - */ - private function mapArray(DataType $type, array $data): array - { - $singleType = $type->getArrayChildType(); - - $array = []; - foreach ($data as $key => $value) { - // If we have a nested array, keep calling this function. - if (\is_array($value) && $singleType->isArray()) { - $array[$key] = self::mapArray($singleType, $value); - } else { - $array[$key] = $this->map($singleType, $value); - } - } - - return $array; - } - - /** - * Map a native php type. - * - * @param DataType $type - * @param mixed $data - * @return mixed - */ - private function mapNative(DataType $type, $data) - { - if ($type->isGenericArray()) { - return (array) $data; - } - - switch ($type->getType()) { - case 'bool': - return \filter_var($data, FILTER_VALIDATE_BOOLEAN); - case 'float': - return \floatval($data); - case 'int': - return \intval($data); - case 'object': - return (object)$data; - case 'string': - return \strval($data); - } - - // Unknown internal type. - return $data; - } - - public function getOptions(): MapperOptions - { - return $this->mapperOptions; + // TODO: more mapping! } } diff --git a/src/MapsItself.php b/src/MapsItself.php deleted file mode 100644 index 85bbaaa..0000000 --- a/src/MapsItself.php +++ /dev/null @@ -1,8 +0,0 @@ -className = $className; - $this->mapsItself = false; - $this->constructorProperties = []; - $this->postMappingFunction = null; - $this->properties = []; - } - - public function getClassName(): string - { - return $this->className; - } - - public function getConstructorProperties(): array - { - return $this->constructorProperties; - } - - public function addConstructorProperty(MethodParameter $parameter): void - { - $this->constructorProperties[] = $parameter; - } - - public function isMappingItself(): bool - { - return $this->mapsItself; - } - - public function setMapsItself(bool $mapsItself = true): void - { - $this->mapsItself = $mapsItself; - } - - public function getProperties(): array - { - return $this->properties; - } - - public function getPostMappingFunction(): ?string - { - return $this->postMappingFunction; - } - - public function setPostMappingFunction(?string $postMappingFunction): void - { - $this->postMappingFunction = $postMappingFunction; - } - - public function getProperty(string $name): ?PropertyBluePrint - { - return $this->properties[$name] ?? null; - } - - public function addProperty(PropertyBluePrint $property): void - { - $this->properties[$property->getPropertyName()] = $property; - } -} diff --git a/src/Models/DataType.php b/src/Models/DataType.php deleted file mode 100644 index 19cd952..0000000 --- a/src/Models/DataType.php +++ /dev/null @@ -1,115 +0,0 @@ -type = $type; - $this->arrayLevel = $arrayLevel; - $this->isNullable = $isNullable; - $this->arrayKeyType = $arrayKeyType; - } - - public function getType(): string - { - return $this->type; - } - - public function setType(string $type): void - { - $this->type = $type; - } - - public function isArray(): bool - { - return $this->arrayLevel > 0; - } - - public function isGenericArray(): bool - { - return \in_array($this->getType(), [ - 'array', - 'iterable', - ], true); - } - - public function isNullable(): bool - { - return $this->isNullable; - } - - public function isNativeType(): bool - { - return \in_array($this->getType(), [ - 'array', - 'bool', - 'float', - 'int', - 'iterable', - 'object', - 'string', - ], true); - } - - public function getArrayChildType(): DataType - { - return new DataType( - $this->getType(), - $this->arrayLevel - 1, - $this->isNullable(), - ); - } - - public function getArrayKeyType(): ?DataType - { - return $this->arrayKeyType; - } - - public static function parse(string $type, bool $forceNullable = false): self - { - $type = \trim($type); - - $arrayLevel = 0; - $arrayKeyType = null; - if (\substr($type, -2) === '[]') { - if (\preg_match('/^(.*?)((\[])+)$/', $type, $matches) === 1) { - $type = $matches[1]; - $arrayLevel = \strlen($matches[2]) / 2; - } - } else if (\preg_match('/array<([^|]+)>/', $type, $matches) === 1) { - $arrayLevel = 1; - if (\strpos($matches[1], ',') > 1) { - [$arrayKeyType, $type] = \preg_split('/\s*,\s*/', $matches[1]); - } else { - $type = $matches[1]; - } - - // Is the value type itself an array? - if (\preg_match('/^(.*?)((\[])+)$/', $type, $matches) === 1) { - $type = $matches[1]; - $arrayLevel += \strlen($matches[2]) / 2; - } - } - - if (\substr($type, '0', 1) === '?') { - $forceNullable = true; - $type = \substr($type, 1); - } - - return new self($type, $arrayLevel, $forceNullable, $arrayKeyType ? DataType::parse($arrayKeyType) : null); - } -} diff --git a/src/Models/MapperOptions.php b/src/Models/MapperOptions.php deleted file mode 100644 index a68e58c..0000000 --- a/src/Models/MapperOptions.php +++ /dev/null @@ -1,21 +0,0 @@ -name = $name; - $this->type = $type; - $this->required = $required; - $this->defaultValue = $defaultValue; - } - - public function getName(): string - { - return $this->name; - } - - public function getType(): ?DataType - { - return $this->type; - } - - public function isRequired(): bool - { - return $this->required; - } - - /** @return mixed|null */ - public function getDefaultValue() - { - return $this->defaultValue; - } -} diff --git a/src/Models/PropertyBluePrint.php b/src/Models/PropertyBluePrint.php deleted file mode 100644 index 5dd1c6b..0000000 --- a/src/Models/PropertyBluePrint.php +++ /dev/null @@ -1,47 +0,0 @@ -propertyName = $propertyName; - $this->types = []; - $this->setter = null; - } - - public function getPropertyName(): string - { - return $this->propertyName; - } - - public function addType(DataType $type): void - { - $this->types[] = $type; - } - - /** @param DataType[] $types */ - public function setTypes(array $types): void - { - $this->types = $types; - } - - /** @return DataType[] */ - public function getTypes(): array - { - return $this->types; - } - - public function getSetter(): ?string - { - return $this->setter; - } -} diff --git a/src/ObjectMapper.php b/src/ObjectMapper.php deleted file mode 100644 index de7a353..0000000 --- a/src/ObjectMapper.php +++ /dev/null @@ -1,108 +0,0 @@ -bluePrinter = $bluePrinter ?? new BluePrinter(); - $this->mapper = $mapper ?? new Mapper(); - } - - /** - * @template T - * @param class-string $className - * @param mixed $data - * @return T - */ - public function map(string $className, $data): object - { - $bluePrint = $this->bluePrinter->print($className); - $className = $bluePrint->getClassName(); - - // Check if this is a PHP enum - if (\enum_exists($className)) { - // If the enum had been cast to an array, the value is now an array with a `name` and `value` key. - if (\is_array($data) && \array_key_exists('value', $data)) { - $data = $data['value']; - } - - return $this->mapper->getOptions()->enumTryFrom ? $className::tryFrom($data) : $className::from($data); - } - - // If the class implements MapsItself, just create the object using the mapObject function - if ($bluePrint->isMappingItself()) { - return \call_user_func([$className, 'mapObject'], $data, $this->mapper); - } - - // If the class has a constructor, try passing the required parameters - if (! empty($bluePrint->getConstructorProperties())) { - $object = $this->createObjectUsingConstructor($bluePrint, (array) $data); - } else { - $object = new $className(); - } - - if (\is_array($data)) { - $this->mapObjectProperties($object, $bluePrint, $data); - } - - if ($bluePrint->getPostMappingFunction() !== null) { - $object->{$bluePrint->getPostMappingFunction()}($data); - } - - return $object; - } - - private function mapObjectProperties(object $object, ClassBluePrint $bluePrint, array $data): void - { - foreach ($bluePrint->getProperties() as $property) { - if (! \array_key_exists($property->getPropertyName(), $data)) { - continue; - } - - $reflectionProperty = new ReflectionProperty($bluePrint->getClassName(), $property->getPropertyName()); - foreach ($property->getTypes() as $type) { - try { - $object->{$property->getPropertyName()} = $this->mapper->map($type, $data[$property->getPropertyName()]); - } catch (Throwable $e) { - continue; - } - - // If the property has a value, we're good to go. - if ($reflectionProperty->isInitialized($object) && $object->{$property->getPropertyName()} !== null) { - break; - } - } - } - } - - private function createObjectUsingConstructor(ClassBluePrint $bluePrint, array $data): object - { - $constructorValues = []; - - foreach ($bluePrint->getConstructorProperties() as $constructorProperty) { - if (\array_key_exists($constructorProperty->getName(), $data)) { - $constructorValues[] = $this->mapper->map($constructorProperty->getType(), $data[$constructorProperty->getName()]); - } else if (! $constructorProperty->isRequired()) { - $constructorValues[] = $constructorProperty->getDefaultValue(); - } else { - throw new ConstructorParameterMissingException($bluePrint->getClassName(), $constructorProperty->getName(), $data); - } - } - - $className = $bluePrint->getClassName(); - return new $className(...$constructorValues); - } -} diff --git a/src/Printers/BluePrinter.php b/src/Printers/BluePrinter.php deleted file mode 100644 index b970749..0000000 --- a/src/Printers/BluePrinter.php +++ /dev/null @@ -1,113 +0,0 @@ -classNamePrinter = $classNamePrinter ?? new ClassNamePrinter(); - $this->propertyPrinter = $propertyPrinter ?? new PropertyPrinter($this->classNamePrinter); - - $this->bluePrintCache = []; - } - - /** - * @param object|string $source - * @return ClassBluePrint - * @throws ClassNotFoundException|ReflectionException - */ - public function print($source): ClassBluePrint - { - if (\is_object($source)) { - $source = \get_class($source); - } - - if (\array_key_exists($source, $this->bluePrintCache)) { - return $this->bluePrintCache[$source]; - } - - $fqcn = $this->classNamePrinter->resolveClassName($source); - if ($fqcn === null) { - throw new ClassNotFoundException($source); - } - - $bluePrint = new ClassBluePrint($fqcn); - - $reflection = new ReflectionClass($fqcn); - if (\in_array(MapsItself::class, \class_implements($source), true)) { - $bluePrint->setMapsItself(); - } else { - // Map properties - foreach ($reflection->getProperties() as $property) { - $bluePrint->addProperty($this->propertyPrinter->print($property)); - } - - // Map constructor - $constructor = $reflection->getConstructor(); - if ($constructor !== null) { - foreach ($constructor->getParameters() as $parameter) { - $dataType = null; - if ($parameter->getType()) { - $dataType = DataType::parse((string) $parameter->getType()); - } - if ($dataType === null || $dataType->isGenericArray()) { - $property = $bluePrint->getProperty($parameter->getName()); - - if ($property !== null && ! empty($property->getTypes())) { - $dataType = $bluePrint->getProperty($parameter->getName())->getTypes()[0]; - } - } - - $bluePrint->addConstructorProperty( - new MethodParameter( - $parameter->getName(), - $dataType, - ! $parameter->isDefaultValueAvailable(), - $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null, - ) - ); - } - } - } - - $this->findPostMappingCallbacks($reflection, $bluePrint); - - $this->bluePrintCache[$source] = $bluePrint; - - return $bluePrint; - } - - private function findPostMappingCallbacks(ReflectionClass $reflection, ClassBluePrint $bluePrint): void - { - if (PHP_MAJOR_VERSION < 8 || ! $reflection->isUserDefined()) { - return; - } - - $postMappingAttributes = $reflection->getAttributes(PostMapping::class); - if (! empty($postMappingAttributes)) { - $arguments = $postMappingAttributes[0]->getArguments(); - $bluePrint->setPostMappingFunction(\reset($arguments)); - return; - } - - if ($reflection->getParentClass() && $reflection->getParentClass()->isUserDefined()) { - $this->findPostMappingCallbacks($reflection->getParentClass(), $bluePrint); - } - } -} diff --git a/src/Printers/ClassName/ClassImportResolver.php b/src/Printers/ClassName/ClassImportResolver.php deleted file mode 100644 index 84a8a21..0000000 --- a/src/Printers/ClassName/ClassImportResolver.php +++ /dev/null @@ -1,56 +0,0 @@ -getImports($classReflection); - foreach ($imports as $importClassName => $fqcn) { - if ($importClassName === $className) { - return $fqcn; - } - } - - return null; - } - - private function getImports(ReflectionClass $classReflection): array - { - $resource = \fopen($classReflection->getFileName(), 'r'); - $imports = []; - - while ($line = \fgets($resource)) { - if (\substr($line, 0, 4) === 'use ') { - $parts = \explode(' ', \trim($line, "; \n")); - - // Skip imports for functions or constants. - if ($parts[1] === 'function' || $parts[1] === 'const') { - continue; - } - - $name = $parts[3] ?? null; - if ($name === null) { - $fqcnParts = \explode('\\', $parts[1]); - $name = \end($fqcnParts); - } - - $imports[$name] = $parts[1]; - } - - // When the class starts, no more imports are available. - if (\substr($line, 0, 6) === 'class ' || \substr($line, 0, 12) === 'final class ') { - break; - } - } - - return $imports; - } -} diff --git a/src/Printers/ClassName/ClassNameResolver.php b/src/Printers/ClassName/ClassNameResolver.php deleted file mode 100644 index 10b2be1..0000000 --- a/src/Printers/ClassName/ClassNameResolver.php +++ /dev/null @@ -1,10 +0,0 @@ -getNamespaceName(), '\\') . '\\' . $className; - if (\class_exists($rootClassName)) { - return $rootClassName; - } - - return null; - } -} diff --git a/src/Printers/ClassName/RootNamespaceResolver.php b/src/Printers/ClassName/RootNamespaceResolver.php deleted file mode 100644 index 88575f7..0000000 --- a/src/Printers/ClassName/RootNamespaceResolver.php +++ /dev/null @@ -1,18 +0,0 @@ -classNameResolvers = $classNameResolvers; - return; - } - - $this->classNameResolvers = [ - new ClassName\RootNamespaceResolver(), - new ClassName\ParentNamespaceResolver(), - new ClassName\ClassImportResolver(), - ]; - } - - public function resolveClassName(string $className, ?ReflectionClass $declaringClass = null): ?string - { - if (\class_exists($className)) { - return $className; - } - - foreach ($this->classNameResolvers as $classNameResolver) { - $resolvedClassName = $classNameResolver->getFullClassName($className, $declaringClass); - if ($resolvedClassName !== null) { - return $resolvedClassName; - } - } - - // Recursively do the same for user defined parent classes. - if ($declaringClass !== null) { - $parentParent = $declaringClass->getParentClass(); - if ($parentParent instanceof ReflectionClass && $parentParent->isUserDefined()) { - return $this->resolveClassName($className, $parentParent); - } - } - - return null; - } -} diff --git a/src/Printers/Property/PhpDocPropertyResolver.php b/src/Printers/Property/PhpDocPropertyResolver.php deleted file mode 100644 index defad45..0000000 --- a/src/Printers/Property/PhpDocPropertyResolver.php +++ /dev/null @@ -1,43 +0,0 @@ -getDocComment(); - if (! \is_string($doc)) { - return null; - } - - $rawTypes = $this->getVarPhpDocString($doc); - if ($rawTypes === null) { - return null; - } - - $allowNull = \in_array('null', $rawTypes, true); - $types = []; - foreach ($rawTypes as $rawType) { - if ($rawType === 'null') { - continue; - } - - $types[] = DataType::parse($rawType, $allowNull); - } - - return $types; - } - - private function getVarPhpDocString(string $docBlock): ?array - { - if (\preg_match('/@var\s+([^\n*]+)/', $docBlock, $matches) === 1 && \count($matches) > 1) { - return \preg_split('/\s*\|\s*/', $matches[1]); - } - - return null; - } -} diff --git a/src/Printers/Property/PropertyTypeResolver.php b/src/Printers/Property/PropertyTypeResolver.php deleted file mode 100644 index 38fabde..0000000 --- a/src/Printers/Property/PropertyTypeResolver.php +++ /dev/null @@ -1,15 +0,0 @@ -getType(); - if ($reflectionType === null) { - return null; - } - - if (PHP_MAJOR_VERSION >= 8 && $reflectionType instanceof ReflectionUnionType) { - $propertyTypes = $this->getPhp8UnionTypes($reflectionType); - if (! empty($propertyTypes)) { - return $propertyTypes; - } - } - - if ($reflectionType instanceof ReflectionNamedType) { - return [ - DataType::parse( - $reflectionType->getName(), - $reflectionType->allowsNull() - ), - ]; - } - - return null; - } - - /** @return DataType[] */ - private function getPhp8UnionTypes(ReflectionUnionType $reflectionUnionType): ?array - { - $allowNull = ! empty( - \array_filter($reflectionUnionType->getTypes(), static fn (ReflectionType $type) => $type->getName() === 'null') - ); - - $types = []; - foreach ($reflectionUnionType->getTypes() as $reflectionType) { - $typeString = $reflectionType->getName(); - if ($typeString === 'null') { - continue; - } - - $types[] = DataType::parse($reflectionType->getName(), $allowNull); - } - - return $types; - } -} diff --git a/src/Printers/PropertyPrinter.php b/src/Printers/PropertyPrinter.php deleted file mode 100644 index 5628a56..0000000 --- a/src/Printers/PropertyPrinter.php +++ /dev/null @@ -1,87 +0,0 @@ -classNamePrinter = $classNamePrinter; - - if ($propertyResolvers !== null) { - $this->propertyTypeResolvers = $propertyResolvers; - return; - } - - $this->propertyTypeResolvers = [ - new Property\TypedPropertyResolver(), - new Property\PhpDocPropertyResolver(), - ]; - } - - public function print(ReflectionProperty $property): PropertyBluePrint - { - $bluePrint = new PropertyBluePrint($property->getName()); - - $this->setPropertyTypes($property, $bluePrint); - - return $bluePrint; - } - - private function setPropertyTypes(ReflectionProperty $property, PropertyBluePrint $bluePrint) - { - $types = $this->getPropertyTypes($property); - - foreach ($types as $type) { - if ($type->isNativeType()) { - continue; - } - - $type->setType( - $this->classNamePrinter->resolveClassName($type->getType(), $property->getDeclaringClass()) - ); - } - - $bluePrint->setTypes($types); - } - - /** - * @param ReflectionProperty $property - * @return DataType[] - */ - private function getPropertyTypes(ReflectionProperty $property): array - { - $genericArray = false; - - foreach ($this->propertyTypeResolvers as $typeResolver) { - $types = $typeResolver->getPropertyType($property); - if (empty($types)) { - continue; - } - - if (\count($types) === 1 && $types[0]->isGenericArray()) { - $genericArray = true; - continue; - } - - return $types; - } - - if ($genericArray) { - return [new DataType('array')]; - } - } -} diff --git a/src/Types/DataType.php b/src/Types/DataType.php new file mode 100644 index 0000000..c7fe21c --- /dev/null +++ b/src/Types/DataType.php @@ -0,0 +1,29 @@ + $genericTypes + */ + public function __construct( + public readonly string $type, + public readonly bool $isNullable, + public readonly array $genericTypes = [], + ) { + } + + public function isArray(): bool + { + return \in_array( + $this->type, + [ + 'array', + 'iterable', + ], + ); + } +} diff --git a/src/Types/DataTypeCollection.php b/src/Types/DataTypeCollection.php new file mode 100644 index 0000000..8f59939 --- /dev/null +++ b/src/Types/DataTypeCollection.php @@ -0,0 +1,21 @@ +types, + static fn (DataType $t) => $t->type === 'null', + ) + ); + } +} diff --git a/src/Types/DataTypeFactory.php b/src/Types/DataTypeFactory.php new file mode 100644 index 0000000..59ea9eb --- /dev/null +++ b/src/Types/DataTypeFactory.php @@ -0,0 +1,127 @@ + */ + private array $typeCache = []; + + public function fromString(string $rawType): DataTypeCollection + { + $types = []; + + $parts = \explode('|', $rawType); + foreach ($parts as $part) { + $types[] = $this->singleFromString(\trim($part)); + } + + return new DataTypeCollection($types); + } + + public function singleFromString(string $rawType): DataType + { + if (\array_key_exists($rawType, $this->typeCache)) { + return $this->typeCache[$rawType]; + } + + $tokens = $this->tokenize($rawType); + if (\count($tokens) === 1) { + return $this->typeCache[$rawType] = new DataType($rawType, false); + } + + return $this->typeCache[$rawType] = $this->parseTokens($tokens); + } + + /** + * @param string $type + * @return array + */ + private function tokenize(string $type): array + { + $tokens = []; + $token = ''; + + for ($i = 0; $i < \strlen($type); $i++) { + $char = $type[$i]; + + if (\in_array($char, ['<', '>', '?', ','])) { + if (! empty($token)) { + $tokens[] = $token; + } + $tokens[] = $char; + $token = ''; + } else if ($char === '[') { + $tokens[] = $token; + $token = $char; + } else { + $token .= $char; + } + } + + if ($token !== '') { + $tokens[] = $token; + } + + return $tokens; + } + + /** + * @param array $tokens + * @return DataType + */ + private function parseTokens(array $tokens): DataType + { + $isNullable = $tokens[0] === '?'; + if ($isNullable) { + \array_shift($tokens); + } + + $type = \array_shift($tokens); + + // If nothing is left, return the type + if (empty($tokens)) { + return new DataType($type, $isNullable); + } + + // If square brackets, it's an array with the type as value + $nextToken = \array_shift($tokens); + if ($nextToken === '[]') { + return new DataType('array', $isNullable, [new DataTypeCollection([new DataType($type, false)])]); + } + + // If beaks, find either a value or a key and value + if ($nextToken === '<') { + $nextGenericType = ''; + $genericTypes = []; + while ($nextToken = \array_shift($tokens)) { + if ($nextToken === '>') { + break; + } + + if ($nextToken === ',') { + $genericTypes[] = $this->fromString($nextGenericType); + $nextGenericType = ''; + + continue; + } + + $nextGenericType .= \trim($nextToken); + } + + if (empty($genericTypes) && empty($nextGenericType)) { + throw new \Exception('Found generic type without subtype'); + } + + if (! empty($nextGenericType)) { + $genericTypes[] = $this->fromString($nextGenericType); + } + + return new DataType($type, $isNullable, $genericTypes); + } + + throw new UnexpectedTokenException($nextToken); + } +} diff --git a/tests/MapperTest.php b/tests/MapperTest.php deleted file mode 100644 index 7b2b124..0000000 --- a/tests/MapperTest.php +++ /dev/null @@ -1,123 +0,0 @@ -mapper = new Mapper(); - } - - /** - * @test - * @dataProvider nativeTypeDataProvider - */ - public function it_should_map_native_types($expected, string $type, $input): void - { - $this->assertSame($expected, $this->mapper->map($type, $input)); - } - - /** - * @test - * @dataProvider arrayTypeDataProvider - */ - public function it_should_map_array_types($expected, string $type, $input): void - { - $this->assertEquals($expected, $this->mapper->map($type, $input)); - } - - /** @test */ - public function it_should_map_nullable_object_as_null(): void - { - $class = new class { - public string $foo; - }; - - $mapped = $this->mapper->map('?' . \get_class($class), null); - - $this->assertNull($mapped); - } - - /** @test */ - public function it_should_throw_exception_when_strict_mapping_null_values(): void - { - $this->expectException(UnexpectedNullValueException::class); - - $this->mapper->map('int', null); - } - - /** @test */ - public function it_should_allow_non_strict_null_mapping_through_options(): void - { - $options = new MapperOptions(); - $options->strictNullMapping = false; - - $this->mapper = new Mapper($options); - $this->assertNull($this->mapper->map('int', null)); - } - - public function nativeTypeDataProvider(): Generator - { - yield [['1', '2', '3'], 'array', ['1', '2', '3']]; - yield [['1'], 'array', '1']; - yield [[], 'array', []]; - yield [['1', '2', '3'], 'iterable', ['1', '2', '3']]; - yield [['1'], 'iterable', '1']; - yield [[], 'iterable', []]; - yield [[['1']], 'string[][]', [[1]]]; - yield [['first' => [1], 'second' => [2, 3]], 'int[][]', ['first' => ['1'], 'second' => ['2', '3']]]; - - yield [true, 'bool', 'true']; - yield [true, 'bool', 1]; - yield [false, 'bool', 'false']; - yield [false, 'bool', 0]; - - yield [5.7, 'float', '5.7']; - yield [5.8, 'float', 5.8]; - - yield [5, 'int', '5']; - yield [6, 'int', 6]; - - yield ['foo', 'string', 'foo']; - yield ['7', 'string', 7]; - - yield [ - StatusEnum::Success, - StatusEnum::class, - 'Success', - ]; - } - - public function arrayTypeDataProvider(): Generator - { - yield [[true, true, false], 'bool[]', ['1', true, 'false']]; - - yield [[1, 2, 3], 'float[]', ['1', 2, '3']]; - yield [[1, 2, 3], 'int[]', ['1', 2, '3']]; - yield [['1', '2', '3'], 'string[]', ['1', 2, '3']]; - yield [['1', '2', '3'], 'string[]', ['1', 2, '3']]; - yield [['a' => 'b', 'b' => 'c', 4 => '3'], 'array', ['a' => 'b', 'b' => 'c', 4 => 3]]; - yield [[7 => 'b', 6 => 'c', 4 => '3'], 'array', ['7' => 'b', '6' => 'c', 4 => 3]]; - yield [[7 => ['b'], 6 => ['c', '3']], 'array', ['7' => ['b'], '6' => ['c', 3]]]; - - $object = new SimpleClass(); - $object->foo = 'bar'; - $object2 = new SimpleClass(); - $object2->foo = 'baz'; - yield [['bar' => $object, 'baz' => $object2], 'array', ['bar' => ['foo' => 'bar'], 'baz' => ['foo' => 'baz']]]; - yield [[$object, $object2], 'array<' . SimpleClass::class . '>', [['foo' => 'bar'], ['foo' => 'baz']]]; - } -} \ No newline at end of file diff --git a/tests/Models/DataTypeTest.php b/tests/Models/DataTypeTest.php deleted file mode 100644 index 70b5f98..0000000 --- a/tests/Models/DataTypeTest.php +++ /dev/null @@ -1,53 +0,0 @@ -assertTrue(DataType::parse('int', true)->isNullable()); - $this->assertTrue(DataType::parse('?int', true)->isNullable()); - $this->assertFalse(DataType::parse('int', false)->isNullable()); - $this->assertTrue(DataType::parse('?int', false)->isNullable()); - } - - /** @test */ - public function it_should_parse_square_brackets_to_array_level(): void - { - $dataType = DataType::parse('int[][][]'); - - $this->assertEquals('int', $dataType->getType()); - $this->assertTrue($dataType->isArray()); - $this->assertTrue($dataType->getArrayChildType()->isArray()); - $this->assertTrue($dataType->getArrayChildType()->getArrayChildType()->isArray()); - $this->assertFalse($dataType->getArrayChildType()->getArrayChildType()->getArrayChildType()->isArray()); - } - - /** - * @test - * @dataProvider parseTestProvider - */ - public function it_should_parse_type_strings(string $input, string $type, bool $isArray, bool $isNullable): void - { - $dataType = DataType::parse($input); - - $this->assertEquals($type, $dataType->getType()); - $this->assertEquals($isArray, $dataType->isArray()); - $this->assertEquals($isNullable, $dataType->isNullable()); - } - - public function parseTestProvider(): Generator - { - yield ['int', 'int', false, false]; - yield ['array', 'array', false, false]; - yield ['int[]', 'int', true, false]; - yield ['?int', 'int', false, true]; - yield ['?int[]', 'int', true, true]; - } -} diff --git a/tests/ObjectMapperTest.php b/tests/ObjectMapperTest.php deleted file mode 100644 index ca53f26..0000000 --- a/tests/ObjectMapperTest.php +++ /dev/null @@ -1,209 +0,0 @@ -mapper = new ObjectMapper( - new Mapper(), - new BluePrinter(), - ); - } - - /** - * @test - * @requires PHP 8.0 - */ - public function it_should_call_post_mapping_function_after_mapping(): void - { - $data = [ - 'value' => 2, - ]; - - $object = $this->mapper->map(PostMappingClass::class, $data); - - $this->assertSame(4, $object->value); - } - - /** @test */ - public function it_should_map_constructor_parameters(): void - { - $result = $this->mapper->map(ConstructorClass::class, [ - 'foo' => 'foo', - 'baz' => 'boo', - ]); - \assert($result instanceof ConstructorClass); - - $this->assertSame('foo', $result->foo); - $this->assertSame('bar', $result->bar); - $this->assertSame('boo', $result->baz); - } - - /** @test */ - public function it_should_map_from_array(): void - { - $class = new class implements MapsItself { - public ?string $foo = null; - private string $bar; - - public function getBar(): string - { - return $this->bar; - } - - public function setBar(string $bar): void - { - $this->bar = $bar; - } - - public static function mapObject($data, Mapper $mapper): MapsItself - { - $object = new self(); - $object->setBar($data['bar']); - - return $object; - } - }; - - $mapped = $this->mapper->map(\get_class($class), [ - 'foo' => 'bar', - 'bar' => 'baz', - ]); - - $this->assertSame('baz', $mapped->getBar()); - $this->assertNull($mapped->foo); - } - - /** @test */ - public function it_should_map_parent_class_properties(): void - { - $class = new class extends SimpleClass { - public string $extended; - }; - - $result = $this->mapper->map(\get_class($class), [ - 'extended' => 112, - 'foo' => 'bar', - 'numberOrString' => '7', - ]); - - $this->assertSame('112', $result->extended); - $this->assertSame('bar', $result->foo); - $this->assertSame(7, $result->numberOrString); - } - - /** @test */ - public function it_should_map_recursive_classes(): void - { - $result = $this->mapper->map(RecursiveClass::class, [ - 'value' => 'foo', - 'recursive' => [ - 'value' => 'bar', - 'recursive' => [ - 'value' => 'baz', - 'recursive' => null, - ], - ], - ]); - \assert($result instanceof RecursiveClass); - - $this->assertSame('foo', $result->value); - $this->assertSame('bar', $result->recursive->value); - $this->assertSame('baz', $result->recursive->recursive->value); - } - - /** @test */ - public function it_should_map_simple_properties(): void - { - $result = $this->mapper->map(SimpleClass::class, [ - 'foo' => 'bar', - 'number' => '7', - 'floatingPoint' => '3.14', - 'checkbox' => 1, - 'numbers' => [ - 'foo' => [1, '2'], - 'bar' => [3], - ] - ]); - - $this->assertSame('bar', $result->foo); - $this->assertSame(7, $result->number); - $this->assertSame(3.14, $result->floatingPoint); - $this->assertTrue($result->checkbox); - $this->assertSame($result->numbers, [ - 'foo' => [1, 2], - 'bar' => [3], - ]); - } - - /** - * @test - * @requires PHP 8.0 - */ - public function it_should_map_union_types(): void - { - $data = [ - 'value' => '5', - 'stringValue' => 'foo', - ]; - - $object = $this->mapper->map(UnionTypesClass::class, $data); - - $this->assertEquals(5, $object->value); - $this->assertEquals('foo', $object->stringValue); - } - - /** @test */ - public function it_should_get_constructor_type_from_property_if_possible(): void - { - $result = $this->mapper->map(ArrayConstructorClass::class, ['integers' => ['5', '18']]); - - $this->assertSame([5, 18], $result->integers); - } - - /** @test */ - public function it_should_throw_error_on_missing_constructor_parameter(): void - { - $this->expectException(ConstructorParameterMissingException::class); - - $this->mapper->map(ConstructorClass::class, [ - 'bar' => 'baa', - 'baz' => 'boo', - ]); - } - - /** @test */ - public function it_should_use_calling_class_to_find_type_namespace(): void - { - $result = $this->mapper->map(CallingClass::class, [ - 'recursive' => [ - [ - 'value' => 'foo', - ], - ], - ]); - - $this->assertInstanceOf(RecursiveClass::class, $result->recursive[0]); - $this->assertSame('foo', $result->recursive[0]->value); - } -} diff --git a/tests/TestClasses/ArrayConstructorClass.php b/tests/TestClasses/ArrayConstructorClass.php deleted file mode 100644 index f5f3dbf..0000000 --- a/tests/TestClasses/ArrayConstructorClass.php +++ /dev/null @@ -1,14 +0,0 @@ -integers = $integers; - } -} diff --git a/tests/TestClasses/CallingClass.php b/tests/TestClasses/CallingClass.php deleted file mode 100644 index 02036b1..0000000 --- a/tests/TestClasses/CallingClass.php +++ /dev/null @@ -1,9 +0,0 @@ -foo = $foo; - $this->bar = $bar; - $this->baz = $baz; - } -} diff --git a/tests/TestClasses/PostMappingClass.php b/tests/TestClasses/PostMappingClass.php deleted file mode 100644 index a50a16a..0000000 --- a/tests/TestClasses/PostMappingClass.php +++ /dev/null @@ -1,16 +0,0 @@ -value *= 2; - } -} \ No newline at end of file diff --git a/tests/TestClasses/RecursiveClass.php b/tests/TestClasses/RecursiveClass.php deleted file mode 100644 index f26d7d9..0000000 --- a/tests/TestClasses/RecursiveClass.php +++ /dev/null @@ -1,9 +0,0 @@ -assertEquals($expectation, (new DataTypeFactory())->singleFromString($input)); + } + + /** + * @test + * @dataProvider unexpectedTokenDataProvider + */ + public function it_should_throw_unexpected_token(string $input): void + { + $this->expectException(UnexpectedTokenException::class); + + (new DataTypeFactory())->singleFromString($input); + } + + public static function singleDataTypeProvider(): Generator + { + yield ['int', new DataType('int', false)]; + + yield ['string[]', new DataType( + 'array', + false, + [ + new DataTypeCollection([ + new DataType('string', false), + ]), + ], + )]; + yield ['array', new DataType( + 'array', + false, + [ + new DataTypeCollection([ + new DataType('string', false), + ]), + ], + )]; + yield ['array', new DataType( + 'array', + false, + [ + new DataTypeCollection([ + new DataType('array', false, [ + new DataTypeCollection([ + new DataType('string', false), + ]), + ]), + ]), + ], + )]; + + yield ['Generic', new DataType( + 'Generic', + false, + [ + new DataTypeCollection([ + new DataType('K', false), + new DataType('bool', false), + ]), + new DataTypeCollection([ + new DataType('V', true), + ]), + ], + )]; + + yield ['Illuminate\\Collection\\LazyCollection', new DataType( + 'Illuminate\\Collection\\LazyCollection', + false, + [ + new DataTypeCollection([ + new DataType('string', false), + ]), + new DataTypeCollection([ + new DataType('Foo', false), + ]), + ], + )]; + } + + public static function unexpectedTokenDataProvider(): Generator + { + yield ['String>']; + } +} From b023b7b47a204d1e54e41e386e3930163d3ad8e6 Mon Sep 17 00:00:00 2001 From: jerodev Date: Tue, 9 May 2023 14:32:47 +0200 Subject: [PATCH 02/25] Map native values --- src/Exceptions/CouldNotMapValueException.php | 17 +++++ src/Mapper.php | 70 ++++++++++++++++++-- src/Types/DataType.php | 21 +++++- src/Types/DataTypeCollection.php | 27 +++++++- tests/MapperTest.php | 41 ++++++++++++ 5 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 src/Exceptions/CouldNotMapValueException.php create mode 100644 tests/MapperTest.php diff --git a/src/Exceptions/CouldNotMapValueException.php b/src/Exceptions/CouldNotMapValueException.php new file mode 100644 index 0000000..47c812e --- /dev/null +++ b/src/Exceptions/CouldNotMapValueException.php @@ -0,0 +1,17 @@ +dataTypeFactory = new DataTypeFactory(); } - /** * Map anything! * - * @param DataType|string $type The type to map to. + * @param DataTypeCollection|string $typeCollection The type to map to. * @param mixed $data Deserialized data to map to the type. * @return mixed */ - public function map($type, $data) + public function map($typeCollection, $data) { - if (\is_string($type)) { + if (\is_string($typeCollection)) { return $this->map( - $this->dataTypeFactory->fromString($type), + $this->dataTypeFactory->fromString($typeCollection), $data, ); } - // TODO: more mapping! + if ($data === 'null' && $typeCollection->isNullable()) { + return null; + } + + // Loop over all possible types and parse to the first one that matches + foreach ($typeCollection->types as $type) { + try { + if ($type->isNative()) { + return $this->mapNativeType($type, $data); + } + + if ($type->isArray()) { + return $this->mapArray($type, $data); + } + } catch (CouldNotMapValueException) { + continue; + } + } + + throw new CouldNotMapValueException($data, $typeCollection); + } + + private function mapNativeType(DataType $type, mixed $data): float|object|bool|int|string|null + { + return match ($type->type) { + 'null' => null, + 'bool' => \filter_var($data, \FILTER_VALIDATE_BOOL), + 'int' => (int) $data, + 'float' => (float) $data, + 'string' => (string) $data, + 'object' => (object) $data, + default => throw new CouldNotMapValueException($data, $type), + }; + } + + private function mapArray(DataType $type, mixed $data): array + { + if (! \is_iterable($data)) { + throw new CouldNotMapValueException($data, $type); + } + + $keyType = null; + $valueType = $type->genericTypes[0]; + if (\count($type->genericTypes) > 1) { + [$keyType, $valueType] = $type->genericTypes; + } + + $mappedArray = []; + foreach ($data as $key => $value) { + if ($keyType !== null) { + $key = $this->map($keyType, $key); + } + + $mappedArray[$key] = $this->map($valueType, $value); + } + + return $mappedArray; } } diff --git a/src/Types/DataType.php b/src/Types/DataType.php index c7fe21c..2f82e3d 100644 --- a/src/Types/DataType.php +++ b/src/Types/DataType.php @@ -17,12 +17,29 @@ public function __construct( } public function isArray(): bool + { + return ! empty($this->genericTypes) + && \in_array( + $this->type, + [ + 'array', + 'iterable', + ], + ); + } + + /** @see https://www.php.net/manual/en/language.types.intro.php */ + public function isNative(): bool { return \in_array( $this->type, [ - 'array', - 'iterable', + 'null', + 'bool', + 'float', + 'int', + 'string', + 'object', ], ); } diff --git a/src/Types/DataTypeCollection.php b/src/Types/DataTypeCollection.php index 8f59939..168004e 100644 --- a/src/Types/DataTypeCollection.php +++ b/src/Types/DataTypeCollection.php @@ -4,9 +4,30 @@ class DataTypeCollection { - public function __construct( - public readonly array $types, - ) { + /** @var array */ + public readonly array $types; + + /** + * @param array $types + */ + public function __construct(array $types) + { + // Ensure null is always the last type + $typesArray = []; + $null = null; + foreach ($types as $type) { + if ($type->type === 'null') { + $null = $type; + } else { + $typesArray[] = $type; + } + } + + if ($null !== null) { + $typesArray[] = $null; + } + + $this->types = $typesArray; } public function isNullable(): bool diff --git a/tests/MapperTest.php b/tests/MapperTest.php new file mode 100644 index 0000000..33e265a --- /dev/null +++ b/tests/MapperTest.php @@ -0,0 +1,41 @@ +assertSame($expectation, (new Mapper())->map($type, $value)); + } + + public static function nativeValuesDataProvider(): Generator + { + yield ['null', null, null]; + + yield ['bool[]', [true, false], [true, false]]; + yield ['bool', '1', true]; + yield ['bool', null, false]; + + yield ['float', 6.8, 6.8]; + yield ['float', 5, 5.0]; + yield ['float', '1', 1.0]; + yield ['float', '1.3', 1.3]; + + yield ['int', 5, 5]; + yield ['int', '8', 8]; + yield ['int', 8.3, 8]; + yield ['int[]', [5, 8], [5, 8]]; + + yield ['string', 4, '4']; + yield ['array', [4, 5], ['4', '5']]; + } +} From 3c7cb6d335e1b383e1bc28a56c08b2d8f24ee71a Mon Sep 17 00:00:00 2001 From: jerodev Date: Tue, 9 May 2023 14:36:41 +0200 Subject: [PATCH 03/25] Map generic array --- .github/workflows/run-tests.yml | 3 +-- src/Mapper.php | 4 ++++ src/Types/DataType.php | 20 ++++++++++++-------- tests/MapperTest.php | 2 ++ 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index ca8c373..9b0aab2 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,7 +1,6 @@ name: run-tests on: - - push - pull_request jobs: @@ -20,7 +19,7 @@ jobs: - name: Cache dependencies uses: actions/cache@v3 - with:with: + with: path: | .phpstan-cache vendor diff --git a/src/Mapper.php b/src/Mapper.php index 8abf8be..4b21260 100644 --- a/src/Mapper.php +++ b/src/Mapper.php @@ -74,6 +74,10 @@ private function mapArray(DataType $type, mixed $data): array throw new CouldNotMapValueException($data, $type); } + if ($type->isGenericArray()) { + return (array) $data; + } + $keyType = null; $valueType = $type->genericTypes[0]; if (\count($type->genericTypes) > 1) { diff --git a/src/Types/DataType.php b/src/Types/DataType.php index 2f82e3d..a8f9450 100644 --- a/src/Types/DataType.php +++ b/src/Types/DataType.php @@ -18,14 +18,18 @@ public function __construct( public function isArray(): bool { - return ! empty($this->genericTypes) - && \in_array( - $this->type, - [ - 'array', - 'iterable', - ], - ); + return \in_array( + $this->type, + [ + 'array', + 'iterable', + ], + ); + } + + public function isGenericArray(): bool + { + return $this->isArray() && empty($this->genericTypes); } /** @see https://www.php.net/manual/en/language.types.intro.php */ diff --git a/tests/MapperTest.php b/tests/MapperTest.php index 33e265a..80daf9e 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -21,6 +21,8 @@ public static function nativeValuesDataProvider(): Generator { yield ['null', null, null]; + yield ['array', [1, 'b'], [1, 'b']]; + yield ['bool[]', [true, false], [true, false]]; yield ['bool', '1', true]; yield ['bool', null, false]; From a6502fcc157e713a2afc6a902f73f6fc7e777911 Mon Sep 17 00:00:00 2001 From: jerodev Date: Tue, 9 May 2023 14:39:16 +0200 Subject: [PATCH 04/25] phpcs --- src/Mapper.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Mapper.php b/src/Mapper.php index 4b21260..15143fd 100644 --- a/src/Mapper.php +++ b/src/Mapper.php @@ -6,7 +6,6 @@ use Jerodev\DataMapper\Types\DataType; use Jerodev\DataMapper\Types\DataTypeCollection; use Jerodev\DataMapper\Types\DataTypeFactory; -use function PHPUnit\Framework\throwException; class Mapper { From 91e75e9a769cefe5fbab77df57065c2813279f7f Mon Sep 17 00:00:00 2001 From: jerodev Date: Tue, 9 May 2023 15:12:14 +0200 Subject: [PATCH 05/25] WIP: ClassResolver --- .editorconfig | 2 +- .../CouldNotResolveClassException.php | 18 ++++++++ src/Mapper.php | 14 ++++++ src/Objects/ClassResolver.php | 45 +++++++++++++++++++ src/Objects/ObjectMapper.php | 27 +++++++++++ src/Types/DataType.php | 2 +- src/Types/DataTypeFactory.php | 2 +- 7 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 src/Exceptions/CouldNotResolveClassException.php create mode 100644 src/Objects/ClassResolver.php create mode 100644 src/Objects/ObjectMapper.php diff --git a/.editorconfig b/.editorconfig index fcdf61e..cf80b4e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,4 +6,4 @@ end_of_line = lf insert_final_newline = true indent_style = space indent_size = 4 -trim_trailing_whitespace = true \ No newline at end of file +trim_trailing_whitespace = true diff --git a/src/Exceptions/CouldNotResolveClassException.php b/src/Exceptions/CouldNotResolveClassException.php new file mode 100644 index 0000000..f923b07 --- /dev/null +++ b/src/Exceptions/CouldNotResolveClassException.php @@ -0,0 +1,18 @@ +dataTypeFactory = new DataTypeFactory(); + $this->objectMapper = new ObjectMapper(); } /** @@ -46,6 +49,8 @@ public function map($typeCollection, $data) if ($type->isArray()) { return $this->mapArray($type, $data); } + + return $this->mapObject($type, $data); } catch (CouldNotMapValueException) { continue; } @@ -94,4 +99,13 @@ private function mapArray(DataType $type, mixed $data): array return $mappedArray; } + + private function mapObject(DataType $type, mixed $data): ?object + { + if ($type->isNullable && $data === null) { + return null; + } + + return $this->objectMapper->map($type, $data); + } } diff --git a/src/Objects/ClassResolver.php b/src/Objects/ClassResolver.php new file mode 100644 index 0000000..4697eb3 --- /dev/null +++ b/src/Objects/ClassResolver.php @@ -0,0 +1,45 @@ +findSourceFile(); + if ($sourceFile === null) { + throw new CouldNotResolveClassException($name); + } + + // TODO: attempt to resolve class name through use statements in file + + throw new CouldNotResolveClassException($name, $sourceFile); + } + + private function findSourceFile(): ?string + { + $backtrace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 10); + $mapperFileSuffix = \implode(\DIRECTORY_SEPARATOR, ['data-mapper', 'src', 'Mapper.php']); + + $foundMapper = false; + foreach ($backtrace as $trace) { + if (\str_ends_with($trace['file'], $mapperFileSuffix)) { + $foundMapper = true; + continue; + } + + if ($foundMapper) { + return $trace['file']; + } + } + + return null; + } +} diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php new file mode 100644 index 0000000..2fc2fde --- /dev/null +++ b/src/Objects/ObjectMapper.php @@ -0,0 +1,27 @@ +classResolver = new ClassResolver(); + } + + public function map(DataType $type, array $data): object + { + $class = $type->type; + if (! \class_exists($class)) { + $class = $this->classResolver->resolve($class); + } + + // TODO: map object + + return new $class(); + } +} diff --git a/src/Types/DataType.php b/src/Types/DataType.php index a8f9450..446eb5c 100644 --- a/src/Types/DataType.php +++ b/src/Types/DataType.php @@ -2,7 +2,7 @@ namespace Jerodev\DataMapper\Types; -final class DataType +class DataType { /** * @param string $type diff --git a/src/Types/DataTypeFactory.php b/src/Types/DataTypeFactory.php index 59ea9eb..cbae2b9 100644 --- a/src/Types/DataTypeFactory.php +++ b/src/Types/DataTypeFactory.php @@ -4,7 +4,7 @@ use Jerodev\DataMapper\Exceptions\UnexpectedTokenException; -final class DataTypeFactory +class DataTypeFactory { /** @var array */ private array $typeCache = []; From bf9d081e0751d734692304e4eeeaade1a510bb95 Mon Sep 17 00:00:00 2001 From: jerodev Date: Tue, 9 May 2023 16:37:43 +0200 Subject: [PATCH 06/25] WIP: ClassBluePrinter --- src/Mapper.php | 9 ++++- src/Objects/ClassBluePrint.php | 12 ++++++ src/Objects/ClassBluePrinter.php | 36 +++++++++++++++++ src/Objects/ClassResolver.php | 66 +++++++++++++++++++++++++++++++- src/Objects/ObjectMapper.php | 31 +++++++++++++-- tests/MapperTest.php | 2 + 6 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 src/Objects/ClassBluePrint.php create mode 100644 src/Objects/ClassBluePrinter.php diff --git a/src/Mapper.php b/src/Mapper.php index da09d4d..ee3df75 100644 --- a/src/Mapper.php +++ b/src/Mapper.php @@ -3,6 +3,7 @@ namespace Jerodev\DataMapper; use Jerodev\DataMapper\Exceptions\CouldNotMapValueException; +use Jerodev\DataMapper\Exceptions\CouldNotResolveClassException; use Jerodev\DataMapper\Objects\ObjectMapper; use Jerodev\DataMapper\Types\DataType; use Jerodev\DataMapper\Types\DataTypeCollection; @@ -16,7 +17,7 @@ class Mapper public function __construct() { $this->dataTypeFactory = new DataTypeFactory(); - $this->objectMapper = new ObjectMapper(); + $this->objectMapper = new ObjectMapper($this); } /** @@ -106,6 +107,10 @@ private function mapObject(DataType $type, mixed $data): ?object return null; } - return $this->objectMapper->map($type, $data); + try { + return $this->objectMapper->map($type, $data); + } catch (CouldNotResolveClassException) { + throw new CouldNotMapValueException($data, $type); + } } } diff --git a/src/Objects/ClassBluePrint.php b/src/Objects/ClassBluePrint.php new file mode 100644 index 0000000..18b7701 --- /dev/null +++ b/src/Objects/ClassBluePrint.php @@ -0,0 +1,12 @@ + */ + public array $constructorArguments; + + /** @var array */ + public array $properties; +} diff --git a/src/Objects/ClassBluePrinter.php b/src/Objects/ClassBluePrinter.php new file mode 100644 index 0000000..8005f28 --- /dev/null +++ b/src/Objects/ClassBluePrinter.php @@ -0,0 +1,36 @@ +printConstructor($reflection, $blueprint); + // TODO: map public properties + + return $blueprint; + } + + private function printConstructor(ReflectionClass $reflection, ClassBluePrint $bluePrint): void + { + $constructor = $reflection->getConstructor(); + if ($constructor === null || $constructor->getNumberOfParameters() === 0) { + return; + } + + // TODO: parse doc block to get more information about argument types + foreach ($constructor->getParameters() as $param) { + $bluePrint->constructorArguments[] = \array_filter([ + 'name' => $param->getName(), + 'type' => $param->getType()->getName(), + 'default' => $param->getDefaultValue(), + ]); + } + } +} diff --git a/src/Objects/ClassResolver.php b/src/Objects/ClassResolver.php index 4697eb3..1fc8bb4 100644 --- a/src/Objects/ClassResolver.php +++ b/src/Objects/ClassResolver.php @@ -18,7 +18,10 @@ public function resolve(string $name): string throw new CouldNotResolveClassException($name); } - // TODO: attempt to resolve class name through use statements in file + $name = $this->findClassNameInFile($name, $sourceFile); + if (\class_exists($name)) { + return $name; + } throw new CouldNotResolveClassException($name, $sourceFile); } @@ -42,4 +45,65 @@ private function findSourceFile(): ?string return null; } + + private function findClassNameInFile(string $name, string $sourceFile): string + { + $file = \file_get_contents($sourceFile); + if ($file === false) { + return $name; + } + + $nameParts = \explode('\\', $name); + $lastPart = \end($nameParts); + + $newline = false; + for ($i = 0; $i < \strlen($file); $i++) { + $char = $file[$i]; + + // Don't care about spaces + if ($char === ' ' || $char === "\t") { + continue; + } + + if ($char === \PHP_EOL) { + $newline = true; + continue; + } + + // If we are after a newline and find a use statement, parse it! + if ($newline && $char === 'u' && \substr($file, $i, 4) === 'use ') { + $i += 4; + + $startAlias = false; + $importName = ''; + $importAlias = ''; + while (($char = $file[$i++]) !== ';') { + if ($char === ' ' && \substr($file, $i, 3) === 'as ') { + $startAlias = true; + $i += 3; + continue; + } + + if ($startAlias) { + $importAlias .= $char; + } else { + $importName .= $char; + } + } + + $nameParts = \explode('\\', $importName); + $lastImportPart = \end($nameParts); + + if ($importAlias === $lastPart || $lastImportPart === $lastPart) { + return $importName; + } + + $i--; + } + + $newline = false; + } + + return $name; + } } diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index 2fc2fde..0f91e08 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -2,17 +2,28 @@ namespace Jerodev\DataMapper\Objects; +use Jerodev\DataMapper\Exceptions\CouldNotResolveClassException; +use Jerodev\DataMapper\Mapper; use Jerodev\DataMapper\Types\DataType; class ObjectMapper { + private readonly ClassBluePrinter $classBluePrinter; private readonly ClassResolver $classResolver; - public function __construct() - { + public function __construct( + private readonly Mapper $mapper, + ) { + $this->classBluePrinter = new ClassBluePrinter(); $this->classResolver = new ClassResolver(); } + /** + * @param DataType $type + * @param array $data + * @return object + * @throws CouldNotResolveClassException + */ public function map(DataType $type, array $data): object { $class = $type->type; @@ -20,8 +31,20 @@ public function map(DataType $type, array $data): object $class = $this->classResolver->resolve($class); } - // TODO: map object + $mapFileName = 'mapper_' . \md5($class); + if (! \file_exists($mapFileName . '.php')) { + \file_put_contents($mapFileName . '.php', $this->createObjectMappingFunction($class)); + } + + // Include the function containing file and call the function. + require_once($mapFileName . '.php'); + return ($mapFileName)($this->mapper, $data); + } + + private function createObjectMappingFunction(string $class): string + { + $blueprint = $this->classBluePrinter->print($class); - return new $class(); + var_dump($blueprint);die(); } } diff --git a/tests/MapperTest.php b/tests/MapperTest.php index 80daf9e..8228ab3 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -19,6 +19,8 @@ public function it_should_map_native_values(string $type, mixed $value, mixed $e public static function nativeValuesDataProvider(): Generator { + yield ['Mapper', [], null]; + yield ['null', null, null]; yield ['array', [1, 'b'], [1, 'b']]; From 96526ee51f98933dcf90f4cc727b2323be9892fe Mon Sep 17 00:00:00 2001 From: jerodev Date: Tue, 9 May 2023 19:40:05 +0200 Subject: [PATCH 07/25] Parse docblocks --- composer.lock | 601 ++++++--------------------- src/Objects/ClassBluePrint.php | 6 +- src/Objects/ClassBluePrinter.php | 54 ++- src/Objects/DocBlock.php | 16 + src/Objects/DocBlockParser.php | 97 +++++ tests/Objects/DocBlockParserTest.php | 50 +++ 6 files changed, 332 insertions(+), 492 deletions(-) create mode 100644 src/Objects/DocBlock.php create mode 100644 src/Objects/DocBlockParser.php create mode 100644 tests/Objects/DocBlockParserTest.php diff --git a/composer.lock b/composer.lock index 3faaa4e..227ccf8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a69d42ae05551c93ebc4a8f02b025c00", + "content-hash": "7b010f8270a95cc28b36c706e8fddfeb", "packages": [], "packages-dev": [ { @@ -84,30 +84,30 @@ }, { "name": "doctrine/instantiator", - "version": "1.4.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9", + "doctrine/coding-standard": "^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.22" + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" }, "type": "library", "autoload": { @@ -134,7 +134,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" }, "funding": [ { @@ -150,7 +150,7 @@ "type": "tidelift" } ], - "time": "2022-03-03T08:28:38+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { "name": "jerodev/code-styles", @@ -190,16 +190,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -237,7 +237,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -245,20 +245,20 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "nikic/php-parser", - "version": "v4.13.2", + "version": "v4.15.4", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077" + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", "shasum": "" }, "require": { @@ -299,9 +299,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" }, - "time": "2021-11-30T19:35:32+00:00" + "time": "2023-03-05T19:49:14+00:00" }, { "name": "phar-io/manifest", @@ -414,245 +414,18 @@ }, "time": "2022-02-21T01:04:05+00:00" }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" - }, - "time": "2021-10-19T17:43:47+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.6.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "77a32518733312af16a44300404e945338981de3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", - "reference": "77a32518733312af16a44300404e945338981de3", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" - }, - "time": "2022-03-15T21:29:03+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "v1.15.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.2", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^6.0 || ^7.0", - "phpunit/phpunit": "^8.0 || ^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" - }, - "time": "2021-12-08T12:19:24+00:00" - }, { "name": "phpstan/phpdoc-parser", - "version": "1.4.3", + "version": "1.20.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "34545bb30a6f8bd86cfa371dbd42140a657bbf0d" + "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/34545bb30a6f8bd86cfa371dbd42140a657bbf0d", - "reference": "34545bb30a6f8bd86cfa371dbd42140a657bbf0d", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", + "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", "shasum": "" }, "require": { @@ -662,6 +435,7 @@ "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5", "symfony/process": "^5.2" @@ -681,29 +455,29 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.4.3" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.4" }, - "time": "2022-04-08T11:30:34+00:00" + "time": "2023-05-02T09:19:37+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.15", + "version": "9.2.26", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^4.15", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -718,8 +492,8 @@ "phpunit/phpunit": "^9.3" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { @@ -752,7 +526,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" }, "funding": [ { @@ -760,7 +534,7 @@ "type": "github" } ], - "time": "2022-03-07T09:28:20+00:00" + "time": "2023-03-06T12:58:08+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1005,20 +779,20 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.20", + "version": "9.6.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba" + "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/12bc8879fb65aef2138b26fc633cb1e3620cffba", - "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", + "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", + "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -1029,7 +803,6 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", "phpunit/php-code-coverage": "^9.2.13", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", @@ -1037,23 +810,19 @@ "phpunit/php-timer": "^5.0.2", "sebastian/cli-parser": "^1.0.1", "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", + "sebastian/comparator": "^4.0.8", "sebastian/diff": "^4.0.3", "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", + "sebastian/exporter": "^4.0.5", "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.0", + "sebastian/type": "^3.2", "sebastian/version": "^3.0.2" }, - "require-dev": { - "ext-pdo": "*", - "phpspec/prophecy-phpunit": "^2.0.1" - }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -1061,7 +830,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-master": "9.6-dev" } }, "autoload": { @@ -1092,7 +861,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.20" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.7" }, "funding": [ { @@ -1102,9 +872,13 @@ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2022-04-01T12:37:26+00:00" + "time": "2023-04-14T08:58:40+00:00" }, { "name": "sebastian/cli-parser", @@ -1275,16 +1049,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", "shasum": "" }, "require": { @@ -1337,7 +1111,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" }, "funding": [ { @@ -1345,7 +1119,7 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2022-09-14T12:41:17+00:00" }, { "name": "sebastian/complexity", @@ -1406,16 +1180,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "shasum": "" }, "require": { @@ -1460,7 +1234,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" }, "funding": [ { @@ -1468,20 +1242,20 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-05-07T05:35:17+00:00" }, { "name": "sebastian/environment", - "version": "5.1.4", + "version": "5.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { @@ -1523,7 +1297,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { @@ -1531,20 +1305,20 @@ "type": "github" } ], - "time": "2022-04-03T09:37:03+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { @@ -1600,7 +1374,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" }, "funding": [ { @@ -1608,7 +1382,7 @@ "type": "github" } ], - "time": "2021-11-11T14:18:36+00:00" + "time": "2022-09-14T06:03:37+00:00" }, { "name": "sebastian/global-state", @@ -1845,16 +1619,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { @@ -1893,10 +1667,10 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { @@ -1904,7 +1678,7 @@ "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { "name": "sebastian/resource-operations", @@ -1963,16 +1737,16 @@ }, { "name": "sebastian/type", - "version": "3.0.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { @@ -1984,7 +1758,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -2007,7 +1781,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { @@ -2015,7 +1789,7 @@ "type": "github" } ], - "time": "2022-03-15T09:54:48+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { "name": "sebastian/version", @@ -2072,32 +1846,32 @@ }, { "name": "slevomat/coding-standard", - "version": "7.1", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "b521bd358b5f7a7d69e9637fd139e036d8adeb6f" + "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b521bd358b5f7a7d69e9637fd139e036d8adeb6f", - "reference": "b521bd358b5f7a7d69e9637fd139e036d8adeb6f", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/aff06ae7a84e4534bf6f821dc982a93a5d477c90", + "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": "^1.4.1", + "phpstan/phpdoc-parser": "^1.5.1", "squizlabs/php_codesniffer": "^3.6.2" }, "require-dev": { - "phing/phing": "2.17.2", + "phing/phing": "2.17.3", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.5.2", + "phpstan/phpstan": "1.4.10|1.7.1", "phpstan/phpstan-deprecation-rules": "1.0.0", - "phpstan/phpstan-phpunit": "1.0.0|1.1.0", - "phpstan/phpstan-strict-rules": "1.1.0", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.19" + "phpstan/phpstan-phpunit": "1.0.0|1.1.1", + "phpstan/phpstan-strict-rules": "1.2.3", + "phpunit/phpunit": "7.5.20|8.5.21|9.5.20" }, "type": "phpcodesniffer-standard", "extra": { @@ -2117,7 +1891,7 @@ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.1" + "source": "https://github.com/slevomat/coding-standard/tree/7.2.1" }, "funding": [ { @@ -2129,20 +1903,20 @@ "type": "tidelift" } ], - "time": "2022-03-29T12:44:16+00:00" + "time": "2022-05-25T10:58:12+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.6.2", + "version": "3.7.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", "shasum": "" }, "require": { @@ -2178,96 +1952,15 @@ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2021-12-12T21:44:58+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.25.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "30885182c981ab175d4d034db0f6f469898070ab" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", - "reference": "30885182c981ab175d4d034db0f6f469898070ab", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-10-20T20:35:02+00:00" + "time": "2023-02-22T23:07:41+00:00" }, { "name": "theseer/tokenizer", @@ -2318,64 +2011,6 @@ } ], "time": "2021-07-28T10:34:58+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" - }, - "time": "2021-03-09T10:59:23+00:00" } ], "aliases": [], @@ -2386,7 +2021,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^7.4|^8.0" + "php": "^8.1" }, "platform-dev": [], "plugin-api-version": "2.3.0" diff --git a/src/Objects/ClassBluePrint.php b/src/Objects/ClassBluePrint.php index 18b7701..8da4332 100644 --- a/src/Objects/ClassBluePrint.php +++ b/src/Objects/ClassBluePrint.php @@ -5,8 +5,8 @@ class ClassBluePrint { /** @var array */ - public array $constructorArguments; + public array $constructorArguments = []; - /** @var array */ - public array $properties; + /** @var array An array with name as key and type as value */ + public array $properties = []; } diff --git a/src/Objects/ClassBluePrinter.php b/src/Objects/ClassBluePrinter.php index 8005f28..b520633 100644 --- a/src/Objects/ClassBluePrinter.php +++ b/src/Objects/ClassBluePrinter.php @@ -6,13 +6,20 @@ class ClassBluePrinter { + private readonly DocBlockParser $docBlockParser; + + public function __construct() + { + $this->docBlockParser = new DocBlockParser(); + } + public function print(string $class): ClassBluePrint { $reflection = new ReflectionClass($class); $blueprint = new ClassBluePrint(); $this->printConstructor($reflection, $blueprint); - // TODO: map public properties + $this->printProperties($reflection, $blueprint); return $blueprint; } @@ -24,13 +31,48 @@ private function printConstructor(ReflectionClass $reflection, ClassBluePrint $b return; } - // TODO: parse doc block to get more information about argument types + // Get information from potential doc comment + $doc = null; + if ($constructor->getDocComment()) { + $doc = $this->docBlockParser->parse($constructor->getDocComment()); + } + + // Loop over each parameter and describe it foreach ($constructor->getParameters() as $param) { - $bluePrint->constructorArguments[] = \array_filter([ + $type = $param->getType()?->getName(); + if ($doc !== null && \in_array($type, [null, 'array', 'iterable'])) { + $type = $doc->getParamType($param->getName()); + } + + $arg = [ 'name' => $param->getName(), - 'type' => $param->getType()->getName(), - 'default' => $param->getDefaultValue(), - ]); + 'type' => $type, + ]; + if ($param->isDefaultValueAvailable()) { + $arg['default'] = $param->getDefaultValue(); + } + + $bluePrint->constructorArguments[] = $arg; + } + } + + private function printProperties(ReflectionClass $reflection, ClassBluePrint $blueprint): void + { + $properties = $reflection->getProperties(); + foreach ($properties as $property) { + if (! $property->isPublic()) { + continue; + } + + $type = $property->getType()?->getName(); + if (\in_array($type, [null, 'array', 'iterable']) && $property->getDocComment()) { + $doc = $this->docBlockParser->parse($property->getDocComment()); + if ($doc->varType !== null) { + $type = $doc->varType; + } + } + + $blueprint->properties[$property->getName()] = $type; } } } diff --git a/src/Objects/DocBlock.php b/src/Objects/DocBlock.php new file mode 100644 index 0000000..6dfa7ea --- /dev/null +++ b/src/Objects/DocBlock.php @@ -0,0 +1,16 @@ + */ + public array $paramTypes = []; + public ?string $returnType = null; + public ?string $varType = null; + + public function getParamType(string $name): ?string + { + return $this->paramTypes[$name] ?? null; + } +} diff --git a/src/Objects/DocBlockParser.php b/src/Objects/DocBlockParser.php new file mode 100644 index 0000000..05d1f48 --- /dev/null +++ b/src/Objects/DocBlockParser.php @@ -0,0 +1,97 @@ + $this->parseParam($contents, $i, $docblock), + '@return', '@var' => $this->parseReturn($contents, $i, $docblock, \substr($identifier, 1) . 'Type'), + default => null, + }; + } + } + + return $docblock; + } + + private function parseParam(string $contents, int &$i, DocBlock $docblock): void + { + $type = $this->parseType($contents, $i); + + $docblock->paramTypes[$this->parseVariable($contents, $i)] = $type; + } + + private function parseReturn(string $contents, int &$i, DocBlock $docblock, string $docProperty = 'returnType'): void + { + $docblock->{$docProperty} = $this->parseType($contents, $i); + } + + private function parseType(string $contents, int &$i): string + { + $type = ''; + $sawSpace = false; + $lastChar = ''; + for (; $i < \strlen($contents); $i++) { + $char = $contents[$i]; + if ($char === ' ') { + $type .= $char; + $sawSpace = true; + continue; + } + + if (\in_array($char, ['|', ','])) { + $type .= $char; + $lastChar = $char; + $sawSpace = false; + continue; + } + + if ($sawSpace && ! \in_array($lastChar, ['|', ','])) { + break; + } + + $type .= $char; + $lastChar = $char; + $sawSpace = false; + } + + return \trim($type); + } + + private function parseVariable(string $contents, int &$i): string + { + $variable = ''; + for (; $i < \strlen($contents); $i++) { + $char = $contents[$i]; + if ($char === ' ') { + if (empty($variable)) { + continue; + } + + break; + } + + if (empty($variable) && $char !== '$') { + throw new \Exception('Expected variable to start with \'$\', found \'' . $char . '\'.'); + } + + $variable .= $char; + } + + return \trim(\substr($variable, 1)); + } +} diff --git a/tests/Objects/DocBlockParserTest.php b/tests/Objects/DocBlockParserTest.php new file mode 100644 index 0000000..8e697c4 --- /dev/null +++ b/tests/Objects/DocBlockParserTest.php @@ -0,0 +1,50 @@ +parse($input); + + $this->assertEquals($paramTypes, $doc->paramTypes); + $this->assertEquals($returnType, $doc->returnType); + $this->assertEquals($varType, $doc->varType); + } + + public static function docBlockDataProvider(): Generator + { + yield [ + <<<'DOC' + /** + * @param string $name Name of the device + * @param int $age Age of the device + * @return string Years of warranty left + */ + DOC, + ['name' => 'string', 'age' => 'int'], + 'string', + null, + ]; + + yield [ + <<<'DOC' + /** + * @var array The length of each key + */ + DOC, + [], + null, + 'array', + ]; + } +} From 1a6adefd503bcf63cfbfe629078ab4a3f63134f2 Mon Sep 17 00:00:00 2001 From: jerodev Date: Tue, 9 May 2023 19:47:49 +0200 Subject: [PATCH 08/25] Blueprinter test --- tests/Objects/ClassBluePrinterTest.php | 32 ++++++++++++++++++++++++++ tests/_Mocks/UserDto.php | 17 ++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/Objects/ClassBluePrinterTest.php create mode 100644 tests/_Mocks/UserDto.php diff --git a/tests/Objects/ClassBluePrinterTest.php b/tests/Objects/ClassBluePrinterTest.php new file mode 100644 index 0000000..40443a2 --- /dev/null +++ b/tests/Objects/ClassBluePrinterTest.php @@ -0,0 +1,32 @@ +print($className); + + $this->assertEquals($constructorArguments, $blueprint->constructorArguments); + $this->assertEquals($properties, $blueprint->properties); + } + + public static function classBlueprintDataProvider(): Generator + { + yield [ + UserDto::class, + [['name' => 'name', 'type' => 'string']], + ['name' => 'string', 'friends' => 'array'], + ]; + } +} diff --git a/tests/_Mocks/UserDto.php b/tests/_Mocks/UserDto.php new file mode 100644 index 0000000..f96a836 --- /dev/null +++ b/tests/_Mocks/UserDto.php @@ -0,0 +1,17 @@ + */ + public array $friends = []; + + public function __construct(string $name) + { + $this->name = $name; + } +} From 88c8d031a5dcf36e8fe4eb76d9303040e5047427 Mon Sep 17 00:00:00 2001 From: jerodev Date: Tue, 9 May 2023 19:57:29 +0200 Subject: [PATCH 09/25] Map enums --- src/Objects/ObjectMapper.php | 14 ++++++++------ tests/MapperTest.php | 3 +++ tests/_Mocks/SuitEnum.php | 11 +++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 tests/_Mocks/SuitEnum.php diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index 0f91e08..a699e3e 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -20,15 +20,17 @@ public function __construct( /** * @param DataType $type - * @param array $data + * @param array|string $data * @return object * @throws CouldNotResolveClassException */ - public function map(DataType $type, array $data): object + public function map(DataType $type, array|string $data): object { - $class = $type->type; - if (! \class_exists($class)) { - $class = $this->classResolver->resolve($class); + $class = $this->classResolver->resolve($type->type); + + // If the data is a string and the class is an enum, create the enum. + if (\is_string($data) && \is_subclass_of($class, \BackedEnum::class)) { + return $class::from($data); } $mapFileName = 'mapper_' . \md5($class); @@ -45,6 +47,6 @@ private function createObjectMappingFunction(string $class): string { $blueprint = $this->classBluePrinter->print($class); - var_dump($blueprint);die(); +// var_dump($blueprint);die(); } } diff --git a/tests/MapperTest.php b/tests/MapperTest.php index 8228ab3..b57a53e 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -4,6 +4,7 @@ use Generator; use Jerodev\DataMapper\Mapper; +use Jerodev\DataMapper\Tests\_Mocks\SuitEnum; use PHPUnit\Framework\TestCase; final class MapperTest extends TestCase @@ -41,5 +42,7 @@ public static function nativeValuesDataProvider(): Generator yield ['string', 4, '4']; yield ['array', [4, 5], ['4', '5']]; + + yield ['array<' . SuitEnum::class . '>', ['H', 'S'], [SuitEnum::Hearts, SuitEnum::Spades]]; } } diff --git a/tests/_Mocks/SuitEnum.php b/tests/_Mocks/SuitEnum.php new file mode 100644 index 0000000..2f51a44 --- /dev/null +++ b/tests/_Mocks/SuitEnum.php @@ -0,0 +1,11 @@ + Date: Wed, 10 May 2023 11:20:44 +0200 Subject: [PATCH 10/25] Rewrite datatype parser --- src/Types/DataType.php | 17 ++- src/Types/DataTypeCollection.php | 8 ++ src/Types/DataTypeFactory.php | 184 ++++++++++++++++++------- tests/Types/DataTypeCollectionTest.php | 32 +++++ tests/Types/DataTypeFactoryTest.php | 22 ++- 5 files changed, 210 insertions(+), 53 deletions(-) create mode 100644 tests/Types/DataTypeCollectionTest.php diff --git a/src/Types/DataType.php b/src/Types/DataType.php index 446eb5c..3bf150f 100644 --- a/src/Types/DataType.php +++ b/src/Types/DataType.php @@ -12,7 +12,7 @@ class DataType public function __construct( public readonly string $type, public readonly bool $isNullable, - public readonly array $genericTypes = [], + public array $genericTypes = [], ) { } @@ -47,4 +47,19 @@ public function isNative(): bool ], ); } + + public function __toString(): string + { + $type = $this->type; + + if ($this->isNullable) { + $type = '?' . $this->type; + } + + if (! empty($this->genericTypes)) { + $type .= '<' . \implode(', ', \array_map(static fn (DataTypeCollection $type) => $type->__toString(), $this->genericTypes)) . '>'; + } + + return $type; + } } diff --git a/src/Types/DataTypeCollection.php b/src/Types/DataTypeCollection.php index 168004e..6a484b2 100644 --- a/src/Types/DataTypeCollection.php +++ b/src/Types/DataTypeCollection.php @@ -39,4 +39,12 @@ public function isNullable(): bool ) ); } + + public function __toString(): string + { + return \implode( + '|', + \array_map(static fn (DataType $type) => $type->__toString(), $this->types), + ); + } } diff --git a/src/Types/DataTypeFactory.php b/src/Types/DataTypeFactory.php index cbae2b9..9c28d09 100644 --- a/src/Types/DataTypeFactory.php +++ b/src/Types/DataTypeFactory.php @@ -6,33 +6,31 @@ class DataTypeFactory { - /** @var array */ + /** @var array */ private array $typeCache = []; public function fromString(string $rawType): DataTypeCollection { - $types = []; - - $parts = \explode('|', $rawType); - foreach ($parts as $part) { - $types[] = $this->singleFromString(\trim($part)); - } + $rawType = \trim($rawType); - return new DataTypeCollection($types); - } - - public function singleFromString(string $rawType): DataType - { if (\array_key_exists($rawType, $this->typeCache)) { return $this->typeCache[$rawType]; } $tokens = $this->tokenize($rawType); if (\count($tokens) === 1) { - return $this->typeCache[$rawType] = new DataType($rawType, false); + return $this->typeCache[$rawType] = new DataTypeCollection([ + new DataType($rawType, false), + ]); } - return $this->typeCache[$rawType] = $this->parseTokens($tokens); + // Parse tokens and make sure nothing is left. + $type = $this->parseTokens($tokens); + if (! empty($tokens)) { + throw new UnexpectedTokenException(\array_shift($tokens)); + } + + return $this->typeCache[$rawType] = $type; } /** @@ -46,16 +44,25 @@ private function tokenize(string $type): array for ($i = 0; $i < \strlen($type); $i++) { $char = $type[$i]; + if ($char === ' ') { + continue; + } - if (\in_array($char, ['<', '>', '?', ','])) { - if (! empty($token)) { + if (\in_array($char, ['<', '>', '?', ',', '|'])) { + if (! empty(\trim($token))) { $tokens[] = $token; } $tokens[] = $char; $token = ''; } else if ($char === '[') { $tokens[] = $token; - $token = $char; + $token = ''; + if ($type[++$i] === ']') { + $tokens[] = '[]'; + continue; + } + + throw new UnexpectedTokenException('['); } else { $token .= $char; } @@ -70,58 +77,135 @@ private function tokenize(string $type): array /** * @param array $tokens - * @return DataType + * @return DataTypeCollection */ - private function parseTokens(array $tokens): DataType + private function parseTokens(array &$tokens): DataTypeCollection { - $isNullable = $tokens[0] === '?'; - if ($isNullable) { - \array_shift($tokens); - } + $types = []; - $type = \array_shift($tokens); + $stringStack = ''; + $nullable = false; + + while ($token = \array_shift($tokens)) { + if ($token === '<') { + $types[] = $this->makeDataType( + $stringStack, + $nullable, + $this->fetchGenericTypes($tokens), + ); + $stringStack = ''; + $nullable = false; + + continue; + } - // If nothing is left, return the type - if (empty($tokens)) { - return new DataType($type, $isNullable); + if ($token === '|') { + $types[] = $this->makeDataType($stringStack, $nullable); + $stringStack = ''; + $nullable = false; + + continue; + } + + if ($token === '?') { + $nullable = true; + continue; + } + + if (\in_array($token, ['>', ','])) { + throw new UnexpectedTokenException($token); + } + + $stringStack .= $token; } - // If square brackets, it's an array with the type as value - $nextToken = \array_shift($tokens); - if ($nextToken === '[]') { - return new DataType('array', $isNullable, [new DataTypeCollection([new DataType($type, false)])]); + if (! empty($stringStack)) { + $types[] = $this->makeDataType($stringStack, $nullable); } - // If beaks, find either a value or a key and value - if ($nextToken === '<') { - $nextGenericType = ''; - $genericTypes = []; - while ($nextToken = \array_shift($tokens)) { - if ($nextToken === '>') { - break; - } + return new DataTypeCollection($types); + } - if ($nextToken === ',') { - $genericTypes[] = $this->fromString($nextGenericType); - $nextGenericType = ''; + /** + * @param array $tokens + * @return array + */ + private function fetchGenericTypes(array &$tokens): array + { + $types = []; + $collection = []; + $stringStack = ''; + $nullable = false; + while ($token = \array_shift($tokens)) { + if ($token === '<') { + $types[] = $this->makeDataType( + $stringStack, + $nullable, + $this->fetchGenericTypes($tokens), + ); + $stringStack = ''; + $nullable = false; + + continue; + } - continue; + if ($token === ',' || $token === '|') { + if (! empty($stringStack)) { + $types[] = $this->makeDataType($stringStack, $nullable); + } + + if ($token === ',') { + $collection[] = new DataTypeCollection($types); + $types = []; } - $nextGenericType .= \trim($nextToken); + $stringStack = ''; + $nullable = false; + continue; } - if (empty($genericTypes) && empty($nextGenericType)) { - throw new \Exception('Found generic type without subtype'); + if ($token === '>') { + break; } - if (! empty($nextGenericType)) { - $genericTypes[] = $this->fromString($nextGenericType); + if ($token === '?') { + $nullable = true; + continue; } - return new DataType($type, $isNullable, $genericTypes); + $stringStack .= $token; + } + + if (! empty($stringStack)) { + $types[] = $this->makeDataType($stringStack, $nullable); + } + if (! empty($types)) { + $collection[] = new DataTypeCollection($types); + } + + return $collection; + } + + /** + * @param string $type + * @param bool $nullable + * @param array $genericTypes + * @return DataType + */ + private function makeDataType(string $type, bool $nullable = false, array $genericTypes = []): DataType + { + if (\str_ends_with($type, '[]')) { + return new DataType( + 'array', + $nullable, + [ + new DataTypeCollection([ + new DataType(\substr($type, 0, -2), false), + ]) + ], + ); } - throw new UnexpectedTokenException($nextToken); + return new DataType($type, $nullable, $genericTypes); } } diff --git a/tests/Types/DataTypeCollectionTest.php b/tests/Types/DataTypeCollectionTest.php new file mode 100644 index 0000000..f67b076 --- /dev/null +++ b/tests/Types/DataTypeCollectionTest.php @@ -0,0 +1,32 @@ +assertEquals( + $expectation, + (new DataTypeFactory())->fromString($input)->__toString() + ); + } + + public static function typeToStringDataProvider(): Generator + { + yield ['string', 'string']; + yield ['string|int', 'string|int']; + yield ['string[]', 'array']; + yield ['array', 'array']; + yield ['Generic', 'Generic']; + yield ['array>', 'array>>']; + } +} diff --git a/tests/Types/DataTypeFactoryTest.php b/tests/Types/DataTypeFactoryTest.php index 001ffea..3005689 100644 --- a/tests/Types/DataTypeFactoryTest.php +++ b/tests/Types/DataTypeFactoryTest.php @@ -17,7 +17,7 @@ final class DataTypeFactoryTest extends TestCase */ public function it_should_parse_single_data_types(string $input, DataType $expectation): void { - $this->assertEquals($expectation, (new DataTypeFactory())->singleFromString($input)); + $this->assertEquals($expectation, (new DataTypeFactory())->fromString($input)->types[0]); } /** @@ -28,7 +28,7 @@ public function it_should_throw_unexpected_token(string $input): void { $this->expectException(UnexpectedTokenException::class); - (new DataTypeFactory())->singleFromString($input); + (new DataTypeFactory())->fromString($input); } public static function singleDataTypeProvider(): Generator @@ -66,6 +66,23 @@ public static function singleDataTypeProvider(): Generator ]), ], )]; + yield ['array>', new DataType( + 'array', + false, + [ + new DataTypeCollection([ + new DataType('string', false), + new DataType('int', false), + ]), + new DataTypeCollection([ + new DataType('array', false, [ + new DataTypeCollection([ + new DataType('K', false), + ]), + ]), + ]), + ], + )]; yield ['Generic', new DataType( 'Generic', @@ -98,5 +115,6 @@ public static function singleDataTypeProvider(): Generator public static function unexpectedTokenDataProvider(): Generator { yield ['String>']; + yield ['array[foo]']; } } From 50dde7a4d927506fe8bbd08e30421709cdc7896e Mon Sep 17 00:00:00 2001 From: jerodev Date: Wed, 10 May 2023 11:24:23 +0200 Subject: [PATCH 11/25] WIP: map class constructor --- src/Objects/ClassBluePrint.php | 6 ++-- src/Objects/ClassBluePrinter.php | 7 +++-- src/Objects/ObjectMapper.php | 54 ++++++++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/Objects/ClassBluePrint.php b/src/Objects/ClassBluePrint.php index 8da4332..aca4516 100644 --- a/src/Objects/ClassBluePrint.php +++ b/src/Objects/ClassBluePrint.php @@ -2,11 +2,13 @@ namespace Jerodev\DataMapper\Objects; +use Jerodev\DataMapper\Types\DataTypeCollection; + class ClassBluePrint { - /** @var array */ + /** @var array */ public array $constructorArguments = []; - /** @var array An array with name as key and type as value */ + /** @var array An array with name as key and type as value */ public array $properties = []; } diff --git a/src/Objects/ClassBluePrinter.php b/src/Objects/ClassBluePrinter.php index b520633..857ef16 100644 --- a/src/Objects/ClassBluePrinter.php +++ b/src/Objects/ClassBluePrinter.php @@ -2,14 +2,17 @@ namespace Jerodev\DataMapper\Objects; +use Jerodev\DataMapper\Types\DataTypeFactory; use ReflectionClass; class ClassBluePrinter { private readonly DocBlockParser $docBlockParser; + private readonly DataTypeFactory $dataTypeFactory; public function __construct() { + $this->dataTypeFactory = new DataTypeFactory(); $this->docBlockParser = new DocBlockParser(); } @@ -46,7 +49,7 @@ private function printConstructor(ReflectionClass $reflection, ClassBluePrint $b $arg = [ 'name' => $param->getName(), - 'type' => $type, + 'type' => $this->dataTypeFactory->fromString($type), ]; if ($param->isDefaultValueAvailable()) { $arg['default'] = $param->getDefaultValue(); @@ -72,7 +75,7 @@ private function printProperties(ReflectionClass $reflection, ClassBluePrint $bl } } - $blueprint->properties[$property->getName()] = $type; + $blueprint->properties[$property->getName()] = $this->dataTypeFactory->fromString($type); } } } diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index a699e3e..1c23e5c 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -5,6 +5,7 @@ use Jerodev\DataMapper\Exceptions\CouldNotResolveClassException; use Jerodev\DataMapper\Mapper; use Jerodev\DataMapper\Types\DataType; +use Jerodev\DataMapper\Types\DataTypeCollection; class ObjectMapper { @@ -35,7 +36,7 @@ public function map(DataType $type, array|string $data): object $mapFileName = 'mapper_' . \md5($class); if (! \file_exists($mapFileName . '.php')) { - \file_put_contents($mapFileName . '.php', $this->createObjectMappingFunction($class)); + \file_put_contents($mapFileName . '.php', $this->createObjectMappingFunction($class, $mapFileName)); } // Include the function containing file and call the function. @@ -43,10 +44,57 @@ public function map(DataType $type, array|string $data): object return ($mapFileName)($this->mapper, $data); } - private function createObjectMappingFunction(string $class): string + private function createObjectMappingFunction(string $class, string $mapFunctionName): string { $blueprint = $this->classBluePrinter->print($class); -// var_dump($blueprint);die(); + // Instantiate a new object + $args = []; + foreach ($blueprint->constructorArguments as $argument) { + $arg = '$data[\'' . $argument['name'] . '\']'; + if ($argument['type'] !== null) { + $arg = $this->castInMapperFunction($argument['type'], $arg); + } + + $args[] = $arg; + } + $content = '$x = new ' . $class . '(' . \implode(', ', $args) . ');'; + + // TODO: map properties + + $mapperClass = Mapper::class; + return <<types) === 1) { + $type = $type->types[0]; + if ($type->isNative()) { + return match ($type->type) { + 'null' => 'null', + 'bool' => "\\filter_var({$value}, \FILTER_VALIDATE_BOOL)", + 'float' => '(float) ' . $value, + 'int' => '(int) ' . $value, + 'string' => '(string) ' . $value, + 'object' => '(object) ' . $value, + default => $value, + }; + } + + if ($type->isGenericArray()) { + return '(array) ' . $value; + } + } + + return '$mapper->map(\'' . $type->__toString() . '\', ' . $value . ')'; } } From 7ff8e13ec645a32e68f598f8267df43b997616f8 Mon Sep 17 00:00:00 2001 From: jerodev Date: Wed, 10 May 2023 13:01:02 +0200 Subject: [PATCH 12/25] Map objects --- src/Objects/ClassBluePrint.php | 2 +- src/Objects/ClassBluePrinter.php | 11 +++++++- src/Objects/ObjectMapper.php | 37 +++++++++++++++++++------- src/Types/DataType.php | 2 +- tests/MapperTest.php | 34 +++++++++++++++++++++-- tests/Objects/ClassBluePrinterTest.php | 2 +- 6 files changed, 72 insertions(+), 16 deletions(-) diff --git a/src/Objects/ClassBluePrint.php b/src/Objects/ClassBluePrint.php index aca4516..c8a5567 100644 --- a/src/Objects/ClassBluePrint.php +++ b/src/Objects/ClassBluePrint.php @@ -9,6 +9,6 @@ class ClassBluePrint /** @var array */ public array $constructorArguments = []; - /** @var array An array with name as key and type as value */ + /** @var array */ public array $properties = []; } diff --git a/src/Objects/ClassBluePrinter.php b/src/Objects/ClassBluePrinter.php index 857ef16..e68d686 100644 --- a/src/Objects/ClassBluePrinter.php +++ b/src/Objects/ClassBluePrinter.php @@ -7,11 +7,13 @@ class ClassBluePrinter { + private readonly ClassResolver $classResolver; private readonly DocBlockParser $docBlockParser; private readonly DataTypeFactory $dataTypeFactory; public function __construct() { + $this->classResolver = new ClassResolver(); $this->dataTypeFactory = new DataTypeFactory(); $this->docBlockParser = new DocBlockParser(); } @@ -75,7 +77,14 @@ private function printProperties(ReflectionClass $reflection, ClassBluePrint $bl } } - $blueprint->properties[$property->getName()] = $this->dataTypeFactory->fromString($type); + $mapped = [ + 'type' => $this->dataTypeFactory->fromString($type) + ]; + if ($property->hasDefaultValue()) { + $mapped['default'] = $property->getDefaultValue(); + } + + $blueprint->properties[$property->getName()] = $mapped; } } } diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index 1c23e5c..06cf3b1 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -51,35 +51,44 @@ private function createObjectMappingFunction(string $class, string $mapFunctionN // Instantiate a new object $args = []; foreach ($blueprint->constructorArguments as $argument) { - $arg = '$data[\'' . $argument['name'] . '\']'; if ($argument['type'] !== null) { - $arg = $this->castInMapperFunction($argument['type'], $arg); + $arg = $this->castInMapperFunction($argument, $argument['name']); } $args[] = $arg; } $content = '$x = new ' . $class . '(' . \implode(', ', $args) . ');'; - // TODO: map properties + foreach ($blueprint->properties as $name => $property) { + $content.= \PHP_EOL . '$x->' . $name . ' = ' . $this->castInMapperFunction($property, $name) . ';'; + } $mapperClass = Mapper::class; return <<types) === 1) { - $type = $type->types[0]; + $value = "\$data['{$propertyName}']"; + $newValue = null; + + if (\count($property['type']->types) === 1) { + $type = $property['type']->types[0]; if ($type->isNative()) { - return match ($type->type) { + $newValue = match ($type->type) { 'null' => 'null', 'bool' => "\\filter_var({$value}, \FILTER_VALIDATE_BOOL)", 'float' => '(float) ' . $value, @@ -91,10 +100,18 @@ private function castInMapperFunction(DataTypeCollection $type, string $value): } if ($type->isGenericArray()) { - return '(array) ' . $value; + $newValue = '(array) ' . $value; } } - return '$mapper->map(\'' . $type->__toString() . '\', ' . $value . ')'; + if ($newValue === null) { + $newValue = '$mapper->map(\'' . $property['type']->__toString() . '\', ' . $value . ')'; + } + + if (\array_key_exists('default', $property)) { + $newValue = "(\\array_key_exists('{$propertyName}', \$data) ? {$newValue} : " . \var_export($property['default'], true) . ')'; + } + + return $newValue; } } diff --git a/src/Types/DataType.php b/src/Types/DataType.php index 3bf150f..4cb8443 100644 --- a/src/Types/DataType.php +++ b/src/Types/DataType.php @@ -12,7 +12,7 @@ class DataType public function __construct( public readonly string $type, public readonly bool $isNullable, - public array $genericTypes = [], + public readonly array $genericTypes = [], ) { } diff --git a/tests/MapperTest.php b/tests/MapperTest.php index b57a53e..cbb1464 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -2,9 +2,11 @@ namespace Jerodev\DataMapper\Tests; +use Couchbase\User; use Generator; use Jerodev\DataMapper\Mapper; use Jerodev\DataMapper\Tests\_Mocks\SuitEnum; +use Jerodev\DataMapper\Tests\_Mocks\UserDto; use PHPUnit\Framework\TestCase; final class MapperTest extends TestCase @@ -17,11 +19,17 @@ public function it_should_map_native_values(string $type, mixed $value, mixed $e { $this->assertSame($expectation, (new Mapper())->map($type, $value)); } + /** + * @test + * @dataProvider objectValuesDataProvider + */ + public function it_should_map_objects(string $type, mixed $value, mixed $expectation): void + { + $this->assertEquals($expectation, (new Mapper())->map($type, $value)); + } public static function nativeValuesDataProvider(): Generator { - yield ['Mapper', [], null]; - yield ['null', null, null]; yield ['array', [1, 'b'], [1, 'b']]; @@ -45,4 +53,26 @@ public static function nativeValuesDataProvider(): Generator yield ['array<' . SuitEnum::class . '>', ['H', 'S'], [SuitEnum::Hearts, SuitEnum::Spades]]; } + + public static function objectValuesDataProvider(): Generator + { + yield ['Mapper', [], new Mapper()]; + + $dto = new UserDto('Jeroen'); + $dto->friends = [ + new UserDto('John'), + new UserDto('Jane'), + ]; + yield [ + UserDto::class, + [ + 'name' => 'Jeroen', + 'friends' => [ + ['name' => 'John'], + ['name' => 'Jane'], + ], + ], + $dto + ]; + } } diff --git a/tests/Objects/ClassBluePrinterTest.php b/tests/Objects/ClassBluePrinterTest.php index 40443a2..2dbc616 100644 --- a/tests/Objects/ClassBluePrinterTest.php +++ b/tests/Objects/ClassBluePrinterTest.php @@ -26,7 +26,7 @@ public static function classBlueprintDataProvider(): Generator yield [ UserDto::class, [['name' => 'name', 'type' => 'string']], - ['name' => 'string', 'friends' => 'array'], + ['name' => ['type' => 'string'], 'friends' => ['type' => 'array', 'default' => []]], ]; } } From 8b47d90e450f3f09909a5dac841e8f6138ad35a0 Mon Sep 17 00:00:00 2001 From: jerodev Date: Wed, 10 May 2023 13:22:17 +0200 Subject: [PATCH 13/25] Fix for self-referencing classes --- src/Objects/ClassBluePrinter.php | 36 ++++++++++++++++++++++++-- src/Objects/ObjectMapper.php | 2 +- tests/Objects/ClassBluePrinterTest.php | 33 +++++++++++++++++++++-- tests/_Mocks/UserDto.php | 2 +- 4 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/Objects/ClassBluePrinter.php b/src/Objects/ClassBluePrinter.php index e68d686..ea783a3 100644 --- a/src/Objects/ClassBluePrinter.php +++ b/src/Objects/ClassBluePrinter.php @@ -2,6 +2,8 @@ namespace Jerodev\DataMapper\Objects; +use Jerodev\DataMapper\Types\DataType; +use Jerodev\DataMapper\Types\DataTypeCollection; use Jerodev\DataMapper\Types\DataTypeFactory; use ReflectionClass; @@ -51,7 +53,7 @@ private function printConstructor(ReflectionClass $reflection, ClassBluePrint $b $arg = [ 'name' => $param->getName(), - 'type' => $this->dataTypeFactory->fromString($type), + 'type' => $this->resolveType($this->dataTypeFactory->fromString($type), $reflection->getName()), ]; if ($param->isDefaultValueAvailable()) { $arg['default'] = $param->getDefaultValue(); @@ -78,7 +80,10 @@ private function printProperties(ReflectionClass $reflection, ClassBluePrint $bl } $mapped = [ - 'type' => $this->dataTypeFactory->fromString($type) + 'type' => $this->resolveType( + $this->dataTypeFactory->fromString($type), + $reflection->getName(), + ), ]; if ($property->hasDefaultValue()) { $mapped['default'] = $property->getDefaultValue(); @@ -87,4 +92,31 @@ private function printProperties(ReflectionClass $reflection, ClassBluePrint $bl $blueprint->properties[$property->getName()] = $mapped; } } + + private function resolveType(DataTypeCollection $type, string $className): DataTypeCollection + { + $baseClassName = \explode('\\', $className); + $baseClassName = \end($baseClassName); + + $collection = []; + foreach ($type->types as $dataType) { + $generics = []; + foreach ($dataType->genericTypes as $genericType) { + $generics[] = $this->resolveType($genericType, $className); + } + + $typeName = $dataType->type; + if (\in_array($typeName, ['self', 'static', $baseClassName])) { + $typeName = $className; + } + + $collection[] = new DataType( + $typeName, + $dataType->isNullable, + $generics, + ); + } + + return new DataTypeCollection($collection); + } } diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index 06cf3b1..6953213 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -60,7 +60,7 @@ private function createObjectMappingFunction(string $class, string $mapFunctionN $content = '$x = new ' . $class . '(' . \implode(', ', $args) . ');'; foreach ($blueprint->properties as $name => $property) { - $content.= \PHP_EOL . '$x->' . $name . ' = ' . $this->castInMapperFunction($property, $name) . ';'; + $content.= \PHP_EOL . ' $x->' . $name . ' = ' . $this->castInMapperFunction($property, $name) . ';'; } $mapperClass = Mapper::class; diff --git a/tests/Objects/ClassBluePrinterTest.php b/tests/Objects/ClassBluePrinterTest.php index 2dbc616..c0d3c7b 100644 --- a/tests/Objects/ClassBluePrinterTest.php +++ b/tests/Objects/ClassBluePrinterTest.php @@ -5,6 +5,8 @@ use Generator; use Jerodev\DataMapper\Objects\ClassBluePrinter; use Jerodev\DataMapper\Tests\_Mocks\UserDto; +use Jerodev\DataMapper\Types\DataType; +use Jerodev\DataMapper\Types\DataTypeCollection; use PHPUnit\Framework\TestCase; final class ClassBluePrinterTest extends TestCase @@ -25,8 +27,35 @@ public static function classBlueprintDataProvider(): Generator { yield [ UserDto::class, - [['name' => 'name', 'type' => 'string']], - ['name' => ['type' => 'string'], 'friends' => ['type' => 'array', 'default' => []]], + [ + [ + 'name' => 'name', + 'type' => new DataTypeCollection([ + new DataType('string', false), + ]), + ], + ], + [ + 'name' => [ + 'type' => new DataTypeCollection([ + new DataType('string', false), + ]), + ], + 'friends' => [ + 'type' => new DataTypeCollection([ + new DataType( + 'array', + false, + [ + new DataTypeCollection([ + new DataType(UserDto::class, false), + ]), + ], + ), + ]), + 'default' => [], + ], + ], ]; } } diff --git a/tests/_Mocks/UserDto.php b/tests/_Mocks/UserDto.php index f96a836..7caea09 100644 --- a/tests/_Mocks/UserDto.php +++ b/tests/_Mocks/UserDto.php @@ -7,7 +7,7 @@ class UserDto /** First name and last name */ public string $name; - /** @var array */ + /** @var array */ public array $friends = []; public function __construct(string $name) From 16aa8e997b66eb0ca7593639d9787e25dc4a9ab7 Mon Sep 17 00:00:00 2001 From: jerodev Date: Wed, 10 May 2023 13:38:22 +0200 Subject: [PATCH 14/25] Map enum properties directly --- src/Objects/ClassBluePrinter.php | 2 +- src/Objects/ObjectMapper.php | 4 ++++ src/Types/DataTypeFactory.php | 4 ++-- tests/MapperTest.php | 2 ++ tests/Objects/ClassBluePrinterTest.php | 7 +++++++ tests/_Mocks/UserDto.php | 1 + 6 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Objects/ClassBluePrinter.php b/src/Objects/ClassBluePrinter.php index ea783a3..b92ddf8 100644 --- a/src/Objects/ClassBluePrinter.php +++ b/src/Objects/ClassBluePrinter.php @@ -81,7 +81,7 @@ private function printProperties(ReflectionClass $reflection, ClassBluePrint $bl $mapped = [ 'type' => $this->resolveType( - $this->dataTypeFactory->fromString($type), + $this->dataTypeFactory->fromString($type, $property->getType()->allowsNull()), $reflection->getName(), ), ]; diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index 6953213..b2bc06f 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -102,6 +102,10 @@ private function castInMapperFunction(array $property, string $propertyName): st if ($type->isGenericArray()) { $newValue = '(array) ' . $value; } + + if (\is_subclass_of($type->type, \BackedEnum::class)) { + $newValue = "{$type->type}::from({$value})"; + } } if ($newValue === null) { diff --git a/src/Types/DataTypeFactory.php b/src/Types/DataTypeFactory.php index 9c28d09..2148f15 100644 --- a/src/Types/DataTypeFactory.php +++ b/src/Types/DataTypeFactory.php @@ -9,7 +9,7 @@ class DataTypeFactory /** @var array */ private array $typeCache = []; - public function fromString(string $rawType): DataTypeCollection + public function fromString(string $rawType, bool $forceNullable = false): DataTypeCollection { $rawType = \trim($rawType); @@ -20,7 +20,7 @@ public function fromString(string $rawType): DataTypeCollection $tokens = $this->tokenize($rawType); if (\count($tokens) === 1) { return $this->typeCache[$rawType] = new DataTypeCollection([ - new DataType($rawType, false), + new DataType($rawType, $forceNullable), ]); } diff --git a/tests/MapperTest.php b/tests/MapperTest.php index cbb1464..28c1bac 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -59,6 +59,7 @@ public static function objectValuesDataProvider(): Generator yield ['Mapper', [], new Mapper()]; $dto = new UserDto('Jeroen'); + $dto->favoriteSuit = SuitEnum::Diamonds; $dto->friends = [ new UserDto('John'), new UserDto('Jane'), @@ -67,6 +68,7 @@ public static function objectValuesDataProvider(): Generator UserDto::class, [ 'name' => 'Jeroen', + 'favoriteSuit' => 'D', 'friends' => [ ['name' => 'John'], ['name' => 'Jane'], diff --git a/tests/Objects/ClassBluePrinterTest.php b/tests/Objects/ClassBluePrinterTest.php index c0d3c7b..66a46d8 100644 --- a/tests/Objects/ClassBluePrinterTest.php +++ b/tests/Objects/ClassBluePrinterTest.php @@ -4,6 +4,7 @@ use Generator; use Jerodev\DataMapper\Objects\ClassBluePrinter; +use Jerodev\DataMapper\Tests\_Mocks\SuitEnum; use Jerodev\DataMapper\Tests\_Mocks\UserDto; use Jerodev\DataMapper\Types\DataType; use Jerodev\DataMapper\Types\DataTypeCollection; @@ -55,6 +56,12 @@ public static function classBlueprintDataProvider(): Generator ]), 'default' => [], ], + 'favoriteSuit' => [ + 'type' => new DataTypeCollection([ + new DataType(SuitEnum::class, true) + ]), + 'default' => null, + ], ], ]; } diff --git a/tests/_Mocks/UserDto.php b/tests/_Mocks/UserDto.php index 7caea09..a5e3664 100644 --- a/tests/_Mocks/UserDto.php +++ b/tests/_Mocks/UserDto.php @@ -9,6 +9,7 @@ class UserDto /** @var array */ public array $friends = []; + public ?SuitEnum $favoriteSuit = null; public function __construct(string $name) { From a893fe7e7d6e8fe10f94927a768d5b1fca6aab2c Mon Sep 17 00:00:00 2001 From: jerodev Date: Wed, 10 May 2023 15:50:54 +0200 Subject: [PATCH 15/25] `array_map` in mapper function --- src/Objects/ObjectMapper.php | 69 ++++++++++++++++++++---------------- tests/MapperTest.php | 2 +- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index b2bc06f..d9faed1 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -51,18 +51,30 @@ private function createObjectMappingFunction(string $class, string $mapFunctionN // Instantiate a new object $args = []; foreach ($blueprint->constructorArguments as $argument) { + $arg = "\$data['{$argument['name']}']"; + if ($argument['type'] !== null) { - $arg = $this->castInMapperFunction($argument, $argument['name']); + $arg = $this->castInMapperFunction($arg, $argument['type']); + if (\array_key_exists('default', $argument)) { + $arg = $this->wrapDefault($arg, $argument['name'], $argument['default']); + } } $args[] = $arg; } $content = '$x = new ' . $class . '(' . \implode(', ', $args) . ');'; + // Map properties foreach ($blueprint->properties as $name => $property) { - $content.= \PHP_EOL . ' $x->' . $name . ' = ' . $this->castInMapperFunction($property, $name) . ';'; + $propertyMap = $this->castInMapperFunction("\$data['{$name}']", $property['type']); + if (\array_key_exists('default', $property)) { + $propertyMap = $this->wrapDefault($propertyMap, $name, $property['default']); + } + + $content.= \PHP_EOL . ' $x->' . $name . ' = ' . $propertyMap . ';'; } + // Render the function $mapperClass = Mapper::class; return <<types) === 1) { - $type = $property['type']->types[0]; + if (\count($type->types) === 1) { + $type = $type->types[0]; if ($type->isNative()) { - $newValue = match ($type->type) { + return match ($type->type) { 'null' => 'null', - 'bool' => "\\filter_var({$value}, \FILTER_VALIDATE_BOOL)", - 'float' => '(float) ' . $value, - 'int' => '(int) ' . $value, - 'string' => '(string) ' . $value, - 'object' => '(object) ' . $value, - default => $value, + 'bool' => "\\filter_var({$propertyName}, \FILTER_VALIDATE_BOOL)", + 'float' => '(float) ' . $propertyName, + 'int' => '(int) ' . $propertyName, + 'string' => '(string) ' . $propertyName, + 'object' => '(object) ' . $propertyName, + default => $propertyName, }; } - if ($type->isGenericArray()) { - $newValue = '(array) ' . $value; + if ($type->isArray()) { + if ($type->isGenericArray()) { + return '(array) ' . $propertyName; + } + if (\count($type->genericTypes) === 1) { + $uniqid = \uniqid(); + return "\\array_map(static fn (\$x{$uniqid}) => " . $this->castInMapperFunction('$x' . $uniqid, $type->genericTypes[0]) . ", {$propertyName})"; + } } if (\is_subclass_of($type->type, \BackedEnum::class)) { - $newValue = "{$type->type}::from({$value})"; + return "{$type->type}::from({$propertyName})"; } } - if ($newValue === null) { - $newValue = '$mapper->map(\'' . $property['type']->__toString() . '\', ' . $value . ')'; - } - - if (\array_key_exists('default', $property)) { - $newValue = "(\\array_key_exists('{$propertyName}', \$data) ? {$newValue} : " . \var_export($property['default'], true) . ')'; - } + return '$mapper->map(\'' . $type->__toString() . '\', ' . $propertyName . ')'; + } - return $newValue; + private function wrapDefault(string $value, string $arrayKey, mixed $defaultValue): string + { + return "(\\array_key_exists('{$arrayKey}', \$data) ? {$value} : " . \var_export($defaultValue, true) . ')'; } } diff --git a/tests/MapperTest.php b/tests/MapperTest.php index 28c1bac..019b8b4 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -51,7 +51,7 @@ public static function nativeValuesDataProvider(): Generator yield ['string', 4, '4']; yield ['array', [4, 5], ['4', '5']]; - yield ['array<' . SuitEnum::class . '>', ['H', 'S'], [SuitEnum::Hearts, SuitEnum::Spades]]; + yield ['array', ['8.0' => 'H', 9 => 'S'], ['8' => SuitEnum::Hearts, '9' => SuitEnum::Spades]]; } public static function objectValuesDataProvider(): Generator From cec8c3fa76b9d21960d1300ea0ac9363db5a88b5 Mon Sep 17 00:00:00 2001 From: Jeroen Deviaene Date: Wed, 10 May 2023 16:56:23 +0200 Subject: [PATCH 16/25] [Next] Post mapping callbacks (#5) --- phpcs.xml | 8 ++++---- src/Attributes/PostMapping.php | 20 ++++++++++++++++++++ src/Objects/ClassBluePrint.php | 4 ++++ src/Objects/ClassBluePrinter.php | 16 ++++++++++++++-- src/Objects/ObjectMapper.php | 12 ++++++++++++ tests/MapperTest.php | 17 +++++++++++++++-- tests/_Mocks/SuperUserDto.php | 19 +++++++++++++++++++ tests/_Mocks/UserDto.php | 8 ++++++++ 8 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 src/Attributes/PostMapping.php create mode 100644 tests/_Mocks/SuperUserDto.php diff --git a/phpcs.xml b/phpcs.xml index 9f5c9f1..e1d7670 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,8 +1,8 @@ - - - + + + src - \ No newline at end of file + diff --git a/src/Attributes/PostMapping.php b/src/Attributes/PostMapping.php new file mode 100644 index 0000000..299894b --- /dev/null +++ b/src/Attributes/PostMapping.php @@ -0,0 +1,20 @@ + */ public array $properties = []; + + /** @var array */ + public array $classAttributes = []; } diff --git a/src/Objects/ClassBluePrinter.php b/src/Objects/ClassBluePrinter.php index b92ddf8..c0e97e6 100644 --- a/src/Objects/ClassBluePrinter.php +++ b/src/Objects/ClassBluePrinter.php @@ -2,6 +2,7 @@ namespace Jerodev\DataMapper\Objects; +use Jerodev\DataMapper\Attributes\PostMapping; use Jerodev\DataMapper\Types\DataType; use Jerodev\DataMapper\Types\DataTypeCollection; use Jerodev\DataMapper\Types\DataTypeFactory; @@ -9,13 +10,11 @@ class ClassBluePrinter { - private readonly ClassResolver $classResolver; private readonly DocBlockParser $docBlockParser; private readonly DataTypeFactory $dataTypeFactory; public function __construct() { - $this->classResolver = new ClassResolver(); $this->dataTypeFactory = new DataTypeFactory(); $this->docBlockParser = new DocBlockParser(); } @@ -27,6 +26,7 @@ public function print(string $class): ClassBluePrint $blueprint = new ClassBluePrint(); $this->printConstructor($reflection, $blueprint); $this->printProperties($reflection, $blueprint); + $this->printAttributes($reflection, $blueprint); return $blueprint; } @@ -93,6 +93,18 @@ private function printProperties(ReflectionClass $reflection, ClassBluePrint $bl } } + private function printAttributes(ReflectionClass $reflection, ClassBluePrint $blueprint): void + { + foreach ($reflection->getAttributes(PostMapping::class) as $attribute) { + $blueprint->classAttributes[] = $attribute->newInstance(); + } + + // Also check parent for relevant attributes + if ($reflection->getParentClass()) { + $this->printAttributes($reflection->getParentClass(), $blueprint); + } + } + private function resolveType(DataTypeCollection $type, string $className): DataTypeCollection { $baseClassName = \explode('\\', $className); diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index d9faed1..5d44d39 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -2,6 +2,7 @@ namespace Jerodev\DataMapper\Objects; +use Jerodev\DataMapper\Attributes\PostMapping; use Jerodev\DataMapper\Exceptions\CouldNotResolveClassException; use Jerodev\DataMapper\Mapper; use Jerodev\DataMapper\Types\DataType; @@ -74,6 +75,17 @@ private function createObjectMappingFunction(string $class, string $mapFunctionN $content.= \PHP_EOL . ' $x->' . $name . ' = ' . $propertyMap . ';'; } + // Post mapping functions? + foreach ($blueprint->classAttributes as $attribute) { + if ($attribute instanceof PostMapping) { + if (\is_string($attribute->postMappingCallback)) { + $content.= \PHP_EOL . \PHP_EOL . " \$x->{$attribute->postMappingCallback}(\$data, \$x);"; + } else { + $content.= \PHP_EOL . \PHP_EOL . " \call_user_func({$attribute->postMappingCallback}, \$data, \$x);"; + } + } + } + // Render the function $mapperClass = Mapper::class; return << 'Jeroen', 'favoriteSuit' => 'D', 'friends' => [ - ['name' => 'John'], - ['name' => 'Jane'], + ['name' => 'john'], + ['name' => 'jane'], ], ], $dto ]; + + $dto = new SuperUserDto('Superman'); // Uppercase because UserDto post mapping + $dto->canFly = true; + $dto->stars = 3; // Increased 3 times by post mapping function + yield [ + SuperUserDto::class, + [ + 'name' => 'superman', + 'canFly' => true, + ], + $dto, + ]; } } diff --git a/tests/_Mocks/SuperUserDto.php b/tests/_Mocks/SuperUserDto.php new file mode 100644 index 0000000..c55fe8a --- /dev/null +++ b/tests/_Mocks/SuperUserDto.php @@ -0,0 +1,19 @@ +stars++; + } +} diff --git a/tests/_Mocks/UserDto.php b/tests/_Mocks/UserDto.php index a5e3664..a854955 100644 --- a/tests/_Mocks/UserDto.php +++ b/tests/_Mocks/UserDto.php @@ -2,6 +2,9 @@ namespace Jerodev\DataMapper\Tests\_Mocks; +use Jerodev\DataMapper\Attributes\PostMapping; + +#[PostMapping('post')] class UserDto { /** First name and last name */ @@ -15,4 +18,9 @@ public function __construct(string $name) { $this->name = $name; } + + public function post(): void + { + $this->name = \ucfirst($this->name); + } } From f7135f0b789317002b40a31c79c49469332f0be4 Mon Sep 17 00:00:00 2001 From: Jeroen Deviaene Date: Thu, 11 May 2023 20:32:25 +0200 Subject: [PATCH 17/25] [Next] Mapper configuration (#7) --- composer.lock | 89 ++++++++++--------- phpcs.xml | 3 +- .../UnexpectedNullValueException.php | 13 +++ src/Mapper.php | 32 ++++--- src/MapperConfig.php | 38 ++++++++ src/Objects/ClassBluePrinter.php | 2 +- src/Objects/ObjectMapper.php | 49 ++++++++-- src/Types/DataTypeCollection.php | 2 +- tests/MapperTest.php | 16 +++- tests/Objects/ObjectMapperTest.php | 70 +++++++++++++++ tests/Types/DataTypeCollectionTest.php | 2 +- 11 files changed, 250 insertions(+), 66 deletions(-) create mode 100644 src/Exceptions/UnexpectedNullValueException.php create mode 100644 src/MapperConfig.php create mode 100644 tests/Objects/ObjectMapperTest.php diff --git a/composer.lock b/composer.lock index 227ccf8..2d1cac1 100644 --- a/composer.lock +++ b/composer.lock @@ -9,35 +9,38 @@ "packages-dev": [ { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.2", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", + "php": ">=5.4", "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0" + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", "extra": { - "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" }, "autoload": { "psr-4": { - "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -53,7 +56,7 @@ }, { "name": "Contributors", - "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", @@ -77,10 +80,10 @@ "tests" ], "support": { - "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", - "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2022-02-04T12:51:07+00:00" + "time": "2023-01-05T11:28:13+00:00" }, { "name": "doctrine/instantiator", @@ -158,16 +161,16 @@ "source": { "type": "git", "url": "https://github.com/jerodev/code-styles.git", - "reference": "24f14bf36e186924c4c2c1416d901ef1e4cb2b2b" + "reference": "3a0d433404bc5d9f398619d28130affcf25eca76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jerodev/code-styles/zipball/24f14bf36e186924c4c2c1416d901ef1e4cb2b2b", - "reference": "24f14bf36e186924c4c2c1416d901ef1e4cb2b2b", + "url": "https://api.github.com/repos/jerodev/code-styles/zipball/3a0d433404bc5d9f398619d28130affcf25eca76", + "reference": "3a0d433404bc5d9f398619d28130affcf25eca76", "shasum": "" }, "require": { - "slevomat/coding-standard": "^7.0", + "slevomat/coding-standard": "^8.0", "squizlabs/php_codesniffer": "^3.6" }, "default-branch": true, @@ -186,7 +189,7 @@ "source": "https://github.com/jerodev/code-styles/tree/master", "issues": "https://github.com/jerodev/code-styles/issues" }, - "time": "2021-05-06T10:12:03+00:00" + "time": "2023-05-10T18:43:22+00:00" }, { "name": "myclabs/deep-copy", @@ -779,16 +782,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.7", + "version": "9.6.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2" + "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", - "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/17d621b3aff84d0c8b62539e269e87d8d5baa76e", + "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e", "shasum": "" }, "require": { @@ -862,7 +865,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.7" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.8" }, "funding": [ { @@ -878,7 +881,7 @@ "type": "tidelift" } ], - "time": "2023-04-14T08:58:40+00:00" + "time": "2023-05-11T05:14:45+00:00" }, { "name": "sebastian/cli-parser", @@ -1846,42 +1849,42 @@ }, { "name": "slevomat/coding-standard", - "version": "7.2.1", + "version": "8.11.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90" + "reference": "af87461316b257e46e15bb041dca6fca3796d822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/aff06ae7a84e4534bf6f821dc982a93a5d477c90", - "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/af87461316b257e46e15bb041dca6fca3796d822", + "reference": "af87461316b257e46e15bb041dca6fca3796d822", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": "^1.5.1", - "squizlabs/php_codesniffer": "^3.6.2" + "phpstan/phpdoc-parser": ">=1.20.0 <1.21.0", + "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { - "phing/phing": "2.17.3", + "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.7.1", - "phpstan/phpstan-deprecation-rules": "1.0.0", - "phpstan/phpstan-phpunit": "1.0.0|1.1.1", - "phpstan/phpstan-strict-rules": "1.2.3", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.20" + "phpstan/phpstan": "1.10.14", + "phpstan/phpstan-deprecation-rules": "1.1.3", + "phpstan/phpstan-phpunit": "1.3.11", + "phpstan/phpstan-strict-rules": "1.5.1", + "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.1.1" }, "type": "phpcodesniffer-standard", "extra": { "branch-alias": { - "dev-master": "7.x-dev" + "dev-master": "8.x-dev" } }, "autoload": { "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard" + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1889,9 +1892,13 @@ "MIT" ], "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.2.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.11.1" }, "funding": [ { @@ -1903,7 +1910,7 @@ "type": "tidelift" } ], - "time": "2022-05-25T10:58:12+00:00" + "time": "2023-04-24T08:19:01+00:00" }, { "name": "squizlabs/php_codesniffer", diff --git a/phpcs.xml b/phpcs.xml index e1d7670..902eb7b 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,8 +1,7 @@ - - src + tests diff --git a/src/Exceptions/UnexpectedNullValueException.php b/src/Exceptions/UnexpectedNullValueException.php new file mode 100644 index 0000000..239f39d --- /dev/null +++ b/src/Exceptions/UnexpectedNullValueException.php @@ -0,0 +1,13 @@ +dataTypeFactory = new DataTypeFactory(); $this->objectMapper = new ObjectMapper($this); + $this->config = $config ?? new MapperConfig(); } /** @@ -36,8 +40,12 @@ public function map($typeCollection, $data) ); } - if ($data === 'null' && $typeCollection->isNullable()) { - return null; + if ($data === 'null' || $data === null) { + if ($this->config->strictNullMapping === false || $typeCollection->isNullable()) { + return null; + } + + throw new UnexpectedNullValueException($typeCollection->__toString()); } // Loop over all possible types and parse to the first one that matches @@ -60,6 +68,14 @@ public function map($typeCollection, $data) throw new CouldNotMapValueException($data, $typeCollection); } + /** + * Remove cached class mappers. + */ + public function clearCache(): void + { + $this->objectMapper->clearCache(); + } + private function mapNativeType(DataType $type, mixed $data): float|object|bool|int|string|null { return match ($type->type) { @@ -103,10 +119,6 @@ private function mapArray(DataType $type, mixed $data): array private function mapObject(DataType $type, mixed $data): ?object { - if ($type->isNullable && $data === null) { - return null; - } - try { return $this->objectMapper->map($type, $data); } catch (CouldNotResolveClassException) { diff --git a/src/MapperConfig.php b/src/MapperConfig.php new file mode 100644 index 0000000..fc36a08 --- /dev/null +++ b/src/MapperConfig.php @@ -0,0 +1,38 @@ +getProperties(); foreach ($properties as $property) { - if (! $property->isPublic()) { + if (! $property->isPublic() || $property->isReadOnly()) { continue; } diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index 5d44d39..c3f2618 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -10,6 +10,8 @@ class ObjectMapper { + private const MAPPER_FUNCTION_PREFIX = 'jmapper_'; + private readonly ClassBluePrinter $classBluePrinter; private readonly ClassResolver $classResolver; @@ -26,23 +28,45 @@ public function __construct( * @return object * @throws CouldNotResolveClassException */ - public function map(DataType $type, array|string $data): object + public function map(DataType $type, array|string $data): ?object { $class = $this->classResolver->resolve($type->type); // If the data is a string and the class is an enum, create the enum. if (\is_string($data) && \is_subclass_of($class, \BackedEnum::class)) { - return $class::from($data); + if ($this->mapper->config->enumTryFrom) { + return $class::tryFrom($data); + } else { + return $class::from($data); + } } - $mapFileName = 'mapper_' . \md5($class); - if (! \file_exists($mapFileName . '.php')) { - \file_put_contents($mapFileName . '.php', $this->createObjectMappingFunction($class, $mapFileName)); + $functionName = self::MAPPER_FUNCTION_PREFIX . \md5($class); + $fileName = $this->mapperDirectory() . \DIRECTORY_SEPARATOR . $functionName . '.php'; + if (! \file_exists($fileName)) { + \file_put_contents($fileName, $this->createObjectMappingFunction($class, $functionName)); } // Include the function containing file and call the function. - require_once($mapFileName . '.php'); - return ($mapFileName)($this->mapper, $data); + require_once($fileName); + return ($functionName)($this->mapper, $data); + } + + public function clearCache(): void + { + foreach (\glob($this->mapperDirectory() . \DIRECTORY_SEPARATOR . self::MAPPER_FUNCTION_PREFIX . '*.php') as $file) { + \unlink($file); + } + } + + private function mapperDirectory(): string + { + $dir = \str_replace('{$TMP}', \sys_get_temp_dir(), $this->mapper->config->classMapperDirectory); + if (! \file_exists($dir)) { + \mkdir($dir, 0777, true); + } + + return $dir; } private function createObjectMappingFunction(string $class, string $mapFunctionName): string @@ -126,7 +150,9 @@ private function castInMapperFunction(string $propertyName, DataTypeCollection $ } if (\is_subclass_of($type->type, \BackedEnum::class)) { - return "{$type->type}::from({$propertyName})"; + $enumFunction = $this->mapper->config->enumTryFrom ? 'tryFrom' : 'from'; + + return "{$type->type}::{$enumFunction}({$propertyName})"; } } @@ -137,4 +163,11 @@ private function wrapDefault(string $value, string $arrayKey, mixed $defaultValu { return "(\\array_key_exists('{$arrayKey}', \$data) ? {$value} : " . \var_export($defaultValue, true) . ')'; } + + public function __destruct() + { + if ($this->mapper->config->debug) { + $this->clearCache(); + } + } } diff --git a/src/Types/DataTypeCollection.php b/src/Types/DataTypeCollection.php index 6a484b2..03ee929 100644 --- a/src/Types/DataTypeCollection.php +++ b/src/Types/DataTypeCollection.php @@ -35,7 +35,7 @@ public function isNullable(): bool return ! empty( \array_filter( $this->types, - static fn (DataType $t) => $t->type === 'null', + static fn (DataType $t) => $t->isNullable || $t->type === 'null', ) ); } diff --git a/tests/MapperTest.php b/tests/MapperTest.php index 92b2e80..d5b3163 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -2,9 +2,10 @@ namespace Jerodev\DataMapper\Tests; -use Couchbase\User; use Generator; +use Jerodev\DataMapper\Exceptions\UnexpectedNullValueException; use Jerodev\DataMapper\Mapper; +use Jerodev\DataMapper\MapperConfig; use Jerodev\DataMapper\Tests\_Mocks\SuitEnum; use Jerodev\DataMapper\Tests\_Mocks\SuperUserDto; use Jerodev\DataMapper\Tests\_Mocks\UserDto; @@ -29,6 +30,17 @@ public function it_should_map_objects(string $type, mixed $value, mixed $expecta $this->assertEquals($expectation, (new Mapper())->map($type, $value)); } + /** @test */ + public function it_should_throw_on_unexpected_null_value(): void + { + $config = new MapperConfig(); + $config->strictNullMapping = true; + + $this->expectException(UnexpectedNullValueException::class); + + (new Mapper($config))->map('string', null); + } + public static function nativeValuesDataProvider(): Generator { yield ['null', null, null]; @@ -37,7 +49,7 @@ public static function nativeValuesDataProvider(): Generator yield ['bool[]', [true, false], [true, false]]; yield ['bool', '1', true]; - yield ['bool', null, false]; + yield ['bool', '', false]; yield ['float', 6.8, 6.8]; yield ['float', 5, 5.0]; diff --git a/tests/Objects/ObjectMapperTest.php b/tests/Objects/ObjectMapperTest.php new file mode 100644 index 0000000..da92541 --- /dev/null +++ b/tests/Objects/ObjectMapperTest.php @@ -0,0 +1,70 @@ +debug = true; + + $objectMapper = new ObjectMapper(new Mapper($config)); + $objectMapper->map(new DataType(Mapper::class, false), []); + unset($objectMapper); + + $this->assertEmpty(\glob($config->classMapperDirectory . '/*.php')); + } + + /** @test */ + public function it_should_parse_enum_using_tryfrom_if_configured(): void + { + $config = new MapperConfig(); + $config->enumTryFrom = true; + + $value = (new ObjectMapper(new Mapper($config)))->map( + new DataType(SuitEnum::class, true), + 'x', + ); + + $this->assertNull($value); + } + + /** @test */ + public function it_should_save_mappers_in_cache_directory(): void + { + $config = new MapperConfig(); + $config->classMapperDirectory = __DIR__ . '/' . \uniqid(); + + (new Mapper($config))->map(MapperConfig::class, []); + + $this->assertCount( + 1, + $files = \glob($config->classMapperDirectory . '/*.php'), + ); + + // Clean up generated files + \unlink($files[0]); + \rmdir($config->classMapperDirectory); + } + + /** @test */ + public function it_should_throw_on_invalid_enum_value(): void + { + $this->expectException(ValueError::class); + + (new ObjectMapper(new Mapper()))->map( + new DataType(SuitEnum::class, false), + 'x', + ); + } +} diff --git a/tests/Types/DataTypeCollectionTest.php b/tests/Types/DataTypeCollectionTest.php index f67b076..870163b 100644 --- a/tests/Types/DataTypeCollectionTest.php +++ b/tests/Types/DataTypeCollectionTest.php @@ -16,7 +16,7 @@ public function it_should_parse_and_convert_back_to_string(string $input, string { $this->assertEquals( $expectation, - (new DataTypeFactory())->fromString($input)->__toString() + (new DataTypeFactory())->fromString($input)->__toString(), ); } From c26c33512d06621e9b332cd537f3524cd801e3e0 Mon Sep 17 00:00:00 2001 From: Jeroen Deviaene Date: Thu, 11 May 2023 21:03:59 +0200 Subject: [PATCH 18/25] [Next] Self mapping classes (#8) --- src/MapsItself.php | 8 ++++++++ src/Objects/ClassBluePrint.php | 2 ++ src/Objects/ClassBluePrinter.php | 8 ++++++++ src/Objects/ObjectMapper.php | 11 +++++++---- tests/Objects/ObjectMapperTest.php | 17 +++++++++++++++++ tests/_Mocks/SelfMapped.php | 19 +++++++++++++++++++ 6 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 src/MapsItself.php create mode 100644 tests/_Mocks/SelfMapped.php diff --git a/src/MapsItself.php b/src/MapsItself.php new file mode 100644 index 0000000..16f7774 --- /dev/null +++ b/src/MapsItself.php @@ -0,0 +1,8 @@ + */ public array $classAttributes = []; + + public bool $mapsItself = false; } diff --git a/src/Objects/ClassBluePrinter.php b/src/Objects/ClassBluePrinter.php index 7af3eae..2af2b95 100644 --- a/src/Objects/ClassBluePrinter.php +++ b/src/Objects/ClassBluePrinter.php @@ -3,6 +3,7 @@ namespace Jerodev\DataMapper\Objects; use Jerodev\DataMapper\Attributes\PostMapping; +use Jerodev\DataMapper\MapsItself; use Jerodev\DataMapper\Types\DataType; use Jerodev\DataMapper\Types\DataTypeCollection; use Jerodev\DataMapper\Types\DataTypeFactory; @@ -24,6 +25,13 @@ public function print(string $class): ClassBluePrint $reflection = new ReflectionClass($class); $blueprint = new ClassBluePrint(); + + // If the class maps itself, don't bother mapping anything else. + if ($reflection->implementsInterface(MapsItself::class)) { + $blueprint->mapsItself = true; + return $blueprint; + } + $this->printConstructor($reflection, $blueprint); $this->printProperties($reflection, $blueprint); $this->printAttributes($reflection, $blueprint); diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index c3f2618..fef4ddc 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -41,10 +41,15 @@ public function map(DataType $type, array|string $data): ?object } } + $blueprint = $this->classBluePrinter->print($class); + if ($blueprint->mapsItself) { + return \call_user_func([$class, 'mapSelf'], $data, $this->mapper); + } + $functionName = self::MAPPER_FUNCTION_PREFIX . \md5($class); $fileName = $this->mapperDirectory() . \DIRECTORY_SEPARATOR . $functionName . '.php'; if (! \file_exists($fileName)) { - \file_put_contents($fileName, $this->createObjectMappingFunction($class, $functionName)); + \file_put_contents($fileName, $this->createObjectMappingFunction($blueprint, $class, $functionName)); } // Include the function containing file and call the function. @@ -69,10 +74,8 @@ private function mapperDirectory(): string return $dir; } - private function createObjectMappingFunction(string $class, string $mapFunctionName): string + private function createObjectMappingFunction(ClassBluePrint $blueprint, string $class, string $mapFunctionName): string { - $blueprint = $this->classBluePrinter->print($class); - // Instantiate a new object $args = []; foreach ($blueprint->constructorArguments as $argument) { diff --git a/tests/Objects/ObjectMapperTest.php b/tests/Objects/ObjectMapperTest.php index da92541..9c4f3ec 100644 --- a/tests/Objects/ObjectMapperTest.php +++ b/tests/Objects/ObjectMapperTest.php @@ -5,6 +5,7 @@ use Jerodev\DataMapper\Mapper; use Jerodev\DataMapper\MapperConfig; use Jerodev\DataMapper\Objects\ObjectMapper; +use Jerodev\DataMapper\Tests\_Mocks\SelfMapped; use Jerodev\DataMapper\Tests\_Mocks\SuitEnum; use Jerodev\DataMapper\Types\DataType; use PHPUnit\Framework\TestCase; @@ -12,6 +13,22 @@ class ObjectMapperTest extends TestCase { + /** @test */ + public function it_should_let_classes_map_themselves(): void + { + $value = (new ObjectMapper(new Mapper()))->map( + new DataType(SelfMapped::class, true), + [ + 'data' => [ + 'John Doe', + 'Jane Doe', + ], + ], + ); + + $this->assertEquals(['John Doe', 'Jane Doe'], $value->users); + } + /** @test */ public function it_should_not_retain_mapper_files_in_debug_mode(): void { diff --git a/tests/_Mocks/SelfMapped.php b/tests/_Mocks/SelfMapped.php new file mode 100644 index 0000000..06bc33e --- /dev/null +++ b/tests/_Mocks/SelfMapped.php @@ -0,0 +1,19 @@ +users = $data['data']; + + return $self; + } +} From a62ba5a526f0bd81171aa6d6fa23b7932e8ca471 Mon Sep 17 00:00:00 2001 From: Jeroen Deviaene Date: Fri, 12 May 2023 11:49:29 +0200 Subject: [PATCH 19/25] [Next] Adding more test cases (#9) --- phpunit.xml | 6 ++-- src/Objects/ObjectMapper.php | 2 +- src/Types/DataTypeFactory.php | 39 ++++++++++++++++++-------- tests/MapperTest.php | 20 +++++++++++++ tests/Objects/ObjectMapperTest.php | 7 ++++- tests/Types/DataTypeCollectionTest.php | 6 +++- 6 files changed, 63 insertions(+), 17 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 1cd2f79..31c24a6 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,10 +7,12 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false"> + stopOnFailure="false" + executionOrder="random" +> ./tests - \ No newline at end of file + diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index fef4ddc..fd41082 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -64,7 +64,7 @@ public function clearCache(): void } } - private function mapperDirectory(): string + public function mapperDirectory(): string { $dir = \str_replace('{$TMP}', \sys_get_temp_dir(), $this->mapper->config->classMapperDirectory); if (! \file_exists($dir)) { diff --git a/src/Types/DataTypeFactory.php b/src/Types/DataTypeFactory.php index 2148f15..d052f78 100644 --- a/src/Types/DataTypeFactory.php +++ b/src/Types/DataTypeFactory.php @@ -55,7 +55,9 @@ private function tokenize(string $type): array $tokens[] = $char; $token = ''; } else if ($char === '[') { - $tokens[] = $token; + if (! empty(\trim($token))) { + $tokens[] = $token; + } $token = ''; if ($type[++$i] === ']') { $tokens[] = '[]'; @@ -88,10 +90,14 @@ private function parseTokens(array &$tokens): DataTypeCollection while ($token = \array_shift($tokens)) { if ($token === '<') { + $genericTypes = $this->fetchGenericTypes($tokens); + while (($tokens[0] ?? '') === '[]') { + $stringStack .= \array_shift($tokens); + } $types[] = $this->makeDataType( $stringStack, $nullable, - $this->fetchGenericTypes($tokens), + $genericTypes, ); $stringStack = ''; $nullable = false; @@ -194,16 +200,25 @@ private function fetchGenericTypes(array &$tokens): array */ private function makeDataType(string $type, bool $nullable = false, array $genericTypes = []): DataType { - if (\str_ends_with($type, '[]')) { - return new DataType( - 'array', - $nullable, - [ - new DataTypeCollection([ - new DataType(\substr($type, 0, -2), false), - ]) - ], - ); + // Parse a stack of [] arrays + $arrayStack = 0; + while (\str_ends_with($type, '[]')) { + $type = \substr($type, 0, -2); + $arrayStack++; + } + if ($arrayStack > 0) { + $type = new DataType($type, false, $genericTypes); + for ($i = 0; $i < $arrayStack; $i++) { + $type = new DataType( + 'array', + $arrayStack - $i === 1 ? $nullable : false, + [ + new DataTypeCollection([$type]), + ], + ); + } + + return $type; } return new DataType($type, $nullable, $genericTypes); diff --git a/tests/MapperTest.php b/tests/MapperTest.php index d5b3163..e40a2c0 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -6,6 +6,7 @@ use Jerodev\DataMapper\Exceptions\UnexpectedNullValueException; use Jerodev\DataMapper\Mapper; use Jerodev\DataMapper\MapperConfig; +use Jerodev\DataMapper\Tests\_Mocks\SelfMapped; use Jerodev\DataMapper\Tests\_Mocks\SuitEnum; use Jerodev\DataMapper\Tests\_Mocks\SuperUserDto; use Jerodev\DataMapper\Tests\_Mocks\UserDto; @@ -60,6 +61,8 @@ public static function nativeValuesDataProvider(): Generator yield ['int', '8', 8]; yield ['int', 8.3, 8]; yield ['int[]', [5, 8], [5, 8]]; + yield ['int[][][][][]', [[[[['5']]]]], [[[[[5]]]]]]; + yield ['array[][]', [[[[['0']]]]], [[[[[0]]]]]]; yield ['string', 4, '4']; yield ['array', [4, 5], ['4', '5']]; @@ -69,6 +72,7 @@ public static function nativeValuesDataProvider(): Generator public static function objectValuesDataProvider(): Generator { + yield ['object[]', [['a' => 'b'], ['c' => 'd']], [(object)['a' => 'b'], (object)['c' => 'd']]]; yield ['Mapper', [], new Mapper()]; $dto = new UserDto('Jeroen'); @@ -101,5 +105,21 @@ public static function objectValuesDataProvider(): Generator ], $dto, ]; + + $dto = new SelfMapped(); + $dto->users = ['me', ['myself', 'I']]; + yield [ + SelfMapped::class, + [ + 'data' => [ + 'me', + [ + 'myself', + 'I', + ], + ], + ], + $dto, + ]; } } diff --git a/tests/Objects/ObjectMapperTest.php b/tests/Objects/ObjectMapperTest.php index 9c4f3ec..10f2102 100644 --- a/tests/Objects/ObjectMapperTest.php +++ b/tests/Objects/ObjectMapperTest.php @@ -36,10 +36,15 @@ public function it_should_not_retain_mapper_files_in_debug_mode(): void $config->debug = true; $objectMapper = new ObjectMapper(new Mapper($config)); + $objectMapper->clearCache(); $objectMapper->map(new DataType(Mapper::class, false), []); + + $filePattern = $objectMapper->mapperDirectory() . '/*.php'; + $this->assertCount(1, \glob($filePattern)); + unset($objectMapper); - $this->assertEmpty(\glob($config->classMapperDirectory . '/*.php')); + $this->assertEmpty(\glob($filePattern)); } /** @test */ diff --git a/tests/Types/DataTypeCollectionTest.php b/tests/Types/DataTypeCollectionTest.php index 870163b..e83687e 100644 --- a/tests/Types/DataTypeCollectionTest.php +++ b/tests/Types/DataTypeCollectionTest.php @@ -6,7 +6,7 @@ use Jerodev\DataMapper\Types\DataTypeFactory; use PHPUnit\Framework\TestCase; -class DataTypeCollectionTest extends TestCase +final class DataTypeCollectionTest extends TestCase { /** * @test @@ -24,9 +24,13 @@ public static function typeToStringDataProvider(): Generator { yield ['string', 'string']; yield ['string|int', 'string|int']; + yield ['string|null|int', 'string|int|null']; // null is always parsed last yield ['string[]', 'array']; + yield ['string[][][]', 'array>>']; + yield ['array[][]', 'array>>>>']; yield ['array', 'array']; yield ['Generic', 'Generic']; yield ['array>', 'array>>']; + yield ['array< int, array< string | int | null | float > >', 'array>']; } } From f2e828cdd7a219ce8754c20239ce6efda2e9f807 Mon Sep 17 00:00:00 2001 From: Jeroen Deviaene Date: Fri, 12 May 2023 13:23:19 +0200 Subject: [PATCH 20/25] [Next] Resolve class names for object mappers (#10) --- .gitignore | 3 +- src/Mapper.php | 10 +++-- src/Objects/ClassBluePrint.php | 8 +++- src/Objects/ClassBluePrinter.php | 3 +- src/Objects/ClassResolver.php | 7 ++-- src/Objects/DocBlockParser.php | 3 +- src/Objects/ObjectMapper.php | 49 +++++++++++++----------- src/Types/DataType.php | 15 -------- src/Types/DataTypeCollection.php | 8 ---- src/Types/DataTypeFactory.php | 53 ++++++++++++++++++++++++++ tests/MapperTest.php | 26 ++++++++++++- tests/Types/DataTypeCollectionTest.php | 36 ----------------- tests/Types/DataTypeFactoryTest.php | 28 ++++++++++++++ tests/_Mocks/Aliases.php | 11 ++++++ 14 files changed, 167 insertions(+), 93 deletions(-) delete mode 100644 tests/Types/DataTypeCollectionTest.php create mode 100644 tests/_Mocks/Aliases.php diff --git a/.gitignore b/.gitignore index 8dbdf9d..dc31da2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ vendor/ -.phpunit.result.cache \ No newline at end of file +jmapper_* +.phpunit.result.cache diff --git a/src/Mapper.php b/src/Mapper.php index 3c7f565..4e4452c 100644 --- a/src/Mapper.php +++ b/src/Mapper.php @@ -19,9 +19,13 @@ class Mapper public function __construct( ?MapperConfig $config = null, ) { - $this->dataTypeFactory = new DataTypeFactory(); - $this->objectMapper = new ObjectMapper($this); $this->config = $config ?? new MapperConfig(); + + $this->dataTypeFactory = new DataTypeFactory(); + $this->objectMapper = new ObjectMapper( + $this, + $this->dataTypeFactory, + ); } /** @@ -45,7 +49,7 @@ public function map($typeCollection, $data) return null; } - throw new UnexpectedNullValueException($typeCollection->__toString()); + throw new UnexpectedNullValueException($this->dataTypeFactory->print($typeCollection)); } // Loop over all possible types and parse to the first one that matches diff --git a/src/Objects/ClassBluePrint.php b/src/Objects/ClassBluePrint.php index b32e4f5..e332dce 100644 --- a/src/Objects/ClassBluePrint.php +++ b/src/Objects/ClassBluePrint.php @@ -7,6 +7,8 @@ class ClassBluePrint { + public bool $mapsItself = false; + /** @var array */ public array $constructorArguments = []; @@ -16,5 +18,9 @@ class ClassBluePrint /** @var array */ public array $classAttributes = []; - public bool $mapsItself = false; + public function __construct( + public readonly string $namespacedClassName, + public readonly string $fileName, + ) { + } } diff --git a/src/Objects/ClassBluePrinter.php b/src/Objects/ClassBluePrinter.php index 2af2b95..e4f7a4f 100644 --- a/src/Objects/ClassBluePrinter.php +++ b/src/Objects/ClassBluePrinter.php @@ -23,8 +23,7 @@ public function __construct() public function print(string $class): ClassBluePrint { $reflection = new ReflectionClass($class); - - $blueprint = new ClassBluePrint(); + $blueprint = new ClassBluePrint($class, $reflection->getFileName()); // If the class maps itself, don't bother mapping anything else. if ($reflection->implementsInterface(MapsItself::class)) { diff --git a/src/Objects/ClassResolver.php b/src/Objects/ClassResolver.php index 1fc8bb4..338e0d0 100644 --- a/src/Objects/ClassResolver.php +++ b/src/Objects/ClassResolver.php @@ -6,14 +6,14 @@ class ClassResolver { - public function resolve(string $name): string + public function resolve(string $name, ?string $sourceFile = null): string { if (\class_exists($name)) { return $name; } // Find the file where this class is mentioned - $sourceFile = $this->findSourceFile(); + $sourceFile ??= $this->findSourceFile(); if ($sourceFile === null) { throw new CouldNotResolveClassException($name); } @@ -57,7 +57,8 @@ private function findClassNameInFile(string $name, string $sourceFile): string $lastPart = \end($nameParts); $newline = false; - for ($i = 0; $i < \strlen($file); $i++) { + $fileLength = \strlen($file); + for ($i = 0; $i < $fileLength; $i++) { $char = $file[$i]; // Don't care about spaces diff --git a/src/Objects/DocBlockParser.php b/src/Objects/DocBlockParser.php index 05d1f48..a74faf7 100644 --- a/src/Objects/DocBlockParser.php +++ b/src/Objects/DocBlockParser.php @@ -8,7 +8,8 @@ public function parse(string $contents): DocBlock { $docblock = new DocBlock(); - for ($i = 0; $i < \strlen($contents); $i++) { + $contentLength = \strlen($contents); + for ($i = 0; $i < $contentLength; $i++) { $char = $contents[$i]; if ($char === '@') { diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index fd41082..ecf7a08 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -7,30 +7,30 @@ use Jerodev\DataMapper\Mapper; use Jerodev\DataMapper\Types\DataType; use Jerodev\DataMapper\Types\DataTypeCollection; +use Jerodev\DataMapper\Types\DataTypeFactory; class ObjectMapper { private const MAPPER_FUNCTION_PREFIX = 'jmapper_'; private readonly ClassBluePrinter $classBluePrinter; - private readonly ClassResolver $classResolver; public function __construct( private readonly Mapper $mapper, + private readonly DataTypeFactory $dataTypeFactory = new DataTypeFactory(), ) { $this->classBluePrinter = new ClassBluePrinter(); - $this->classResolver = new ClassResolver(); } /** * @param DataType $type * @param array|string $data - * @return object + * @return object|null * @throws CouldNotResolveClassException */ public function map(DataType $type, array|string $data): ?object { - $class = $this->classResolver->resolve($type->type); + $class = $this->dataTypeFactory->classResolver->resolve($type->type); // If the data is a string and the class is an enum, create the enum. if (\is_string($data) && \is_subclass_of($class, \BackedEnum::class)) { @@ -49,7 +49,7 @@ public function map(DataType $type, array|string $data): ?object $functionName = self::MAPPER_FUNCTION_PREFIX . \md5($class); $fileName = $this->mapperDirectory() . \DIRECTORY_SEPARATOR . $functionName . '.php'; if (! \file_exists($fileName)) { - \file_put_contents($fileName, $this->createObjectMappingFunction($blueprint, $class, $functionName)); + \file_put_contents($fileName, $this->createObjectMappingFunction($blueprint, $functionName)); } // Include the function containing file and call the function. @@ -67,22 +67,24 @@ public function clearCache(): void public function mapperDirectory(): string { $dir = \str_replace('{$TMP}', \sys_get_temp_dir(), $this->mapper->config->classMapperDirectory); - if (! \file_exists($dir)) { - \mkdir($dir, 0777, true); + if (! \file_exists($dir) && ! \mkdir($dir, 0777, true) && ! \is_dir($dir)) { + throw new \RuntimeException("Could not create caching directory '{$dir}'"); } - return $dir; + return \rtrim($dir, \DIRECTORY_SEPARATOR); } - private function createObjectMappingFunction(ClassBluePrint $blueprint, string $class, string $mapFunctionName): string + private function createObjectMappingFunction(ClassBluePrint $blueprint, string $mapFunctionName): string { + $tab = ' '; + // Instantiate a new object $args = []; foreach ($blueprint->constructorArguments as $argument) { $arg = "\$data['{$argument['name']}']"; if ($argument['type'] !== null) { - $arg = $this->castInMapperFunction($arg, $argument['type']); + $arg = $this->castInMapperFunction($arg, $argument['type'], $blueprint); if (\array_key_exists('default', $argument)) { $arg = $this->wrapDefault($arg, $argument['name'], $argument['default']); } @@ -90,25 +92,25 @@ private function createObjectMappingFunction(ClassBluePrint $blueprint, string $ $args[] = $arg; } - $content = '$x = new ' . $class . '(' . \implode(', ', $args) . ');'; + $content = '$x = new ' . $blueprint->namespacedClassName . '(' . \implode(', ', $args) . ');'; // Map properties foreach ($blueprint->properties as $name => $property) { - $propertyMap = $this->castInMapperFunction("\$data['{$name}']", $property['type']); + $propertyMap = $this->castInMapperFunction("\$data['{$name}']", $property['type'], $blueprint); if (\array_key_exists('default', $property)) { $propertyMap = $this->wrapDefault($propertyMap, $name, $property['default']); } - $content.= \PHP_EOL . ' $x->' . $name . ' = ' . $propertyMap . ';'; + $content.= \PHP_EOL . $tab . $tab . '$x->' . $name . ' = ' . $propertyMap . ';'; } // Post mapping functions? foreach ($blueprint->classAttributes as $attribute) { if ($attribute instanceof PostMapping) { if (\is_string($attribute->postMappingCallback)) { - $content.= \PHP_EOL . \PHP_EOL . " \$x->{$attribute->postMappingCallback}(\$data, \$x);"; + $content.= \PHP_EOL . \PHP_EOL . $tab . $tab . "\$x->{$attribute->postMappingCallback}(\$data, \$x);"; } else { - $content.= \PHP_EOL . \PHP_EOL . " \call_user_func({$attribute->postMappingCallback}, \$data, \$x);"; + $content.= \PHP_EOL . \PHP_EOL . $tab . $tab . "\call_user_func({$attribute->postMappingCallback}, \$data, \$x);"; } } } @@ -117,16 +119,19 @@ private function createObjectMappingFunction(ClassBluePrint $blueprint, string $ $mapperClass = Mapper::class; return <<types) === 1) { $type = $type->types[0]; @@ -148,7 +153,7 @@ private function castInMapperFunction(string $propertyName, DataTypeCollection $ } if (\count($type->genericTypes) === 1) { $uniqid = \uniqid(); - return "\\array_map(static fn (\$x{$uniqid}) => " . $this->castInMapperFunction('$x' . $uniqid, $type->genericTypes[0]) . ", {$propertyName})"; + return "\\array_map(static fn (\$x{$uniqid}) => " . $this->castInMapperFunction('$x' . $uniqid, $type->genericTypes[0], $bluePrint) . ", {$propertyName})"; } } @@ -159,7 +164,7 @@ private function castInMapperFunction(string $propertyName, DataTypeCollection $ } } - return '$mapper->map(\'' . $type->__toString() . '\', ' . $propertyName . ')'; + return '$mapper->map(\'' . $this->dataTypeFactory->print($type, $bluePrint->fileName) . '\', ' . $propertyName . ')'; } private function wrapDefault(string $value, string $arrayKey, mixed $defaultValue): string diff --git a/src/Types/DataType.php b/src/Types/DataType.php index 4cb8443..446eb5c 100644 --- a/src/Types/DataType.php +++ b/src/Types/DataType.php @@ -47,19 +47,4 @@ public function isNative(): bool ], ); } - - public function __toString(): string - { - $type = $this->type; - - if ($this->isNullable) { - $type = '?' . $this->type; - } - - if (! empty($this->genericTypes)) { - $type .= '<' . \implode(', ', \array_map(static fn (DataTypeCollection $type) => $type->__toString(), $this->genericTypes)) . '>'; - } - - return $type; - } } diff --git a/src/Types/DataTypeCollection.php b/src/Types/DataTypeCollection.php index 03ee929..473e653 100644 --- a/src/Types/DataTypeCollection.php +++ b/src/Types/DataTypeCollection.php @@ -39,12 +39,4 @@ public function isNullable(): bool ) ); } - - public function __toString(): string - { - return \implode( - '|', - \array_map(static fn (DataType $type) => $type->__toString(), $this->types), - ); - } } diff --git a/src/Types/DataTypeFactory.php b/src/Types/DataTypeFactory.php index d052f78..3c41eda 100644 --- a/src/Types/DataTypeFactory.php +++ b/src/Types/DataTypeFactory.php @@ -3,12 +3,18 @@ namespace Jerodev\DataMapper\Types; use Jerodev\DataMapper\Exceptions\UnexpectedTokenException; +use Jerodev\DataMapper\Objects\ClassResolver; class DataTypeFactory { /** @var array */ private array $typeCache = []; + public function __construct( + public readonly ClassResolver $classResolver = new ClassResolver(), + ) { + } + public function fromString(string $rawType, bool $forceNullable = false): DataTypeCollection { $rawType = \trim($rawType); @@ -33,6 +39,53 @@ public function fromString(string $rawType, bool $forceNullable = false): DataTy return $this->typeCache[$rawType] = $type; } + /** + * Prints a type with resolved class names, if possible. + * + * @param DataTypeCollection|DataType $type + * @param string|null $sourceFile + * @return string + */ + public function print(DataTypeCollection|DataType $type, ?string $sourceFile = null): string + { + if ($type instanceof DataType) { + $type = new DataTypeCollection([$type]); + } + \assert($type instanceof DataTypeCollection); + + $types = []; + foreach ($type->types as $t) { + $typeString = $t->type; + + // Attempt to resolve the class name + try { + $typeString = $t->isNative() || $t->isArray() + ? $typeString + : $this->classResolver->resolve($t->type, $sourceFile); + } catch (\Throwable) { + } + + // Add generic types between < > + if (\count($t->genericTypes) > 0) { + $typeString .= '<' . \implode( + ', ', + \array_map( + fn (DataTypeCollection $c) => $this->print($c, $sourceFile), + $t->genericTypes, + ), + ) . '>'; + } + + if ($t->isNullable) { + $typeString = '?' . $typeString; + } + + $types[] = $typeString; + } + + return \implode('|', $types); + } + /** * @param string $type * @return array diff --git a/tests/MapperTest.php b/tests/MapperTest.php index e40a2c0..0368698 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -6,6 +6,7 @@ use Jerodev\DataMapper\Exceptions\UnexpectedNullValueException; use Jerodev\DataMapper\Mapper; use Jerodev\DataMapper\MapperConfig; +use Jerodev\DataMapper\Tests\_Mocks\Aliases; use Jerodev\DataMapper\Tests\_Mocks\SelfMapped; use Jerodev\DataMapper\Tests\_Mocks\SuitEnum; use Jerodev\DataMapper\Tests\_Mocks\SuperUserDto; @@ -28,7 +29,10 @@ public function it_should_map_native_values(string $type, mixed $value, mixed $e */ public function it_should_map_objects(string $type, mixed $value, mixed $expectation): void { - $this->assertEquals($expectation, (new Mapper())->map($type, $value)); + $config = new MapperConfig(); + $config->classMapperDirectory = __DIR__ . '/..'; + + $this->assertEquals($expectation, (new Mapper($config))->map($type, $value)); } /** @test */ @@ -121,5 +125,25 @@ public static function objectValuesDataProvider(): Generator ], $dto, ]; + + $dto = new Aliases(); + $dto->userAliases = [ + 'Jerodev' => new UserDto('Jeroen'), + 'Foo' => new UserDto('Bar'), + ]; + yield [ + Aliases::class, + [ + 'userAliases' => [ + 'Jerodev' => [ + 'name' => 'Jeroen', + ], + 'Foo' => [ + 'name' => 'Bar', + ], + ], + ], + $dto, + ]; } } diff --git a/tests/Types/DataTypeCollectionTest.php b/tests/Types/DataTypeCollectionTest.php deleted file mode 100644 index e83687e..0000000 --- a/tests/Types/DataTypeCollectionTest.php +++ /dev/null @@ -1,36 +0,0 @@ -assertEquals( - $expectation, - (new DataTypeFactory())->fromString($input)->__toString(), - ); - } - - public static function typeToStringDataProvider(): Generator - { - yield ['string', 'string']; - yield ['string|int', 'string|int']; - yield ['string|null|int', 'string|int|null']; // null is always parsed last - yield ['string[]', 'array']; - yield ['string[][][]', 'array>>']; - yield ['array[][]', 'array>>>>']; - yield ['array', 'array']; - yield ['Generic', 'Generic']; - yield ['array>', 'array>>']; - yield ['array< int, array< string | int | null | float > >', 'array>']; - } -} diff --git a/tests/Types/DataTypeFactoryTest.php b/tests/Types/DataTypeFactoryTest.php index 3005689..fae1c4d 100644 --- a/tests/Types/DataTypeFactoryTest.php +++ b/tests/Types/DataTypeFactoryTest.php @@ -31,6 +31,20 @@ public function it_should_throw_unexpected_token(string $input): void (new DataTypeFactory())->fromString($input); } + /** + * @test + * @dataProvider typeToStringDataProvider + */ + public function it_should_parse_and_convert_back_to_string(string $input, string $expectation): void + { + $factory = new DataTypeFactory(); + + $this->assertEquals( + $expectation, + $factory->print($factory->fromString($input)), + ); + } + public static function singleDataTypeProvider(): Generator { yield ['int', new DataType('int', false)]; @@ -117,4 +131,18 @@ public static function unexpectedTokenDataProvider(): Generator yield ['String>']; yield ['array[foo]']; } + + public static function typeToStringDataProvider(): Generator + { + yield ['string', 'string']; + yield ['string|int', 'string|int']; + yield ['string|null|int', 'string|int|null']; // null is always parsed last + yield ['string[]', 'array']; + yield ['string[][][]', 'array>>']; + yield ['array[][]', 'array>>>>']; + yield ['array', 'array']; + yield ['Generic', 'Generic']; + yield ['array>', 'array>>']; + yield ['array< int, array< string | int | null | float > >', 'array>']; + } } diff --git a/tests/_Mocks/Aliases.php b/tests/_Mocks/Aliases.php new file mode 100644 index 0000000..5ce4bc5 --- /dev/null +++ b/tests/_Mocks/Aliases.php @@ -0,0 +1,11 @@ + */ + public array $userAliases; +} From bb7fd6a4411ac480d070a24b1643aadb4c5aed74 Mon Sep 17 00:00:00 2001 From: Jeroen Deviaene Date: Mon, 15 May 2023 20:06:34 +0200 Subject: [PATCH 21/25] [Next] Optimize mapper functions (#11) --- composer.lock | 16 ++++++------ src/Objects/ClassBluePrint.php | 2 -- src/Objects/ClassBluePrinter.php | 7 ----- src/Objects/ObjectMapper.php | 45 +++++++++++++++++++++++++++----- tests/MapperTest.php | 5 ++++ tests/_Mocks/Aliases.php | 7 +++++ 6 files changed, 59 insertions(+), 23 deletions(-) diff --git a/composer.lock b/composer.lock index 2d1cac1..d76e1e1 100644 --- a/composer.lock +++ b/composer.lock @@ -1849,16 +1849,16 @@ }, { "name": "slevomat/coding-standard", - "version": "8.11.1", + "version": "8.12.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "af87461316b257e46e15bb041dca6fca3796d822" + "reference": "cc04334ed0ce5a251389112fbd2dbe1dbc931ae8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/af87461316b257e46e15bb041dca6fca3796d822", - "reference": "af87461316b257e46e15bb041dca6fca3796d822", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/cc04334ed0ce5a251389112fbd2dbe1dbc931ae8", + "reference": "cc04334ed0ce5a251389112fbd2dbe1dbc931ae8", "shasum": "" }, "require": { @@ -1870,11 +1870,11 @@ "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.10.14", + "phpstan/phpstan": "1.10.15", "phpstan/phpstan-deprecation-rules": "1.1.3", "phpstan/phpstan-phpunit": "1.3.11", "phpstan/phpstan-strict-rules": "1.5.1", - "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.1.1" + "phpunit/phpunit": "7.5.20|8.5.21|9.6.8|10.1.3" }, "type": "phpcodesniffer-standard", "extra": { @@ -1898,7 +1898,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.11.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.12.0" }, "funding": [ { @@ -1910,7 +1910,7 @@ "type": "tidelift" } ], - "time": "2023-04-24T08:19:01+00:00" + "time": "2023-05-14T20:06:01+00:00" }, { "name": "squizlabs/php_codesniffer", diff --git a/src/Objects/ClassBluePrint.php b/src/Objects/ClassBluePrint.php index e332dce..b169a4c 100644 --- a/src/Objects/ClassBluePrint.php +++ b/src/Objects/ClassBluePrint.php @@ -7,8 +7,6 @@ class ClassBluePrint { - public bool $mapsItself = false; - /** @var array */ public array $constructorArguments = []; diff --git a/src/Objects/ClassBluePrinter.php b/src/Objects/ClassBluePrinter.php index e4f7a4f..c97488c 100644 --- a/src/Objects/ClassBluePrinter.php +++ b/src/Objects/ClassBluePrinter.php @@ -3,7 +3,6 @@ namespace Jerodev\DataMapper\Objects; use Jerodev\DataMapper\Attributes\PostMapping; -use Jerodev\DataMapper\MapsItself; use Jerodev\DataMapper\Types\DataType; use Jerodev\DataMapper\Types\DataTypeCollection; use Jerodev\DataMapper\Types\DataTypeFactory; @@ -25,12 +24,6 @@ public function print(string $class): ClassBluePrint $reflection = new ReflectionClass($class); $blueprint = new ClassBluePrint($class, $reflection->getFileName()); - // If the class maps itself, don't bother mapping anything else. - if ($reflection->implementsInterface(MapsItself::class)) { - $blueprint->mapsItself = true; - return $blueprint; - } - $this->printConstructor($reflection, $blueprint); $this->printProperties($reflection, $blueprint); $this->printAttributes($reflection, $blueprint); diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index ecf7a08..51e7839 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -5,6 +5,7 @@ use Jerodev\DataMapper\Attributes\PostMapping; use Jerodev\DataMapper\Exceptions\CouldNotResolveClassException; use Jerodev\DataMapper\Mapper; +use Jerodev\DataMapper\MapsItself; use Jerodev\DataMapper\Types\DataType; use Jerodev\DataMapper\Types\DataTypeCollection; use Jerodev\DataMapper\Types\DataTypeFactory; @@ -31,20 +32,20 @@ public function __construct( public function map(DataType $type, array|string $data): ?object { $class = $this->dataTypeFactory->classResolver->resolve($type->type); + if (\is_subclass_of($class, MapsItself::class)) { + return \call_user_func([$class, 'mapSelf'], $data, $this->mapper); + } // If the data is a string and the class is an enum, create the enum. if (\is_string($data) && \is_subclass_of($class, \BackedEnum::class)) { if ($this->mapper->config->enumTryFrom) { return $class::tryFrom($data); - } else { - return $class::from($data); } + + return $class::from($data); } $blueprint = $this->classBluePrinter->print($class); - if ($blueprint->mapsItself) { - return \call_user_func([$class, 'mapSelf'], $data, $this->mapper); - } $functionName = self::MAPPER_FUNCTION_PREFIX . \md5($class); $fileName = $this->mapperDirectory() . \DIRECTORY_SEPARATOR . $functionName . '.php'; @@ -96,12 +97,19 @@ private function createObjectMappingFunction(ClassBluePrint $blueprint, string $ // Map properties foreach ($blueprint->properties as $name => $property) { + // Use a foreach to map key/value arrays + if (\count($property['type']->types) === 1 && $property['type']->types[0]->isArray() && \count($property['type']->types[0]->genericTypes) === 2) { + $content .= $this->buildPropertyForeachMapping($name, $property, $blueprint); + + continue; + } + $propertyMap = $this->castInMapperFunction("\$data['{$name}']", $property['type'], $blueprint); if (\array_key_exists('default', $property)) { $propertyMap = $this->wrapDefault($propertyMap, $name, $property['default']); } - $content.= \PHP_EOL . $tab . $tab . '$x->' . $name . ' = ' . $propertyMap . ';'; + $content .= \PHP_EOL . $tab . $tab . '$x->' . $name . ' = ' . $propertyMap . ';'; } // Post mapping functions? @@ -162,6 +170,10 @@ private function castInMapperFunction(string $propertyName, DataTypeCollection $ return "{$type->type}::{$enumFunction}({$propertyName})"; } + + if (\is_subclass_of($type->type, MapsItself::class)) { + return "{$type->type}::mapSelf({$propertyName}, \$mapper)"; + } } return '$mapper->map(\'' . $this->dataTypeFactory->print($type, $bluePrint->fileName) . '\', ' . $propertyName . ')'; @@ -178,4 +190,25 @@ public function __destruct() $this->clearCache(); } } + + /** @param array{type: DataTypeCollection, default?: mixed} $property */ + private function buildPropertyForeachMapping(string $propertyName, array $property, ClassBluePrint $blueprint): string + { + $canHaveDefault = \array_key_exists('default', $property); + + $foreach = \PHP_EOL . \str_repeat(' ', $canHaveDefault ? 3 : 2) . '$x->' . $propertyName . ' = [];'; + $foreach .= \PHP_EOL . \str_repeat(' ', $canHaveDefault ? 3 : 2) . 'foreach ($data[\'' . $propertyName . '\'] as $key => $value) {'; + $foreach .= \PHP_EOL . \str_repeat(' ', $canHaveDefault ? 4 : 3) . '$x->' . $propertyName . '[' . $this->castInMapperFunction('$key', $property['type']->types[0]->genericTypes[0], $blueprint) . '] = '; + $foreach .= $this->castInMapperFunction('$value', $property['type']->types[0]->genericTypes[1], $blueprint) . ';'; + $foreach .= \PHP_EOL . \str_repeat(' ', $canHaveDefault ? 3 : 2) . '}'; + + if ($canHaveDefault) { + $foreach = \PHP_EOL . \str_repeat(' ', 2) . 'if (\\array_key_exists(\'' . $propertyName . '\', $data)) {' . $foreach; + $foreach .= \PHP_EOL . \str_repeat(' ', 2) . '} else {'; + $foreach .= \PHP_EOL . \str_repeat(' ', 3) . '$x->' . $propertyName . ' = ' . \var_export($property['default'], true) . ';'; + $foreach .= \PHP_EOL . \str_repeat(' ', 2) . '}'; + } + + return $foreach; + } } diff --git a/tests/MapperTest.php b/tests/MapperTest.php index 0368698..4c29e11 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -131,6 +131,8 @@ public static function objectValuesDataProvider(): Generator 'Jerodev' => new UserDto('Jeroen'), 'Foo' => new UserDto('Bar'), ]; + $dto->sm = new SelfMapped(); + $dto->sm->users = ['q']; yield [ Aliases::class, [ @@ -142,6 +144,9 @@ public static function objectValuesDataProvider(): Generator 'name' => 'Bar', ], ], + 'sm' => [ + 'data' => ['q'], + ], ], $dto, ]; diff --git a/tests/_Mocks/Aliases.php b/tests/_Mocks/Aliases.php index 5ce4bc5..bed6d24 100644 --- a/tests/_Mocks/Aliases.php +++ b/tests/_Mocks/Aliases.php @@ -8,4 +8,11 @@ class Aliases { /** @var array */ public array $userAliases; + + /** @var array */ + public array $defaultUsers = [ + 'foo' => 5, + ]; + + public SelfMapped $sm; } From d1f81192d2b15f2374374628f9cbd29799b4ef7c Mon Sep 17 00:00:00 2001 From: Jeroen Deviaene Date: Mon, 15 May 2023 22:32:49 +0200 Subject: [PATCH 22/25] [Next] Readme updates (#12) --- .editorconfig | 3 + .gitattributes | 3 + doc/class-mapper-function.md | 82 ++++++++++++++++++++++ readme.md | 132 ++++++++++++++++++++++++++++------- 4 files changed, 195 insertions(+), 25 deletions(-) create mode 100644 .gitattributes create mode 100644 doc/class-mapper-function.md diff --git a/.editorconfig b/.editorconfig index cf80b4e..56f3b6b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,3 +7,6 @@ insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..782cb24 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +.github/ export-ignore +doc/ export-ignore +tests/ export-ignore diff --git a/doc/class-mapper-function.md b/doc/class-mapper-function.md new file mode 100644 index 0000000..46dd45b --- /dev/null +++ b/doc/class-mapper-function.md @@ -0,0 +1,82 @@ +# Class mapper function +When needing to map a class, the mapper will use reflection to create a mapper function for that class that can be +reused on subsequent calls. +To get the best performance out of these mapper functions, we attempt to map as many properties as possible directly in +the mapper function. + +Here we will go through the different types of properties that will be mapped directly in the mapper function. + +### Simple mappings +For simple mappings to native types is done through direct casting. + +```php +public int $score; + +// Will be mapped as: + +$x->score = (int) $data['score']; +``` + +If the value is not a native type, the function will call the mapper. + +```php +public UserDto $user; + +// Will be mapped as: + +$x->user = $mapper->map('\Jerodev\DataMapper\Tests\_Mocks\UserDto', $data['user']); +``` + +### Default values +When a default value is provided for a property, the property is seen as optional. The mapper will use that default +value if the property is not provided in the data array. + +```php +public int $score = 10; + +// Will be mapped as: + +$x->score = (\array_key_exists('score', $data) ? (int) $data['score'] : 10); +``` + +### Enums +Enums are mapped using the `::from()` method of the enum class. If the mapper [is configured to use `::tryFrom()`](../readme.md#configuration), +this method will be used instead. + +```php +public SuitEnum $cardType; + +// Will be mapped as: + +$x->cardType = \Jerodev\DataMapper\Tests\_Mocks\SuitEnum::from($data['cardType']); +``` + +### Arrays with value type defined +Arrays with a value type defined will use the [`array_map`](https://www.php.net/manual/en/function.array-map.php) +function in combination with simple mappings to map all values in the data array. + +```php +/** @var array */ +public array $usernames; + +// Will be mapped as: + +$x->usernames = \array_map(static fn ($xyz) => (string) $xyz, $data['usernames']); +``` + +### Arrays with key and value type defined +When an array has both its key and value type defined, a foreach is used to map the values to the property. + +```php +/** @var array */ +public array $userScores; + +// Will be mapped as: + +$x->userScores = []; +foreach ($data['userScores'] as $key => $value) { + $x->userScores[(string) $key] = (int) $value; +} +``` + + diff --git a/readme.md b/readme.md index 85d1cd5..c0c721c 100644 --- a/readme.md +++ b/readme.md @@ -1,24 +1,32 @@ # Data Mapper ![run-tests](https://github.com/jerodev/data-mapper/workflows/run-tests/badge.svg) -> :warning: While the package currently works, it's still a work in progress, and things might break in future releases. - -This package will map any raw data into a strong typed PHP object using a set of rules. +This package will map any raw data into a strong typed PHP object. +- [Installation](#installation) - [Basic mapping](#basic-mapping) - - [Public properties](#public-properties) - - [Property setters](#property-setters) -- [Custom mapping](#custom-mapping) + - [Typing properties](#typing-properties) + - [Custom mapping](#custom-mapping) + - [Post mapping](#post-mapping) +- [Configuration](#configuration) +- [Under the hood](#under-the-hood) + +## Installation +The mapper has no external dependencies apart from PHP8.1 or higher. It can be installed using composer: + +```bash +composer require jerodev/data-mapper +``` ## Basic mapping Let's start with the basics. The mapper will map data directly to public properties on objects. If these properties have -types defined either using PHP7.4 property types or through PHPDOC, the mapper will attempt to cast the data to these +types defined either using types introduced in PHP7.4 or through [PHPDoc](https://phpstan.org/writing-php-code/phpdoc-types), the mapper will attempt to cast the data to these types. For example: imagine having an `Entity` class with the public properties `$id` and `$name`: ```php -class Entity +class User { public int $id; public string $name; @@ -29,33 +37,107 @@ To map data from an array we simply pass the class name and an array with data t ```php $mapper = new \Jerodev\DataMapper\Mapper(); -$entity = $mapper->map(Entity::class, [ +$entity = $mapper->map(User::class, [ 'id' => '5', - 'name' => 'foo', + 'name' => 'John Doe', ]); -// Entity { +// User { // +id: 5, -// +name: "foo", +// +name: "John Doe", // } ``` -### Public properties -The easiest properties to map are public properties. The mapper will try to get the type for these properties using one -of the following definitions: -1. PHP7.4 property type (also supports PHP8.0 union types) -2. PHPDoc type definition using [`@var`](https://manual.phpdoc.org/HTMLSmartyConverter/HandS/phpDocumentor/tutorial_tags.var.pkg.html) +This is a simple example, but the mapper can also map nested objects, arrays of objects, keyed arrays, and even multi-level arrays. -The different definitions are checked in this order until a valid type is found. +### Typing properties +The type of the properties is checked from two places: +1. The type of the property itself. This can be defined using typehints [introduced in PHP7.4](https://wiki.php.net/rfc/typed_properties_v2); +2. [PHPDoc types](https://phpstan.org/writing-php-code/phpdoc-types) for properties and constructor parameters. -If no valid type was found for a property, the provided data will be set to the property directly. +First the native type of the property is checked, if this is defined and can be mapped the type will be used. +If no type is provided or the type is a generic array, the mapper will check the PHPDoc for type of the property. -### Property setters -> :warning: Work in progress +When a property is typed using a [union type](https://wiki.php.net/rfc/union_types_v2), the mapper will try to map any +of the provided types from first to last until one mapping succeeds. The only exception is that `null` is always tried +last. -## Custom mapping -Sometimes, classes have a constructor that cannot be mapped automatically. For these cases there is a -[`MapsItself`](https://github.com/jerodev/data-mapper/blob/master/src/MapsItself.php) interface that defines one +If no valid type was found for a property, the provided data will be set to the property directly without any +conversion. + +### Custom mapping +Sometimes, classes have a constructor that cannot be mapped automatically. For these cases there is a +[`MapsItself`](https://github.com/jerodev/data-mapper/blob/master/src/MapsItself.php) interface that defines one static function: `mapObject`. -When the mapper comes across a class that implements this interface, instead of using the constructor, the mapper will +When the mapper comes across a class that implements this interface, instead of using the constructor, the mapper will call the `MapsItself` with the provided data and is expected to return an instance of the current class. + +### Post mapping +The mapper also comes with a post mapping attribute. When adding the [`#[PostMapping]` attribute](https://github.com/jerodev/data-mapper/blob/master/src/Attributes/PostMapping.php) +to a class with a string parameter, this function will be called directly after mapping the object. + +A class can have multiple of these attributes and the attributes will be called in the same order as they are defined. + +## Configuration +The mapper comes with a few configuration options that can be set using the [`MapperConfig`](https://github.com/jerodev/data-mapper/blob/master/src/MapperConfig.php) +object and passed to the mappers' constructor. This is not required, if no configuration is passed, the default config +is used. + +| Option | Type | Default | Description | +|------------------------|----------|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `classMapperDirectory` | `string` | `/tmp/mappers` | This is the location the mapper will create cached mapper functions for objects.
The default location is a mappers function in the operating system temporary folder. | +| `debug` | `bool` | `false` | Enabling debug will clear all cached mapper functions after mapping has completed. | +| `enumTryFrom` | `bool` | `false` | Enabling this will use the `::tryFrom()` method instead of `::from()` to parse strings to enums. | +| `strictNullMapping` | `bool` | `true` | If enabled, the mapper will throw an error when a `null` value is passed for a property that was not typed as nullable. | + +## Under the hood +For simple native types, the mapper will use casting to convert the data to the correct type. + +When requesting an array type, the mapper will call itself with the type of the array elements for each of the elements in the +array. + +For object types, some magic happens. On the very first run for a certain class, the mapper will use reflection to +gather information about the class and build a mapper function based on the properties of the class. +The function will also take into account required and optional properties that are passed to the constructor. + +The goal is to have as much and as simple mapping as possible in these generated functions without having to go back +to the mapper, to reach the best performance. For more information refer to the [class mapper function docs](./doc/class-mapper-function.md). + +As an example, this is one of the testing classes of this library and its generated mapper function: + +```php +#[PostMapping('post')] +class UserDto +{ + /** First name and last name */ + public string $name; + + /** @var array */ + public array $friends = []; + public ?SuitEnum $favoriteSuit = null; + + public function __construct(string $name) + { + $this->name = $name; + } + + public function post(): void + { + $this->name = \ucfirst($this->name); + } +} +``` + +```php +function jmapper_8cf8f45dc33c7f58ab728699ac3ebec3(Jerodev\DataMapper\Mapper $mapper, array $data) +{ + $x = new Jerodev\DataMapper\Tests\_Mocks\UserDto((string) $data['name']); + $x->name = (string) $data['name']; + $x->friends = (\array_key_exists('friends', $data) ? \array_map(static fn ($x6462755ab00b1) => $mapper->map('Jerodev\DataMapper\Tests\_Mocks\UserDto', $x6462755ab00b1), $data['friends']) : []); + $x->favoriteSuit = (\array_key_exists('favoriteSuit', $data) ? Jerodev\DataMapper\Tests\_Mocks\SuitEnum::from($data['favoriteSuit']) : NULL); + + $x->post($data, $x); + + return $x; +} +``` From 5491496909a0a80ed3be5a1ce98f369814c0ed3f Mon Sep 17 00:00:00 2001 From: Jeroen Deviaene Date: Wed, 17 May 2023 12:40:03 +0200 Subject: [PATCH 23/25] [Next] Allow uninitialized properties (#13) --- readme.md | 13 +++++++------ src/MapperConfig.php | 15 +++++++-------- src/Objects/ObjectMapper.php | 21 ++++++++++++++++++++- tests/MapperTest.php | 8 ++++++++ 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/readme.md b/readme.md index c0c721c..d1d96e5 100644 --- a/readme.md +++ b/readme.md @@ -83,12 +83,13 @@ The mapper comes with a few configuration options that can be set using the [`Ma object and passed to the mappers' constructor. This is not required, if no configuration is passed, the default config is used. -| Option | Type | Default | Description | -|------------------------|----------|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `classMapperDirectory` | `string` | `/tmp/mappers` | This is the location the mapper will create cached mapper functions for objects.
The default location is a mappers function in the operating system temporary folder. | -| `debug` | `bool` | `false` | Enabling debug will clear all cached mapper functions after mapping has completed. | -| `enumTryFrom` | `bool` | `false` | Enabling this will use the `::tryFrom()` method instead of `::from()` to parse strings to enums. | -| `strictNullMapping` | `bool` | `true` | If enabled, the mapper will throw an error when a `null` value is passed for a property that was not typed as nullable. | +| Option | Type | Default | Description | +|----------------------------|----------|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `allowUninitializedFields` | `bool` | `true` | If disabled, the mapper will fail if one of the class properties that does not have a default value was not present in the data array. | +| `classMapperDirectory` | `string` | `/tmp/mappers` | This is the location the mapper will create cached mapper functions for objects.
The default location is a mappers function in the operating system temporary folder. | +| `debug` | `bool` | `false` | Enabling debug will clear all cached mapper functions after mapping has completed. | +| `enumTryFrom` | `bool` | `false` | Enabling this will use the `::tryFrom()` method instead of `::from()` to parse strings to enums. | +| `strictNullMapping` | `bool` | `true` | If enabled, the mapper will throw an error when a `null` value is passed for a property that was not typed as nullable. | ## Under the hood For simple native types, the mapper will use casting to convert the data to the correct type. diff --git a/src/MapperConfig.php b/src/MapperConfig.php index fc36a08..b130ec6 100644 --- a/src/MapperConfig.php +++ b/src/MapperConfig.php @@ -4,35 +4,34 @@ class MapperConfig { + /** + * If disabled, all properties of an object must either be present in the data array or have a default value for + * mapping to succeed. + * If enabled, properties that are not present in the data array will be left uninitialized. + */ + public bool $allowUninitializedFields = true; + /** * This is the directly where generated mappers will be stored. * It is recommended to prune this directory on every deploy to prevent old mappers from being used. * The prefix `{$TMP}` is replaced with the system's temporary directory. - * - * @var string */ public string $classMapperDirectory = '{$TMP}' . \DIRECTORY_SEPARATOR . 'mappers'; /** * In debug mode, the generated mapper files are deleted as soon as mapping is done. * This let you edit mapped classes without having to worry about the mapper cache. - * - * @var bool */ public bool $debug = false; /** * If true, enums will be mapped using the `tryFrom` method instead of the `from` method. * This might result in null values being mapped to non-nullable fields. - * - * @var bool */ public bool $enumTryFrom = false; /** * If true, mapping a null value to a non-nullable field will throw an UnexpectedNullValueException. - * - * @var bool */ public bool $strictNullMapping = true; } diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index 51e7839..c6264af 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -109,7 +109,13 @@ private function createObjectMappingFunction(ClassBluePrint $blueprint, string $ $propertyMap = $this->wrapDefault($propertyMap, $name, $property['default']); } - $content .= \PHP_EOL . $tab . $tab . '$x->' . $name . ' = ' . $propertyMap . ';'; + $propertySet = \PHP_EOL . $tab . $tab . '$x->' . $name . ' = ' . $propertyMap . ';'; + + if ($this->mapper->config->allowUninitializedFields && ! \array_key_exists('default', $property)) { + $propertySet = $this->wrapArrayKeyExists($propertySet, $name); + } + + $content .= $propertySet; } // Post mapping functions? @@ -184,6 +190,15 @@ private function wrapDefault(string $value, string $arrayKey, mixed $defaultValu return "(\\array_key_exists('{$arrayKey}', \$data) ? {$value} : " . \var_export($defaultValue, true) . ')'; } + private function wrapArrayKeyExists(string $expression, string $arrayKey): string + { + $content = \PHP_EOL . \str_repeat(' ', 2) . "if (\\array_key_exists('{$arrayKey}', \$data)) {"; + $content .= \str_replace(\PHP_EOL, \PHP_EOL . ' ', $expression) . \PHP_EOL; + $content .= \str_repeat(' ', 2) . '}'; + + return $content; + } + public function __destruct() { if ($this->mapper->config->debug) { @@ -209,6 +224,10 @@ private function buildPropertyForeachMapping(string $propertyName, array $proper $foreach .= \PHP_EOL . \str_repeat(' ', 2) . '}'; } + if ($this->mapper->config->allowUninitializedFields && ! \array_key_exists('default', $property)) { + $foreach = $this->wrapArrayKeyExists($foreach, $propertyName); + } + return $foreach; } } diff --git a/tests/MapperTest.php b/tests/MapperTest.php index 4c29e11..35bd09f 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -150,5 +150,13 @@ public static function objectValuesDataProvider(): Generator ], $dto, ]; + + // Allow uninitialized properties + $dto = new Aliases(); + yield [ + Aliases::class, + [], + $dto, + ]; } } From 43d02c6ee2e9202888c205c83ab83c2c1d93a091 Mon Sep 17 00:00:00 2001 From: Jeroen Deviaene Date: Wed, 17 May 2023 13:14:53 +0200 Subject: [PATCH 24/25] [Next] Resolve classes in same namespace as caller (#14) --- src/Objects/ClassBluePrinter.php | 5 ++++- src/Objects/ClassResolver.php | 14 ++++++++++++++ tests/MapperTest.php | 17 +++++++++++++++++ tests/_Mocks/Constructor.php | 14 ++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/_Mocks/Constructor.php diff --git a/src/Objects/ClassBluePrinter.php b/src/Objects/ClassBluePrinter.php index c97488c..b1c03a4 100644 --- a/src/Objects/ClassBluePrinter.php +++ b/src/Objects/ClassBluePrinter.php @@ -53,7 +53,10 @@ private function printConstructor(ReflectionClass $reflection, ClassBluePrint $b $arg = [ 'name' => $param->getName(), - 'type' => $this->resolveType($this->dataTypeFactory->fromString($type), $reflection->getName()), + 'type' => $this->resolveType( + $this->dataTypeFactory->fromString($type), + $reflection->getName(), + ), ]; if ($param->isDefaultValueAvailable()) { $arg['default'] = $param->getDefaultValue(); diff --git a/src/Objects/ClassResolver.php b/src/Objects/ClassResolver.php index 338e0d0..3f9d3bb 100644 --- a/src/Objects/ClassResolver.php +++ b/src/Objects/ClassResolver.php @@ -71,6 +71,20 @@ private function findClassNameInFile(string $name, string $sourceFile): string continue; } + // Find the class in the same namespace as the caller + if ($newline && $char === 'n' && \substr($file, $i, 10) === 'namespace ') { + $i += 10; + $namespace = ''; + while (($char = $file[$i++]) !== ';') { + $namespace .= $char; + } + + $classInNamespace = $namespace . '\\' . $lastPart; + if (\class_exists($classInNamespace)) { + return $classInNamespace; + } + } + // If we are after a newline and find a use statement, parse it! if ($newline && $char === 'u' && \substr($file, $i, 4) === 'use ') { $i += 4; diff --git a/tests/MapperTest.php b/tests/MapperTest.php index 35bd09f..6250a88 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -7,6 +7,7 @@ use Jerodev\DataMapper\Mapper; use Jerodev\DataMapper\MapperConfig; use Jerodev\DataMapper\Tests\_Mocks\Aliases; +use Jerodev\DataMapper\Tests\_Mocks\Constructor; use Jerodev\DataMapper\Tests\_Mocks\SelfMapped; use Jerodev\DataMapper\Tests\_Mocks\SuitEnum; use Jerodev\DataMapper\Tests\_Mocks\SuperUserDto; @@ -158,5 +159,21 @@ public static function objectValuesDataProvider(): Generator [], $dto, ]; + + // Array in constructor + $dto = new Constructor([ + new UserDto('Jerodev'), + ]); + yield [ + Constructor::class, + [ + 'users' => [ + [ + 'name' => 'Jerodev', + ], + ], + ], + $dto, + ]; } } diff --git a/tests/_Mocks/Constructor.php b/tests/_Mocks/Constructor.php new file mode 100644 index 0000000..16d0247 --- /dev/null +++ b/tests/_Mocks/Constructor.php @@ -0,0 +1,14 @@ + Date: Wed, 17 May 2023 13:33:37 +0200 Subject: [PATCH 25/25] [Next] Deduplicate constrctor arguments (#15) --- src/Objects/ClassBluePrint.php | 2 +- src/Objects/ClassBluePrinter.php | 8 ++++++-- src/Objects/ObjectMapper.php | 6 +++--- tests/Objects/ClassBluePrinterTest.php | 8 +------- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Objects/ClassBluePrint.php b/src/Objects/ClassBluePrint.php index b169a4c..e32e841 100644 --- a/src/Objects/ClassBluePrint.php +++ b/src/Objects/ClassBluePrint.php @@ -7,7 +7,7 @@ class ClassBluePrint { - /** @var array */ + /** @var array */ public array $constructorArguments = []; /** @var array */ diff --git a/src/Objects/ClassBluePrinter.php b/src/Objects/ClassBluePrinter.php index b1c03a4..35879bd 100644 --- a/src/Objects/ClassBluePrinter.php +++ b/src/Objects/ClassBluePrinter.php @@ -52,7 +52,6 @@ private function printConstructor(ReflectionClass $reflection, ClassBluePrint $b } $arg = [ - 'name' => $param->getName(), 'type' => $this->resolveType( $this->dataTypeFactory->fromString($type), $reflection->getName(), @@ -62,7 +61,7 @@ private function printConstructor(ReflectionClass $reflection, ClassBluePrint $b $arg['default'] = $param->getDefaultValue(); } - $bluePrint->constructorArguments[] = $arg; + $bluePrint->constructorArguments[$param->getName()] = $arg; } } @@ -74,6 +73,11 @@ private function printProperties(ReflectionClass $reflection, ClassBluePrint $bl continue; } + // Already mapped through constructor? + if (\array_key_exists($property->getName(), $blueprint->constructorArguments)) { + continue; + } + $type = $property->getType()?->getName(); if (\in_array($type, [null, 'array', 'iterable']) && $property->getDocComment()) { $doc = $this->docBlockParser->parse($property->getDocComment()); diff --git a/src/Objects/ObjectMapper.php b/src/Objects/ObjectMapper.php index c6264af..5373b47 100644 --- a/src/Objects/ObjectMapper.php +++ b/src/Objects/ObjectMapper.php @@ -81,13 +81,13 @@ private function createObjectMappingFunction(ClassBluePrint $blueprint, string $ // Instantiate a new object $args = []; - foreach ($blueprint->constructorArguments as $argument) { - $arg = "\$data['{$argument['name']}']"; + foreach ($blueprint->constructorArguments as $name => $argument) { + $arg = "\$data['{$name}']"; if ($argument['type'] !== null) { $arg = $this->castInMapperFunction($arg, $argument['type'], $blueprint); if (\array_key_exists('default', $argument)) { - $arg = $this->wrapDefault($arg, $argument['name'], $argument['default']); + $arg = $this->wrapDefault($arg, $name, $argument['default']); } } diff --git a/tests/Objects/ClassBluePrinterTest.php b/tests/Objects/ClassBluePrinterTest.php index 66a46d8..fe7a8dd 100644 --- a/tests/Objects/ClassBluePrinterTest.php +++ b/tests/Objects/ClassBluePrinterTest.php @@ -29,19 +29,13 @@ public static function classBlueprintDataProvider(): Generator yield [ UserDto::class, [ - [ - 'name' => 'name', + 'name' => [ 'type' => new DataTypeCollection([ new DataType('string', false), ]), ], ], [ - 'name' => [ - 'type' => new DataTypeCollection([ - new DataType('string', false), - ]), - ], 'friends' => [ 'type' => new DataTypeCollection([ new DataType(