• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 13202629907

07 Feb 2025 03:13PM UTC coverage: 7.554% (+0.3%) from 7.283%
13202629907

Pull #6954

github

web-flow
Merge 696625eca into 89816721e
Pull Request #6954: perf: various optimizations for Laravel/Symfony

10 of 18 new or added lines in 4 files covered. (55.56%)

94 existing lines in 8 files now uncovered.

9931 of 131474 relevant lines covered (7.55%)

4.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Laravel/Eloquent/Metadata/ModelMetadata.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types=1);
13

14
namespace ApiPlatform\Laravel\Eloquent\Metadata;
15

16
use Illuminate\Database\Eloquent\Model;
17
use Illuminate\Database\Eloquent\Relations\Relation;
18
use Illuminate\Support\Collection;
19
use Illuminate\Support\Str;
20

21
/**
22
 * Inspired from Illuminate\Database\Console\ShowModelCommand.
23
 *
24
 * @internal
25
 */
26
final class ModelMetadata
27
{
28
    /**
29
     * @var array<class-string, Collection<string, mixed>>
30
     */
31
    private $attributesLocalCache = [];
32

33
    /**
34
     * @var array<class-string, Collection<int, mixed>>
35
     */
36
    private $relationsLocalCache = [];
37

38
    /**
39
     * The methods that can be called in a model to indicate a relation.
40
     *
41
     * @var string[]
42
     */
43
    public const RELATION_METHODS = [
44
        'hasMany',
45
        'hasManyThrough',
46
        'hasOneThrough',
47
        'belongsToMany',
48
        'hasOne',
49
        'belongsTo',
50
        'morphOne',
51
        'morphTo',
52
        'morphMany',
53
        'morphToMany',
54
        'morphedByMany',
55
    ];
56

57
    /**
58
     * Gets the column attributes for the given model.
59
     *
60
     * @return Collection<string, mixed>
61
     */
62
    public function getAttributes(Model $model): Collection
63
    {
NEW
64
        if (isset($this->attributesLocalCache[$model::class])) {
×
NEW
65
            return $this->attributesLocalCache[$model::class];
×
66
        }
67

68
        $connection = $model->getConnection();
×
69
        $schema = $connection->getSchemaBuilder();
×
70
        $table = $model->getTable();
×
71
        $columns = $schema->getColumns($table);
×
72
        $indexes = $schema->getIndexes($table);
×
73
        $relations = $this->getRelations($model);
×
74

NEW
75
        return $this->attributesLocalCache[$model::class] = collect($columns)
×
76
            ->reject(
×
77
                fn ($column) => $relations->contains(
×
78
                    fn ($relation) => $relation['foreign_key'] === $column['name']
×
79
                )
×
80
            )
×
81
            ->map(fn ($column) => [
×
82
                'name' => $column['name'],
×
83
                'type' => $column['type'],
×
84
                'increments' => $column['auto_increment'],
×
85
                'nullable' => $column['nullable'],
×
86
                'default' => $this->getColumnDefault($column, $model),
×
87
                'unique' => $this->columnIsUnique($column['name'], $indexes),
×
88
                'fillable' => $model->isFillable($column['name']),
×
89
                'hidden' => $this->attributeIsHidden($column['name'], $model),
×
90
                'appended' => null,
×
91
                'cast' => $this->getCastType($column['name'], $model),
×
92
                'primary' => $this->isColumnPrimaryKey($indexes, $column['name']),
×
93
            ])
×
94
            ->merge($this->getVirtualAttributes($model, $columns));
×
95
    }
96

97
    /**
98
     * @param array<int, array{columns: string[]}> $indexes
99
     */
100
    private function isColumnPrimaryKey(array $indexes, string $column): bool
101
    {
102
        foreach ($indexes as $index) {
×
103
            if (\in_array($column, $index['columns'], true)) {
×
104
                return true;
×
105
            }
106
        }
107

108
        return false;
×
109
    }
110

111
    /**
112
     * Get the virtual (non-column) attributes for the given model.
113
     *
114
     * @param array<string, mixed> $columns
115
     *
116
     * @return Collection<int, mixed>
117
     */
118
    private function getVirtualAttributes(Model $model, array $columns): Collection
119
    {
120
        $class = new \ReflectionClass($model);
×
121

122
        return collect($class->getMethods())
×
123
            ->reject(
×
124
                fn (\ReflectionMethod $method) => $method->isStatic()
×
125
                    || $method->isAbstract()
×
126
                    || Model::class === $method->getDeclaringClass()->getName()
×
127
            )
×
128
            ->mapWithKeys(function (\ReflectionMethod $method) use ($model) {
×
129
                if (1 === preg_match('/^get(.+)Attribute$/', $method->getName(), $matches)) {
×
130
                    return [Str::snake($matches[1]) => 'accessor'];
×
131
                }
132
                if ($model->hasAttributeMutator($method->getName())) {
×
133
                    return [Str::snake($method->getName()) => 'attribute'];
×
134
                }
135

136
                return [];
×
137
            })
×
138
            ->reject(fn ($cast, $name) => collect($columns)->contains('name', $name))
×
139
            ->map(fn ($cast, $name) => [
×
140
                'name' => $name,
×
141
                'type' => null,
×
142
                'increments' => false,
×
143
                'nullable' => null,
×
144
                'default' => null,
×
145
                'unique' => null,
×
146
                'fillable' => $model->isFillable($name),
×
147
                'hidden' => $this->attributeIsHidden($name, $model),
×
148
                'appended' => $model->hasAppended($name),
×
149
                'cast' => $cast,
×
150
            ])
×
151
            ->values();
×
152
    }
153

154
    /**
155
     * Gets the relations from the given model.
156
     *
157
     * @return Collection<int, mixed>
158
     */
159
    public function getRelations(Model $model): Collection
160
    {
NEW
161
        if (isset($this->relationsLocalCache[$model::class])) {
×
NEW
162
            return $this->relationsLocalCache[$model::class];
×
163
        }
164

NEW
165
        return $this->relationsLocalCache[$model::class] = collect(get_class_methods($model))
×
166
            ->map(fn ($method) => new \ReflectionMethod($model, $method))
×
167
            ->reject(
×
168
                fn (\ReflectionMethod $method) => $method->isStatic()
×
169
                    || $method->isAbstract()
×
170
                    || Model::class === $method->getDeclaringClass()->getName()
×
171
                    || $method->getNumberOfParameters() > 0
×
172
                    || $this->attributeIsHidden($method->getName(), $model)
×
173
            )
×
174
            ->filter(function (\ReflectionMethod $method) {
×
175
                if ($method->getReturnType() instanceof \ReflectionNamedType
×
176
                    && is_subclass_of($method->getReturnType()->getName(), Relation::class)) {
×
177
                    return true;
×
178
                }
179

180
                if (false === $method->getFileName()) {
×
181
                    return false;
×
182
                }
183

184
                $file = new \SplFileObject($method->getFileName());
×
185
                $file->seek($method->getStartLine() - 1);
×
186
                $code = '';
×
187
                while ($file->key() < $method->getEndLine()) {
×
188
                    $current = $file->current();
×
189
                    if (\is_string($current)) {
×
190
                        $code .= trim($current);
×
191
                    }
192

193
                    $file->next();
×
194
                }
195

196
                return collect(self::RELATION_METHODS)
×
197
                    ->contains(fn ($relationMethod) => str_contains($code, '$this->'.$relationMethod.'('));
×
198
            })
×
199
            ->map(function (\ReflectionMethod $method) use ($model) {
×
200
                $relation = $method->invoke($model);
×
201

202
                if (!$relation instanceof Relation) {
×
203
                    return null;
×
204
                }
205

206
                return [
×
207
                    'name' => $method->getName(),
×
208
                    'type' => $relation::class,
×
209
                    'related' => \get_class($relation->getRelated()),
×
210
                    'foreign_key' => method_exists($relation, 'getForeignKeyName') ? $relation->getForeignKeyName() : null,
×
211
                ];
×
212
            })
×
213
            ->filter()
×
214
            ->values();
×
215
    }
216

217
    /**
218
     * Gets the cast type for the given column.
219
     */
220
    private function getCastType(string $column, Model $model): ?string
221
    {
222
        if ($model->hasGetMutator($column) || $model->hasSetMutator($column)) {
×
223
            return 'accessor';
×
224
        }
225

226
        if ($model->hasAttributeMutator($column)) {
×
227
            return 'attribute';
×
228
        }
229

230
        return $this->getCastsWithDates($model)->get($column) ?? null;
×
231
    }
232

233
    /**
234
     * Gets the model casts, including any date casts.
235
     *
236
     * @return Collection<string, mixed>
237
     */
238
    private function getCastsWithDates(Model $model): Collection
239
    {
240
        return collect($model->getDates())
×
241
            ->filter()
×
242
            ->flip()
×
243
            ->map(fn () => 'datetime')
×
244
            ->merge($model->getCasts());
×
245
    }
246

247
    /**
248
     * Gets the default value for the given column.
249
     *
250
     * @param array<string, mixed>&array{name: string, default: string} $column
251
     */
252
    private function getColumnDefault(array $column, Model $model): mixed
253
    {
254
        $attributeDefault = $model->getAttributes()[$column['name']] ?? null;
×
255

256
        return match (true) {
×
257
            $attributeDefault instanceof \BackedEnum => $attributeDefault->value,
×
258
            $attributeDefault instanceof \UnitEnum => $attributeDefault->name,
×
259
            default => $attributeDefault ?? $column['default'],
×
260
        };
×
261
    }
262

263
    /**
264
     * Determines if the given attribute is hidden.
265
     */
266
    private function attributeIsHidden(string $attribute, Model $model): bool
267
    {
268
        if ($visible = $model->getVisible()) {
×
269
            return !\in_array($attribute, $visible, true);
×
270
        }
271

272
        if ($hidden = $model->getHidden()) {
×
273
            return \in_array($attribute, $hidden, true);
×
274
        }
275

276
        return false;
×
277
    }
278

279
    /**
280
     * Determines if the given attribute is unique.
281
     *
282
     * @param array<int, array{columns: string[], unique: bool}> $indexes
283
     */
284
    private function columnIsUnique(string $column, array $indexes): bool
285
    {
286
        return collect($indexes)->contains(
×
287
            fn ($index) => 1 === \count($index['columns']) && $index['columns'][0] === $column && $index['unique']
×
288
        );
×
289
    }
290
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc