1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* This file is part of the Abstract builder package, an RunOpenCode project. |
4
|
|
|
* |
5
|
|
|
* (c) 2017 RunOpenCode |
6
|
|
|
* |
7
|
|
|
* For the full copyright and license information, please view the LICENSE |
8
|
|
|
* file that was distributed with this source code. |
9
|
|
|
*/ |
10
|
|
|
namespace RunOpenCode\AbstractBuilder\Command; |
11
|
|
|
|
12
|
|
|
use RunOpenCode\AbstractBuilder\AbstractBuilder; |
13
|
|
|
use RunOpenCode\AbstractBuilder\Ast\Metadata\FileMetadata; |
14
|
|
|
use RunOpenCode\AbstractBuilder\Ast\MetadataLoader; |
15
|
|
|
use RunOpenCode\AbstractBuilder\Ast\Printer; |
16
|
|
|
use RunOpenCode\AbstractBuilder\Command\Question\ClassChoice; |
17
|
|
|
use RunOpenCode\AbstractBuilder\Command\Question\GetterMethodChoice; |
18
|
|
|
use RunOpenCode\AbstractBuilder\Command\Question\MethodChoice; |
19
|
|
|
use RunOpenCode\AbstractBuilder\Command\Question\SetterMethodChoice; |
20
|
|
|
use RunOpenCode\AbstractBuilder\Command\Style\RunOpenCodeStyle; |
21
|
|
|
use RunOpenCode\AbstractBuilder\Exception\InvalidArgumentException; |
22
|
|
|
use RunOpenCode\AbstractBuilder\Exception\RuntimeException; |
23
|
|
|
use RunOpenCode\AbstractBuilder\Generator\BuilderClassFactory; |
24
|
|
|
use RunOpenCode\AbstractBuilder\ReflectiveAbstractBuilder; |
25
|
|
|
use RunOpenCode\AbstractBuilder\Utils\ClassUtils; |
26
|
|
|
use Symfony\Component\Console\Command\Command; |
27
|
|
|
use Symfony\Component\Console\Input\InputArgument; |
28
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
29
|
|
|
use Symfony\Component\Console\Input\InputOption; |
30
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
31
|
|
|
use Symfony\Component\Console\Question\ChoiceQuestion; |
32
|
|
|
use Symfony\Component\Console\Question\Question; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Class GenerateBuilderCommand |
36
|
|
|
* |
37
|
|
|
* @package RunOpenCode\AbstractBuilder\Command |
38
|
|
|
*/ |
39
|
|
|
class GenerateBuilderCommand extends Command |
40
|
|
|
{ |
41
|
|
|
/** |
42
|
|
|
* @var RunOpenCodeStyle |
43
|
|
|
*/ |
44
|
|
|
private $style; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @var InputInterface |
48
|
|
|
*/ |
49
|
|
|
private $input; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var OutputInterface |
53
|
|
|
*/ |
54
|
|
|
private $output; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* {@inheritdoc} |
58
|
|
|
*/ |
59
|
|
|
protected function configure() |
60
|
|
|
{ |
61
|
|
|
$this |
62
|
|
|
->setName('runopencode:generate:builder') |
63
|
|
|
->setDescription('Generates builder class skeleton for provided class.') |
64
|
|
|
->addArgument('class', InputArgument::OPTIONAL, 'Full qualified class name of building object that can be autoloaded, or path to file with class definition.') |
65
|
|
|
->addArgument('builder', InputArgument::OPTIONAL, 'Full qualified class name of builder class can be autoloaded, or it will be autoloaded, or path to file with class definition.') |
66
|
|
|
->addArgument('location', InputArgument::OPTIONAL, 'Path to location of file where builder class will be saved.') |
67
|
|
|
->addOption('all', '-a', InputOption::VALUE_NONE, 'Generate all methods by default.') |
68
|
|
|
->addOption('rtd', '-r', InputOption::VALUE_NONE, 'Generate methods with return types declarations.') |
69
|
|
|
->addOption('print', '-p', InputOption::VALUE_NONE, 'Only display code without creating/modifying class file.'); |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* {@inheritdoc} |
74
|
|
|
*/ |
75
|
|
|
public function execute(InputInterface $input, OutputInterface $output) |
76
|
|
|
{ |
77
|
|
|
$this->input = $input; |
78
|
|
|
$this->output = $output; |
79
|
|
|
$this->style = new RunOpenCodeStyle($input, $output); |
80
|
|
|
|
81
|
|
|
$this->style->displayLogo(); |
82
|
|
|
|
83
|
|
|
$this->style->title('Generate builder class'); |
84
|
|
|
|
85
|
|
|
try { |
86
|
|
|
/** |
87
|
|
|
* @var ClassChoice $subjectChoice |
88
|
|
|
*/ |
89
|
|
|
$subjectChoice = $this->getBuildingClass(); |
90
|
|
|
$this->style->info(sprintf('Builder class for class "%s" will be generated.', $subjectChoice->getClass()->getName())); |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* @var ClassChoice $builderChoice |
94
|
|
|
*/ |
95
|
|
|
$builderChoice = $this->getBuilderClass($subjectChoice); |
96
|
|
|
$this->style->info(sprintf('Full qualified namespace for builder class is "%s".', $builderChoice->getClass()->getName())); |
97
|
|
|
$this->style->info(sprintf('Path to file where builder class will be saved is "%s".', $builderChoice->getFile()->getFilename())); |
98
|
|
|
$builderChoice->getClass()->isAutoloadable() ? $this->style->info('Existing builder class will be updated.') : $this->style->info('New builder class will be created.'); |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* @var MethodChoice[] $methods |
102
|
|
|
*/ |
103
|
|
|
$methodChoices = $this->getMethodsToGenerate($subjectChoice, $builderChoice); |
104
|
|
|
$this->style->info('Methods to generate are:'); |
105
|
|
|
$this->style->ul($methodChoices); |
106
|
|
|
|
107
|
|
|
$classFactory = new BuilderClassFactory($subjectChoice->getClass(), $builderChoice->getClass(), $this->input->getOption('rtd')); |
108
|
|
|
|
109
|
|
|
foreach ($methodChoices as $methodChoice) { |
110
|
|
|
|
111
|
|
|
if ($methodChoice instanceof GetterMethodChoice) { |
112
|
|
|
$classFactory->addGetter($methodChoice->getMethodName(), $methodChoice->getParameter()); |
113
|
|
|
continue; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
if ($methodChoice instanceof SetterMethodChoice) { |
117
|
|
|
$classFactory->addSetter($methodChoice->getMethodName(), $methodChoice->getParameter()); |
118
|
|
|
continue; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
throw new RuntimeException(sprintf('Expected instance of "%s" or "%s", got "%s".', GetterMethodChoice::class, SetterMethodChoice::class, get_class($methodChoice))); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
$this->write($builderChoice->getFile()); |
125
|
|
|
|
126
|
|
|
$this->style->success('Builder class successfully generated!'); |
127
|
|
|
return 0; |
128
|
|
|
|
129
|
|
|
} catch (\Exception $e) { |
130
|
|
|
$this->style->error($e->getMessage()); |
131
|
|
|
return -1; |
132
|
|
|
} |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Get class name for which skeleton should be built. |
137
|
|
|
* |
138
|
|
|
* @return ClassChoice |
139
|
|
|
* |
140
|
|
|
* @throws \RunOpenCode\AbstractBuilder\Exception\InvalidArgumentException |
141
|
|
|
* @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException |
142
|
|
|
*/ |
143
|
|
|
private function getBuildingClass() |
144
|
|
|
{ |
145
|
|
|
$class = $this->input->getArgument('class'); |
146
|
|
|
|
147
|
|
View Code Duplication |
if (null === $class) { |
|
|
|
|
148
|
|
|
$helper = $this->getHelper('question'); |
149
|
|
|
$question = new Question('Enter full qualified class name, or path to file with class, for which you want to generate builder class: ', null); |
150
|
|
|
|
151
|
|
|
$class = $helper->ask($this->input, $this->output, $question); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
$fileMetadata = MetadataLoader::create()->load($class); |
155
|
|
|
$classMetadata = null; |
156
|
|
|
|
157
|
|
|
if (class_exists($class)) { |
158
|
|
|
$classMetadata = $fileMetadata->getClass($class); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
if (1 === count($fileMetadata->getClasses())) { |
162
|
|
|
$classMetadata = array_values($fileMetadata->getClasses())[0]; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
View Code Duplication |
if (null === $classMetadata) { |
|
|
|
|
166
|
|
|
throw new RuntimeException(sprintf('It is not possible to extract single class metadata from "%s", found %s definition(s).', $class, count($fileMetadata->getClasses()))); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
if (!ClassUtils::isBuildable($classMetadata)) { |
170
|
|
|
throw new InvalidArgumentException(sprintf('Builder class can not be generated for "%s", class has to have constructor with some parameters.', $classMetadata->getName())); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
return new ClassChoice($fileMetadata, $classMetadata); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Get class name for builder class. |
178
|
|
|
* |
179
|
|
|
* @param ClassChoice $subjectChoice |
180
|
|
|
* |
181
|
|
|
* @return ClassChoice |
182
|
|
|
* @throws \RunOpenCode\AbstractBuilder\Exception\InvalidArgumentException |
183
|
|
|
* |
184
|
|
|
* @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException |
185
|
|
|
*/ |
186
|
|
|
private function getBuilderClass(ClassChoice $subjectChoice) |
187
|
|
|
{ |
188
|
|
|
$class = $this->input->getArgument('builder'); |
189
|
|
|
|
190
|
|
|
if (null === $class) { |
191
|
|
|
$default = sprintf('%sBuilder', $subjectChoice->getClass()->getShortName()); |
192
|
|
|
$helper = $this->getHelper('question'); |
193
|
|
|
$question = new Question(sprintf('Enter full qualified class name of your builder class (default: "%s"): ', $default), $default); |
194
|
|
|
|
195
|
|
|
$class = $helper->ask($this->input, $this->output, $question); |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
$classChoice = null; |
199
|
|
|
|
200
|
|
|
if (class_exists($class, true)) { |
201
|
|
|
$fileMetadata = MetadataLoader::create()->load($class); |
202
|
|
|
$classMetadata = $fileMetadata->getClass($class); |
203
|
|
|
$classChoice = new ClassChoice($fileMetadata, $classMetadata); |
204
|
|
|
|
205
|
|
|
if (null !== $this->input->getArgument('location')) { |
206
|
|
|
throw new InvalidArgumentException('Builder class already exists and its location can not be changed.'); |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
if (file_exists($class)) { |
211
|
|
|
$fileMetadata = MetadataLoader::create()->load($class); |
212
|
|
|
|
213
|
|
View Code Duplication |
if (1 !== count($fileMetadata->getClasses())) { |
|
|
|
|
214
|
|
|
throw new RuntimeException(sprintf('It is not possible to extract single class metadata from "%s", found %s definition(s).', $class, count($fileMetadata->getClasses()))); |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
$classMetadata = array_values($fileMetadata->getClasses())[0]; |
218
|
|
|
$classChoice = new ClassChoice($fileMetadata, $classMetadata); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
if (null === $classChoice) { |
222
|
|
|
$classChoice = $this->generateBuilder($subjectChoice, $class); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
if (!ClassUtils::isBuilder($classChoice->getClass())) { |
226
|
|
|
throw new RuntimeException(sprintf('Builder class must implement either "%s" or "%s", none of those detected for "%s".', ReflectiveAbstractBuilder::class, AbstractBuilder::class, $classChoice->getClass()->getName())); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
return $classChoice; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Generate new builder class. |
234
|
|
|
* |
235
|
|
|
* @param ClassChoice $subjectChoice |
236
|
|
|
* @param string $builderClassName |
237
|
|
|
* |
238
|
|
|
* @return ClassChoice |
239
|
|
|
* |
240
|
|
|
* @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException |
241
|
|
|
* @throws \RunOpenCode\AbstractBuilder\Exception\InvalidArgumentException |
242
|
|
|
*/ |
243
|
|
|
private function generateBuilder(ClassChoice $subjectChoice, $builderClassName) |
244
|
|
|
{ |
245
|
|
|
$location = $this->input->getArgument('location'); |
246
|
|
|
|
247
|
|
View Code Duplication |
if (null === $location) { |
|
|
|
|
248
|
|
|
$helper = $this->getHelper('question'); |
249
|
|
|
$question = new Question('Enter path where you want to store a builder class: ', null); |
250
|
|
|
|
251
|
|
|
$location = $helper->ask($this->input, $this->output, $question); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
$location = str_replace('\\', '/', ltrim($location, '/')); |
255
|
|
|
|
256
|
|
|
if (substr($location, -strlen($location)) !== '.php') { |
257
|
|
|
$location = $location.'/'.ClassUtils::getShortName($builderClassName).'.php'; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
$directory = realpath(dirname($location)); |
261
|
|
|
|
262
|
|
|
if (!is_dir($directory)) { |
263
|
|
|
throw new RuntimeException(sprintf('Provided path to directory "%s" where builder class ought to be stored is not path to directory.', $directory)); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
if (!is_writable($directory)) { |
267
|
|
|
throw new RuntimeException(sprintf('Directory on path "%s" is not writeable.', $directory)); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
if (is_file($location) && !is_writable($location)) { |
271
|
|
|
throw new RuntimeException(sprintf('Provided path to builder class "%s" is not writeable.', $location)); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
$fileMetadata = (new BuilderClassFactory($subjectChoice->getClass(), null, $this->input->getOption('rtd')))->initialize($location, $builderClassName); |
275
|
|
|
$classMetadata = array_values($fileMetadata->getClasses())[0]; |
276
|
|
|
|
277
|
|
|
return new ClassChoice($fileMetadata, $classMetadata); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
/** |
281
|
|
|
* Get methods which ought to be generated. |
282
|
|
|
* |
283
|
|
|
* @param ClassChoice $subjectChoice |
284
|
|
|
* @param ClassChoice $builderChoice |
285
|
|
|
* |
286
|
|
|
* @return MethodChoice[] |
287
|
|
|
* |
288
|
|
|
* @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException |
289
|
|
|
*/ |
290
|
|
|
private function getMethodsToGenerate(ClassChoice $subjectChoice, ClassChoice $builderChoice) |
291
|
|
|
{ |
292
|
|
|
$methods = []; |
293
|
|
|
|
294
|
|
|
$buildingClass = $subjectChoice->getClass(); |
295
|
|
|
$builderClass = $builderChoice->getClass(); |
296
|
|
|
|
297
|
|
|
$parameters = $buildingClass->getPublicMethod('__construct')->getParameters(); |
298
|
|
|
|
299
|
|
|
foreach ($parameters as $parameter) { |
300
|
|
|
$getter = new GetterMethodChoice($parameter); |
301
|
|
|
$setter = new SetterMethodChoice($parameter); |
302
|
|
|
|
303
|
|
|
if (!$builderClass->hasPublicMethod($getter->getMethodName())) { |
304
|
|
|
$methods[] = $getter; |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
if (!$builderClass->hasPublicMethod($setter->getMethodName())) { |
308
|
|
|
$methods[] = $setter; |
309
|
|
|
} |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
if (0 === count($methods)) { |
313
|
|
|
throw new RuntimeException('There are no methods to generate.'); |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
if (true !== $this->input->getOption('all')) { |
317
|
|
|
|
318
|
|
|
$helper = $this->getHelper('question'); |
319
|
|
|
|
320
|
|
|
$question = new ChoiceQuestion( |
321
|
|
|
'Choose which methods you want to generate for your builder class (separate choices with coma, enter none for all choices):', |
322
|
|
|
$methods, |
323
|
|
|
implode(',', array_keys($methods)) |
324
|
|
|
); |
325
|
|
|
|
326
|
|
|
$question->setMultiselect(true); |
327
|
|
|
|
328
|
|
|
$selected = $helper->ask($this->input, $this->output, $question); |
329
|
|
|
|
330
|
|
|
$methods = array_filter($methods, function(MethodChoice $choice) use ($selected) { |
331
|
|
|
return in_array((string) $choice, $selected, true); |
332
|
|
|
}); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
return $methods; |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* Print or display builder class file |
340
|
|
|
* |
341
|
|
|
* @param FileMetadata $file |
342
|
|
|
*/ |
343
|
|
|
private function write(FileMetadata $file) |
344
|
|
|
{ |
345
|
|
|
if ($this->input->getOption('print')) { |
346
|
|
|
|
347
|
|
|
$this->style->title('Generated code:'); |
348
|
|
|
|
349
|
|
|
$lines = explode("\n", Printer::getInstance()->print($file)); |
350
|
|
|
|
351
|
|
|
$counter = 0; |
352
|
|
|
|
353
|
|
|
foreach ($lines as $line) { |
354
|
|
|
$this->style->writeln(sprintf('%s: %s', ++$counter, $line)); |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
return; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
Printer::getInstance()->dump($file); |
361
|
|
|
} |
362
|
|
|
} |
363
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.