Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Illuminate\Foundation\Console;
4
 
5
use BackedEnum;
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\Index;
8
use Doctrine\DBAL\Types\DecimalType;
9
use Illuminate\Console\Command;
10
use Illuminate\Contracts\Container\BindingResolutionException;
11
use Illuminate\Database\Console\DatabaseInspectionCommand;
12
use Illuminate\Database\Eloquent\Relations\Relation;
13
use Illuminate\Support\Facades\Gate;
14
use Illuminate\Support\Str;
15
use ReflectionClass;
16
use ReflectionMethod;
17
use SplFileObject;
18
use Symfony\Component\Console\Attribute\AsCommand;
19
use Symfony\Component\Console\Output\OutputInterface;
20
use UnitEnum;
21
 
22
#[AsCommand(name: 'model:show')]
23
class ShowModelCommand extends DatabaseInspectionCommand
24
{
25
    /**
26
     * The console command name.
27
     *
28
     * @var string
29
     */
30
    protected $name = 'model:show {model}';
31
 
32
    /**
33
     * The name of the console command.
34
     *
35
     * This name is used to identify the command during lazy loading.
36
     *
37
     * @var string|null
38
     *
39
     * @deprecated
40
     */
41
    protected static $defaultName = 'model:show';
42
 
43
    /**
44
     * The console command description.
45
     *
46
     * @var string
47
     */
48
    protected $description = 'Show information about an Eloquent model';
49
 
50
    /**
51
     * The console command signature.
52
     *
53
     * @var string
54
     */
55
    protected $signature = 'model:show {model : The model to show}
56
                {--database= : The database connection to use}
57
                {--json : Output the model as JSON}';
58
 
59
    /**
60
     * The methods that can be called in a model to indicate a relation.
61
     *
62
     * @var array
63
     */
64
    protected $relationMethods = [
65
        'hasMany',
66
        'hasManyThrough',
67
        'hasOneThrough',
68
        'belongsToMany',
69
        'hasOne',
70
        'belongsTo',
71
        'morphOne',
72
        'morphTo',
73
        'morphMany',
74
        'morphToMany',
75
        'morphedByMany',
76
    ];
77
 
78
    /**
79
     * Execute the console command.
80
     *
81
     * @return void
82
     */
83
    public function handle()
84
    {
85
        if (! $this->ensureDependenciesExist()) {
86
            return 1;
87
        }
88
 
89
        $class = $this->qualifyModel($this->argument('model'));
90
 
91
        try {
92
            $model = $this->laravel->make($class);
93
 
94
            $class = get_class($model);
95
        } catch (BindingResolutionException $e) {
96
            return $this->components->error($e->getMessage());
97
        }
98
 
99
        if ($this->option('database')) {
100
            $model->setConnection($this->option('database'));
101
        }
102
 
103
        $this->display(
104
            $class,
105
            $model->getConnection()->getName(),
106
            $model->getConnection()->getTablePrefix().$model->getTable(),
107
            $this->getPolicy($model),
108
            $this->getAttributes($model),
109
            $this->getRelations($model),
110
            $this->getObservers($model),
111
        );
112
    }
113
 
114
    /**
115
     * Get the first policy associated with this model.
116
     *
117
     * @param  \Illuminate\Database\Eloquent\Model  $model
118
     * @return Illuminate\Support\Collection
119
     */
120
    protected function getPolicy($model)
121
    {
122
        return collect(Gate::policies())
123
            ->filter(fn ($policy, $modelClass) => $modelClass === get_class($model))
124
            ->values()
125
            ->first();
126
    }
127
 
128
    /**
129
     * Get the column attributes for the given model.
130
     *
131
     * @param  \Illuminate\Database\Eloquent\Model  $model
132
     * @return \Illuminate\Support\Collection
133
     */
134
    protected function getAttributes($model)
135
    {
136
        $schema = $model->getConnection()->getDoctrineSchemaManager();
137
        $this->registerTypeMappings($schema->getDatabasePlatform());
138
        $table = $model->getConnection()->getTablePrefix().$model->getTable();
139
        $columns = $schema->listTableColumns($table);
140
        $indexes = $schema->listTableIndexes($table);
141
 
142
        return collect($columns)
143
            ->values()
144
            ->map(fn (Column $column) => [
145
                'name' => $column->getName(),
146
                'type' => $this->getColumnType($column),
147
                'increments' => $column->getAutoincrement(),
148
                'nullable' => ! $column->getNotnull(),
149
                'default' => $this->getColumnDefault($column, $model),
150
                'unique' => $this->columnIsUnique($column->getName(), $indexes),
151
                'fillable' => $model->isFillable($column->getName()),
152
                'hidden' => $this->attributeIsHidden($column->getName(), $model),
153
                'appended' => null,
154
                'cast' => $this->getCastType($column->getName(), $model),
155
            ])
156
            ->merge($this->getVirtualAttributes($model, $columns));
157
    }
158
 
159
    /**
160
     * Get the virtual (non-column) attributes for the given model.
161
     *
162
     * @param  \Illuminate\Database\Eloquent\Model  $model
163
     * @param  \Doctrine\DBAL\Schema\Column[]  $columns
164
     * @return \Illuminate\Support\Collection
165
     */
166
    protected function getVirtualAttributes($model, $columns)
167
    {
168
        $class = new ReflectionClass($model);
169
 
170
        return collect($class->getMethods())
171
            ->reject(
172
                fn (ReflectionMethod $method) => $method->isStatic()
173
                    || $method->isAbstract()
174
                    || $method->getDeclaringClass()->getName() !== get_class($model)
175
            )
176
            ->mapWithKeys(function (ReflectionMethod $method) use ($model) {
177
                if (preg_match('/^get(.+)Attribute$/', $method->getName(), $matches) === 1) {
178
                    return [Str::snake($matches[1]) => 'accessor'];
179
                } elseif ($model->hasAttributeMutator($method->getName())) {
180
                    return [Str::snake($method->getName()) => 'attribute'];
181
                } else {
182
                    return [];
183
                }
184
            })
185
            ->reject(fn ($cast, $name) => collect($columns)->has($name))
186
            ->map(fn ($cast, $name) => [
187
                'name' => $name,
188
                'type' => null,
189
                'increments' => false,
190
                'nullable' => null,
191
                'default' => null,
192
                'unique' => null,
193
                'fillable' => $model->isFillable($name),
194
                'hidden' => $this->attributeIsHidden($name, $model),
195
                'appended' => $model->hasAppended($name),
196
                'cast' => $cast,
197
            ])
198
            ->values();
199
    }
200
 
201
    /**
202
     * Get the relations from the given model.
203
     *
204
     * @param  \Illuminate\Database\Eloquent\Model  $model
205
     * @return \Illuminate\Support\Collection
206
     */
207
    protected function getRelations($model)
208
    {
209
        return collect(get_class_methods($model))
210
            ->map(fn ($method) => new ReflectionMethod($model, $method))
211
            ->reject(
212
                fn (ReflectionMethod $method) => $method->isStatic()
213
                    || $method->isAbstract()
214
                    || $method->getDeclaringClass()->getName() !== get_class($model)
215
            )
216
            ->filter(function (ReflectionMethod $method) {
217
                $file = new SplFileObject($method->getFileName());
218
                $file->seek($method->getStartLine() - 1);
219
                $code = '';
220
                while ($file->key() < $method->getEndLine()) {
150 lars 221
                    $code .= trim($file->current());
148 lars 222
                    $file->next();
223
                }
224
 
225
                return collect($this->relationMethods)
226
                    ->contains(fn ($relationMethod) => str_contains($code, '$this->'.$relationMethod.'('));
227
            })
228
            ->map(function (ReflectionMethod $method) use ($model) {
229
                $relation = $method->invoke($model);
230
 
231
                if (! $relation instanceof Relation) {
232
                    return null;
233
                }
234
 
235
                return [
236
                    'name' => $method->getName(),
237
                    'type' => Str::afterLast(get_class($relation), '\\'),
238
                    'related' => get_class($relation->getRelated()),
239
                ];
240
            })
241
            ->filter()
242
            ->values();
243
    }
244
 
245
    /**
246
     * Get the Observers watching this model.
247
     *
248
     * @param  \Illuminate\Database\Eloquent\Model  $model
249
     * @return Illuminate\Support\Collection
250
     */
251
    protected function getObservers($model)
252
    {
253
        $listeners = $this->getLaravel()->make('events')->getRawListeners();
254
 
255
        // Get the Eloquent observers for this model...
256
        $listeners = array_filter($listeners, function ($v, $key) use ($model) {
257
            return Str::startsWith($key, 'eloquent.') && Str::endsWith($key, $model::class);
258
        }, ARRAY_FILTER_USE_BOTH);
259
 
260
        // Format listeners Eloquent verb => Observer methods...
261
        $extractVerb = function ($key) {
262
            preg_match('/eloquent.([a-zA-Z]+)\: /', $key, $matches);
263
 
264
            return $matches[1] ?? '?';
265
        };
266
 
267
        $formatted = [];
268
 
269
        foreach ($listeners as $key => $observerMethods) {
270
            $formatted[] = [
271
                'event' => $extractVerb($key),
272
                'observer' => array_map(fn ($obs) => is_string($obs) ? $obs : 'Closure', $observerMethods),
273
            ];
274
        }
275
 
276
        return collect($formatted);
277
    }
278
 
279
    /**
280
     * Render the model information.
281
     *
282
     * @param  string  $class
283
     * @param  string  $database
284
     * @param  string  $table
285
     * @param  string  $policy
286
     * @param  \Illuminate\Support\Collection  $attributes
287
     * @param  \Illuminate\Support\Collection  $relations
288
     * @param  \Illuminate\Support\Collection  $observers
289
     * @return void
290
     */
291
    protected function display($class, $database, $table, $policy, $attributes, $relations, $observers)
292
    {
293
        $this->option('json')
294
            ? $this->displayJson($class, $database, $table, $policy, $attributes, $relations, $observers)
295
            : $this->displayCli($class, $database, $table, $policy, $attributes, $relations, $observers);
296
    }
297
 
298
    /**
299
     * Render the model information as JSON.
300
     *
301
     * @param  string  $class
302
     * @param  string  $database
303
     * @param  string  $table
304
     * @param  string  $policy
305
     * @param  \Illuminate\Support\Collection  $attributes
306
     * @param  \Illuminate\Support\Collection  $relations
307
     * @param  \Illuminate\Support\Collection  $observers
308
     * @return void
309
     */
310
    protected function displayJson($class, $database, $table, $policy, $attributes, $relations, $observers)
311
    {
312
        $this->output->writeln(
313
            collect([
314
                'class' => $class,
315
                'database' => $database,
316
                'table' => $table,
317
                'policy' => $policy,
318
                'attributes' => $attributes,
319
                'relations' => $relations,
320
                'observers' => $observers,
321
            ])->toJson()
322
        );
323
    }
324
 
325
    /**
326
     * Render the model information for the CLI.
327
     *
328
     * @param  string  $class
329
     * @param  string  $database
330
     * @param  string  $table
331
     * @param  string  $policy
332
     * @param  \Illuminate\Support\Collection  $attributes
333
     * @param  \Illuminate\Support\Collection  $relations
334
     * @param  \Illuminate\Support\Collection  $observers
335
     * @return void
336
     */
337
    protected function displayCli($class, $database, $table, $policy, $attributes, $relations, $observers)
338
    {
339
        $this->newLine();
340
 
341
        $this->components->twoColumnDetail('<fg=green;options=bold>'.$class.'</>');
342
        $this->components->twoColumnDetail('Database', $database);
343
        $this->components->twoColumnDetail('Table', $table);
344
 
345
        if ($policy) {
346
            $this->components->twoColumnDetail('Policy', $policy);
347
        }
348
 
349
        $this->newLine();
350
 
351
        $this->components->twoColumnDetail(
352
            '<fg=green;options=bold>Attributes</>',
353
            'type <fg=gray>/</> <fg=yellow;options=bold>cast</>',
354
        );
355
 
356
        foreach ($attributes as $attribute) {
357
            $first = trim(sprintf(
358
                '%s %s',
359
                $attribute['name'],
360
                collect(['increments', 'unique', 'nullable', 'fillable', 'hidden', 'appended'])
361
                    ->filter(fn ($property) => $attribute[$property])
362
                    ->map(fn ($property) => sprintf('<fg=gray>%s</>', $property))
363
                    ->implode('<fg=gray>,</> ')
364
            ));
365
 
366
            $second = collect([
367
                $attribute['type'],
368
                $attribute['cast'] ? '<fg=yellow;options=bold>'.$attribute['cast'].'</>' : null,
369
            ])->filter()->implode(' <fg=gray>/</> ');
370
 
371
            $this->components->twoColumnDetail($first, $second);
372
 
373
            if ($attribute['default'] !== null) {
374
                $this->components->bulletList(
375
                    [sprintf('default: %s', $attribute['default'])],
376
                    OutputInterface::VERBOSITY_VERBOSE
377
                );
378
            }
379
        }
380
 
381
        $this->newLine();
382
 
383
        $this->components->twoColumnDetail('<fg=green;options=bold>Relations</>');
384
 
385
        foreach ($relations as $relation) {
386
            $this->components->twoColumnDetail(
387
                sprintf('%s <fg=gray>%s</>', $relation['name'], $relation['type']),
388
                $relation['related']
389
            );
390
        }
391
 
392
        $this->newLine();
393
 
394
        $this->components->twoColumnDetail('<fg=green;options=bold>Observers</>');
395
 
396
        if ($observers->count()) {
397
            foreach ($observers as $observer) {
398
                $this->components->twoColumnDetail(
399
                    sprintf('%s', $observer['event']),
400
                    implode(', ', $observer['observer'])
401
                );
402
            }
403
        }
404
 
405
        $this->newLine();
406
    }
407
 
408
    /**
409
     * Get the cast type for the given column.
410
     *
411
     * @param  string  $column
412
     * @param  \Illuminate\Database\Eloquent\Model  $model
413
     * @return string|null
414
     */
415
    protected function getCastType($column, $model)
416
    {
417
        if ($model->hasGetMutator($column) || $model->hasSetMutator($column)) {
418
            return 'accessor';
419
        }
420
 
421
        if ($model->hasAttributeMutator($column)) {
422
            return 'attribute';
423
        }
424
 
425
        return $this->getCastsWithDates($model)->get($column) ?? null;
426
    }
427
 
428
    /**
429
     * Get the model casts, including any date casts.
430
     *
431
     * @param  \Illuminate\Database\Eloquent\Model  $model
432
     * @return \Illuminate\Support\Collection
433
     */
434
    protected function getCastsWithDates($model)
435
    {
436
        return collect($model->getDates())
437
            ->filter()
438
            ->flip()
439
            ->map(fn () => 'datetime')
440
            ->merge($model->getCasts());
441
    }
442
 
443
    /**
444
     * Get the type of the given column.
445
     *
446
     * @param  \Doctrine\DBAL\Schema\Column  $column
447
     * @return string
448
     */
449
    protected function getColumnType($column)
450
    {
451
        $name = $column->getType()->getName();
452
 
453
        $unsigned = $column->getUnsigned() ? ' unsigned' : '';
454
 
455
        $details = match (get_class($column->getType())) {
456
            DecimalType::class => $column->getPrecision().','.$column->getScale(),
457
            default => $column->getLength(),
458
        };
459
 
460
        if ($details) {
461
            return sprintf('%s(%s)%s', $name, $details, $unsigned);
462
        }
463
 
464
        return sprintf('%s%s', $name, $unsigned);
465
    }
466
 
467
    /**
468
     * Get the default value for the given column.
469
     *
470
     * @param  \Doctrine\DBAL\Schema\Column  $column
471
     * @param  \Illuminate\Database\Eloquent\Model  $model
472
     * @return mixed|null
473
     */
474
    protected function getColumnDefault($column, $model)
475
    {
476
        $attributeDefault = $model->getAttributes()[$column->getName()] ?? null;
477
 
478
        return match (true) {
479
            $attributeDefault instanceof BackedEnum => $attributeDefault->value,
480
            $attributeDefault instanceof UnitEnum => $attributeDefault->name,
481
            default => $attributeDefault ?? $column->getDefault(),
482
        };
483
    }
484
 
485
    /**
486
     * Determine if the given attribute is hidden.
487
     *
488
     * @param  string  $attribute
489
     * @param  \Illuminate\Database\Eloquent\Model  $model
490
     * @return bool
491
     */
492
    protected function attributeIsHidden($attribute, $model)
493
    {
494
        if (count($model->getHidden()) > 0) {
495
            return in_array($attribute, $model->getHidden());
496
        }
497
 
498
        if (count($model->getVisible()) > 0) {
499
            return ! in_array($attribute, $model->getVisible());
500
        }
501
 
502
        return false;
503
    }
504
 
505
    /**
506
     * Determine if the given attribute is unique.
507
     *
508
     * @param  string  $column
509
     * @param  \Doctrine\DBAL\Schema\Index[]  $indexes
510
     * @return bool
511
     */
512
    protected function columnIsUnique($column, $indexes)
513
    {
514
        return collect($indexes)
515
            ->filter(fn (Index $index) => count($index->getColumns()) === 1 && $index->getColumns()[0] === $column)
516
            ->contains(fn (Index $index) => $index->isUnique());
517
    }
518
 
519
    /**
520
     * Qualify the given model class base name.
521
     *
522
     * @param  string  $model
523
     * @return string
524
     *
525
     * @see \Illuminate\Console\GeneratorCommand
526
     */
527
    protected function qualifyModel(string $model)
528
    {
529
        if (str_contains($model, '\\') && class_exists($model)) {
530
            return $model;
531
        }
532
 
533
        $model = ltrim($model, '\\/');
534
 
535
        $model = str_replace('/', '\\', $model);
536
 
537
        $rootNamespace = $this->laravel->getNamespace();
538
 
539
        if (Str::startsWith($model, $rootNamespace)) {
540
            return $model;
541
        }
542
 
543
        return is_dir(app_path('Models'))
544
            ? $rootNamespace.'Models\\'.$model
545
            : $rootNamespace.$model;
546
    }
547
}