Skip to content

Fix Loading::depth Attribute that was not working at all

Pre-release
Pre-release

Choose a tag to compare

@kassko kassko released this 25 Jan 23:24
· 1 commit to 2.0 since this release
710b7fd

Summary: Fix Loading::depth Attribute

Date: 2026-01-25
Session Duration: ~2 hours
Branch: fix/code/loading_depth

Objective

Fix the bug where Loading::depth attribute value was always null because it was being read from the current property instead of the root property that triggered the loading.

Problem Analysis

The Loading::depth parameter allows limiting recursive hydration depth. However, it was never correctly applied because:

  1. In applyRecursiveHydration, the code read Loading::depth from the property currently being hydrated
  2. Nested properties (e.g., Team inside Department) don't have their own Loading attribute
  3. Therefore, $loading->depth was always null, meaning no depth limit was ever enforced

Expected Behavior

  • depth=0: Hydrate only the root object's scalar properties (no nested objects)
  • depth=1: Hydrate root + direct child objects
  • depth=2: Hydrate root + children + grandchildren
  • depth=null: No limit (default behavior)

Solution Implemented

1. Contextual Property Approach

Added a property $currentDepthLimit to the Loader class that stores the depth limit from the root property:

private ?int $currentDepthLimit = null;

In loadProperty() (centralized location for ALL loading variations):

  • Read Loading::depth from the property being loaded
  • Store in $this->currentDepthLimit
  • Delegate to new doLoadProperty() method
  • Restore after completion (using try/finally)

This ensures depth limit applies to:

  • loadSingleProperty() - SinglePropDataSource
  • loadMultipleProperties() - MultiPropDataSource
  • loadPropertyWithFallbacks() - DataSourceRef with fallbacks
  • loadPropertyWithCandidates() - DataSourceRef with candidates
  • loadPropertyWithProviders() - DataSourceRef with providers

2. HydrationResult::Skip Sentinel

Created a new enum to signal "don't call setter":

enum HydrationResult {
    case Skip;
}

When depth is exceeded, we can't pass raw array data to a typed setter expecting an object. Returning HydrationResult::Skip tells the caller to skip the setter entirely.

3. Moved Depth Check After Scalar Detection

Critical fix: The depth check was initially at the top of applyRecursiveHydration, which prevented scalar properties from being hydrated at depth 0. The check was moved AFTER the scalar detection logic:

// Scalars return early - always hydrated
if (!is_array($value)) {
    return $value;
}

// Now check depth - only affects object hydration
if ($this->currentDepthLimit !== null && $currentDepth > $this->currentDepthLimit) {
    return HydrationResult::Skip;
}

Files Modified/Created

New Files

  • src/Enum/HydrationResult.php - Sentinel enum for skipping hydration
  • docs/attributes/Loading.md - Complete attribute documentation
  • tests/Integration/Attributes/LoadingCheckDepth/ - 5 fixtures + test class (SinglePropDataSource)
  • tests/Integration/Attributes/LoadingCheckDepthMultiProp/ - 5 fixtures + test class (MultiPropDataSource)
  • tests/Integration/AttributeCollaboration/LoadingDepthAndPropertyExpand/ - 5 fixtures + test class

Modified Files

  • src/Loader/Loader.php - Core fix: moved depth logic to loadProperty, added doLoadProperty
  • README.md - Added Loading section with depth examples
  • EXAMPLE.md - Added depth control examples

Test Coverage

LoadingCheckDepth Tests (4 tests, SinglePropDataSource)

  1. testDepth0HydratesOnlyRootObject - Verifies scalars hydrated, nested objects skipped
  2. testDepth1HydratesRootAndDirectChildren - Verifies one level of nesting
  3. testDepth2HydratesThreeLevels - Verifies two levels of nesting
  4. testNoDepthLimitHydratesAllLevels - Verifies unlimited depth works

LoadingCheckDepthMultiProp Tests (2 tests, MultiPropDataSource)

  1. testMultiPropDepth0HydratesOnlyRootScalars - depth=0 with MultiPropDataSource
  2. testMultiPropDepth1HydratesRootAndChildren - depth=1 with MultiPropDataSource

LoadingDepthAndPropertyExpand Tests (3 tests)

  1. testDepth1WithExpandFiltersProperties - Depth + expand collaboration
  2. testDepth1WithNoExpandFiltersProperties - Depth + noExpand collaboration
  3. testDepth0TakesPrecedenceOverExpand - Proves depth evaluated before expand

Test Results

PHPUnit 11.5.46
OK (507 tests, 1400 assertions)

All 507 tests pass with no failures.

Key Insights

  1. Scalar vs Object Hydration: Depth limits should only apply to nested object creation, not scalar properties. A property like Department.name should always be hydrated regardless of depth.

  2. Depth Semantics: depth=0 means "hydrate only the root object's scalars", not "don't hydrate anything".

  3. Order of Evaluation: Depth is checked BEFORE expand/noExpand filters. This ensures depth takes precedence.

Collaboration with expand/noExpand

The implementation correctly handles the interaction:

  • Depth is evaluated first - if exceeded, the property is completely skipped
  • expand/noExpand filters which properties are hydrated within the allowed depth
  • Example: depth=0, expand='name,mainTeam' → mainTeam NOT hydrated because depth=0

No Breaking Changes

This is a bug fix. The depth parameter was previously accepted but never correctly applied. Existing code that didn't rely on depth behavior will continue to work unchanged.