Completed
Push — master ( 31cf94...72342f )
by Nikola
03:37
created

GenerateBuilderCommand::configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
ccs 0
cts 10
cp 0
rs 9.4285
cc 1
eloc 8
nc 1
nop 0
crap 2
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\Exception\RuntimeException;
13
use RunOpenCode\AbstractBuilder\Helper\Style;
14
use RunOpenCode\AbstractBuilder\Helper\Tokenizer;
15
use Symfony\Component\Console\Command\Command;
16
use Symfony\Component\Console\Input\InputArgument;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Input\InputOption;
19
use Symfony\Component\Console\Output\OutputInterface;
20
use Symfony\Component\Console\Question\ChoiceQuestion;
21
use Symfony\Component\Console\Question\Question;
22
23
/**
24
 * Class GenerateBuilderCommand
25
 *
26
 * @package RunOpenCode\AbstractBuilder\Command
27
 */
28
class GenerateBuilderCommand extends Command
29
{
30
    /**
31
     * @var Style
32
     */
33
    private $style;
34
35
    /**
36
     * @var InputInterface
37
     */
38
    private $input;
39
40
    /**
41
     * @var OutputInterface
42
     */
43
    private $output;
44
45
    /**
46
     * {@inheritdoc}
47
     */
48
    protected function configure()
49
    {
50
        $this
51
            ->setName('runopencode:generate:builder')
52
            ->setDescription('Generates builder class skeleton for provided class.')
53
            ->addArgument('class', InputArgument::OPTIONAL, 'Full qualified class name of building object that can be autoloaded, or path to file with class definition.')
54
            ->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.')
55
            ->addArgument('location', InputArgument::OPTIONAL, 'Path to location of file where builder class will be saved.')
56
            ->addOption('all', '-a', InputOption::VALUE_NONE, 'Should all methods be generated by default.');
57
    }
58
59
    /**
60
     * {@inheritdoc}
61
     */
62
    public function execute(InputInterface $input, OutputInterface $output)
63
    {
64
        $this->input = $input;
65
        $this->output = $output;
66
        $this->style = new Style($input, $output);
67
68
        $this->style->displayLogo();
69
70
        $this->style->title('Generate builder class');
71
72
        try {
73
            $buildingClass = $this->getBuildingClass();
74
            $this->style->info(sprintf('Builder class for class "%s" will be generated.', $buildingClass));
75
76
            $builderClass = $this->getBuilderClass(sprintf('%sBuilder', $buildingClass));
77
            $this->style->info(sprintf('Full qualified namespace for builder class is "%s".', $builderClass));
78
79
            $filePath = $this->getBuilderLocation($builderClass);
80
            $this->style->info(sprintf('Path to file where builder class will be saved is "%s".', $filePath));
81
82
            file_exists($filePath) ? $this->style->info('Existing builder class will be updated.') : $this->style->info('New builder class will be created.');
83
84
            $methods = $this->getMethods($buildingClass, $builderClass);
85
            $this->style->info(sprintf('Methods to generate are: "%s".', implode('", "', $methods)));
86
87
        } catch (\Exception $e) {
88
            $this->style->error($e->getMessage());
89
            return 0;
90
        }
91
92
    }
93
94
    /**
95
     * Get class name for which skeleton should be built.
96
     *
97
     * @return string
98
     *
99
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
100
     */
101
    private function getBuildingClass()
102
    {
103
        $class = $this->input->getArgument('class');
104
105 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...
106
            $helper = $this->getHelper('question');
107
            $question = new Question('Enter full qualified class name, or path to file with class, for which you want to generate builder class: ', null);
108
109
            $class = $helper->ask($this->input, $this->output, $question);
110
        }
111
112
        if (!class_exists($class, true)) {
113
            $class = Tokenizer::findClass($class);
114
        }
115
116
        if (!class_exists($class, true)) {
117
            throw new RuntimeException(sprintf('Unable to autoload class "%s". Does this class exists? Can it be autoloaded?', $class));
118
        }
119
120
        return ltrim(str_replace('\\\\', '\\', $class), '\\');
121
    }
122
123
    /**
124
     * Get class name for builder class.
125
     *
126
     * @param string $suggest
127
     *
128
     * @return string
129
     *
130
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
131
     */
