GenerateIdeHelperLayoutsCommand   A
last analyzed

Complexity

Total Complexity 29

Size/Duplication

Total Lines 188
Duplicated Lines 0 %

Test Coverage

Coverage 3.13%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 94
c 2
b 0
f 0
dl 0
loc 188
ccs 3
cts 96
cp 0.0313
rs 10
wmc 29

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A handle() 0 15 2
B getPropertiesFromMethods() 0 36 9
A extractReflectionTypes() 0 16 4
A getReturnTypeFromReflection() 0 16 3
A getReflectionNamedType() 0 8 2
A createLayoutPhpDocs() 0 29 3
A generateDocs() 0 36 5
1
<?php
2
3
namespace NovaFlexibleContent\Console\Commands;
4
5
use Illuminate\Console\Command;
6
use Illuminate\Database\Eloquent\Casts\Attribute;
7
use Illuminate\Filesystem\Filesystem;
8
use Illuminate\Support\Str;
9
use Laravel\Nova\Fields\Field;
10
use NovaFlexibleContent\Flexible;
11
use NovaFlexibleContent\Layouts\Collections\LayoutsCollection;
12
use NovaFlexibleContent\Layouts\Layout;
13
use ReflectionClass;
14
use ReflectionNamedType;
15
use ReflectionType;
16
use Symfony\Component\Finder\Finder;
17
18
class GenerateIdeHelperLayoutsCommand extends Command
19
{
20
    protected $signature = 'nova-flexible-content:ide-helper:layouts
21
     {--filename= : File name}
22
     ';
23
24
    protected $description = '';
25
26
    protected Filesystem $files;
27
28 38
    public function __construct(Filesystem $files)
29
    {
30 38
        parent::__construct();
31 38
        $this->files = $files;
32
    }
33
34
    public function handle()
35
    {
36
        $filename = $this->option('filename') ?? '_ide_helper_flexible_layouts.php';
37
38
39
        $content = $this->generateDocs();
40
41
        $written = $this->files->put($filename, $content);
42
        if ($written !== false) {
43
            $this->info("Layout(s) information was written to $filename");
44
        } else {
45
            $this->error("Failed to write model information to $filename");
46
        }
47
48
        return 0;
49
    }
50
51
    protected function generateDocs(): string
52
    {
53
        $formatterPrefix = '@';
54
        $output          = "<?php
55
// {$formatterPrefix}formatter:off
56
/**
57
 * A helper file for your Flexible Layouts
58
 *
59
 * @author Think Dev Team <[email protected]>
60
 */
61
\n\n";
62
63
        $namespace = app()->getNamespace();
0 ignored issues
show
introduced by
The method getNamespace() does not exist on Illuminate\Container\Container. Are you sure you never get this type here, but always one of the subclasses? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

63
        $namespace = app()->/** @scrutinizer ignore-call */ getNamespace();
Loading history...
64
65
        $layouts = [];
66
67
        foreach ((new Finder())->in(app_path('Nova'))->files() as $resource) {
68
            $resource = $namespace . str_replace(
69
                ['/', '.php'],
70
                ['\\', ''],
71
                Str::after($resource->getPathname(), app_path() . DIRECTORY_SEPARATOR)
72
            );
73
74
            if (
75
                is_subclass_of($resource, Layout::class) &&
76
                !(new ReflectionClass($resource))->isAbstract()
77
            ) {
78
                $layouts[] = $resource;
79
            }
80
        }
81
82
        foreach ($layouts as $layout) {
83
            $output .= $this->createLayoutPhpDocs($layout);
84
        }
85
86
        return $output;
87
    }
88
89
    /**
90
     * @param class-string<Layout> $layout
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<Layout> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<Layout>.
Loading history...
91
     * @return string
92
     */
93
    protected function createLayoutPhpDocs(string $layout): string
94
    {
95
        $output                     = '';
96
        $parentClassName            = Layout::class;
97
        $layoutsCollectionClassName = LayoutsCollection::class;
98
        $className                  = class_basename($layout);
99
        $namespace                  = trim(Str::beforeLast($layout, $className), '\\');
100
101
        $output .= "namespace {$namespace} {\n\n";
102
103
        $output .= "/**\n";
104
        $output .= "* {$layout}\n";
105
        $output .= "*\n";
106
        /** @var Field $field */
107
        foreach ($layout::make()->fieldsCollection() as $field) {
108
            $output .= "* @property-read mixed \${$field->attribute}\n";
109
            if ($field instanceof Flexible) {
110
                $fieldName = 'flexible' . Str::ucfirst(Str::camel($field->attribute));
111
112
                $output .= "* @property-read \\{$layoutsCollectionClassName} \${$fieldName}\n";
113
            }
114
        }
115
        $output .= $this->getPropertiesFromMethods($layout);
116
        $output .= "*/\n";
117
        $output .= "class {$className} extends {$parentClassName} {}\n\n";
118
119
        $output .= "}\n\n";
120
121
        return $output;
122
    }
123
124
    public function getPropertiesFromMethods(string $layout): string
125
    {
126
127
        $properties = [];
128
129
        $methods = get_class_methods($layout);
130
        if ($methods) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $methods of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
131
            sort($methods);
132
            foreach ($methods as $method) {
133
                $reflection  = new \ReflectionMethod($layout, $method);
134
                $type        = $this->getReturnTypeFromReflection($reflection);
135
                $isAttribute = is_a($type, Attribute::class, true);
136
                if (
137
                    Str::startsWith($method, 'get') && Str::endsWith(
138
                        $method,
139
                        'Attribute'
140
                    ) && $method !== 'getAttribute'
141
                ) {
142
                    //Magic get<name>Attribute
143
                    $name = Str::snake(substr($method, 3, -9));
144
                    if (!empty($name)) {
145
                        $type              = $this->getReturnTypeFromReflection($reflection);
146
                        $properties[$name] = $type;
147
                    }
148
                } elseif ($isAttribute) {
149
                    $properties[Str::snake($method)] = 'mixed';
150
                }
151
            }
152
        }
153
154
        $output = '';
155
        foreach ($properties as $name => $type) {
156
            $output .= "* @property-read $type \${$name}\n";
157
        }
158
159
        return $output;
160
    }
