Completed
Push — master ( ed922a...ce73d9 )
by Nikola
03:26
created

GenerateBuilderCommand   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 238
Duplicated Lines 2.52 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 0%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 26
lcom 1
cbo 11
dl 6
loc 238
ccs 0
cts 121
cp 0
rs 10
c 3
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 13 1
B execute() 0 34 3
B getBuildingClass() 0 23 4
A getBuilderClass() 0 18 4
C getBuilderLocation() 0 31 8
B getMethods() 6 49 6

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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\ClassLoader;
13
use RunOpenCode\AbstractBuilder\Ast\ClassMetadata;
14
use RunOpenCode\AbstractBuilder\Ast\MethodMetadata;
15
use RunOpenCode\AbstractBuilder\Exception\InvalidArgumentException;
16
use RunOpenCode\AbstractBuilder\Exception\RuntimeException;
17
use Symfony\Component\Console\Command\Command;
18
use Symfony\Component\Console\Input\InputArgument;
19
use Symfony\Component\Console\Input\InputInterface;
20
use Symfony\Component\Console\Input\InputOption;
21
use Symfony\Component\Console\Output\OutputInterface;
22
use Symfony\Component\Console\Question\ChoiceQuestion;
23
use Symfony\Component\Console\Question\Question;
24
25
/**
26
 * Class GenerateBuilderCommand
27
 *
28
 * @package RunOpenCode\AbstractBuilder\Command
29
 */
