Skip to content

Commit 5ffd7bc

Browse files
committed
cleanup
1 parent 4ae8bba commit 5ffd7bc

4 files changed

Lines changed: 178 additions & 23 deletions

File tree

src/Laravel/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,6 @@ private function buildNestedPropertyInfo(string $property, string $modelClass):
133133
$leafProperty = $this->nameConverter->normalize($parts[\count($parts) - 1]);
134134

135135
return [
136-
'original_path' => $property,
137-
'path_segments' => $parts,
138136
'relation_segments' => $relationSegments,
139137
'converted_relation_segments' => $relationSegments,
140138
'relation_classes' => $relationClasses,

src/Metadata/Parameter.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,29 @@
2121
abstract class Parameter
2222
{
2323
/**
24-
* @param array<string, mixed>|null $schema
25-
* @param array<string, mixed> $extraProperties
24+
* @param array<string, mixed>|null $schema
25+
* @param array<string, mixed> $extraProperties {
26+
* Special keys used internally:
27+
*
28+
* @var array{
29+
* relation_segments: list<string>,
30+
* converted_relation_segments: list<string>,
31+
* relation_classes: list<class-string>,
32+
* leaf_property: string,
33+
* leaf_class: class-string,
34+
* } $nested_property_info Metadata for nested (dot-separated) property paths.
35+
* - `relation_segments`: original relation names as declared in the entity (e.g. ['product', 'productVariations']),
36+
* used by Laravel Eloquent for whereHas() calls
37+
* - `converted_relation_segments`: name-converted relation segments (e.g. ['product', 'product_variations']),
38+
* used by Doctrine ORM for DQL joins
39+
* - `relation_classes`: the owning class of each relation segment
40+
* - `leaf_property`: the name-converted final property (e.g. 'variant_name')
41+
* - `leaf_class`: the class that owns the leaf property
42+
* @var array<string, array> $nested_properties_info Per-property map of nested_property_info entries,
43+
* keyed by property path. Used on parameters with plural `properties` (e.g. FreeTextQueryFilter)
44+
* so that sub-parameters created via withProperty() can look up their nested info.
45+
* }
46+
*
2647
* @param ParameterProviderInterface|callable|string|null $provider
2748
* @param list<string> $properties a list of properties this parameter applies to (works with the :property placeholder)
2849
* @param FilterInterface|string|null $filter

src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ private function getProperties(string $resourceClass, ?Parameter $parameter = nu
154154
$convertedRelationSegments[] = $convertedPart;
155155

156156
// Try to get the class name from property type
157-
$nextClass = $this->getRelatedClassFromProperty($propertyMetadata, $currentClass, $part);
157+
$nextClass = $this->getClassNameFromProperty($propertyMetadata);
158158

159159
if (!$nextClass) {
160160
$traversalSuccessful = false;
@@ -173,8 +173,6 @@ private function getProperties(string $resourceClass, ?Parameter $parameter = nu
173173
$properties[$property] = $propertyMetadata->withExtraProperties([
174174
...$propertyMetadata->getExtraProperties(),
175175
'nested_property_info' => [
176-
'original_path' => $property,
177-
'path_segments' => $parts,
178176
'relation_segments' => $relationSegments,
179177
'converted_relation_segments' => $convertedRelationSegments,
180178
'relation_classes' => $relationClasses,
@@ -462,20 +460,4 @@ private function getFilterInstance(object|string|null $filter): ?object
462460

463461
return $this->filterLocator->get($filter);
464462
}
465-
466-
/**
467-
* Gets the related class from a property during nested property traversal.
468-
*
469-
* This method can be overridden by framework-specific implementations to provide
470-
* custom logic for resolving relationship classes (e.g., Laravel Eloquent models).
471-
*
472-
* @param string $currentClass The current class being traversed
473-
* @param string $property The property/relationship name
474-
*
475-
* @return class-string|null The related class, or null if not found
476-
*/
477-
protected function getRelatedClassFromProperty(ApiProperty $propertyMetadata, string $currentClass, string $property): ?string
478-
{
479-
return $this->getClassNameFromProperty($propertyMetadata);
480-
}
481463
}

src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use PHPUnit\Framework\TestCase;
3131
use Psr\Container\ContainerInterface;
3232
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
33+
use Symfony\Component\TypeInfo\Type;
3334

3435
class ParameterResourceMetadataCollectionFactoryTest extends TestCase
3536
{
@@ -278,6 +279,110 @@ public function testNestedPropertyWithNameConverter(): void
278279
$this->assertSame('related.nested', $searchNestedParam->getProperty());
279280
$this->assertSame('search[related.nested]', $searchNestedParam->getKey());
280281
}
282+
283+
private function createNestedPropertyFactory(): ParameterResourceMetadataCollectionFactory
284+
{
285+
$nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class);
286+
$nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', 'name']));
287+
288+
$propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class);
289+
$propertyMetadata->method('create')->willReturnCallback(
290+
static function (string $class, string $property): ApiProperty {
291+
if (NestedTestOrder::class === $class && 'product' === $property) {
292+
return new ApiProperty(readable: true, nativeType: Type::object(NestedTestProduct::class));
293+
}
294+
if (NestedTestProduct::class === $class && 'productVariations' === $property) {
295+
return new ApiProperty(readable: true, nativeType: Type::list(Type::object(NestedTestVariation::class)));
296+
}
297+
298+
return new ApiProperty(readable: true);
299+
}
300+
);
301+
302+
$filterLocator = $this->createStub(ContainerInterface::class);
303+
$filterLocator->method('has')->willReturn(false);
304+
305+
return new ParameterResourceMetadataCollectionFactory(
306+
$nameCollection,
307+
$propertyMetadata,
308+
new AttributesResourceMetadataCollectionFactory(),
309+
$filterLocator,
310+
new CamelCaseToSnakeCaseNameConverter(),
311+
);
312+
}
313+
314+
public function testNestedPropertyInfoOnSingularProperty(): void
315+
{
316+
$factory = $this->createNestedPropertyFactory();
317+
$collection = $factory->create(NestedTestOrder::class);
318+
$operation = $collection->getOperation(forceCollection: true);
319+
$parameters = $operation->getParameters();
320+
321+
$param = $parameters->get('product.name');
322+
$this->assertNotNull($param, 'Parameter product.name should exist');
323+
324+
$extra = $param->getExtraProperties();
325+
$this->assertArrayHasKey('nested_property_info', $extra);
326+
327+
$info = $extra['nested_property_info'];
328+
$this->assertSame(['product'], $info['relation_segments']);
329+
$this->assertSame(['product'], $info['converted_relation_segments']);
330+
$this->assertSame('name', $info['leaf_property']);
331+
$this->assertSame(NestedTestProduct::class, $info['leaf_class']);
332+
$this->assertSame([NestedTestOrder::class], $info['relation_classes']);
333+
}
334+
335+
public function testNestedPropertyInfoOnDeeplyNestedProperty(): void
336+
{
337+
$factory = $this->createNestedPropertyFactory();
338+
$collection = $factory->create(NestedTestOrder::class);
339+
$operation = $collection->getOperation('deep_collection');
340+
$parameters = $operation->getParameters();
341+
342+
$param = $parameters->get('product.productVariations.variantName');
343+
$this->assertNotNull($param, 'Parameter product.productVariations.variantName should exist');
344+
345+
$extra = $param->getExtraProperties();
346+
$this->assertArrayHasKey('nested_property_info', $extra);
347+
348+
$info = $extra['nested_property_info'];
349+
$this->assertSame(['product', 'productVariations'], $info['relation_segments']);
350+
$this->assertSame(['product', 'product_variations'], $info['converted_relation_segments']);
351+
$this->assertSame('variant_name', $info['leaf_property']);
352+
$this->assertSame(NestedTestVariation::class, $info['leaf_class']);
353+
$this->assertSame([NestedTestOrder::class, NestedTestProduct::class], $info['relation_classes']);
354+
}
355+
356+
public function testNestedPropertyInfoOnExpandedPlaceholderParameter(): void
357+
{
358+
$factory = $this->createNestedPropertyFactory();
359+
$collection = $factory->create(NestedTestOrder::class);
360+
$operation = $collection->getOperation('search_collection');
361+
$parameters = $operation->getParameters();
362+
363+
$searchProductName = $parameters->get('search[product.name]');
364+
$this->assertNotNull($searchProductName, 'Parameter search[product.name] should exist');
365+
366+
$extra = $searchProductName->getExtraProperties();
367+
$this->assertArrayHasKey('nested_property_info', $extra);
368+
369+
$info = $extra['nested_property_info'];
370+
$this->assertSame(['product'], $info['relation_segments']);
371+
$this->assertSame('name', $info['leaf_property']);
372+
$this->assertSame(NestedTestProduct::class, $info['leaf_class']);
373+
}
374+
375+
public function testSimplePropertyHasNoNestedPropertyInfo(): void
376+
{
377+
$factory = $this->createNestedPropertyFactory();
378+
$collection = $factory->create(NestedTestOrder::class);
379+
$operation = $collection->getOperation(forceCollection: true);
380+
$parameters = $operation->getParameters();
381+
382+
$param = $parameters->get('name');
383+
$this->assertNotNull($param);
384+
$this->assertArrayNotHasKey('nested_property_info', $param->getExtraProperties());
385+
}
281386
}
282387

