Passed
Branch feature/first-implementation (72fcf8)
by Andrea Marco
04:38
created

MakeDtoCommand::buildClass()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 9
rs 10
1
<?php
2
3
namespace Cerbero\LaravelDto\Console\Commands;
4
5
use Cerbero\LaravelDto\Console\DtoGenerationData;
6
use Cerbero\LaravelDto\Console\Manifest;
7
use Cerbero\LaravelDto\Console\ModelPropertiesMapper;
8
use Cerbero\LaravelDto\Console\DtoQualifierContract;
9
use Illuminate\Console\GeneratorCommand;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Filesystem\Filesystem;
12
use Symfony\Component\Console\Exception\InvalidArgumentException;
13
14
/**
15
 * The Artisan command to generate DTOs.
16
 *
17
 */
18
class MakeDtoCommand extends GeneratorCommand
19
{
20
    /**
21
     * The console command description.
22
     *
23
     * @var string
24
     */
25
    protected $description = 'Create a new DTO class';
26
27
    /**
28
     * The name and signature of the console command.
29
     *
30
     * @var string
31
     */
32
    protected $signature = 'make:dto
33
        {name : The model to create the DTO for, e.g. App/User}
34
        {--f|force : Create the class even if the DTO already exists}';
35
36
    /**
37
     * The models use statements.
38
     *
39
     * @var array
40
     */
41
    protected $useStatements = [];
42
43
    /**
44
     * The DTO class qualifier.
45
     *
46
     * @var DtoQualifierContract
47
     */
48
    protected $qualifier;
49
50
    /**
51
     * The DTO generation manifest.
52
     *
53
     * @var Manifest
54
     */
55
    protected $manifest;
56
57
    /**
58
     * Instantiate the class.
59
     *
60
     * @param Filesystem $files
61
     * @param DtoQualifierContract $qualifier
62
     * @param Manifest $manifest
63
     */
64
    public function __construct(Filesystem $files, DtoQualifierContract $qualifier, Manifest $manifest)
65
    {
66
        parent::__construct($files);
67
68
        $this->qualifier = $qualifier;
69
        $this->manifest = $manifest;
70
    }
71
72
    /**
73
     * Get the stub file for the generator.
74
     *
75
     * @return string
76
     */
77
    protected function getStub(): string
78
    {
79
        return __DIR__ . '/../stubs/dto.stub';
80
    }
81
82
    /**
83
     * Parse the class name and format according to the root namespace.
84
     *
85
     * @param string $name
86
     * @return string
87
     */
88
    protected function qualifyClass($name): string
89
    {
90
        return $this->type = $this->qualifier->qualify($name);
91
    }
92
93
    /**
94
     * Get the desired class name from the input.
95
     *
96
     * @return string
97
     */
98
    protected function getNameInput(): string
99
    {
100
        $model = str_replace('/', '\\', parent::getNameInput());
101
102
        if (is_subclass_of($model, Model::class)) {
103
            return $model;
104
        }
105
106
        throw new InvalidArgumentException("Invalid model [$model]");
107
    }
108
109
    /**
110
     * Build the class with the given name.
111
     *
112
     * @param string $name
113
     * @return string
114
     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
115
     */
116
    protected function buildClass($name): string
117
    {
118
        $this->manifest->addStartingDto($name)->addGeneratingDto($name)->save();
119
120
        $search = ['DummyModel', 'DummyProperties', 'DummyUseStatements'];
121
        $replace = [class_basename($this->getNameInput()), $this->getModelProperties(), $this->getUseStatements()];
122
        $content = parent::buildClass($name);
123
124
        return $this->sortUseStatements(str_replace($search, $replace, $content));
125
    }
126
127
    /**
128
     * Retrieve the given model properties
129
     *
130
     * @return string
131
     */
132
    protected function getModelProperties(): string
133
    {
134
        $properties = '';
135
        $map = $this->laravel->make(ModelPropertiesMapper::class)->map($this->getCommandDto());
136
        $this->type = $this->manifest->getGeneratingDto();
137
138
        foreach ($map as $name => $types) {
139
            foreach ($types as &$type) {
140
                $normalizedType = rtrim($type, '[]');
141
142
                if ($this->shouldBeAddedToUseStatements($normalizedType)) {
143
                    $this->useStatements[$normalizedType] = true;
144
                }
145
146
                $type = class_basename($type);
147
            }
148
149
            $type = implode('|', $types);
150
            $properties .= " * @property {$type} \${$name}\n";
151
        }
152
153
        return rtrim($properties);
154
    }
155
156
    /**
157
     * Retrieve the DTO generation data
158
     *
159
     * @return DtoGenerationData
160
     */
161
    protected function getCommandDto(): DtoGenerationData
162
    {
163
        return DtoGenerationData::make([
164
            'model_class' => $model = $this->getNameInput(),
165
            'model' => new $model,
166
            'forced' => $this->option('force') ?: false,
167
            'output' => $this->getOutput(),
168
        ]);
169
    }
170
171
    /**
172
     * Determine whether the given type should be added to use statements
173
     *
174
     * @param string $type
175
     * @return bool
176
     */
177
    protected function shouldBeAddedToUseStatements(string $type): bool
178
    {
179
        if ($this->hasSameNamespace($type)) {
180
            return false;
181
        }
182
183
        $usesStartingDto = $this->manifest->isStartingDto($type) && !$this->manifest->isGenerating($type);
184
        $generatedOrGenerating = $this->manifest->generated($type) || $this->manifest->generating($type);
185
186
        return $usesStartingDto || $generatedOrGenerating || class_exists($type);
187
    }
188
189
    /**
190
     * Determine whether the given class has the same namespace of the DTO that is going to be generated
191
     * 
192
     * @param string $class
193
     * @return bool
194
     */
195
    protected function hasSameNamespace(string $class): bool
196
    {
197
        $segmentsClass = explode('\\', $class);
198
        $segmentsDto = explode('\\', $this->manifest->getGeneratingDto());
199
200
        array_pop($segmentsClass);
201
        array_pop($segmentsDto);
202
203
        return $segmentsClass === $segmentsDto;
204
    }
205
206
    /**
207
     * Retrieve the use statements
208
     *
209
     * @return string|null
210
     */
211
    protected function getUseStatements(): ?string
212
    {
213
        $useStatements = '';
214
215
        foreach ($this->useStatements as $class => $value) {
216
            $useStatements .= "\nuse {$class};";
217
            unset($this->useStatements[$class]);
218
        }
219
220
        return $useStatements;
221
    }
222
223
    /**
224
     * Previous versions of Laravel didn't sort imports.
225
     * Overriding this method and using sortUseStatements() instead keeps generated DTOs consistent across all versions
226
     *
227
     * @param string $stub
228
     * @return string
229
     */
230
    protected function sortImports($stub)
231
    {
232
        return $stub;
233
    }
234
235
    /**
236
     * Alphabetically sorts the use statements for the given stub
237
     *
238
     * @param string $stub
239
     * @return string
240
     */
241
    protected function sortUseStatements($stub)
242
    {
243
        if (preg_match('/(?P<imports>(?:use [^;]+;$\n?)+)/m', $stub, $match)) {
244
            $imports = explode("\n", trim($match['imports']));
245
246
            sort($imports);
247
248
            return str_replace(trim($match['imports']), implode("\n", $imports), $stub);
249
        }
250
251
        return $stub;
252
    }
253
254
    /**
255
     * Destroy the class
256
     *
257
     * @return void
258
     */
259
    public function __destruct()
260
    {
261
        $this->manifest->delete();
262
    }
263
}
264