Completed
Push — master ( ed5962...9634df )
by Nikola
03:22
created

GenerateBuilderCommand::execute()   B

Complexity

Conditions 3
Paths 18

Size

Total Lines 39
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 3
Bugs 0 Features 0
Metric Value
dl 0
loc 39
ccs 0
cts 23
cp 0
rs 8.8571
c 3
b 0
f 0
cc 3
eloc 21
nc 18
nop 2
crap 12
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\Ast\ClassBuilder;
13
use RunOpenCode\AbstractBuilder\Ast\ClassLoader;
14
use RunOpenCode\AbstractBuilder\Ast\ClassMetadata;
15
use RunOpenCode\AbstractBuilder\Command\Question\GetterMethodChoice;
16
use RunOpenCode\AbstractBuilder\Command\Question\MethodChoice;
17
use RunOpenCode\AbstractBuilder\Command\Question\SetterMethodChoice;
18
use RunOpenCode\AbstractBuilder\Command\Style\RunOpenCodeStyle;
19
use RunOpenCode\AbstractBuilder\Exception\InvalidArgumentException;
20
use RunOpenCode\AbstractBuilder\Exception\RuntimeException;
21
use Symfony\Component\Console\Command\Command;
22
use Symfony\Component\Console\Input\InputArgument;
23
use Symfony\Component\Console\Input\InputInterface;
24
use Symfony\Component\Console\Input\InputOption;
25
use Symfony\Component\Console\Output\OutputInterface;
26
use Symfony\Component\Console\Question\ChoiceQuestion;
27
use Symfony\Component\Console\Question\Question;
28
29
/**
30
 * Class GenerateBuilderCommand
31
 *
32
 * @package RunOpenCode\AbstractBuilder\Command
33
 */