283388
#[ApiResource(
@@ -319,3 +424,52 @@ class HasNestedParameterAttribute
319424
public $name;
320425
public $related;
321426
}
427+
428+
#[ApiResource(
429+
operations: [
430+
new GetCollection(
431+
parameters: [
432+
'name' => new QueryParameter(),
433+
'product.name' => new QueryParameter(property: 'product.name'),
434+
]
435+
),
436+
new GetCollection(
437+
uriTemplate: '/nested_test_orders/deep',
438+
name: 'deep_collection',
439+
parameters: [
440+
'product.productVariations.variantName' => new QueryParameter(property: 'product.productVariations.variantName'),
441+
]
442+
),
443+
new GetCollection(
444+
uriTemplate: '/nested_test_orders/search',
445+
name: 'search_collection',
446+
parameters: [
447+
'search[:property]' => new QueryParameter(
448+
properties: ['name', 'product.name']
449+
),
450+
]
451+
),
452+
]
453+
)]
454+
class NestedTestOrder
455+
{
456+
public ?int $id = null;
457+
public ?string $name = null;
458+
public ?NestedTestProduct $product = null;
459+
}
460+
461+
#[ApiResource]
462+
class NestedTestProduct
463+
{
464+
public ?int $id = null;
465+
public ?string $name = null;
466+
/** @var NestedTestVariation[] */
467+
public array $productVariations = [];
468+
}
469+
470+
#[ApiResource]
471+
class NestedTestVariation
472+
{
473+
public ?int $id = null;
474+
public ?string $variantName = null;
475+
}

0 commit comments

Comments
 (0)