30
class GenerateBuilderCommand extends Command
31
{
32
    /**
33
     * @var Style
34
     */
35
    private $style;
36
37
    /**
38
     * @var InputInterface
39
     */
40
    private $input;
41
42
    /**
43
     * @var OutputInterface
44
     */
45
    private $output;
46
47
    /**
48
     * @var ClassLoader
49
     */
50
    private $loader;
51
52
    /**
53
     * {@inheritdoc}
54
     */
55
    protected function configure()
56
    {
57
        $this
58
            ->setName('runopencode:generate:builder')
59
            ->setDescription('Generates builder class skeleton for provided class.')
60
            ->addArgument('class', InputArgument::OPTIONAL, 'Full qualified class name of building object that can be autoloaded, or path to file with class definition.')
61
            ->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.')
62
            ->addArgument('location', InputArgument::OPTIONAL, 'Path to location of file where builder class will be saved.')
63
            ->addOption('all', '-a', InputOption::VALUE_NONE, 'Generate all methods by default.')
64
            ->addOption('withReturnTypes', '-r', InputOption::VALUE_NONE, 'Generate methods with return types declarations.');
65
66
        $this->loader = new ClassLoader();
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72
    public function execute(InputInterface $input, OutputInterface $output)
73
    {
74
        $this->input = $input;
75
        $this->output = $output;
76
        $this->style = new Style($input, $output);
77
78
        $this->style->displayLogo();
79
80
        $this->style->title('Generate builder class');
81
82
        try {
83
            /**
84
             * @var ClassMetadata $buildingClass
85
             */
86
            $buildingClass = $this->getBuildingClass();
87
            $this->style->info(sprintf('Builder class for class "%s" will be generated.', $buildingClass->getFqcn()));
88
89
            /**
90
             * @var ClassMetadata $builderClass
91
             */
92
            $builderClass = $this->getBuilderClass($buildingClass);
93
            $this->style->info(sprintf('Full qualified namespace for builder class is "%s".', $builderClass->getFqcn()));
94
            $this->style->info(sprintf('Path to file where builder class will be saved is "%s".', $builderClass->getFilename()));
95
            $builderClass->isAutoloadable() ? $this->style->info('Existing builder class will be updated.') : $this->style->info('New builder class will be created.');
96
97
            $methods = $this->getMethods($buildingClass, $builderClass);
98
            $this->style->info(sprintf('Methods to generate are: "%s".', implode('", "', $methods)));
99
100
        } catch (\Exception $e) {
101
            $this->style->error($e->getMessage());
102
            return 0;
103
        }
104
105
    }
106
107
    /**
108
     * Get class name for which skeleton should be built.
109
     *
110
     * @return ClassMetadata
111
     *
112
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
113
     */
114
    private function getBuildingClass()
115
    {
116
        $class = $this->input->getArgument('class');
117
118
        if (null === $class) {
119
            $helper = $this->getHelper('question');
120
            $question = new Question('Enter full qualified class name, or path to file with class, for which you want to generate builder class: ', null);
121
122
            $class = $helper->ask($this->input, $this->output, $question);
123
        }
124
125
        $metadata = $this->loader->load($class);
126
127
        if (null === ($constructor = $metadata->getConstructor())) {
128
            throw new InvalidArgumentException('Builder class can not be generated for class without constructor.');
129
        }
130
131
        if (0 === count($constructor->getParameters())) {
132
            throw new InvalidArgumentException('Builder class can not be generated for class with constructor without arguments.');
133
        }
134
135
        return $metadata;
136
    }
137
138
    /**
139
     * Get class name for builder class.
140
     *
141
     * @param ClassMetadata $buildingClass
142
     *
143
     * @return ClassMetadata
144
     *
145
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
146
     */
147
    private function getBuilderClass(ClassMetadata $buildingClass)
148
    {
149
        $class = $this->input->getArgument('builder');
150
151
        if (null === $class) {
152
            $default = sprintf('%sBuilder', $buildingClass->getFqcn());
153
            $helper = $this->getHelper('question');
154
            $question = new Question(sprintf('Enter full qualified class name of your builder class (default: "%s"): ', $default), $default);
155
156
            $class = $helper->ask($this->input, $this->output, $question);
157
        }
158
159
        if (class_exists($class, true) || file_exists($class)) {
160
            return $this->loader->load($class);
161
        }
162
163
        return $this->getBuilderLocation(ClassMetadata::create($class));
164
    }
165
166
    /**
167
     * Get builder class location.
168
     *
169
     * @param ClassMetadata $builderClass
170
     *
171
     * @return ClassMetadata
172
     *
173
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
174
     * @throws \RunOpenCode\AbstractBuilder\Exception\InvalidArgumentException
175
     */
176
    private function getBuilderLocation(ClassMetadata $builderClass)
177
    {
178
        $location = $this->input->getArgument('location');
179
180
        if (null !== $location && $builderClass->isAutoloadable() && $location !== $builderClass->getFilename()) {
181
            throw new InvalidArgumentException(sprintf('You can not provide new file location for existing builder ("%s" to "%s").', $builderClass->getFilename(), $location));
182
        }
183
184
        if ($builderClass->isAutoloadable()) {
185
            return $builderClass;
186
        }
187
188
        if (null === $location) {
189
            $helper = $this->getHelper('question');
190
            $question = new Question('Enter path to directory where you want to store builder class: ', null);
191
192
            $path = str_replace('\\', '/', ltrim($helper->ask($this->input, $this->output, $question), '/'));
193
194
            if (!is_dir($path)) {
195
                throw new RuntimeException(sprintf('Provided path "%s" is not path to directory.', $path));
196
            }
197
198
            if (!is_writable($path)) {
199
                throw new RuntimeException(sprintf('Directory on path "%s" is not writeable.', $path));
200
            }
201
202
            $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...
203
        }
204
205
        return ClassMetadata::clone($builderClass, [ 'filename' => $location ]);
206
    }
207
208
    /**
209
     * Get methods which ought to be generated.
210
     *
211
     * @param ClassMetadata $buildingClass
212
     * @param ClassMetadata $builderClass
213
     *
214
     * @return array
215
     *
216
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
217
     */
218
    private function getMethods(ClassMetadata $buildingClass, ClassMetadata $builderClass)
219
    {
220
        $methods = [];
221
222
        $parameters = $buildingClass->getConstructor()->getParameters();
223
224
        foreach ($parameters as $parameter) {
225
            $getter = sprintf('get%s', ucfirst($parameter->getName()));
226
            $setter = sprintf('set%s', ucfirst($parameter->getName()));
227
228 View Code Duplication
            if (!$builderClass->hasPublicMethod($getter)) {
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...
229
                $methods[] = new MethodMetadata($getter, false, false, MethodMetadata::PUBLIC, $parameter->getType(), false, false, []);
230
            }
231
232 View Code Duplication
            if (!$builderClass->hasPublicMethod($setter)) {
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...
233
                $methods[] = new MethodMetadata($setter, false, false, MethodMetadata::PUBLIC, $builderClass, false, false, [$parameter]);;
234
            }
235
        }
236
237
        $methods = array_map(function(MethodMetadata $method) {
238
            return new MethodChoice($method);
239
        }, $methods);
240
241
242
        if (true !== $this->input->getOption('all')) {
243
244
            $helper = $this->getHelper('question');
245
246
            $question = new ChoiceQuestion(
247
                'Choose which methods you want to generate for your builder class (separate choices with coma, enter none for all choices):',
248
                $methods,
249
                implode(',', array_keys($methods))
250
            );
251
252
            $question->setMultiselect(true);
253
254
            $selected = $helper->ask($this->input, $this->output, $question);
255
256
            $methods = array_filter($methods, function(MethodChoice $choice) use ($selected) {
257
                return in_array((string) $choice, $selected, true);
258
            });
259
        }
260
261
        if (0 === count($methods)) {
262
            throw new RuntimeException('There is no method to generate.');
263
        }
264
265
        return $methods;
266
    }
267
}
268