GenerateBuilderCommand::getBuilderClass()   C
last analyzed

Complexity

Conditions 8
Paths 38

Size

Total Lines 45
Code Lines 25

Duplication

Lines 3
Ratio 6.67 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 3
loc 45
ccs 0
cts 34
cp 0
rs 5.3846
c 1
b 0
f 0
cc 8
eloc 25
nc 38
nop 1
crap 72
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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())) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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