Completed
Push — master ( 72342f...ed922a )
by Nikola
06:37
created

GenerateBuilderCommand::getMethods()   D

Complexity

Conditions 9
Paths 70

Size

Total Lines 44
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 44
ccs 0
cts 34
cp 0
rs 4.909
cc 9
eloc 25
nc 70
nop 2
crap 90
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\Exception\InvalidArgumentException;
15
use RunOpenCode\AbstractBuilder\Exception\RuntimeException;
16
use RunOpenCode\AbstractBuilder\Helper\Tokenizer;
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, 'Should all methods be generated by default.');
64
65
        $this->loader = new ClassLoader();
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    public function execute(InputInterface $input, OutputInterface $output)
72
    {
73
        $this->input = $input;
74
        $this->output = $output;
75
        $this->style = new Style($input, $output);
76
77
        $this->style->displayLogo();
78
79
        $this->style->title('Generate builder class');
80
81
        try {
82
            /**
83
             * @var ClassMetadata $buildingClass
84
             */
85
            $buildingClass = $this->getBuildingClass();
86
            $this->style->info(sprintf('Builder class for class "%s" will be generated.', $buildingClass->getFqcn()));
87
88
            /**
89
             * @var ClassMetadata $builderClass
90
             */
91
            $builderClass = $this->getBuilderClass(sprintf('%sBuilder', $buildingClass->getClass()));
0 ignored issues
show
Documentation introduced by
sprintf('%sBuilder', $buildingClass->getClass()) is of type string, but the function expects a object<RunOpenCode\Abstr...lder\Ast\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...
92
            $this->style->info(sprintf('Full qualified namespace for builder class is "%s".', $builderClass->getFqcn()));
93
            $this->style->info(sprintf('Path to file where builder class will be saved is "%s".', $builderClass->getFilename()));
94
            class_exists($builderClass->isDefined()) ? $this->style->info('Existing builder class will be updated.') : $this->style->info('New builder class will be created.');
95
96
            $methods = $this->getMethods($buildingClass, $builderClass);
97
            $this->style->info(sprintf('Methods to generate are: "%s".', implode('", "', $methods)));
98
99
        } catch (\Exception $e) {
100
            $this->style->error($e->getMessage());
101
            return 0;
102
        }
103
104
    }
105
106
    /**
107
     * Get class name for which skeleton should be built.
108
     *
109
     * @return ClassMetadata
110
     *
111
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
112
     */
113
    private function getBuildingClass()
114
    {
115
        $class = $this->input->getArgument('class');
116
117
        if (null === $class) {
118
            $helper = $this->getHelper('question');
119
            $question = new Question('Enter full qualified class name, or path to file with class, for which you want to generate builder class: ', null);
120
121
            $class = $helper->ask($this->input, $this->output, $question);
122
        }
123
124
        return $this->loader->load($class);
125
    }
126
127
    /**
128
     * Get class name for builder class.
129
     *
130
     * @param ClassMetadata $buildingClass
131
     *
132
     * @return ClassMetadata
133
     *
134
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
135
     */
136
    private function getBuilderClass(ClassMetadata $buildingClass)
137
    {
138
        $class = $this->input->getArgument('builder');
139
140
        if (null === $class) {
141
            $default = sprintf('%sBuilder', $buildingClass->getFqcn());
142
            $helper = $this->getHelper('question');
143
            $question = new Question(sprintf('Enter full qualified class name of your builder class (default: "%s"): ', $default), $default);
144
145
            $class = $helper->ask($this->input, $this->output, $question);
146
        }
147
148
        if (class_exists($class, true) || file_exists($class)) {
149
            return $this->loader->load($class);
150
        }
151
152
        return $this->getBuilderLocation(ClassMetadata::create($class));
153
    }
154
155
    /**
156
     * Get builder class location.
157
     *
158
     * @param ClassMetadata $builderClass
159
     *
160
     * @return ClassMetadata
161
     *
162
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
163
     * @throws \RunOpenCode\AbstractBuilder\Exception\InvalidArgumentException
164
     */
165
    private function getBuilderLocation(ClassMetadata $builderClass)
166
    {
167
        $location = $this->input->getArgument('location');
168
169
        if (null !== $location && $builderClass->isDefined() && $location !== $builderClass->getFilename()) {
170
            throw new InvalidArgumentException(sprintf('You can not provide new file location for existing builder ("%s" to "%s").', $builderClass->getFilename(), $location));
171
        }
172
173
        if ($builderClass->isDefined()) {
174
            return $builderClass;
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
            $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...
192
        }
193
194
        return ClassMetadata::clone($builderClass, [ 'filename' => $location ]);
195
    }
196
197
    /**
198
     * Get methods which ought to be generated.
199
     *
200
     * @param ClassMetadata $buildingClass
201
     * @param ClassMetadata $builderClass
202
     *
203
     * @return array
204
     *
205
     * @throws \RunOpenCode\AbstractBuilder\Exception\RuntimeException
206
     */
207
    private function getMethods(ClassMetadata $buildingClass, ClassMetadata $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[] = sprintf('%s()', $getter);
220
            }
221
222
            if (!in_array($setter, $builderMethods, true)) {
223
                $type = (null !== $parameter->getType()) ? '\\'.$parameter->getType().' ' : '';
224
                $methods[] = sprintf('%s(%s$%s)', $setter, $type, $parameter->getName());
225
            }
226
        }
227
228
        if (0 === count($methods)) {
229
            return [];
230
        }
231
232
        if (true !== $this->input->getOption('all')) {
233
234
            $helper = $this->getHelper('question');
235
            $question = new ChoiceQuestion(
236
                'Choose which methods you want to generate for your builder class (separate choices with coma, enter none for all choices):',
237
                $methods,
238
                implode(',', array_keys($methods))
239
            );
240
            $question->setMultiselect(true);
241
242
            $methods = $helper->ask($this->input, $this->output, $question);
243
        }
244
245
        if (0 === count($methods)) {
246
            throw new RuntimeException('You have to choose at least one method to generate.');
247
        }
248
249
        return $methods;
250
    }
251
}
252