@@ -2810,16 +2810,17 @@ private function performComprehensiveResourceAnalysis(string $controller, string
28102810 $ properties = $ this ->generateResourceDefaults ($ resourceClass );
28112811 }
28122812
2813+ // Apply Laravel Resource wrapping for collections
2814+ $ wrappedSchema = $ this ->applyResourceCollectionWrapper ($ fullResourceClass , $ properties );
2815+
28132816 return [
2814- 'type ' => 'array ' ,
2815- 'items ' => [
2816- 'type ' => 'object ' ,
2817- 'properties ' => $ properties ,
2818- ],
2819- 'example ' => [$ this ->generateExampleFromProperties ($ properties )],
2817+ 'type ' => 'object ' ,
2818+ 'properties ' => $ wrappedSchema ['properties ' ],
2819+ 'example ' => $ wrappedSchema ['example ' ],
28202820 'enhanced_analysis ' => true ,
28212821 'detected_resource ' => $ fullResourceClass ,
28222822 'detection_method ' => 'comprehensive_pattern_analysis ' ,
2823+ 'wrapper_applied ' => $ wrappedSchema ['wrapper_name ' ],
28232824 ];
28242825 } else {
28252826 // Return array schema with defaults even if class isn't found
@@ -2851,13 +2852,17 @@ private function performComprehensiveResourceAnalysis(string $controller, string
28512852 $ properties = $ this ->generateResourceDefaults ($ resourceClass );
28522853 }
28532854
2855+ // Apply Laravel Resource wrapping
2856+ $ wrappedSchema = $ this ->applyResourceWrapper ($ fullResourceClass , $ properties );
2857+
28542858 return [
28552859 'type ' => 'object ' ,
2856- 'properties ' => $ properties ,
2857- 'example ' => $ this -> generateExampleFromProperties ( $ properties ) ,
2860+ 'properties ' => $ wrappedSchema [ ' properties ' ] ,
2861+ 'example ' => $ wrappedSchema [ ' example ' ] ,
28582862 'enhanced_analysis ' => true ,
28592863 'detected_resource ' => $ fullResourceClass ,
28602864 'detection_method ' => 'resource_make_pattern_analysis ' ,
2865+ 'wrapper_applied ' => $ wrappedSchema ['wrapper_name ' ],
28612866 ];
28622867 } else {
28632868 // Return object schema with defaults even if class isn't found
@@ -3506,4 +3511,173 @@ private function analyzeJsonResponsePattern(string $data, string $status, string
35063511 'detection_method ' => 'json_response_object ' ,
35073512 ];
35083513 }
3514+
3515+ /**
3516+ * Apply Laravel Resource wrapper to schema based on Resource class configuration
3517+ */
3518+ private function applyResourceWrapper (string $ resourceClass , array $ properties ): array
3519+ {
3520+ try {
3521+ $ reflection = new ReflectionClass ($ resourceClass );
3522+
3523+ // Get the wrapper name from the Resource class
3524+ $ wrapperName = $ this ->getResourceWrapperName ($ reflection );
3525+
3526+ // Generate example from original properties
3527+ $ originalExample = $ this ->generateExampleFromProperties ($ properties );
3528+
3529+ // Apply wrapper to both schema and example
3530+ if ($ wrapperName ) {
3531+ return [
3532+ 'properties ' => [
3533+ $ wrapperName => [
3534+ 'type ' => 'object ' ,
3535+ 'properties ' => $ properties ,
3536+ 'description ' => 'Resource data wrapped in ' . $ wrapperName ,
3537+ ],
3538+ ],
3539+ 'example ' => [
3540+ $ wrapperName => $ originalExample ,
3541+ ],
3542+ 'wrapper_name ' => $ wrapperName ,
3543+ ];
3544+ }
3545+
3546+ // No wrapper - return as is
3547+ return [
3548+ 'properties ' => $ properties ,
3549+ 'example ' => $ originalExample ,
3550+ 'wrapper_name ' => null ,
3551+ ];
3552+
3553+ } catch (Throwable ) {
3554+ // Fallback: assume default Laravel "data" wrapper
3555+ $ originalExample = $ this ->generateExampleFromProperties ($ properties );
3556+
3557+ return [
3558+ 'properties ' => [
3559+ 'data ' => [
3560+ 'type ' => 'object ' ,
3561+ 'properties ' => $ properties ,
3562+ 'description ' => 'Resource data wrapped in default Laravel data wrapper ' ,
3563+ ],
3564+ ],
3565+ 'example ' => [
3566+ 'data ' => $ originalExample ,
3567+ ],
3568+ 'wrapper_name ' => 'data ' ,
3569+ ];
3570+ }
3571+ }
3572+
3573+ /**
3574+ * Apply Laravel Resource wrapper to collection schema
3575+ */
3576+ private function applyResourceCollectionWrapper (string $ resourceClass , array $ itemProperties ): array
3577+ {
3578+ try {
3579+ $ reflection = new ReflectionClass ($ resourceClass );
3580+
3581+ // Get the wrapper name from the Resource class
3582+ $ wrapperName = $ this ->getResourceWrapperName ($ reflection );
3583+
3584+ // Generate example item from properties
3585+ $ itemExample = $ this ->generateExampleFromProperties ($ itemProperties );
3586+
3587+ // Apply wrapper to collection
3588+ if ($ wrapperName ) {
3589+ return [
3590+ 'properties ' => [
3591+ $ wrapperName => [
3592+ 'type ' => 'array ' ,
3593+ 'items ' => [
3594+ 'type ' => 'object ' ,
3595+ 'properties ' => $ itemProperties ,
3596+ ],
3597+ 'description ' => 'Collection of resources wrapped in ' . $ wrapperName ,
3598+ ],
3599+ ],
3600+ 'example ' => [
3601+ $ wrapperName => [$ itemExample ],
3602+ ],
3603+ 'wrapper_name ' => $ wrapperName ,
3604+ ];
3605+ }
3606+
3607+ // No wrapper - return unwrapped collection
3608+ return [
3609+ 'properties ' => $ itemProperties ,
3610+ 'example ' => [$ itemExample ],
3611+ 'wrapper_name ' => null ,
3612+ ];
3613+
3614+ } catch (Throwable ) {
3615+ // Fallback: assume default Laravel "data" wrapper for collections
3616+ $ itemExample = $ this ->generateExampleFromProperties ($ itemProperties );
3617+
3618+ return [
3619+ 'properties ' => [
3620+ 'data ' => [
3621+ 'type ' => 'array ' ,
3622+ 'items ' => [
3623+ 'type ' => 'object ' ,
3624+ 'properties ' => $ itemProperties ,
3625+ ],
3626+ 'description ' => 'Collection of resources wrapped in default Laravel data wrapper ' ,
3627+ ],
3628+ ],
3629+ 'example ' => [
3630+ 'data ' => [$ itemExample ],
3631+ ],
3632+ 'wrapper_name ' => 'data ' ,
3633+ ];
3634+ }
3635+ }
3636+
3637+ /**
3638+ * Get the wrapper name for a Laravel Resource class
3639+ */
3640+ private function getResourceWrapperName (ReflectionClass $ resourceReflection ): ?string
3641+ {
3642+ // Check if the resource has a custom $wrap property
3643+ if ($ resourceReflection ->hasProperty ('wrap ' )) {
3644+ $ wrapProperty = $ resourceReflection ->getProperty ('wrap ' );
3645+
3646+ // Check if the property is accessible (public or has getter)
3647+ if ($ wrapProperty ->isPublic () || $ wrapProperty ->isProtected ()) {
3648+ $ wrapProperty ->setAccessible (true );
3649+
3650+ // Get default value from property
3651+ $ defaultProps = $ resourceReflection ->getDefaultProperties ();
3652+ if (isset ($ defaultProps ['wrap ' ])) {
3653+ return $ defaultProps ['wrap ' ];
3654+ }
3655+ }
3656+ }
3657+
3658+ // Check parent classes for global wrapping configuration
3659+ $ currentClass = $ resourceReflection ;
3660+ while ($ currentClass = $ currentClass ->getParentClass ()) {
3661+ // If we reach JsonResource class, check if wrapping is disabled globally
3662+ if ($ currentClass ->getName () === 'Illuminate\Http\Resources\Json\JsonResource ' ) {
3663+ // TODO: Check if JsonResource::withoutWrapping() has been called globally
3664+ // For now, assume default Laravel behavior: use "data" wrapper
3665+ return 'data ' ;
3666+ }
3667+
3668+ // Check parent class for $wrap property
3669+ if ($ currentClass ->hasProperty ('wrap ' )) {
3670+ $ wrapProperty = $ currentClass ->getProperty ('wrap ' );
3671+ $ wrapProperty ->setAccessible (true );
3672+
3673+ $ defaultProps = $ currentClass ->getDefaultProperties ();
3674+ if (isset ($ defaultProps ['wrap ' ])) {
3675+ return $ defaultProps ['wrap ' ];
3676+ }
3677+ }
3678+ }
3679+
3680+ // Default Laravel behavior: use "data" wrapper
3681+ return 'data ' ;
3682+ }
35093683}
0 commit comments