Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea/
vendor/
.phpunit.result.cache
jmapper_*
.phpunit.result.cache
10 changes: 7 additions & 3 deletions src/Mapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}

/**
Expand All @@ -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
Expand Down
8 changes: 7 additions & 1 deletion src/Objects/ClassBluePrint.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

class ClassBluePrint
{
public bool $mapsItself = false;

/** @var array<array{name: string, type: DataTypeCollection, default?: mixed}> */
public array $constructorArguments = [];

Expand All @@ -16,5 +18,9 @@ class ClassBluePrint
/** @var array<Attribute> */
public array $classAttributes = [];

public bool $mapsItself = false;
public function __construct(
public readonly string $namespacedClassName,
public readonly string $fileName,
) {
}
}
3 changes: 1 addition & 2 deletions src/Objects/ClassBluePrinter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
7 changes: 4 additions & 3 deletions src/Objects/ClassResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/Objects/DocBlockParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 === '@') {
Expand Down
49 changes: 27 additions & 22 deletions src/Objects/ObjectMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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.
Expand All @@ -67,48 +67,50 @@ 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']);
}
}

$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);";
}
}
}
Expand All @@ -117,16 +119,19 @@ private function createObjectMappingFunction(ClassBluePrint $blueprint, string $
$mapperClass = Mapper::class;
return <<<PHP
<?php
function {$mapFunctionName}({$mapperClass} \$mapper, array \$data)
{
{$content}

return \$x;
if (! \\function_exists('{$mapFunctionName}')) {
function {$mapFunctionName}({$mapperClass} \$mapper, array \$data)
{
{$content}

return \$x;
}
}
PHP;
}

private function castInMapperFunction(string $propertyName, DataTypeCollection $type): string
private function castInMapperFunction(string $propertyName, DataTypeCollection $type, ClassBluePrint $bluePrint): string
{
if (\count($type->types) === 1) {
$type = $type->types[0];
Expand All @@ -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})";
}
}

Expand All @@ -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
Expand Down
15 changes: 0 additions & 15 deletions src/Types/DataType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
8 changes: 0 additions & 8 deletions src/Types/DataTypeCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,4 @@ public function isNullable(): bool
)
);
}

public function __toString(): string
{
return \implode(
'|',
\array_map(static fn (DataType $type) => $type->__toString(), $this->types),
);
}
}
53 changes: 53 additions & 0 deletions src/Types/DataTypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
namespace Jerodev\DataMapper\Types;

use Jerodev\DataMapper\Exceptions\UnexpectedTokenException;
use Jerodev\DataMapper\Objects\ClassResolver;

class DataTypeFactory
{
/** @var array<string, DataTypeCollection> */
private array $typeCache = [];

public function __construct(
public readonly ClassResolver $classResolver = new ClassResolver(),
) {
}

public function fromString(string $rawType, bool $forceNullable = false): DataTypeCollection
{
$rawType = \trim($rawType);
Expand All @@ -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<string>
Expand Down
26 changes: 25 additions & 1 deletion tests/MapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 */
Expand Down Expand Up @@ -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,
];
}
}
Loading