Skip to content

Commit fbc8819

Browse files
author
Daniel Held
committed
feat: add isCollection support for DataResponse attribute
1 parent c02171a commit fbc8819

File tree

4 files changed

+107
-6
lines changed

4 files changed

+107
-6
lines changed

src/Attributes/DataResponse.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,11 @@
99
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
1010
class DataResponse
1111
{
12-
public function __construct(public int $status, public string $description = '', public null|string|array $resource = [], public array $headers = []) {}
12+
public function __construct(
13+
public int $status,
14+
public string $description = '',
15+
public null|string|array $resource = [],
16+
public array $headers = [],
17+
public bool $isCollection = false,
18+
) {}
1319
}

src/Services/EnhancedResponseAnalyzer.php

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,15 @@ public function analyzeResourceNew(New_ $node, string $controller, string $metho
564564

565565
$resourceClass = $node->class->toString();
566566

567+
if (!class_exists($resourceClass)) {
568+
$resolvedClass = $this->resolveClassNameFromController($resourceClass, $controller);
569+
if ($resolvedClass && class_exists($resolvedClass)) {
570+
$resourceClass = $resolvedClass;
571+
} else {
572+
return;
573+
}
574+
}
575+
567576
// Use existing ResponseAnalyzer for resource analysis
568577
$analysis = $this->responseAnalyzer->analyzeJsonResourceResponse($resourceClass);
569578

@@ -579,6 +588,50 @@ public function analyzeResourceNew(New_ $node, string $controller, string $metho
579588
}
580589
}
581590

591+
private function resolveClassNameFromController(string $shortClassName, string $controllerClass): ?string
592+
{
593+
if (!class_exists($controllerClass)) {
594+
return null;
595+
}
596+
597+
try {
598+
$reflection = new \ReflectionClass($controllerClass);
599+
$filename = $reflection->getFileName();
600+
601+
if (!$filename || !file_exists($filename)) {
602+
return null;
603+
}
604+
605+
$content = file_get_contents($filename);
606+
$namespace = $reflection->getNamespaceName();
607+
608+
if (preg_match('/^use\s+([^\s;]+\\\\' . preg_quote($shortClassName, '/') . ')\s*;/m', $content, $matches)) {
609+
return $matches[1];
610+
}
611+
612+
$sameNamespaceClass = $namespace . '\\' . $shortClassName;
613+
if (class_exists($sameNamespaceClass)) {
614+
return $sameNamespaceClass;
615+
}
616+
617+
$resourceNamespaces = [
618+
str_replace('\\Controllers\\', '\\Resources\\', $namespace),
619+
str_replace('\\Http\\Controllers\\', '\\Http\\Resources\\', $namespace),
620+
];
621+
622+
foreach ($resourceNamespaces as $resourceNamespace) {
623+
$potentialClass = $resourceNamespace . '\\' . $shortClassName;
624+
if (class_exists($potentialClass)) {
625+
return $potentialClass;
626+
}
627+
}
628+
629+
return null;
630+
} catch (\Throwable $e) {
631+
return null;
632+
}
633+
}
634+
582635
/**
583636
* Detect resource wrapping configuration with inheritance support
584637
*/
@@ -735,6 +788,7 @@ private function processDataResponseAttributes(ReflectionMethod $method): void
735788
$description = $instance->description ?: $this->getStatusDescription($statusCode);
736789
$resource = $instance->resource;
737790
$headers = $instance->headers;
791+
$isCollection = $instance->isCollection;
738792

739793
// Create response schema based on resource if specified
740794
$schema = null;
@@ -752,7 +806,8 @@ private function processDataResponseAttributes(ReflectionMethod $method): void
752806
'application/json',
753807
$schema,
754808
$headers,
755-
is_string($resource) ? $resource : null
809+
is_string($resource) ? $resource : null,
810+
$isCollection
756811
);
757812
} else {
758813
$this->addResponse($statusCode, $description, 'application/json', $schema, $headers);
@@ -1426,22 +1481,45 @@ private function addResponse(string $statusCode, string $description, ?string $c
14261481
/**
14271482
* Add success response with detailed schema analysis
14281483
*/
1429-
private function addSuccessResponse(string $statusCode, string $description, string $controller, string $method, ?string $contentType = 'application/json', ?array $additionalSchema = null, array $headers = [], ?string $resourceClass = null): void
1484+
private function addSuccessResponse(string $statusCode, string $description, string $controller, string $method, ?string $contentType = 'application/json', ?array $additionalSchema = null, array $headers = [], ?string $resourceClass = null, bool $isCollection = false): void
14301485
{
14311486
if (! isset($this->detectedResponses[$statusCode])) {
14321487
// Use existing ResponseAnalyzer for detailed schema analysis
14331488
$detailedSchema = $this->responseAnalyzer->analyzeControllerMethod($controller, $method);
14341489

1435-
// Merge with any additional schema information
1436-
if ($additionalSchema) {
1490+
// Prefer additionalSchema (from resource class) if provided and has properties
1491+
if ($additionalSchema && !empty($additionalSchema['properties'])) {
1492+
$detailedSchema = $additionalSchema;
1493+
} elseif ($additionalSchema) {
14371494
$detailedSchema = array_merge($detailedSchema ?: [], $additionalSchema);
14381495
}
14391496

14401497
// Apply Parameter attributes to enhance the schema
14411498
$detailedSchema = $this->applyParameterAttributesToSchema($detailedSchema, $controller, $method);
14421499

1500+
// Wrap schema in array if isCollection is true
1501+
if ($isCollection) {
1502+
// Ensure we have at least a basic schema for collections
1503+
if (empty($detailedSchema)) {
1504+
$detailedSchema = [
1505+
'type' => 'object',
1506+
'properties' => [],
1507+
];
1508+
}
1509+
1510+
$detailedSchema = [
1511+
'type' => 'array',
1512+
'items' => $detailedSchema,
1513+
];
1514+
}
1515+
14431516
// Generate MediaType-level example from schema property examples
1444-
$responseExample = $this->generateResponseExampleFromSchema($detailedSchema);
1517+
$responseExample = $this->generateResponseExampleFromSchema($isCollection ? ($detailedSchema['items'] ?? null) : $detailedSchema);
1518+
1519+
// Wrap example in array if isCollection is true
1520+
if ($isCollection && $responseExample) {
1521+
$responseExample = [$responseExample];
1522+
}
14451523

14461524
$response = [
14471525
'description' => $description,

src/Services/ResponseAnalyzer.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,19 @@ public function analyzeJsonResourceResponse(string $resourceClass): array
896896
'enhanced_analysis' => true,
897897
];
898898
}
899+
900+
// Fallback for classes that are neither ResourceCollection nor JsonResource
901+
return [
902+
'type' => 'object',
903+
'properties' => [
904+
'data' => [
905+
'type' => 'object',
906+
'description' => 'Resource data',
907+
],
908+
],
909+
'example' => ['data' => []],
910+
'enhanced_analysis' => true,
911+
];
899912
} catch (Throwable $e) {
900913
return [
901914
'type' => 'object',

src/Services/RouteComposition.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,10 @@ private function normalizeEnhancedResponses(array $enhancedResponses): array
590590
'detection_method' => 'enhanced_ast_analysis',
591591
];
592592

593+
if (isset($response['schema']['items'])) {
594+
$normalizedResponses[$statusCode]['items'] = $response['schema']['items'];
595+
}
596+
593597
// Include resource if available
594598
if (isset($response['resource'])) {
595599
$normalizedResponses[$statusCode]['resource'] = $response['resource'];

0 commit comments

Comments
 (0)