Skip to content

Commit aef8bf4

Browse files
authored
Merge pull request #17 from nuxtifyts/lazy-objects-implementation
Lazy objects implementation
2 parents c45cbbb + 271a221 commit aef8bf4

File tree

13 files changed

+454
-50
lines changed

13 files changed

+454
-50
lines changed

docs/LazyData.md

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
Lazy Data
2+
=
3+
4+
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).
5+
You can achieve that by calling either `createLazy` or `createLazyUsing` methods, depending
6+
on whether you want to pass properties from the get-go or not.
7+
8+
Let's take for example `UserData` class:
9+
10+
```php
11+
use Nuxtifyts\PhpDto\Data;
12+
13+
final readonly class UserData extends Data
14+
{
15+
public function __construct(
16+
public int $id,
17+
public string $firstName,
18+
public string $lastName
19+
) {}
20+
}
21+
```
22+
23+
We can create a lazy object like this:
24+
25+
```php
26+
$user = UserData::createLazy(
27+
id: 1,
28+
firstName: 'John',
29+
lastName: 'Doe'
30+
);
31+
```
32+
33+
Or, if we have more complex logic to run before creating the object, we can do:
34+
35+
```php
36+
// Supposedly, we know the user id.
37+
$userId = 1;
38+
39+
$user = UserData::createLazyUsing(
40+
static function () use($userId): UserData {
41+
// Fetch user data from the database. then create the DTO.
42+
return UserData::from(UserModel::find($userId));
43+
}
44+
)
45+
```
46+
47+
The `createLazyUsing` method accepts a closure that returns the object.
48+
This closure will be called only once, and the object will be cached for future calls.
49+
50+
> For more information about lazy objects. Please refer to the [PHP documentation](https://www.php.net/manual/en/language.oop5.lazy-objects.php).
51+
52+
Lazy Data Attribute
53+
-
54+
55+
Sometimes we may want to enable lazy data for a specific `Data` class.
56+
In order to do that, we can user the `Lazy` attribute.
57+
58+
```php
59+
use Nuxtifyts\PhpDto\Data;
60+
use Nuxtifyts\PhpDto\Attributes\Class\Lazy;
61+
62+
#[Lazy]
63+
final readonly class UserData extends Data
64+
{
65+
public function __construct(
66+
public int $id,
67+
public string $firstName,
68+
public string $lastName
69+
) {}
70+
}
71+
```
72+
73+
This will enable lazy data for all the "essential" functions that `Data` provides:
74+
[create](https://github.com/nuxtifyts/php-dto/blob/main/docs/DefaultValues.md),
75+
[from](https://github.com/nuxtifyts/php-dto/blob/main/docs/Quickstart.md),
76+
[empty](https://github.com/nuxtifyts/php-dto/blob/main/docs/EmptyData.md)
77+
and [clone](https://github.com/nuxtifyts/php-dto/blob/main/docs/CloneableData.md).
78+
79+
80+

docs/Quickstart.md

+1
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,5 @@ can be found here:
8282
- [Data Refiners](https://github.com/nuxtifyts/php-dto/blob/main/docs/DataRefiners.md)
8383
- [Empty Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/EmptyData.md)
8484
- [Cloneable Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/CloneableData.md)
85+
- [Lazy Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/LazyData.md)
8586
- [Data Configuration](https://github.com/nuxtifyts/php-dto/blob/main/docs/DataConfiguration.md)

src/Attributes/Class/Lazy.php

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Attributes\Class;
4+
5+
use Attribute;
6+
7+
#[Attribute(Attribute::TARGET_CLASS)]
8+
class Lazy
9+
{
10+
}

src/Concerns/BaseData.php

+30-42
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,20 @@ final public static function create(mixed ...$args): static
3131
throw DataCreationException::invalidParamsPassed(static::class);
3232
}
3333

34-
$data = DeserializePipeline::createFromArray()
35-
->sendThenReturn(new DeserializePipelinePassable(
36-
classContext: $context,
37-
data: $value
38-
))
39-
->data;
40-
41-
return static::instanceWithConstructorCallFrom($context, $data);
34+
$dataCreationClosure = static function () use ($context, $value): static {
35+
$data = DeserializePipeline::createFromArray()
36+
->sendThenReturn(new DeserializePipelinePassable(
37+
classContext: $context,
38+
data: $value
39+
))
40+
->data;
41+
42+
return $context->constructFromArray($data);
43+
};
44+
45+
return $context->isLazy
46+
? $context->newLazyProxy($dataCreationClosure)
47+
: $dataCreationClosure();
4248
} catch (Throwable $e) {
4349
throw DataCreationException::unableToCreateInstance(static::class, $e);
4450
}
@@ -59,16 +65,22 @@ final public static function from(mixed $value): static
5965
throw DeserializeException::invalidValue();
6066
}
6167

62-
$data = DeserializePipeline::hydrateFromArray()
63-
->sendThenReturn(new DeserializePipelinePassable(
64-
classContext: $context,
65-
data: $value
66-
))
67-
->data;
68-
69-
return $context->hasComputedProperties
70-
? static::instanceWithConstructorCallFrom($context, $data)
71-
: static::instanceWithoutConstructorFrom($context, $data);
68+
$dataCreationClosure = static function () use ($context, $value): static {
69+
$data = DeserializePipeline::hydrateFromArray()
70+
->sendThenReturn(new DeserializePipelinePassable(
71+
classContext: $context,
72+
data: $value
73+
))
74+
->data;
75+
76+
return $context->hasComputedProperties
77+
? $context->constructFromArray($data)
78+
: static::instanceWithoutConstructorFrom($context, $data);
79+
};
80+
81+
return $context->isLazy
82+
? $context->newLazyProxy($dataCreationClosure)
83+
: $dataCreationClosure();
7284
} catch (Throwable $e) {
7385
throw DeserializeException::generic($e);
7486
}
@@ -93,30 +105,6 @@ protected static function instanceWithoutConstructorFrom(ClassContext $context,
93105
return $instance;
94106
}
95107

96-
/**
97-
* @param ClassContext<static> $context
98-
* @param array<string, mixed> $value
99-
*
100-
* @throws Throwable
101-
*/
102-
protected static function instanceWithConstructorCallFrom(ClassContext $context, array $value): static
103-
{
104-
/** @var array<string, mixed> $args */
105-
$args = [];
106-
107-
foreach ($context->constructorParams as $paramName) {
108-
$propertyContext = $context->properties[$paramName] ?? null;
109-
110-
if (!$propertyContext) {
111-
throw DeserializeException::invalidParamsPassed();
112-
}
113-
114-
$args[$paramName] = $propertyContext->deserializeFrom($value);
115-
}
116-
117-
return $context->newInstanceWithConstructorCall(...$args);
118-
}
119-
120108
/**
121109
* @return array<string, mixed>
122110
*

src/Concerns/CloneableData.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@ public function with(mixed ...$args): static
3232
throw DataCreationException::invalidParamsPassed(static::class);
3333
}
3434

35-
return $context->hasComputedProperties
35+
$cloneDataClosure = fn (): static => $context->hasComputedProperties
3636
? $this->cloneInstanceWithConstructorCall($context, $value)
3737
: $this->cloneInstanceWithoutConstructorCall($context, $value);
38+
39+
return $context->isLazy
40+
? $context->newLazyProxy($cloneDataClosure)
41+
: $cloneDataClosure();
3842
} catch (Throwable $t) {
3943
throw DataCreationException::unableToCloneInstanceWithNewData(static::class, $t);
4044
}

src/Concerns/LazyData.php

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Concerns;
4+
5+
use Nuxtifyts\PhpDto\Contexts\ClassContext;
6+
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
7+
use Nuxtifyts\PhpDto\Normalizers\Concerns\HasNormalizers;
8+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipeline;
9+
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable;
10+
use Throwable;
11+
12+
trait LazyData
13+
{
14+
use HasNormalizers;
15+
16+
/**
17+
* @throws DataCreationException
18+
*/
19+
public static function createLazy(mixed ...$args): static
20+
{
21+
try {
22+
/** @var ClassContext<static> $context */
23+
$context = ClassContext::getInstance(static::class);
24+
25+
$value = static::normalizeValue($args, static::class, $context->normalizers)
26+
?: static::normalizeValue($args[0] ?? [], static::class, $context->normalizers);
27+
28+
if ($value === false) {
29+
throw DataCreationException::invalidParamsPassed(static::class);
30+
}
31+
32+
return $context->newLazyProxy(
33+
static function () use($context, $value): static {
34+
$data = DeserializePipeline::createFromArray()
35+
->sendThenReturn(new DeserializePipelinePassable(
36+
classContext: $context,
37+
data: $value
38+
))
39+
->data;
40+
41+
return $context->constructFromArray($data);
42+
}
43+
);
44+
} catch (Throwable $e) {
45+
throw DataCreationException::unableToCreateLazyInstance(static::class, $e);
46+
}
47+
}
48+
49+
/**
50+
* @param callable(static $data): static $callable
51+
*
52+
* @throws DataCreationException
53+
*/
54+
public static function createLazyUsing(callable $callable): static
55+
{
56+
try {
57+
/** @var ClassContext<static> $context */
58+
$context = ClassContext::getInstance(static::class);
59+
60+
return $context->newLazyProxy($callable);
61+
// @codeCoverageIgnoreStart
62+
} catch (Throwable $e) {
63+
throw DataCreationException::unableToCreateLazyInstance(static::class, $e);
64+
}
65+
// @codeCoverageIgnoreEnd
66+
}
67+
}

src/Contexts/ClassContext.php

+56-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Nuxtifyts\PhpDto\Contexts;
44

5+
use Exception;
6+
use Nuxtifyts\PhpDto\Attributes\Class\Lazy;
57
use Nuxtifyts\PhpDto\Attributes\Class\MapName;
68
use Nuxtifyts\PhpDto\Attributes\Class\WithNormalizer;
79
use Nuxtifyts\PhpDto\Contexts\ClassContext\NameMapperConfig;
@@ -40,6 +42,8 @@ class ClassContext
4042

4143
private(set) ?NameMapperConfig $nameMapperConfig = null;
4244

45+
private(set) bool $isLazy = false;
46+
4347
/**
4448
* @param ReflectionClass<T> $reflection
4549
*
@@ -134,6 +138,8 @@ private function syncClassAttributes(): void
134138
to: $instance->to
135139
);
136140
}
141+
142+
$this->isLazy = !empty($this->reflection->getAttributes(Lazy::class));
137143
}
138144

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

159165
/**
166+
* @desc Creates an instance from an array of values using the constructor
167+
*
168+
* @param array<string, mixed> $value
169+
*
160170
* @return T
161171
*
162-
* @throws ReflectionException
163-
* @throws UnsupportedTypeException
164-
* @throws DataCreationException
172+
* @throws Exception
165173
*/
166-
public function emptyValue(): mixed
174+
public function constructFromArray(array $value): mixed
167175
{
168176
/** @var array<string, mixed> $args */
169177
$args = [];
@@ -172,12 +180,54 @@ public function emptyValue(): mixed
172180
$propertyContext = $this->properties[$paramName] ?? null;
173181

174182
if (!$propertyContext) {
175-
throw DataCreationException::invalidProperty();
183+
throw new Exception('invalid_params_passed');
176184
}
177185

178-
$args[$paramName] = $propertyContext->emptyValue();
186+
$args[$paramName] = $propertyContext->deserializeFrom($value);
179187
}
180188

181189
return $this->newInstanceWithConstructorCall(...$args);
182190
}
191+
192+
/**
193+
* @param callable(T $object): T $lazyProxyCallable
194+
*
195+
* @return T
196+
*/
197+
public function newLazyProxy(callable $lazyProxyCallable): mixed
198+
{
199+
/** @phpstan-ignore-next-line */
200+
return $this->reflection->newLazyProxy($lazyProxyCallable);
201+
}
202+
203+
/**
204+
* @return T
205+
*
206+
* @throws ReflectionException
207+
* @throws UnsupportedTypeException
208+
* @throws DataCreationException
209+
*/
210+
public function emptyValue(): mixed
211+
{
212+
$emptyValueCreationClosure = function () {
213+
/** @var array<string, mixed> $args */
214+
$args = [];
215+
216+
foreach ($this->constructorParams as $paramName) {
217+
$propertyContext = $this->properties[$paramName] ?? null;
218+
219+
if (!$propertyContext) {
220+
throw DataCreationException::invalidProperty();
221+
}
222+
223+
$args[$paramName] = $propertyContext->emptyValue();
224+
}
225+
226+
return $this->newInstanceWithConstructorCall(...$args);
227+
};
228+
229+
return $this->isLazy
230+
? $this->newLazyProxy($emptyValueCreationClosure)
231+
: $emptyValueCreationClosure();
232+
}
183233
}

src/Contracts/LazyData.php

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Nuxtifyts\PhpDto\Contracts;
4+
5+
use Nuxtifyts\PhpDto\Data;
6+
use Nuxtifyts\PhpDto\Exceptions\DataCreationException;
7+
8+
interface LazyData
9+
{
10+
/**
11+
* @throws DataCreationException
12+
*/
13+
public static function createLazy(mixed ...$args): static;
14+
15+
/**
16+
* @param callable(static $data): static $callable
17+
*
18+
* @throws DataCreationException
19+
*/
20+
public static function createLazyUsing(callable $callable): static;
21+
}

0 commit comments

Comments
 (0)