Skip to content
Closed
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
63 changes: 57 additions & 6 deletions src/data/src/Concerns/IncludeableData.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,73 @@ trait IncludeableData
{
use ForwardsToPartialsDefinition;

protected ?PartialsDefinition $partialDefinitions = null;
/**
* Null before defaults are inspected, false when they are empty, or the mutable definition store.
*/
protected PartialsDefinition|false|null $partialDefinitions = null;

/**
* Determine whether this object has partial definitions.
*
* @phpstan-impure
*/
public function hasPartialsDefinition(): bool
{
if ($this->partialDefinitions instanceof PartialsDefinition) {
return ! $this->partialDefinitions->isEmpty();
}

if ($this->partialDefinitions === false) {
return false;
}

$includes = $this->includeProperties();
$excludes = $this->excludeProperties();
$only = $this->onlyProperties();
$except = $this->exceptProperties();

if ($includes === [] && $excludes === [] && $only === [] && $except === []) {
$this->partialDefinitions = false;

return false;
}

$partialDefinitions = new PartialsDefinition;
$partialDefinitions->addDefaults('include', $includes);
$partialDefinitions->addDefaults('exclude', $excludes);
$partialDefinitions->addDefaults('only', $only);
$partialDefinitions->addDefaults('except', $except);

if ($partialDefinitions->isEmpty()) {
$this->partialDefinitions = false;

return false;
}

$this->partialDefinitions = $partialDefinitions;

return true;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* Get the current partial definitions.
*/
public function getPartialsDefinition(): PartialsDefinition
{
if ($this->partialDefinitions !== null) {
if ($this->partialDefinitions instanceof PartialsDefinition) {
return $this->partialDefinitions;
}

if ($this->partialDefinitions === null) {
// Initialize class-owned defaults before creating an empty store for explicit writes.
$this->hasPartialsDefinition();

if ($this->partialDefinitions instanceof PartialsDefinition) {
return $this->partialDefinitions;
}
}

$this->partialDefinitions = new PartialsDefinition;
$this->partialDefinitions->addDefaults('include', $this->includeProperties());
$this->partialDefinitions->addDefaults('exclude', $this->excludeProperties());
$this->partialDefinitions->addDefaults('only', $this->onlyProperties());
$this->partialDefinitions->addDefaults('except', $this->exceptProperties());

return $this->partialDefinitions;
}
Expand Down
5 changes: 5 additions & 0 deletions src/data/src/Contracts/IncludeableData.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ public function onlyWhen(string $only, bool|Closure $condition, bool $permanent
*/
public function exceptWhen(string $except, bool|Closure $condition, bool $permanent = false): object;

/**
* Determine whether this object has partial definitions.
*/
public function hasPartialsDefinition(): bool;
Comment thread
binaryfire marked this conversation as resolved.

/**
* Get the current partial definitions.
*/
Expand Down
7 changes: 5 additions & 2 deletions src/data/src/Support/Factories/DataClassFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ public function build(ReflectionClass $reflectionClass): DataClass
$redirectRoute = $attributes->first(RedirectToRoute::class)?->newInstance();
$lifecycleMethods = $this->resolveLifecycleMethods($reflectionClass);
$propertyMorphable = $reflectionClass->implementsInterface(PropertyMorphableData::class);
$transformationRecipe = $this->resolveTransformationRecipe($properties);
$transformable = $reflectionClass->implementsInterface(TransformableData::class);
$transformationRecipe = $transformable
? $this->resolveTransformationRecipe($properties)
: null;
$bulkCopyTransformation = $transformationRecipe !== null
&& $this->supportsBulkCopyTransformation($properties);

Expand All @@ -136,7 +139,7 @@ public function build(ReflectionClass $reflectionClass): DataClass
appendable: $reflectionClass->implementsInterface(AppendableData::class),
includeable: $reflectionClass->implementsInterface(IncludeableData::class),
responsable: $reflectionClass->implementsInterface(ResponsableData::class),
transformable: $reflectionClass->implementsInterface(TransformableData::class),
transformable: $transformable,
validateable: $reflectionClass->implementsInterface(ValidateableData::class),
wrappable: $reflectionClass->implementsInterface(WrappableData::class),
emptyData: $reflectionClass->implementsInterface(EmptyData::class),
Expand Down
50 changes: 35 additions & 15 deletions src/data/src/Support/Transformation/DataTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use Hypervel\Data\Support\DataConfig;
use Hypervel\Data\Support\DataProperty;
use Hypervel\Data\Support\Lazy\DefaultLazy;
use Hypervel\Data\Support\Partials\PartialDefinition;
use Hypervel\Data\Support\Types\Type;
use Hypervel\Data\Support\Wrapping\WrapExecutionType;
use Hypervel\Data\Transformers\Transformer;
Expand Down Expand Up @@ -78,7 +79,7 @@ public function __construct(
*/
public function defaultContext(object $data): TransformationContext
{
if (! $data instanceof IncludeableData || $data->getPartialsDefinition()->isEmpty()) {
if (! $data instanceof IncludeableData || ! $data->hasPartialsDefinition()) {
return $this->defaultContext;
}

Expand All @@ -92,7 +93,7 @@ public function defaultContext(object $data): TransformationContext
*/
public function allContext(object $data): TransformationContext
{
if (! $data instanceof IncludeableData || $data->getPartialsDefinition()->isEmpty()) {
if (! $data instanceof IncludeableData || ! $data->hasPartialsDefinition()) {
return $this->allContext;
}

Expand Down Expand Up @@ -191,7 +192,7 @@ protected function transformData(
}

// Raw storage keeps excluded property hooks from running as a side effect.
$values = get_mangled_object_vars($data);
$values = (array) $data;
$transformed = [];

foreach ($dataClass->properties as $property) {
Expand Down Expand Up @@ -268,6 +269,7 @@ protected function transformCollectable(
foreach ($rootItems ?? $this->collectableItems($data) as $key => $item) {
if (! $context->transformValues) {
if ($context->hasPartials() && $item instanceof IncludeableData) {
// Non-transforming root contexts compile their selections from these same definitions.
$item->getPartialsDefinition()->addResolved($context->partialDefinitions);
}

Expand Down Expand Up @@ -430,7 +432,7 @@ protected function transformUsingRecipe(
array &$extensions,
): array {
// Raw storage keeps uninitialized properties from invoking public access.
$values = get_mangled_object_vars($data);
$values = (array) $data;
$transformed = [];

foreach ($recipe->properties as $property) {
Expand Down Expand Up @@ -605,17 +607,14 @@ protected function mergeInstancePartials(
BaseData|BaseDataCollectable $value,
TransformationContext $context,
): TransformationContext {
if ($context->constructable || ! $value instanceof IncludeableData) {
return $context;
}

$partialDefinitions = $value->getPartialsDefinition();

if ($partialDefinitions->isEmpty()) {
if ($context->constructable
|| ! $value instanceof IncludeableData
|| ! $value->hasPartialsDefinition()
) {
return $context;
}

return $context->withMergedPartials($partialDefinitions->resolve(
return $context->withMergedPartials($value->getPartialsDefinition()->resolve(
$value,
consumeTemporary: true,
));
Expand Down Expand Up @@ -834,9 +833,13 @@ protected function propagatePartials(
return;
}

$value->getPartialsDefinition()->addResolved(
$context->partialsForNestedProperty($property),
);
$definitions = $context->partialsForNestedProperty($property);

if (! self::hasResolvedPartials($definitions)) {
return;
}

$value->getPartialsDefinition()->addResolved($definitions);
}

/**
Expand All @@ -853,13 +856,30 @@ protected function propagateIterablePartials(

$definitions = $context->partialsForNestedProperty($property);

if (! self::hasResolvedPartials($definitions)) {
return;
}

foreach ($items as $item) {
if ($item instanceof IncludeableData) {
$item->getPartialsDefinition()->addResolved($definitions);
}
}
}

/**
* Determine whether a resolved partial set contains any definitions.
*
* @param array{include: list<PartialDefinition>, exclude: list<PartialDefinition>, only: list<PartialDefinition>, except: list<PartialDefinition>} $definitions
*/
private static function hasResolvedPartials(array $definitions): bool
{
return $definitions['include'] !== []
|| $definitions['exclude'] !== []
|| $definitions['only'] !== []
|| $definitions['except'] !== [];
}

/**
* Apply only and except selections to a plain array value.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public function get(object $data): TransformationContext
return static::persistenceContext($this->configuredMaxDepth);
}

$dataPartials = $data instanceof IncludeableData
$dataPartials = $data instanceof IncludeableData && $data->hasPartialsDefinition()
? $data->getPartialsDefinition()
: null;

Expand Down
17 changes: 15 additions & 2 deletions src/docs/data-objects.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ Use `withValidator(Validator $validator)` and `after(): array` like a FormReques
<a name="creation-factories"></a>
### Creation Factories

The `factory` method returns a fluent factory for a single creation:
The `factory` method returns a fluent factory for creating data objects:

```php
$user = UserData::factory()
Expand All @@ -582,6 +582,19 @@ $user = UserData::factory()
->from($payload);
```

Within one operation, you may reuse a factory to avoid repeating creation setup for every payload:

```php
$factory = UserData::factory();

$users = array_map(
fn (array $payload): UserData => $factory->from($payload),
$payloads,
);
```

Use `collect()` instead when the payloads form one collection. In addition to preserving supported collection shapes and keys, `collect()` allows collection validation rules and hooks to inspect the complete payload.

Factories may change the validation strategy, enable or disable name mapping and named factories, ignore selected named methods, and add casts or normalizers. They also provide the following hooks, which run in this order:

1. `prepareData`
Expand All @@ -595,7 +608,7 @@ Factories may change the validation strategy, enable or disable name mapping and

The `prepareData`, `beforeCreation`, and `afterCreation` hooks run even when validation is skipped. The other hooks run while generating rules or validating, as appropriate. Call `alwaysValidate()` when validation hooks should also apply to an array, model, JSON value, or another non-request source.

Each call to `factory()` returns a new factory. Configure and use the factory where it is created instead of storing one and reusing it across requests.
Each call to `factory()` returns a new factory. Keep a reused factory scoped to the current operation instead of storing it across requests.

<a name="transformation"></a>
## Transformation
Expand Down
38 changes: 33 additions & 5 deletions tests/Benchmarks/Data/compare-data-object.php
Original file line number Diff line number Diff line change
Expand Up @@ -422,16 +422,21 @@ function measureCold(string $mode): array
/**
* Measure retained instance bytes over a large held set.
*
* @param Closure(int): object $factory
* @template TInstance of object
*
* @param Closure(int): TInstance $factory
* @param null|Closure(TInstance): void $prepare
*/
function retainedInstanceBytes(Closure $factory): float
function retainedInstanceBytes(Closure $factory, ?Closure $prepare = null): float
{
gc_collect_cycles();
$baseline = memory_get_usage(false);
$instances = [];

for ($index = 1; $index <= 20_000; ++$index) {
$instances[] = $factory($index);
$instance = $factory($index);
$prepare?->__invoke($instance);
$instances[] = $instance;
}

$bytes = (memory_get_usage(false) - $baseline) / count($instances);
Expand Down Expand Up @@ -666,8 +671,31 @@ function () use ($newObjects): int {
}

printf("\nRetained instance bytes\n");
printf("%-38s %12.1f %12.1f\n", 'flat', retainedInstanceBytes(fn (int $id): OldFlat => OldFlat::from([...$flat, 'id' => $id])), retainedInstanceBytes(fn (int $id): NewFlat => NewFlat::from([...$flat, 'id' => $id])));
printf("%-38s %12.1f %12.1f\n", 'wide', retainedInstanceBytes(fn (int $id): OldWide => OldWide::from([...$wide, 'one' => $id])), retainedInstanceBytes(fn (int $id): NewWide => NewWide::from([...$wide, 'one' => $id])));
printf("%-38s %12s %12s\n", 'scenario', 'old', 'data');
printf("%-38s %12.1f %12.1f\n", 'flat, untransformed', retainedInstanceBytes(fn (int $id): OldFlat => OldFlat::from([...$flat, 'id' => $id])), retainedInstanceBytes(fn (int $id): NewFlat => NewFlat::from([...$flat, 'id' => $id])));
printf("%-38s %12.1f %12.1f\n", 'flat, transformed', retainedInstanceBytes(
fn (int $id): OldFlat => OldFlat::from([...$flat, 'id' => $id]),
static function (OldFlat $data): void {
$data->toArray();
},
), retainedInstanceBytes(
fn (int $id): NewFlat => NewFlat::from([...$flat, 'id' => $id]),
static function (NewFlat $data): void {
$data->toArray();
},
));
printf("%-38s %12.1f %12.1f\n", 'wide, untransformed', retainedInstanceBytes(fn (int $id): OldWide => OldWide::from([...$wide, 'one' => $id])), retainedInstanceBytes(fn (int $id): NewWide => NewWide::from([...$wide, 'one' => $id])));
printf("%-38s %12.1f %12.1f\n", 'wide, transformed', retainedInstanceBytes(
fn (int $id): OldWide => OldWide::from([...$wide, 'one' => $id]),
static function (OldWide $data): void {
$data->toArray();
},
), retainedInstanceBytes(
fn (int $id): NewWide => NewWide::from([...$wide, 'one' => $id]),
static function (NewWide $data): void {
$data->toArray();
},
));

$repository = $application->make(DataClassRepository::class);
$repository->get(NewWarm::class);
Expand Down
Loading