1515
1616use Illuminate \Database \Eloquent \Model ;
1717use Illuminate \Database \Eloquent \Relations \Relation ;
18- use Illuminate \Support \Collection ;
1918use Illuminate \Support \Str ;
2019use Symfony \Component \Serializer \NameConverter \CamelCaseToSnakeCaseNameConverter ;
2120use Symfony \Component \Serializer \NameConverter \NameConverterInterface ;
2827final class ModelMetadata
2928{
3029 /**
31- * @var array<class-string, Collection <string, mixed>>
30+ * @var array<class-string, array <string, mixed>>
3231 */
3332 private $ attributesLocalCache = [];
3433
3534 /**
36- * @var array<class-string, Collection<int , mixed>>
35+ * @var array<class-string, array<string , mixed>>
3736 */
3837 private $ relationsLocalCache = [];
3938
@@ -63,9 +62,9 @@ public function __construct(private NameConverterInterface $relationNameConverte
6362 /**
6463 * Gets the column attributes for the given model.
6564 *
66- * @return Collection <string, mixed>
65+ * @return array <string, mixed>
6766 */
68- public function getAttributes (Model $ model ): Collection
67+ public function getAttributes (Model $ model ): array
6968 {
7069 if (isset ($ this ->attributesLocalCache [$ model ::class])) {
7170 return $ this ->attributesLocalCache [$ model ::class];
@@ -78,13 +77,15 @@ public function getAttributes(Model $model): Collection
7877 $ indexes = $ schema ->getIndexes ($ table );
7978 $ relations = $ this ->getRelations ($ model );
8079
81- return $ this ->attributesLocalCache [$ model ::class] = collect ($ columns )
82- ->reject (
83- fn ($ column ) => $ relations ->contains (
84- fn ($ relation ) => $ relation ['foreign_key ' ] === $ column ['name ' ]
85- )
86- )
87- ->map (fn ($ column ) => [
80+ $ foreignKeys = array_flip (array_column ($ relations , 'foreign_key ' ));
81+ $ attributes = [];
82+
83+ foreach ($ columns as $ column ) {
84+ if (isset ($ foreignKeys [$ column ['name ' ]])) {
85+ continue ;
86+ }
87+
88+ $ attributes [$ column ['name ' ]] = [
8889 'name ' => $ column ['name ' ],
8990 'type ' => $ column ['type ' ],
9091 'increments ' => $ column ['auto_increment ' ],
@@ -96,8 +97,10 @@ public function getAttributes(Model $model): Collection
9697 'appended ' => null ,
9798 'cast ' => $ this ->getCastType ($ column ['name ' ], $ model ),
9899 'primary ' => $ this ->isColumnPrimaryKey ($ indexes , $ column ['name ' ]),
99- ])
100- ->merge ($ this ->getVirtualAttributes ($ model , $ columns ));
100+ ];
101+ }
102+
103+ return $ this ->attributesLocalCache [$ model ::class] = array_merge ($ attributes , $ this ->getVirtualAttributes ($ model , $ columns ));
101104 }
102105
103106 /**
@@ -119,30 +122,43 @@ private function isColumnPrimaryKey(array $indexes, string $column): bool
119122 *
120123 * @param array<string, mixed> $columns
121124 *
122- * @return Collection<int , mixed>
125+ * @return array<string , mixed>
123126 */
124- private function getVirtualAttributes (Model $ model , array $ columns ): Collection
127+ private function getVirtualAttributes (Model $ model , array $ columns ): array
125128 {
126129 $ class = new \ReflectionClass ($ model );
130+ $ virtualAttributes = [];
131+
132+ $ columnNames = array_flip (array_column ($ columns , 'name ' ));
133+
134+ foreach ($ class ->getMethods () as $ method ) {
135+ if (
136+ $ method ->isStatic ()
137+ || $ method ->isAbstract ()
138+ // Skips methods from the base Eloquent Model class
139+ || Model::class === $ method ->getDeclaringClass ()->getName ()
140+ ) {
141+ continue ;
142+ }
127143
128- return collect ($ class ->getMethods ())
129- ->reject (
130- fn (\ReflectionMethod $ method ) => $ method ->isStatic ()
131- || $ method ->isAbstract ()
132- || Model::class === $ method ->getDeclaringClass ()->getName ()
133- )
134- ->mapWithKeys (function (\ReflectionMethod $ method ) use ($ model ) {
135- if (1 === preg_match ('/^get(.+)Attribute$/ ' , $ method ->getName (), $ matches )) {
136- return [Str::snake ($ matches [1 ]) => 'accessor ' ];
137- }
138- if ($ model ->hasAttributeMutator ($ method ->getName ())) {
139- return [Str::snake ($ method ->getName ()) => 'attribute ' ];
140- }
144+ $ methodName = $ method ->getName ();
145+ $ name = null ;
146+ $ cast = null ;
141147
142- return [];
143- })
144- ->reject (fn ($ cast , $ name ) => collect ($ columns )->contains ('name ' , $ name ))
145- ->map (fn ($ cast , $ name ) => [
148+ if (1 === preg_match ('/^get(.+)Attribute$/ ' , $ methodName , $ matches )) {
149+ $ name = Str::snake ($ matches [1 ]);
150+ $ cast = 'accessor ' ;
151+ } elseif ($ model ->hasAttributeMutator ($ methodName )) {
152+ $ name = Str::snake ($ methodName );
153+ $ cast = 'attribute ' ;
154+ }
155+
156+ // If the method is not a virtual attribute, or if it conflicts with a real column, skip it.
157+ if (null === $ name || isset ($ columnNames [$ name ])) {
158+ continue ;
159+ }
160+
161+ $ virtualAttributes [$ name ] = [
146162 'name ' => $ name ,
147163 'type ' => null ,
148164 'increments ' => false ,
@@ -153,42 +169,41 @@ private function getVirtualAttributes(Model $model, array $columns): Collection
153169 'hidden ' => $ this ->attributeIsHidden ($ name , $ model ),
154170 'appended ' => $ model ->hasAppended ($ name ),
155171 'cast ' => $ cast ,
156- ])
157- ->values ();
172+ ];
173+ }
174+
175+ return $ virtualAttributes ;
158176 }
159177
160178 /**
161179 * Gets the relations from the given model.
162180 *
163- * @return Collection<int , mixed>
181+ * @return array<string , mixed>
164182 */
165- public function getRelations (Model $ model ): Collection
183+ public function getRelations (Model $ model ): array
166184 {
167185 if (isset ($ this ->relationsLocalCache [$ model ::class])) {
168186 return $ this ->relationsLocalCache [$ model ::class];
169187 }
170188
171- return $ this ->relationsLocalCache [$ model ::class] = collect (get_class_methods ($ model ))
172- ->map (fn ($ method ) => new \ReflectionMethod ($ model , $ method ))
173- ->reject (
174- fn (\ReflectionMethod $ method ) => $ method ->isStatic ()
175- || $ method ->isAbstract ()
176- || Model::class === $ method ->getDeclaringClass ()->getName ()
177- || $ method ->getNumberOfParameters () > 0
178- || $ this ->attributeIsHidden ($ method ->getName (), $ model )
179- )
180- ->filter (function (\ReflectionMethod $ method ) {
181- if (
182- $ method ->getReturnType () instanceof \ReflectionNamedType
183- && is_subclass_of ($ method ->getReturnType ()->getName (), Relation::class)
184- ) {
185- return true ;
186- }
189+ $ relations = [];
190+ $ class = new \ReflectionClass ($ model );
187191
188- if (false === $ method ->getFileName ()) {
189- return false ;
190- }
192+ foreach ($ class ->getMethods () as $ method ) {
193+ if (
194+ $ method ->isStatic ()
195+ || $ method ->isAbstract ()
196+ || $ method ->getNumberOfParameters () > 0
197+ || Model::class === $ method ->getDeclaringClass ()->getName ()
198+ || $ this ->attributeIsHidden ($ method ->getName (), $ model )
199+ ) {
200+ continue ;
201+ }
191202
203+ $ isRelation = false ;
204+ if ($ method ->getReturnType () instanceof \ReflectionNamedType && is_subclass_of ($ method ->getReturnType ()->getName (), Relation::class)) {
205+ $ isRelation = true ;
206+ } elseif (false !== $ method ->getFileName ()) {
192207 $ file = new \SplFileObject ($ method ->getFileName ());
193208 $ file ->seek ($ method ->getStartLine () - 1 );
194209 $ code = '' ;
@@ -197,30 +212,37 @@ public function getRelations(Model $model): Collection
197212 if (\is_string ($ current )) {
198213 $ code .= trim ($ current );
199214 }
200-
201215 $ file ->next ();
202216 }
203217
204- return collect (self ::RELATION_METHODS )
205- ->contains (fn ($ relationMethod ) => str_contains ($ code , '$this-> ' .$ relationMethod .'( ' ));
206- })
207- ->map (function (\ReflectionMethod $ method ) use ($ model ) {
208- $ relation = $ method ->invoke ($ model );
209-
210- if (!$ relation instanceof Relation) {
211- return null ;
218+ foreach (self ::RELATION_METHODS as $ relationMethod ) {
219+ if (str_contains ($ code , '$this-> ' .$ relationMethod .'( ' )) {
220+ $ isRelation = true ;
221+ break ;
222+ }
212223 }
224+ }
225+
226+ if (!$ isRelation ) {
227+ continue ;
228+ }
213229
214- return [
215- 'name ' => $ this ->relationNameConverter ->normalize ($ method ->getName ()),
216- 'method_name ' => $ method ->getName (),
217- 'type ' => $ relation ::class,
218- 'related ' => \get_class ($ relation ->getRelated ()),
219- 'foreign_key ' => method_exists ($ relation , 'getForeignKeyName ' ) ? $ relation ->getForeignKeyName () : null ,
220- ];
221- })
222- ->filter ()
223- ->values ();
230+ $ relation = $ method ->invoke ($ model );
231+ if (!$ relation instanceof Relation) {
232+ continue ;
233+ }
234+
235+ $ relationName = $ this ->relationNameConverter ->normalize ($ method ->getName ());
236+ $ relations [$ relationName ] = [
237+ 'name ' => $ relationName ,
238+ 'method_name ' => $ method ->getName (),
239+ 'type ' => $ relation ::class,
240+ 'related ' => \get_class ($ relation ->getRelated ()),
241+ 'foreign_key ' => method_exists ($ relation , 'getForeignKeyName ' ) ? $ relation ->getForeignKeyName () : null ,
242+ ];
243+ }
244+
245+ return $ this ->relationsLocalCache [$ model ::class] = $ relations ;
224246 }
225247
226248 /**
@@ -236,21 +258,25 @@ private function getCastType(string $column, Model $model): ?string
236258 return 'attribute ' ;
237259 }
238260
239- return $ this ->getCastsWithDates ($ model )-> get ( $ column) ?? null ;
261+ return $ this ->getCastsWithDates ($ model )[ $ column] ?? null ;
240262 }
241263
242264 /**
243265 * Gets the model casts, including any date casts.
244266 *
245- * @return Collection <string, mixed>
267+ * @return array <string, mixed>
246268 */
247- private function getCastsWithDates (Model $ model ): Collection
269+ private function getCastsWithDates (Model $ model ): array
248270 {
249- return collect ($ model ->getDates ())
250- ->filter ()
251- ->flip ()
252- ->map (fn () => 'datetime ' )
253- ->merge ($ model ->getCasts ());
271+ $ dateCasts = [];
272+
273+ foreach ($ model ->getDates () as $ date ) {
274+ if (!empty ($ date )) {
275+ $ dateCasts [$ date ] = 'datetime ' ;
276+ }
277+ }
278+
279+ return array_merge ($ dateCasts , $ model ->getCasts ());
254280 }
255281
256282 /**
0 commit comments