@@ -93,112 +93,250 @@ public function __construct(
9393 /**
9494 * Get the complete schema for a model class.
9595 *
96- * When reference caching is enabled (default), only depth 1 schemas are cached
97- * individually. Deeper schemas are composed on-the-fly by linking to cached
98- * depth 1 schemas, dramatically reducing cache size and duplication .
96+ * Returns a deduplicated schema where each related model appears only once
97+ * in a 'definitions' object. Relationships reference these definitions
98+ * instead of duplicating the schema inline .
9999 */
100100 public function getSchema (string $ modelClass , int $ maxDepth = 2 ): ?array
101101 {
102102 if (! $ this ->isValidModel ($ modelClass )) {
103103 return null ;
104104 }
105105
106- if ( $ this -> cacheTtl <= 0 ) {
107- return $ this ->buildSchema ($ modelClass , 0 , $ maxDepth, [] );
108- }
106+ // Always use deduplicated format to minimize payload size
107+ return $ this ->getDeduplicatedSchema ($ modelClass , $ maxDepth );
108+ }
109109
110- // Use reference-based caching: cache depth 1 schemas, compose deeper on-the-fly
111- if ($ this ->useReferenceCaching && $ maxDepth > 1 ) {
112- return $ this ->getSchemaWithReferences ($ modelClass , $ maxDepth );
110+ /**
111+ * Get schema in deduplicated format with definitions.
112+ *
113+ * Returns structure:
114+ * [
115+ * 'model' => 'App\Models\User',
116+ * 'columns' => [...],
117+ * 'relationships' => [
118+ * 'posts' => ['name' => 'posts', 'related_model' => 'App\Models\Post', ...]
119+ * ],
120+ * 'definitions' => [
121+ * 'App\Models\Post' => ['model' => '...', 'columns' => [...], 'relationships' => [...]]
122+ * ]
123+ * ]
124+ */
125+ public function getDeduplicatedSchema (string $ modelClass , int $ maxDepth = 2 ): ?array
126+ {
127+ if (! $ this ->isValidModel ($ modelClass )) {
128+ return null ;
113129 }
114130
115- return Cache:: remember (
116- $ this ->getCacheKey ($ modelClass , $ maxDepth),
117- $ this -> cacheTtl ,
118- fn () => $ this -> buildSchema ( $ modelClass , 0 , $ maxDepth , [])
119- ) ;
131+ $ definitions = [];
132+ $ schema = $ this ->buildSchemaCollectingDefinitions ($ modelClass , 0 , $ maxDepth, [], $ definitions );
133+ $ schema [ ' definitions ' ] = $ definitions ;
134+
135+ return $ schema ;
120136 }
121137
122138 /**
123- * Get schema by composing from cached depth 1 schemas .
139+ * Build schema while collecting unique model definitions .
124140 *
125- * This avoids storing duplicate relationship schemas in cache.
126- * Each model's base schema is cached once, then composed for deeper requests.
127- */
128- protected function getSchemaWithReferences (string $ modelClass , int $ maxDepth ): array
129- {
130- // Get the base schema at depth 1 (cached)
131- $ baseSchema = $ this ->getBaseSchema ($ modelClass );
141+ * Instead of embedding full schemas in relationships, we collect each
142+ * unique model's schema in $definitions and relationships just reference them.
143+ */
144+ protected function buildSchemaCollectingDefinitions (
145+ string $ modelClass ,
146+ int $ depth ,
147+ int $ maxDepth ,
148+ array $ visited ,
149+ array &$ definitions
150+ ): array {
151+ if (in_array ($ modelClass , $ visited )) {
152+ return [
153+ 'model ' => $ modelClass ,
154+ 'model_short ' => class_basename ($ modelClass ),
155+ 'circular ' => true ,
156+ ];
157+ }
158+
159+ $ model = new $ modelClass ;
160+ $ newVisited = [...$ visited , $ modelClass ];
132161
133- if ($ maxDepth === 1 || empty ($ baseSchema ['relationships ' ])) {
134- return $ baseSchema ;
162+ $ schema = [
163+ 'model ' => $ modelClass ,
164+ 'model_short ' => class_basename ($ modelClass ),
165+ 'table ' => $ model ->getTable (),
166+ 'columns ' => $ this ->getColumns ($ model ),
167+ 'relationships ' => [],
168+ ];
169+
170+ if ($ depth < $ maxDepth ) {
171+ $ schema ['relationships ' ] = $ this ->getRelationshipsWithDefinitions (
172+ $ model ,
173+ $ modelClass ,
174+ $ depth ,
175+ $ maxDepth ,
176+ $ newVisited ,
177+ $ definitions
178+ );
135179 }
136180
137- // Compose deeper levels by linking to cached depth 1 schemas
138- return $ this ->composeSchemaWithDepth ($ baseSchema , 1 , $ maxDepth , [$ modelClass ]);
181+ return $ schema ;
139182 }
140183
141184 /**
142- * Get the base (depth 1) schema for a model, from cache or freshly built .
185+ * Get relationships, adding related model schemas to definitions instead of inline .
143186 */
144- protected function getBaseSchema (string $ modelClass ): array
145- {
146- return Cache::remember (
147- $ this ->getCacheKey ($ modelClass , 1 ),
148- $ this ->cacheTtl ,
149- fn () => $ this ->buildSchema ($ modelClass , 0 , 1 , [])
150- );
187+ protected function getRelationshipsWithDefinitions (
188+ Model $ model ,
189+ string $ modelClass ,
190+ int $ depth ,
191+ int $ maxDepth ,
192+ array $ visited ,
193+ array &$ definitions
194+ ): array {
195+ $ reflection = new ReflectionClass ($ model );
196+
197+ return collect ($ reflection ->getMethods (ReflectionMethod::IS_PUBLIC ))
198+ ->filter (fn (ReflectionMethod $ method ) => $ method ->class === $ modelClass )
199+ ->reject (fn (ReflectionMethod $ method ) => $ method ->getNumberOfRequiredParameters () > 0 )
200+ ->reject (fn (ReflectionMethod $ method ) => $ this ->isExcludedMethod ($ method ->getName ()))
201+ ->mapWithKeys (function (ReflectionMethod $ method ) use ($ model , $ depth , $ maxDepth , $ visited , &$ definitions ) {
202+ return $ this ->parseRelationshipWithDefinitions (
203+ $ model ,
204+ $ method ,
205+ $ depth ,
206+ $ maxDepth ,
207+ $ visited ,
208+ $ definitions
209+ );
210+ })
211+ ->filter ()
212+ ->all ();
151213 }
152214
153215 /**
154- * Compose a schema with additional depth by linking relationship schemas .
216+ * Parse a relationship method, storing related schema in definitions .
155217 */
156- protected function composeSchemaWithDepth (array $ schema , int $ currentDepth , int $ maxDepth , array $ visited ): array
157- {
158- if ($ currentDepth >= $ maxDepth || empty ($ schema ['relationships ' ])) {
159- return $ schema ;
218+ protected function parseRelationshipWithDefinitions (
219+ Model $ model ,
220+ ReflectionMethod $ method ,
221+ int $ depth ,
222+ int $ maxDepth ,
223+ array $ visited ,
224+ array &$ definitions
225+ ): array {
226+ // Check return type hint first
227+ $ returnType = $ method ->getReturnType ();
228+
229+ if ($ returnType instanceof ReflectionNamedType && in_array ($ returnType ->getName (), self ::RELATION_TYPES )) {
230+ $ info = $ this ->buildRelationshipWithDefinitions ($ model , $ method , $ depth , $ maxDepth , $ visited , $ definitions );
231+
232+ return $ info ? [$ method ->getName () => $ info ] : [];
160233 }
161234
162- $ schema ['relationships ' ] = collect ($ schema ['relationships ' ])
163- ->map (function (array $ relationInfo ) use ($ currentDepth , $ maxDepth , $ visited ) {
164- // Skip if no related model or already has schema
165- if (! isset ($ relationInfo ['related_model ' ])) {
166- return $ relationInfo ;
167- }
235+ // If method has a non-relation return type, skip invoking it
236+ if ($ returnType instanceof ReflectionNamedType && ! in_array ($ returnType ->getName (), ['mixed ' , 'void ' , 'null ' , 'static ' , 'self ' ])) {
237+ return [];
238+ }
239+
240+ // Try invoking the method (only for methods without type hints)
241+ try {
242+ $ result = $ method ->invoke ($ model );
168243
169- $ relatedModel = $ relationInfo ['related_model ' ];
244+ if ($ result instanceof Relation) {
245+ return [$ method ->getName () => $ this ->buildRelationshipInfoWithDefinitions (
246+ $ result ,
247+ $ method ->getName (),
248+ $ depth ,
249+ $ maxDepth ,
250+ $ visited ,
251+ $ definitions
252+ )];
253+ }
254+ } catch (\Throwable ) {
255+ // Method threw an exception, skip it
256+ }
170257
171- // Check for circular reference
172- if (in_array ($ relatedModel , $ visited )) {
173- $ relationInfo ['schema ' ] = [
174- 'model ' => $ relatedModel ,
175- 'model_short ' => class_basename ($ relatedModel ),
176- 'circular ' => true ,
177- ];
258+ return [];
259+ }
178260
179- return $ relationInfo ;
180- }
261+ /**
262+ * Build relationship info by invoking the method, storing related schema in definitions.
263+ */
264+ protected function buildRelationshipWithDefinitions (
265+ Model $ model ,
266+ ReflectionMethod $ method ,
267+ int $ depth ,
268+ int $ maxDepth ,
269+ array $ visited ,
270+ array &$ definitions
271+ ): ?array {
272+ try {
273+ $ result = $ method ->invoke ($ model );
181274
182- // Get the related model's base schema (from cache)
183- $ relatedSchema = $ this ->getBaseSchema ($ relatedModel );
275+ return $ result instanceof Relation
276+ ? $ this ->buildRelationshipInfoWithDefinitions ($ result , $ method ->getName (), $ depth , $ maxDepth , $ visited , $ definitions )
277+ : null ;
278+ } catch (\Throwable ) {
279+ $ typeName = $ method ->getReturnType ()->getName ();
184280
185- // Recursively compose if we need more depth
186- if ($ currentDepth + 1 < $ maxDepth ) {
187- $ relatedSchema = $ this ->composeSchemaWithDepth (
188- $ relatedSchema ,
189- $ currentDepth + 1 ,
190- $ maxDepth ,
191- [...$ visited , $ relatedModel ]
192- );
193- }
281+ return [
282+ 'name ' => $ method ->getName (),
283+ 'type ' => class_basename ($ typeName ),
284+ 'type_full ' => $ typeName ,
285+ 'is_collection ' => in_array ($ typeName , self ::COLLECTION_RELATIONS ),
286+ ];
287+ }
288+ }
289+
290+ /**
291+ * Build relationship info, adding related model's schema to definitions (once).
292+ */
293+ protected function buildRelationshipInfoWithDefinitions (
294+ Relation $ relation ,
295+ string $ name ,
296+ int $ depth ,
297+ int $ maxDepth ,
298+ array $ visited ,
299+ array &$ definitions
300+ ): array {
301+ $ relatedModel = get_class ($ relation ->getRelated ());
302+ $ typeName = get_class ($ relation );
194303
195- $ relationInfo ['schema ' ] = $ relatedSchema ;
304+ $ info = [
305+ 'name ' => $ name ,
306+ 'type ' => class_basename ($ typeName ),
307+ 'type_full ' => $ typeName ,
308+ 'related_model ' => $ relatedModel ,
309+ 'related_model_short ' => class_basename ($ relatedModel ),
310+ 'is_collection ' => in_array ($ typeName , self ::COLLECTION_RELATIONS ),
311+ ];
196312
197- return $ relationInfo ;
198- })
199- ->all ();
313+ // Add related model to definitions if not already there and not circular
314+ if ($ depth + 1 <= $ maxDepth && ! in_array ($ relatedModel , $ visited )) {
315+ if (! isset ($ definitions [$ relatedModel ])) {
316+ // Build and store the related model's schema in definitions
317+ $ definitions [$ relatedModel ] = $ this ->buildSchemaCollectingDefinitions (
318+ $ relatedModel ,
319+ $ depth + 1 ,
320+ $ maxDepth ,
321+ $ visited ,
322+ $ definitions
323+ );
324+ }
325+ }
200326
201- return $ schema ;
327+ return $ info ;
328+ }
329+
330+ /**
331+ * Get the base (depth 1) schema for a model, from cache or freshly built.
332+ */
333+ protected function getBaseSchema (string $ modelClass ): array
334+ {
335+ return Cache::remember (
336+ $ this ->getCacheKey ($ modelClass , 1 ),
337+ $ this ->cacheTtl ,
338+ fn () => $ this ->buildSchema ($ modelClass , 0 , 1 , [])
339+ );
202340 }
203341
204342 /**
0 commit comments