Completed
Push — master ( b1e8a1...edf0e8 )
by Nikola
03:49
created

GenerateBuilderCommand::getMethodsToGenerate()   B

Complexity

Conditions 6
Paths 20

Size

Total Lines 44
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 44
ccs 0
cts 32
cp 0
rs 8.439
c 0
b 0
f 0
cc 6
eloc 23
nc 20
nop 2
crap 42
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\ClassMetadata;
14
use RunOpenCode\AbstractBuilder\Ast\MetadataLoader;
15
use RunOpenCode\AbstractBuilder\Command\Question\ClassChoice;
16
use RunOpenCode\AbstractBuilder\Command\Question\GetterMethodChoice;
17
use RunOpenCode\AbstractBuilder\Command\Question\MethodChoice;
18
use RunOpenCode\AbstractBuilder\Command\Question\SetterMethodChoice;
19
use RunOpenCode\AbstractBuilder\Command\Style\RunOpenCodeStyle;
20
use RunOpenCode\AbstractBuilder\Exception\InvalidArgumentException;
21
use RunOpenCode\AbstractBuilder\Exception\RuntimeException;
22
use RunOpenCode\AbstractBuilder\Generator\BuilderGenerator;
23
use RunOpenCode\AbstractBuilder\ReflectiveAbstractBuilder;
24
use RunOpenCode\AbstractBuilder\Utils\ClassUtils;
25
use Symfony\Component\Console\Command\Command;
26
use Symfony\Component\Console\Input\InputArgument;
27
use Symfony\Component\Console\Input\InputInterface;
28
use Symfony\Component\Console\Input\InputOption;
29
use Symfony\Component\Console\Output\OutputInterface;
30
use Symfony\Component\Console\Question\ChoiceQuestion;
31
use Symfony\Component\Console\Question\Question;
32
33
/**
34
 * Class GenerateBuilderCommand
35
 *
36
 * @package RunOpenCode\AbstractBuilder\Command
37
 */