34
class GenerateBuilderCommand extends Command
35
{
36
    /**
37
     * @var RunOpenCodeStyle
38
     */
39
    private $style;
40
41
    /**
42
     * @var InputInterface
43
     */
44
    private $input;
45
46
    /**
47
     * @var OutputInterface
48
     */
49
    private $output;
50
51
    /**
52
     * @var ClassLoader
53
     */
54
    private $loader;
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('withReturnTypes', '-r', InputOption::VALUE_NONE, 'Generate methods with return types declarations.');
69
70
        $this->loader = new ClassLoader();
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76
    public function execute(InputInterface $input, OutputInterface $output)
77
    {
78
        $this->input = $input;
79
        $this->output = $output;
80
        $this->style = new RunOpenCodeStyle($input, $output);
81
82
        $this->style->displayLogo();
83
84
        $this->style->title('Generate builder class');
85
86
        try {
87
            /**
88
             * @var ClassMetadata $buildingClass
89
             */
90
            $buildingClass = $this->getBuildingClass();
91
            $this->style->info(sprintf('Builder class for class "%s" will be generated.', $buildingClass->getFqcn()));
92
93
            /**
94
             * @var ClassMetadata $builderClass
95
             */
96
            $builderClass = $this->getBuilderClass($buildingClass);
97
            $this->style->info(sprintf('Full qualified namespace for builder class is "%s".', $builderClass->getFqcn()));
98
            $this->style->info(sprintf('Path to file where builder class will be saved is "%s".', $builderClass->getFilename()));
99
            $builderClass->isAutoloadable() ? $this->style->info('Existing builder class will be updated.') : $this->style->info('New builder class will be created.');
100
101
            $methods = $this->getMethodsToGenerate($buildingClass, $builderClass);
102
            $this->style->info('Methods to generate are:');
103
            $this->style->ul($methods);
104
105
            $builder = ClassBuilder::create($buildingClass, $builderClass, array_map(function(MethodChoice $choice) { return $choice->getMethod(); }, $methods));
0 ignored issues
show
Bug introduced by
The method getMethod() does not exist on RunOpenCode\AbstractBuil...d\Question\MethodChoice. Did you maybe mean getMethodName()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
106
107
            $this->style->write($builder->display());
108
109
        } catch (\Exception $e) {
110
            $this->style->error($e->getMessage());
111
            return 0;
112
        }
113
114
    }
115
116
    /**
117
     * Get class name for which skeleton should be built.
118
     *
119
     * @return ClassMetadata
120
     *
121
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
122
     */
123
    private function getBuildingClass()
124
    {
125
        $class = $this->input->getArgument('class');
126
127
        if (null === $class) {
128
            $helper = $this->getHelper('question');
129
            $question = new Question('Enter full qualified class name, or path to file with class, for which you want to generate builder class: ', null);
130
131
            $class = $helper->ask($this->input, $this->output, $question);
132
        }
133
134
        $metadata = $this->loader->load($class);
135
136
        if (null === ($constructor = $metadata->getConstructor())) {
137
            throw new InvalidArgumentException('Builder class can not be generated for class without constructor.');
138
        }
139
140
        if (0 === count($constructor->getParameters())) {
141
            throw new InvalidArgumentException('Builder class can not be generated for class with constructor without arguments.');
142
        }
143
144
        return $metadata;
145
    }
146
147
    /**
148
     * Get class name for builder class.
149
     *
150
     * @param ClassMetadata $buildingClass
151
     *
152
     * @return ClassMetadata
153
     *
154
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
155
     */
156
    private function getBuilderClass(ClassMetadata $buildingClass)
157
    {
158
        $class = $this->input->getArgument('builder');
159
160
        if (null === $class) {
161
            $default = sprintf('%sBuilder', $buildingClass->getFqcn());
162
            $helper = $this->getHelper('question');
163
            $question = new Question(sprintf('Enter full qualified class name of your builder class (default: "%s"): ', $default), $default);
164
165
            $class = $helper->ask($this->input, $this->output, $question);
166
        }
167
168
        if (class_exists($class, true) || file_exists($class)) {
169
            return $this->loader->load($class);
170
        }
171
172
        return $this->getBuilderLocation(ClassMetadata::create($class));
173
    }
174
175
    /**
176
     * Get builder class location.
177
     *
178
     * @param ClassMetadata $builderClass
179
     *
180
     * @return ClassMetadata
181
     *
182
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
183
     * @throws \RunOpenCode\AbstractBuilder\Exception\InvalidArgumentException
184
     */
185
    private function getBuilderLocation(ClassMetadata $builderClass)
186
    {
187
        $location = $this->input->getArgument('location');
188
189
        if (null !== $location && $builderClass->isAutoloadable() && $location !== $builderClass->getFilename()) {
190
            throw new InvalidArgumentException(sprintf('You can not provide new file location for existing builder ("%s" to "%s").', $builderClass->getFilename(), $location));
191
        }
192
193
        if ($builderClass->isAutoloadable()) {
194
            return $builderClass;
195
        }
196
197
        if (null === $location) {
198
            $helper = $this->getHelper('question');
199
            $question = new Question('Enter path to directory where you want to store builder class: ', null);
200
201
            $path = str_replace('\\', '/', ltrim($helper->ask($this->input, $this->output, $question), '/'));
202
203
            if (!is_dir($path)) {
204
                throw new RuntimeException(sprintf('Provided path "%s" is not path to directory.', $path));
205
            }
206
207
            if (!is_writable($path)) {
208
                throw new RuntimeException(sprintf('Directory on path "%s" is not writeable.', $path));
209
            }
210
211
            $location = $path.'/'.end(explode('/', $builderClass->getClass())).'.php';
0 ignored issues
show
Bug introduced by
explode('/', $builderClass->getClass()) cannot be passed to end() as the parameter $array expects a reference.
Loading history...
212
        }
213
214
        return ClassMetadata::clone($builderClass, [ 'filename' => $location ]);
215
    }
216
217
    /**
218
     * Get methods which ought to be generated.
219
     *
220
     * @param ClassMetadata $buildingClass
221
     * @param ClassMetadata $builderClass
222
     *
223
     * @return MethodChoice[]
224
     *
225
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
226
     */
227
    private function getMethodsToGenerate(ClassMetadata $buildingClass, ClassMetadata $builderClass)
228
    {
229
        $methods = [];
230
231
        $parameters = $buildingClass->getConstructor()->getParameters();
232
233
        foreach ($parameters as $parameter) {
234
            $getter = new GetterMethodChoice($parameter);
235
            $setter = new SetterMethodChoice($parameter);
236
237
            if (!$builderClass->hasPublicMethod($getter->getMethodName())) {
238
                $methods[] = $getter;
239
            }
240
241
            if (!$builderClass->hasPublicMethod($setter->getMethodName())) {
242
                $methods[] = $setter;
243
            }
244
        }
245
246
        if (true !== $this->input->getOption('all')) {
247
248
            $helper = $this->getHelper('question');
249
250
            $question = new ChoiceQuestion(
251
                'Choose which methods you want to generate for your builder class (separate choices with coma, enter none for all choices):',
252
                $methods,
253
                implode(',', array_keys($methods))
254
            );
255
256
            $question->setMultiselect(true);
257
258
            $selected = $helper->ask($this->input, $this->output, $question);
259
260
            $methods = array_filter($methods, function(MethodChoice $choice) use ($selected) {
261
                return in_array((string) $choice, $selected, true);
262
            });
263
        }
264
265
        if (0 === count($methods)) {
266
            throw new RuntimeException('There is no method to generate.');
267
        }
268
269
        return $methods;
270
    }
271
}
272