161
162
    protected function getReturnTypeFromReflection(\ReflectionMethod $reflection): ?string
163
    {
164
        $returnType = $reflection->getReturnType();
165
        if (!$returnType) {
166
            return null;
167
        }
168
169
        $types = $this->extractReflectionTypes($returnType);
170
171
        $type = implode('|', $types);
172
173
        if ($returnType->allowsNull()) {
174
            $type .= '|null';
175
        }
176
177
        return $type;
178
    }
179
180
    protected function extractReflectionTypes(ReflectionType $reflection_type): array
181
    {
182
        if ($reflection_type instanceof ReflectionNamedType) {
183
            $types[] = $this->getReflectionNamedType($reflection_type);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$types was never initialized. Although not strictly required by PHP, it is generally a good practice to add $types = array(); before regardless.
Loading history...
184
        } else {
185
            $types = [];
186
            foreach ($reflection_type->getTypes() as $named_type) {
0 ignored issues
show
Bug introduced by
The method getTypes() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionUnionType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

186
            foreach ($reflection_type->/** @scrutinizer ignore-call */ getTypes() as $named_type) {
Loading history...
187
                if ($named_type->getName() === 'null') {
188
                    continue;
189
                }
190
191
                $types[] = $this->getReflectionNamedType($named_type);
192
            }
193
        }
194
195
        return $types;
196
    }
197
198
    protected function getReflectionNamedType(ReflectionNamedType $paramType): string
199
    {
200
        $parameterName = $paramType->getName();
201
        if (!$paramType->isBuiltin()) {
202
            $parameterName = '\\' . $parameterName;
203
        }
204
205
        return $parameterName;
206
    }
207
}
208