38
class GenerateBuilderCommand extends Command
39
{
40
    /**
41
     * @var RunOpenCodeStyle
42
     */
43
    private $style;
44
45
    /**
46
     * @var InputInterface
47
     */
48
    private $input;
49
50
    /**
51
     * @var OutputInterface
52
     */
53
    private $output;
54
55
    /**
56
     * @var MetadataLoader
57
     */
58
    private $loader;
59
60
    /**
61
     * @var BuilderGenerator
62
     */
63
    private $generator;
64
65
    /**
66
     * {@inheritdoc}
67
     */
68
    protected function configure()
69
    {
70
        $this
71
            ->setName('runopencode:generate:builder')
72
            ->setDescription('Generates builder class skeleton for provided class.')
73
            ->addArgument('class', InputArgument::OPTIONAL, 'Full qualified class name of building object that can be autoloaded, or path to file with class definition.')
74
            ->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.')
75
            ->addArgument('location', InputArgument::OPTIONAL, 'Path to location of file where builder class will be saved.')
76
            ->addOption('all', '-a', InputOption::VALUE_NONE, 'Generate all methods by default.')
77
            ->addOption('withReturnTypes', '-r', InputOption::VALUE_NONE, 'Generate methods with return types declarations.');
78
79
        $this->loader = new MetadataLoader();
80
        $this->generator = new BuilderGenerator();
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function execute(InputInterface $input, OutputInterface $output)
87
    {
88
        $this->input = $input;
89
        $this->output = $output;
90
        $this->style = new RunOpenCodeStyle($input, $output);
91
92
        $this->style->displayLogo();
93
94
        $this->style->title('Generate builder class');
95
96
        try {
97
            /**
98
             * @var ClassChoice $buildingClassChoice
99
             */
100
            $buildingClassChoice = $this->getBuildingClass();
101
            $this->style->info(sprintf('Builder class for class "%s" will be generated.', $buildingClassChoice->getClass()->getName()));
102
103
            /**
104
             * @var ClassChoice $builderClassChoice
105
             */
106
            $builderClassChoice = $this->getBuilderClass($buildingClassChoice);
107
            $this->style->info(sprintf('Full qualified namespace for builder class is "%s".', $builderClassChoice->getClass()->getName()));
108
            $this->style->info(sprintf('Path to file where builder class will be saved is "%s".', $builderClassChoice->getFile()->getFilename()));
109
            $builderClassChoice->getClass()->isAutoloadable() ? $this->style->info('Existing builder class will be updated.') : $this->style->info('New builder class will be created.');
110
111
            $methods = $this->getMethodsToGenerate($buildingClassChoice, $builderClassChoice);
0 ignored issues
show
Documentation introduced by
$buildingClassChoice is of type object<RunOpenCode\Abstr...d\Question\ClassChoice>, but the function expects a object<RunOpenCode\Abstr...Metadata\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
$builderClassChoice is of type object<RunOpenCode\Abstr...d\Question\ClassChoice>, but the function expects a object<RunOpenCode\Abstr...Metadata\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
112
            $this->style->info('Methods to generate are:');
113
            $this->style->ul($methods);
114
115
        } catch (\Exception $e) {
116
            $this->style->error($e->getMessage());
117
            return 0;
118
        }
119
120
    }
121
122
    /**
123
     * Get class name for which skeleton should be built.
124
     *
125
     * @return ClassChoice
126
     *
127
     * @throws \RunOpenCode\AbstractBuilder\Exception\InvalidArgumentException
128
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
129
     */
130
    private function getBuildingClass()
131
    {
132
        $class = $this->input->getArgument('class');
133
134
        if (null === $class) {
135
            $helper = $this->getHelper('question');
136
            $question = new Question('Enter full qualified class name, or path to file with class, for which you want to generate builder class: ', null);
137
138
            $class = $helper->ask($this->input, $this->output, $question);
139
        }
140
141
        $fileMetadata = $this->loader->load($class);
142
        $classMetadata = null;
143
144
        if (class_exists($class)) {
145
            $classMetadata = $fileMetadata->getClass($class);
146
        }
147
148
        if (1 === count($fileMetadata->getClasses())) {
149
            $classMetadata = array_values($fileMetadata->getClasses())[0];
150
        }
151
152 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...
153
            throw new RuntimeException(sprintf('It is not possible to extract single class metadata from "%s", found %s definition(s).', $class, count($fileMetadata->getClasses())));
154
        }
155
156
        if (!ClassUtils::isBuildable($classMetadata)) {
157
            throw new InvalidArgumentException(sprintf('Builder class can not be generated for "%s", class has to have constructor with some parameters.', $classMetadata->getName()));
158
        }
159
160
        return new ClassChoice($fileMetadata, $classMetadata);
161
    }
162
163
    /**
164
     * Get class name for builder class.
165
     *
166
     * @param ClassChoice $buildingClass
167
     *
168
     * @return ClassChoice
169
     * @throws \RunOpenCode\AbstractBuilder\Exception\InvalidArgumentException
170
     *
171
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
172
     */
173
    private function getBuilderClass(ClassChoice $buildingClass)
174
    {
175
        $class = $this->input->getArgument('builder');
176
177
        if (null === $class) {
178
            $default = sprintf('%sBuilder', $buildingClass->getClass()->getShortName());
179
            $helper = $this->getHelper('question');
180
            $question = new Question(sprintf('Enter full qualified class name of your builder class (default: "%s"): ', $default), $default);
181
182
            $class = $helper->ask($this->input, $this->output, $question);
183
        }
184
185
        $classChoice = null;
186
187
        if (class_exists($class, true)) {
188
            $fileMetadata = $this->loader->load($class);
189
            $classMetadata = $fileMetadata->getClass($class);
190
            $classChoice = new ClassChoice($fileMetadata, $classMetadata);
191
192
            if (null !== $this->input->getArgument('location')) {
193
                throw new InvalidArgumentException('Builder class already exists and its location can not be changed.');
194
            }
195
        }
196
197
        if (file_exists($class)) {
198
            $fileMetadata = $this->loader->load($class);
199
200 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...
201
                throw new RuntimeException(sprintf('It is not possible to extract single class metadata from "%s", found %s definition(s).', $class, count($fileMetadata->getClasses())));
202
            }
203
204
            $classMetadata = array_values($fileMetadata->getClasses())[0];
205
            $classChoice = new ClassChoice($fileMetadata, $classMetadata);
206
        }
207
208
        if (null === $classChoice) {
209
            $classChoice = $this->generateBuilder($buildingClass, $class);
210
        }
211
212
        if (!ClassUtils::isBuilder($classChoice->getClass())) {
213
            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()));
