Skip to content

Lazy objects implementation #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 1, 2025
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
80 changes: 80 additions & 0 deletions docs/LazyData.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
Lazy Data
=

Out of the box, extending `Data` will give you the ability to create [Lazy objects](https://www.php.net/manual/en/language.oop5.lazy-objects.php).
You can achieve that by calling either `createLazy` or `createLazyUsing` methods, depending
on whether you want to pass properties from the get-go or not.

Let's take for example `UserData` class:

```php
use Nuxtifyts\PhpDto\Data;

final readonly class UserData extends Data
{
public function __construct(
public int $id,
public string $firstName,
public string $lastName
) {}
}
```

We can create a lazy object like this:

```php
$user = UserData::createLazy(
id: 1,
firstName: 'John',
lastName: 'Doe'
);
```

Or, if we have more complex logic to run before creating the object, we can do:

```php
// Supposedly, we know the user id.
$userId = 1;

$user = UserData::createLazyUsing(
static function () use($userId): UserData {
// Fetch user data from the database. then create the DTO.
return UserData::from(UserModel::find($userId));
}
)
```

The `createLazyUsing` method accepts a closure that returns the object.
This closure will be called only once, and the object will be cached for future calls.

> For more information about lazy objects. Please refer to the [PHP documentation](https://www.php.net/manual/en/language.oop5.lazy-objects.php).

Lazy Data Attribute
-

Sometimes we may want to enable lazy data for a specific `Data` class.
In order to do that, we can user the `Lazy` attribute.

```php
use Nuxtifyts\PhpDto\Data;
use Nuxtifyts\PhpDto\Attributes\Class\Lazy;

#[Lazy]
final readonly class UserData extends Data
{
public function __construct(
public int $id,
public string $firstName,
public string $lastName
) {}
}
```

This will enable lazy data for all the "essential" functions that `Data` provides:
[create](https://github.com/nuxtifyts/php-dto/blob/main/docs/DefaultValues.md),
[from](https://github.com/nuxtifyts/php-dto/blob/main/docs/Quickstart.md),
[empty](https://github.com/nuxtifyts/php-dto/blob/main/docs/EmptyData.md)
and [clone](https://github.com/nuxtifyts/php-dto/blob/main/docs/CloneableData.md).



1 change: 1 addition & 0 deletions docs/Quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,5 @@ can be found here:
- [Data Refiners](https://github.com/nuxtifyts/php-dto/blob/main/docs/DataRefiners.md)
- [Empty Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/EmptyData.md)
- [Cloneable Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/CloneableData.md)
- [Lazy Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/LazyData.md)
- [Data Configuration](https://github.com/nuxtifyts/php-dto/blob/main/docs/DataConfiguration.md)
10 changes: 10 additions & 0 deletions src/Attributes/Class/Lazy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Nuxtifyts\PhpDto\Attributes\Class;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
class Lazy
{
}
72 changes: 30 additions & 42 deletions src/Concerns/BaseData.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,20 @@ final public static function create(mixed ...$args): static
throw DataCreationException::invalidParamsPassed(static::class);
}

$data = DeserializePipeline::createFromArray()
->sendThenReturn(new DeserializePipelinePassable(
classContext: $context,
data: $value
))
->data;

return static::instanceWithConstructorCallFrom($context, $data);
$dataCreationClosure = static function () use ($context, $value): static {
$data = DeserializePipeline::createFromArray()
->sendThenReturn(new DeserializePipelinePassable(
classContext: $context,
data: $value
))
->data;

return $context->constructFromArray($data);
};

return $context->isLazy
? $context->newLazyProxy($dataCreationClosure)
: $dataCreationClosure();
} catch (Throwable $e) {
throw DataCreationException::unableToCreateInstance(static::class, $e);
}
Expand All @@ -59,16 +65,22 @@ final public static function from(mixed $value): static
throw DeserializeException::invalidValue();
}

$data = DeserializePipeline::hydrateFromArray()
->sendThenReturn(new DeserializePipelinePassable(
classContext: $context,
data: $value
))
->data;

return $context->hasComputedProperties
? static::instanceWithConstructorCallFrom($context, $data)
: static::instanceWithoutConstructorFrom($context, $data);
$dataCreationClosure = static function () use ($context, $value): static {
$data = DeserializePipeline::hydrateFromArray()
->sendThenReturn(new DeserializePipelinePassable(
classContext: $context,
data: $value
))
->data;

return $context->hasComputedProperties
? $context->constructFromArray($data)
: static::instanceWithoutConstructorFrom($context, $data);
};

return $context->isLazy
? $context->newLazyProxy($dataCreationClosure)
: $dataCreationClosure();
} catch (Throwable $e) {
throw DeserializeException::generic($e);
}
Expand All @@ -93,30 +105,6 @@ protected static function instanceWithoutConstructorFrom(ClassContext $context,
return $instance;
}

/**
* @param ClassContext<static> $context
* @param array<string, mixed> $value
*
* @throws Throwable
*/
protected static function instanceWithConstructorCallFrom(ClassContext $context, array $value): static
{
/** @var array<string, mixed> $args */
$args = [];

foreach ($context->constructorParams as $paramName) {
$propertyContext = $context->properties[$paramName] ?? null;

if (!$propertyContext) {
throw DeserializeException::invalidParamsPassed();
}

$args[$paramName] = $propertyContext->deserializeFrom($value);
}

return $context->newInstanceWithConstructorCall(...$args);
}

/**
* @return array<string, mixed>
*
Expand Down
6 changes: 5 additions & 1 deletion src/Concerns/CloneableData.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@ public function with(mixed ...$args): static
throw DataCreationException::invalidParamsPassed(static::class);
}

return $context->hasComputedProperties
$cloneDataClosure = fn (): static => $context->hasComputedProperties
? $this->cloneInstanceWithConstructorCall($context, $value)
: $this->cloneInstanceWithoutConstructorCall($context, $value);

return $context->isLazy
? $context->newLazyProxy($cloneDataClosure)
: $cloneDataClosure();
} catch (Throwable $t) {
throw DataCreationException::unableToCloneInstanceWithNewData(static::class, $t);
}
Expand Down
67 changes: 67 additions & 0 deletions src/Concerns/LazyData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace Nuxtifyts\PhpDto\Concerns;

use Nuxtifyts\PhpDto\Contexts\ClassContext;
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
use Nuxtifyts\PhpDto\Normalizers\Concerns\HasNormalizers;
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipeline;
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable;
use Throwable;

trait LazyData
{
use HasNormalizers;

/**
* @throws DataCreationException
*/
public static function createLazy(mixed ...$args): static
{
try {
/** @var ClassContext<static> $context */
$context = ClassContext::getInstance(static::class);

$value = static::normalizeValue($args, static::class, $context->normalizers)
?: static::normalizeValue($args[0] ?? [], static::class, $context->normalizers);

if ($value === false) {
throw DataCreationException::invalidParamsPassed(static::class);
}

return $context->newLazyProxy(
static function () use($context, $value): static {
$data = DeserializePipeline::createFromArray()
->sendThenReturn(new DeserializePipelinePassable(
classContext: $context,
data: $value
))
->data;

return $context->constructFromArray($data);
}
);
} catch (Throwable $e) {
throw DataCreationException::unableToCreateLazyInstance(static::class, $e);
}
}

/**
* @param callable(static $data): static $callable
*
* @throws DataCreationException
*/
public static function createLazyUsing(callable $callable): static
{
try {
/** @var ClassContext<static> $context */
$context = ClassContext::getInstance(static::class);

return $context->newLazyProxy($callable);
// @codeCoverageIgnoreStart
} catch (Throwable $e) {
throw DataCreationException::unableToCreateLazyInstance(static::class, $e);
}
// @codeCoverageIgnoreEnd
}
}
62 changes: 56 additions & 6 deletions src/Contexts/ClassContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Nuxtifyts\PhpDto\Contexts;

use Exception;
use Nuxtifyts\PhpDto\Attributes\Class\Lazy;
use Nuxtifyts\PhpDto\Attributes\Class\MapName;
use Nuxtifyts\PhpDto\Attributes\Class\WithNormalizer;
use Nuxtifyts\PhpDto\Contexts\ClassContext\NameMapperConfig;
Expand Down Expand Up @@ -40,6 +42,8 @@ class ClassContext

private(set) ?NameMapperConfig $nameMapperConfig = null;

private(set) bool $isLazy = false;

/**
* @param ReflectionClass<T> $reflection
*
Expand Down Expand Up @@ -134,6 +138,8 @@ private function syncClassAttributes(): void
to: $instance->to
);
}

$this->isLazy = !empty($this->reflection->getAttributes(Lazy::class));
}

/**
Expand All @@ -157,13 +163,15 @@ public function newInstanceWithConstructorCall(mixed ...$args): mixed
}

/**
* @desc Creates an instance from an array of values using the constructor
*
* @param array<string, mixed> $value
*
* @return T
*
* @throws ReflectionException
* @throws UnsupportedTypeException
* @throws DataCreationException
* @throws Exception
*/
public function emptyValue(): mixed
public function constructFromArray(array $value): mixed
{
/** @var array<string, mixed> $args */
$args = [];
Expand All @@ -172,12 +180,54 @@ public function emptyValue(): mixed
$propertyContext = $this->properties[$paramName] ?? null;

if (!$propertyContext) {
throw DataCreationException::invalidProperty();
throw new Exception('invalid_params_passed');
}

$args[$paramName] = $propertyContext->emptyValue();
$args[$paramName] = $propertyContext->deserializeFrom($value);
}

return $this->newInstanceWithConstructorCall(...$args);
}

/**
* @param callable(T $object): T $lazyProxyCallable
*
* @return T
*/
public function newLazyProxy(callable $lazyProxyCallable): mixed
{
/** @phpstan-ignore-next-line */
return $this->reflection->newLazyProxy($lazyProxyCallable);
}

/**
* @return T
*
* @throws ReflectionException
* @throws UnsupportedTypeException
* @throws DataCreationException
*/
public function emptyValue(): mixed
{
$emptyValueCreationClosure = function () {
/** @var array<string, mixed> $args */
$args = [];

foreach ($this->constructorParams as $paramName) {
$propertyContext = $this->properties[$paramName] ?? null;

if (!$propertyContext) {
throw DataCreationException::invalidProperty();
}

$args[$paramName] = $propertyContext->emptyValue();
}

return $this->newInstanceWithConstructorCall(...$args);
};

return $this->isLazy
? $this->newLazyProxy($emptyValueCreationClosure)
: $emptyValueCreationClosure();
}
}
21 changes: 21 additions & 0 deletions src/Contracts/LazyData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Nuxtifyts\PhpDto\Contracts;

use Nuxtifyts\PhpDto\Data;
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;

interface LazyData
{
/**
* @throws DataCreationException
*/
public static function createLazy(mixed ...$args): static;

/**
* @param callable(static $data): static $callable
*
* @throws DataCreationException
*/
public static function createLazyUsing(callable $callable): static;
}
Loading
Loading