Create   A
last analyzed

Complexity

Total Complexity 42

Size/Duplication

Total Lines 291
Duplicated Lines 0 %

Test Coverage

Coverage 71.64%

Importance

Changes 0
Metric Value
wmc 42
eloc 132
dl 0
loc 291
ccs 96
cts 134
cp 0.7164
rs 9.0399
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 21 1
A getCreateMigrationDirectoryQuestion() 0 3 1
A getMigrationPath() 0 36 5
A getSelectMigrationPathQuestion() 0 3 1
F execute() 0 174 34

How to fix   Complexity   

Complex Class

Complex classes like Create often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Create, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * MIT License
5
 * For full license information, please view the LICENSE file that was distributed with this source code.
6
 */
7
8
namespace Phinx\Console\Command;
9
10
use Exception;
11
use InvalidArgumentException;
12
use Phinx\Config\NamespaceAwareInterface;
13
use Phinx\Util\Util;
14
use RuntimeException;
15
use Symfony\Component\Console\Input\InputArgument;
16
use Symfony\Component\Console\Input\InputInterface;
17
use Symfony\Component\Console\Input\InputOption;
18
use Symfony\Component\Console\Output\OutputInterface;
19
use Symfony\Component\Console\Question\ChoiceQuestion;
20
use Symfony\Component\Console\Question\ConfirmationQuestion;
21
22
class Create extends AbstractCommand
23
{
24
    /**
25
     * @var string
26
     */
27
    protected static $defaultName = 'create';
28
29
    /**
30
     * The name of the interface that any external template creation class is required to implement.
31
     */
32
    public const CREATION_INTERFACE = 'Phinx\Migration\CreationInterface';
33
34
    /**
35
     * {@inheritDoc}
36
     *
37
     * @return void
38
     */
39
    protected function configure()
40
    {
41
        parent::configure();
42
43
        $this->setDescription('Create a new migration')
44
            ->addArgument('name', InputArgument::OPTIONAL, 'Class name of the migration (in CamelCase)')
45
            ->setHelp(sprintf(
46
                '%sCreates a new database migration%s',
47
                PHP_EOL,
48
                PHP_EOL
49
            ));
50
51 35
        // An alternative template.
52
        $this->addOption('template', 't', InputOption::VALUE_REQUIRED, 'Use an alternative template');
53 35
54
        // A classname to be used to gain access to the template content as well as the ability to
55 35
        // have a callback once the migration file has been created.
56 35
        $this->addOption('class', 'l', InputOption::VALUE_REQUIRED, 'Use a class implementing "' . self::CREATION_INTERFACE . '" to generate the template');
57 35
58 35
        // Allow the migration path to be chosen non-interactively.
59 35
        $this->addOption('path', null, InputOption::VALUE_REQUIRED, 'Specify the path in which to create this migration');
60 35
    }
61
62 35
    /**
63
     * Get the confirmation question asking if the user wants to create the
64
     * migrations directory.
65 35
     *
66
     * @return \Symfony\Component\Console\Question\ConfirmationQuestion
67
     */
68
    protected function getCreateMigrationDirectoryQuestion()
69 35
    {
70
        return new ConfirmationQuestion('Create migrations directory? [y]/n ', true);
71
    }
72 35
73 35
    /**
74
     * Get the question that allows the user to select which migration path to use.
75
     *
76
     * @param string[] $paths Paths
77
     * @return \Symfony\Component\Console\Question\ChoiceQuestion
78
     */
79
    protected function getSelectMigrationPathQuestion(array $paths)
80
    {
81
        return new ChoiceQuestion('Which migrations path would you like to use?', $paths, 0);
82
    }
83
84
    /**
85
     * Returns the migration path to create the migration in.
86
     *
87
     * @param \Symfony\Component\Console\Input\InputInterface $input Input
88
     * @param \Symfony\Component\Console\Output\OutputInterface $output Output
89
     * @throws \Exception
90
     * @return string
91
     */
92
    protected function getMigrationPath(InputInterface $input, OutputInterface $output)
93
    {
94
        // First, try the non-interactive option:
95
        $path = $input->getOption('path');
96
97
        if (!empty($path)) {
98
            return $path;
99
        }
100
101
        $paths = $this->getConfig()->getMigrationPaths();
102
103
        // No paths? That's a problem.
104
        if (empty($paths)) {
105 13
            throw new Exception('No migration paths set in your Phinx configuration file.');
106
        }
107
108 13
        $paths = Util::globAll($paths);
109
110 13
        if (empty($paths)) {
111
            throw new Exception(
112
                'You probably used curly braces to define migration path in your Phinx configuration file, ' .
113
                'but no directories have been matched using this pattern. ' .
114 13
                'You need to create a migration directory manually.'
115
            );
116
        }
117 13
118
        // Only one path set, so select that:
119
        if (count($paths) === 1) {
120
            return array_shift($paths);
121 13
        }
122
123 13
        /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
124
        $helper = $this->getHelper('question');
125
        $question = $this->getSelectMigrationPathQuestion($paths);
126
127
        return $helper->ask($input, $output, $question);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $helper->ask($input, $output, $question) also could return the type boolean|string[] which is incompatible with the documented return type string.
Loading history...
128
    }
129
130
    /**
131
     * Create the new migration.
132 13
     *
133 13
     * @param \Symfony\Component\Console\Input\InputInterface $input Input
134
     * @param \Symfony\Component\Console\Output\OutputInterface $output Output
135
     * @throws \RuntimeException
136
     * @throws \InvalidArgumentException
137
     * @return int 0 on success
138
     */
139
    protected function execute(InputInterface $input, OutputInterface $output)
140
    {
141
        $this->bootstrap($input, $output);
142
143
        // get the migration path from the config
144
        $path = $this->getMigrationPath($input, $output);
145
146
        if (!file_exists($path)) {
147
            /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
148
            $helper = $this->getHelper('question');
149
            $question = $this->getCreateMigrationDirectoryQuestion();
150
151
            if ($helper->ask($input, $output, $question)) {
152 13
                mkdir($path, 0755, true);
153
            }
154 13
        }
155
156
        $this->verifyMigrationDirectory($path);
157 13
158
        $config = $this->getConfig();
159 13
        $namespace = $config instanceof NamespaceAwareInterface ? $config->getMigrationNamespaceByPath($path) : null;
160
161
        $path = realpath($path);
162
        $className = $input->getArgument('name');
163
        if ($className === null) {
164
            $currentTimestamp = Util::getCurrentTimestamp();
165
            $className = 'V' . $currentTimestamp;
166
            $fileName = $currentTimestamp . '.php';
167
        } else {
168 13
            if (!Util::isValidPhinxClassName($className)) {
169
                throw new InvalidArgumentException(sprintf(
170 13
                    'The migration class name "%s" is invalid. Please use CamelCase format.',
171 13
                    $className
172
                ));
173 13
            }
174 13
175
            // Compute the file path
176 13
            $fileName = Util::mapClassNameToFileName($className);
177
        }
178
179
        if (!Util::isUniqueMigrationClassName($className, $path)) {
180
            throw new InvalidArgumentException(sprintf(
181
                'The migration class name "%s%s" already exists',
182
                $namespace ? $namespace . '\\' : '',
183 13
                $className
184 2
            ));
185 2
        }
186 2
187
        $filePath = $path . DIRECTORY_SEPARATOR . $fileName;
188 2
189
        if (is_file($filePath)) {
190
            throw new InvalidArgumentException(sprintf(
191
                'The file "%s" already exists',
192 13
                $filePath
193 13
            ));
194
        }
195 13
196
        // Get the alternative template and static class options from the config, but only allow one of them.
197
        $defaultAltTemplate = $this->getConfig()->getTemplateFile();
198
        $defaultCreationClassName = $this->getConfig()->getTemplateClass();
199
        if ($defaultAltTemplate && $defaultCreationClassName) {
200
            throw new InvalidArgumentException('Cannot define template:class and template:file at the same time');
201
        }
202
203 13
        // Get the alternative template and static class options from the command line, but only allow one of them.
204 13
        /** @phpstan-var class-string|null $altTemplate */
205 13
        $altTemplate = $input->getOption('template');
206 1
        /** @phpstan-var class-string|null $creationClassName */
207
        $creationClassName = $input->getOption('class');
208
        if ($altTemplate && $creationClassName) {
209
            throw new InvalidArgumentException('Cannot use --template and --class at the same time');
210 12
        }
211 12
212 12
        // If no commandline options then use the defaults.
213 1
        if (!$altTemplate && !$creationClassName) {
214
            $altTemplate = $defaultAltTemplate;
215
            $creationClassName = $defaultCreationClassName;
216
        }
217 11
218 5
        // Verify the alternative template file's existence.
219 5
        if ($altTemplate && !is_file($altTemplate)) {
220 5
            throw new InvalidArgumentException(sprintf(
221
                'The alternative template file "%s" does not exist',
222
                $altTemplate
223 11
            ));
224
        }
225
226
        // Verify that the template creation class (or the aliased class) exists and that it implements the required interface.
227
        $aliasedClassName = null;
228
        if ($creationClassName) {
229
            // Supplied class does not exist, is it aliased?
230
            if (!class_exists($creationClassName)) {
231 11
                $aliasedClassName = $this->getConfig()->getAlias($creationClassName);
232 11
                if ($aliasedClassName && !class_exists($aliasedClassName)) {
233
                    throw new InvalidArgumentException(sprintf(
234 9
                        'The class "%s" via the alias "%s" does not exist',
235 3
                        $aliasedClassName,
236 3
                        $creationClassName
237
                    ));
238
                } elseif (!$aliasedClassName) {
239
                    throw new InvalidArgumentException(sprintf(
240
                        'The class "%s" does not exist',
241
                        $creationClassName
242 3
                    ));
243
                }
244
            }
245
246
            // Does the class implement the required interface?
247
            if (!$aliasedClassName && !is_subclass_of($creationClassName, self::CREATION_INTERFACE)) {
248 3
                throw new InvalidArgumentException(sprintf(
249
                    'The class "%s" does not implement the required interface "%s"',
250
                    $creationClassName,
251 9
                    self::CREATION_INTERFACE
252 2
                ));
253 2
            } elseif ($aliasedClassName && !is_subclass_of($aliasedClassName, self::CREATION_INTERFACE)) {
254 2
                throw new InvalidArgumentException(sprintf(
255
                    'The class "%s" via the alias "%s" does not implement the required interface "%s"',
256 2
                    $aliasedClassName,
257 7
                    $creationClassName,
258 1
                    self::CREATION_INTERFACE
259 1
                ));
260 1
            }
261 1
        }
262
263 1
        // Use the aliased class.
264
        $creationClassName = $aliasedClassName ?: $creationClassName;
265 6
266
        // Determine the appropriate mechanism to get the template
267
        if ($creationClassName) {
268 8
            // Get the template from the creation class
269
            $creationClass = new $creationClassName($input, $output);
270
            $contents = $creationClass->getMigrationTemplate();
271 8
        } else {
272
            // Load the alternative template if it is defined.
273 6
            $contents = file_get_contents($altTemplate ?: $this->getMigrationTemplateFilename());
274 6
        }
275 6
276
        // inject the class names appropriate to this migration
277 2
        $classes = [
278
            '$namespaceDefinition' => $namespace !== null ? (PHP_EOL . 'namespace ' . $namespace . ';' . PHP_EOL) : '',
279
            '$namespace' => $namespace,
280
            '$useClassName' => $this->getConfig()->getMigrationBaseClassName(false),
281
            '$className' => $className,
282 8
            '$version' => Util::getVersionFromFileName($fileName),
283 8
            '$baseClassName' => $this->getConfig()->getMigrationBaseClassName(true),
284 8
        ];
285 8
        $contents = strtr($contents, $classes);
286 8
287 8
        if (file_put_contents($filePath, $contents) === false) {
288 8
            throw new RuntimeException(sprintf(
289 8
                'The file "%s" could not be written to',
290
                $path
291 8
            ));
292
        }
293
294
        // Do we need to do the post creation call to the creation class?
295
        if (isset($creationClass)) {
296
            /** @var \Phinx\Migration\CreationInterface $creationClass */
297
            $creationClass->postMigrationCreation($filePath, $className, $this->getConfig()->getMigrationBaseClassName());
298
        }
299 8
300 6
        $output->writeln('<info>using migration base class</info> ' . $classes['$useClassName'], $this->verbosityLevel);
301 6
302
        if (!empty($altTemplate)) {
303 8
            $output->writeln('<info>using alternative template</info> ' . $altTemplate, $this->verbosityLevel);
304
        } elseif (!empty($creationClassName)) {
305 8
            $output->writeln('<info>using template creation class</info> ' . $creationClassName, $this->verbosityLevel);
306
        } else {
307 8
            $output->writeln('<info>using default template</info>', $this->verbosityLevel);
308 6
        }
309 6
310 2
        $output->writeln('<info>created</info> ' . Util::relativePath($filePath), $this->verbosityLevel);
311
312
        return self::CODE_SUCCESS;
313 8
    }
314
}
315