Completed
Push — master ( e40af7...31cf94 )
by Nikola
04:12
created

GenerateBuilderCommand::execute()   B

Complexity

Conditions 2
Paths 9

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 28
ccs 0
cts 21
cp 0
rs 8.8571
cc 2
eloc 18
nc 9
nop 2
crap 6
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
            $methods = $this->getMethods($buildingClass, $builderClass);
83
            $this->style->info(sprintf('Methods to generate are: "%s()".', implode('()", "', $methods)));
84
        } catch (\Exception $e) {
85
            $this->style->error($e->getMessage());
86
            return 0;
87
        }
88
89
    }
90
91
    /**
92
     * Get class name for which skeleton should be built.
93
     *
94
     * @return string
95
     *
96
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
97
     */
98
    private function getBuildingClass()
99
    {
100
        $class = $this->input->getArgument('class');
101
102 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...
103
            $helper = $this->getHelper('question');
104
            $question = new Question('Enter full qualified class name, or path to file with class, for which you want to generate builder class: ', null);
105
106
            $class = $helper->ask($this->input, $this->output, $question);
107
        }
108
109
        if (!class_exists($class, true)) {
110
            $class = Tokenizer::findClass($class);
111
        }
112
113
        if (!class_exists($class, true)) {
114
            throw new RuntimeException(sprintf('Unable to autoload class "%s". Does this class exists? Can it be autoloaded?', $class));
115
        }
116
117
        return ltrim(str_replace('\\\\', '\\', $class), '\\');
118
    }
119
120
    /**
121
     * Get class name for builder class.
122
     *
123
     * @param string $suggest
124
     *
125
     * @return string
126
     *
127
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
128
     */
129
    private function getBuilderClass($suggest)
130
    {
131
        $class = $this->input->getArgument('builder');
132
133 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...
134
            $helper = $this->getHelper('question');
135
            $question = new Question(sprintf('Enter full qualified class name of your builder class (default: "%s"): ', $suggest), $suggest);
136
137
            $class = $helper->ask($this->input, $this->output, $question);
138
        }
139
140
        if (file_exists($class) && !class_exists($class, true)) {
141
            $class = Tokenizer::findClass($class);
142
        }
143
144
        $class = ltrim(str_replace('\\\\', '\\', $class), '\\');
145
146
        if ('' === $class) {
147
            throw new RuntimeException('Builder class name must be provided.');
148
        }
149
150
        foreach (explode('\\', $class) as $part) {
151
152
            if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $part)) {
153
                throw new RuntimeException(sprintf('Provided builder class name "%s" is not valid PHP class name.', $class));
154
            }
155
        }
156
157
        return $class;
158
    }
159
160
    /**
161
     * Get builder class location.
162
     *
163
     * @param string $builderClass
164
     *
165
     * @return string
166
     *
167
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
168
     */
169
    private function getBuilderLocation($builderClass)
170
    {
171
        $location = $this->input->getArgument('location');
172
173
        if (null === $location && class_exists($builderClass)) {
174
            $location = (new \ReflectionClass($builderClass))->getFileName();
175
        }
176
177
        if (null === $location) {
178
            $helper = $this->getHelper('question');
179
            $question = new Question('Enter path to directory where you want to store builder class: ', null);
180
181
            $path = str_replace('\\', '/', ltrim($helper->ask($this->input, $this->output, $question), '/'));
182
183
            if (!is_dir($path)) {
184
                throw new RuntimeException(sprintf('Provided path "%s" is not path to directory.', $path));
185
            }
186
187
            if (!is_writable($path)) {
188
                throw new RuntimeException(sprintf('Directory on path "%s" is not writeable.', $path));
189
            }
190
191
            $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...
192
        }
193
194
        return $location;
195
    }
196
197
    /**
198
     * Get methods which ought to be generated.
199
     *
200
     * @param string $buildingClass
201
     * @param string $builderClass
202
     *
203
     * @return array
204
     *
205
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
206
     */
207
    private function getMethods($buildingClass, $builderClass)
208
    {
209
        $constructorParameters = (new \ReflectionClass($buildingClass))->getConstructor()->getParameters();
210
        $builderMethods = class_exists($builderClass, true) ? get_class_methods($builderClass) : [];
211
212
        $methods = [];
213
214
        foreach ($constructorParameters as $parameter) {
215
            $getter = sprintf('get%s', ucfirst($parameter->getName()));
216
            $setter = sprintf('set%s', ucfirst($parameter->getName()));
217
218
            if (!in_array($getter, $builderMethods, true)) {
219
                $methods[] = $getter;
220
            }
221
222
            if (!in_array($setter, $builderMethods, true)) {
223
                $methods[] = $setter;
224
            }
225
        }
226
227
        if (0 === count($methods)) {
228
            return [];
229
        }
230
231
        if (true !== $this->input->getOption('all')) {
232
233
            $helper = $this->getHelper('question');
234
            $question = new ChoiceQuestion(
235
                'Choose which methods you want to generate for your builder class (separate choices with coma, enter none for all choices):',
236
                $methods,
237
                implode(',', array_keys($methods))
238
            );
239
            $question->setMultiselect(true);
240
241
            $methods = $helper->ask($this->input, $this->output, $question);
242
        }
243
244
        if (0 === count($methods)) {
245
            throw new RuntimeException('You have to choose at least one method to generate.');
246
        }
247
248
        return $methods;
249
    }
250
}
251