Skip to content
Open
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
158 changes: 158 additions & 0 deletions src/Resolvers/DataValidationRulesResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@

class DataValidationRulesResolver
{
/** @var array<string, array|false> Cache of static validation rules. False means not cacheable. */
protected array $staticRulesCache = [];

public function __construct(
protected DataConfig $dataConfig,
protected RuleNormalizer $ruleAttributesResolver,
Expand Down Expand Up @@ -200,6 +203,102 @@ protected function resolveDataCollectionSpecificRules(
shouldBePresent: true
);

$collectionPayload = Arr::get($fullPayload, $propertyPath->get());

// If collection payload is null or not an array, no nested rules needed
if (! is_array($collectionPayload)) {
return;
}

$nestedDataClass = $this->dataConfig->getDataClass($dataProperty->type->dataClass);

// Optimization: If nested class has NO dynamic rules method, use direct approach
if (! $nestedDataClass->hasDynamicValidationRules) {
$this->resolveStaticCollectionRules(
$nestedDataClass,
$collectionPayload,
$fullPayload,
$propertyPath,
$dataRules
);

return;
}

$this->resolveDynamicCollectionRules(
$dataProperty,
$fullPayload,
$propertyPath,
$dataRules
);
}

protected function resolveStaticCollectionRules(
DataClass $nestedDataClass,
array $collectionPayload,
array $fullPayload,
ValidationPath $propertyPath,
DataRules $dataRules,
): void {
$cacheKey = $nestedDataClass->name;

// Check if we've already determined cacheability for this class
if (! array_key_exists($cacheKey, $this->staticRulesCache)) {
if ($this->canSafelyCacheRules($nestedDataClass)) {
// Generate static rules once for caching
$staticRules = $this->generateStaticRules($nestedDataClass);
$this->staticRulesCache[$cacheKey] = $staticRules;
} else {
// Mark as not cacheable
$this->staticRulesCache[$cacheKey] = false;
}
}

$cachedRules = $this->staticRulesCache[$cacheKey];

if ($cachedRules !== false) {
// Use cached static rules
foreach ($collectionPayload as $collectionItemKey => $collectionItemValue) {
$itemPath = $propertyPath->property($collectionItemKey);

if (! is_array($collectionItemValue)) {
$dataRules->add($itemPath, ['array']);
continue;
}

// Apply cached rules with proper path adjustment
foreach ($cachedRules as $ruleKey => $ruleValue) {
$adjustedPath = $itemPath->property($ruleKey);
$dataRules->add($adjustedPath, $ruleValue);
}
}
} else {
// Fallback to full context-aware rule generation
foreach ($collectionPayload as $collectionItemKey => $collectionItemValue) {
$itemPath = $propertyPath->property($collectionItemKey);

if (! is_array($collectionItemValue)) {
$dataRules->add($itemPath, ['array']);
continue;
}

$this->execute(
$nestedDataClass->name,
$fullPayload,
$itemPath,
$dataRules
);
}
}
}

protected function resolveDynamicCollectionRules(
DataProperty $dataProperty,
array $fullPayload,
ValidationPath $propertyPath,
DataRules $dataRules,
): void {
// Use Rule::forEach for dynamic rules (classes with rules() method)
$dataRules->addCollection($propertyPath, Rule::forEach(function (mixed $value, mixed $attribute) use ($fullPayload, $dataProperty) {
if (! is_array($value)) {
return ['array'];
Expand Down Expand Up @@ -302,4 +401,63 @@ protected function inferRulesForDataProperty(
$path
);
}

protected function canSafelyCacheRules(DataClass $dataClass): bool
{
// Very conservative approach - only cache if ALL properties are safe
foreach ($dataClass->properties as $property) {
// Check for common validation attributes that might cause context dependencies
$potentiallyUnsafeAttributes = [
'Spatie\LaravelData\Attributes\Validation\RequiredIf',
'Spatie\LaravelData\Attributes\Validation\RequiredUnless',
'Spatie\LaravelData\Attributes\Validation\RequiredWith',
'Spatie\LaravelData\Attributes\Validation\Same',
'Spatie\LaravelData\Attributes\Validation\Different',
'Spatie\LaravelData\Attributes\Validation\ConfirmedIf',
'Spatie\LaravelData\Attributes\Validation\AcceptedIf',
'Spatie\LaravelData\Attributes\Validation\DeclinedIf',
'Spatie\LaravelData\Attributes\Validation\ProhibitedIf',
'Spatie\LaravelData\Attributes\Validation\ProhibitedUnless',
'Spatie\LaravelData\Attributes\Validation\ExcludeIf',
'Spatie\LaravelData\Attributes\Validation\ExcludeUnless',
];

// If any potentially unsafe validation attributes exist, don't cache
foreach ($potentiallyUnsafeAttributes as $attributeClass) {
if ($property->attributes->has($attributeClass)) {
return false;
}
}

// Don't cache if property is morphable - complex logic
if ($property->morphable) {
return false;
}

// Don't cache nested data objects/collections - they may have their own complex rules
if ($property->type->kind->isDataObject() || $property->type->kind->isDataCollectable()) {
return false;
}
}

// Don't cache abstract or morphable classes
if ($dataClass->isAbstract || $dataClass->propertyMorphable) {
return false;
}

return true;
}

protected function generateStaticRules(DataClass $dataClass): array
{
// Generate rules using an empty payload and root path to get only structural rules
$rules = $this->execute(
$dataClass->name,
[], // Empty payload - only structural rules, no context dependencies
ValidationPath::create(),
DataRules::create()
);

return $rules;
}
}
1 change: 1 addition & 0 deletions src/Support/DataClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function __construct(
public readonly bool $validateable,
public readonly bool $wrappable,
public readonly bool $emptyData,
public readonly bool $hasDynamicValidationRules,
public readonly DataAttributesCollection $attributes,
public readonly array $dataIterablePropertyAnnotations,
public DataStructureProperty $allowedRequestIncludes,
Expand Down
1 change: 1 addition & 0 deletions src/Support/Factories/DataClassFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public function build(ReflectionClass $reflectionClass): DataClass
validateable: $reflectionClass->implementsInterface(ValidateableData::class),
wrappable: $reflectionClass->implementsInterface(WrappableData::class),
emptyData: $reflectionClass->implementsInterface(EmptyData::class),
hasDynamicValidationRules: method_exists($reflectionClass->name, 'rules'),
attributes: $attributes,
dataIterablePropertyAnnotations: $dataIterablePropertyAnnotations,
allowedRequestIncludes: new LazyDataStructureProperty(fn (): ?array => $responsable ? $name::allowedRequestIncludes() : null),
Expand Down
Loading