214
        }
215
216
        return $classChoice;
217
    }
218
219
    /**
220
     * Generate new builder class.
221
     *
222
     * @param ClassChoice $buildingClass
223
     * @param string $builderClassName
224
     *
225
     * @return ClassChoice
226
     *
227
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
228
     * @throws \RunOpenCode\AbstractBuilder\Exception\InvalidArgumentException
229
     */
230
    private function generateBuilder(ClassChoice $buildingClass, $builderClassName)
231
    {
232
        $location = $this->input->getArgument('location');
233
234
        if (null === $location) {
235
            $helper = $this->getHelper('question');
236
            $question = new Question('Enter path to directory where you want to store builder class: ', null);
237
238
            $path = str_replace('\\', '/', ltrim($helper->ask($this->input, $this->output, $question), '/'));
239
240
            if (!is_dir($path)) {
241
                throw new RuntimeException(sprintf('Provided path "%s" is not path to directory.', $path));
242
            }
243
244
            if (!is_writable($path)) {
245
                throw new RuntimeException(sprintf('Directory on path "%s" is not writeable.', $path));
246
            }
247
248
            $parts = explode('/', $builderClassName);
249
            $location = $path.'/'.end($parts).'.php';
250
        }
251
252
        $fileMetadata = $this->generator->create($buildingClass, $location, $builderClassName);
253
        $classMetadata = array_values($fileMetadata->getClasses())[0];
254
255
        return new ClassChoice($fileMetadata, $classMetadata);
256
    }
257
258
    /**
259
     * Get methods which ought to be generated.
260
     *
261
     * @param ClassMetadata $buildingClass
262
     * @param ClassMetadata $builderClass
263
     *
264
     * @return MethodChoice[]
265
     *
266
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
267
     */
268
    private function getMethodsToGenerate(ClassMetadata $buildingClass, ClassMetadata $builderClass)
269
    {
270
        $methods = [];
271
272
        $parameters = $buildingClass->getPublicMethod('__construct')->getParameters();
273
274
        foreach ($parameters as $parameter) {
275
            $getter = new GetterMethodChoice($parameter);
276
            $setter = new SetterMethodChoice($parameter);
277
278
            if (!$builderClass->hasPublicMethod($getter->getMethodName())) {
279
                $methods[] = $getter;
280
            }
281
282
            if (!$builderClass->hasPublicMethod($setter->getMethodName())) {
283
                $methods[] = $setter;
284
            }
285
        }
286
287
        if (true !== $this->input->getOption('all')) {
288
289
            $helper = $this->getHelper('question');
290
291
            $question = new ChoiceQuestion(
292
                'Choose which methods you want to generate for your builder class (separate choices with coma, enter none for all choices):',
293
                $methods,
294
                implode(',', array_keys($methods))
295
            );
296
297
            $question->setMultiselect(true);
298
299
            $selected = $helper->ask($this->input, $this->output, $question);
300
301
            $methods = array_filter($methods, function(MethodChoice $choice) use ($selected) {
302
                return in_array((string) $choice, $selected, true);
303
            });
304
        }
305
306
        if (0 === count($methods)) {
307
            throw new RuntimeException('There is no method to generate.');
308
        }
309
310
        return $methods;
311
    }
312
}
313