132
    private function getBuilderClass($suggest)
133
    {
134
        $class = $this->input->getArgument('builder');
135
136 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...
137
            $helper = $this->getHelper('question');
138
            $question = new Question(sprintf('Enter full qualified class name of your builder class (default: "%s"): ', $suggest), $suggest);
139
140
            $class = $helper->ask($this->input, $this->output, $question);
141
        }
142
143
        if (file_exists($class) && !class_exists($class, true)) {
144
            $class = Tokenizer::findClass($class);
145
        }
146
147
        $class = ltrim(str_replace('\\\\', '\\', $class), '\\');
148
149
        if ('' === $class) {
150
            throw new RuntimeException('Builder class name must be provided.');
151
        }
152
153
        foreach (explode('\\', $class) as $part) {
154
155
            if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $part)) {
156
                throw new RuntimeException(sprintf('Provided builder class name "%s" is not valid PHP class name.', $class));
157
            }
158
        }
159
160
        return $class;
161
    }
162
163
    /**
164
     * Get builder class location.
165
     *
166
     * @param string $builderClass
167
     *
168
     * @return string
169
     *
170
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
171
     */
172
    private function getBuilderLocation($builderClass)
173
    {
174
        $location = $this->input->getArgument('location');
175
176
        if (null === $location && class_exists($builderClass)) {
177
            $location = (new \ReflectionClass($builderClass))->getFileName();
178
        }
179
180
        if (null === $location) {
181
            $helper = $this->getHelper('question');
182
            $question = new Question('Enter path to directory where you want to store builder class: ', null);
183
184
            $path = str_replace('\\', '/', ltrim($helper->ask($this->input, $this->output, $question), '/'));
185
186
            if (!is_dir($path)) {
187
                throw new RuntimeException(sprintf('Provided path "%s" is not path to directory.', $path));
188
            }
189
190
            if (!is_writable($path)) {
191
                throw new RuntimeException(sprintf('Directory on path "%s" is not writeable.', $path));
192
            }
193
194
            $path = $path.'/'.end(explode('/', $builderClass)).'.php';
0 ignored issues
show
Bug introduced by
explode('/', $builderClass) cannot be passed to end() as the parameter $array expects a reference.
Loading history...
Unused Code introduced by
$path is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
195
        }
196
197
        return $location;
198
    }
199
200
    /**
201
     * Get methods which ought to be generated.
202
     *
203
     * @param string $buildingClass
204
     * @param string $builderClass
205
     *
206
     * @return array
207
     *
208
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
209
     */
210
    private function getMethods($buildingClass, $builderClass)
211
    {
212
        $constructorParameters = (new \ReflectionClass($buildingClass))->getConstructor()->getParameters();
213
        $builderMethods = class_exists($builderClass, true) ? get_class_methods($builderClass) : [];
214
215
        $methods = [];
216
217
        foreach ($constructorParameters as $parameter) {
218
            $getter = sprintf('get%s', ucfirst($parameter->getName()));
219
            $setter = sprintf('set%s', ucfirst($parameter->getName()));
220
221
            if (!in_array($getter, $builderMethods, true)) {
222
                $methods[] = sprintf('%s()', $getter);
223
            }
224
225
            if (!in_array($setter, $builderMethods, true)) {
226
                $type = (null !== $parameter->getType()) ? '\\'.$parameter->getType().' ' : '';
227
                $methods[] = sprintf('%s(%s$%s)', $setter, $type, $parameter->getName());
228
            }
229
        }
230
231
        if (0 === count($methods)) {
232
            return [];
233
        }
234
235
        if (true !== $this->input->getOption('all')) {
236
237
            $helper = $this->getHelper('question');
238
            $question = new ChoiceQuestion(
239
                'Choose which methods you want to generate for your builder class (separate choices with coma, enter none for all choices):',
240
                $methods,
241
                implode(',', array_keys($methods))
242
            );
243
            $question->setMultiselect(true);
244
245
            $methods = $helper->ask($this->input, $this->output, $question);
246
        }
247
248
        if (0 === count($methods)) {
249
            throw new RuntimeException('You have to choose at least one method to generate.');
250
        }
251
252
        return $methods;
253
    }
254
}
255