Completed
Push — master ( 05902f...9e6322 )
by mark
01:41 queued 11s
created

Create::getMigrationPath()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6.8088

Importance

Changes 0
Metric Value
dl 0
loc 37
ccs 7
cts 12
cp 0.5833
rs 9.0168
c 0
b 0
f 0
cc 5
nc 5
nop 2
crap 6.8088
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
     *
78
     * @return \Symfony\Component\Console\Question\ChoiceQuestion
79
     */
80
    protected function getSelectMigrationPathQuestion(array $paths)
81
    {
82
        return new ChoiceQuestion('Which migrations path would you like to use?', $paths, 0);
83
    }
84
85
    /**
86
     * Returns the migration path to create the migration in.
87
     *
88
     * @param \Symfony\Component\Console\Input\InputInterface $input Input
89
     * @param \Symfony\Component\Console\Output\OutputInterface $output Output
90
     *
91
     * @throws \Exception
92
     *
93
     * @return string
94
     */
95
    protected function getMigrationPath(InputInterface $input, OutputInterface $output)
96
    {
97
        // First, try the non-interactive option:
98
        $path = $input->getOption('path');
99
100
        if (!empty($path)) {
101
            return $path;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $path; (string|string[]|boolean) is incompatible with the return type documented by Phinx\Console\Command\Create::getMigrationPath of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
102
        }
103
104
        $paths = $this->getConfig()->getMigrationPaths();
105 13
106
        // No paths? That's a problem.
107
        if (empty($paths)) {
108 13
            throw new Exception('No migration paths set in your Phinx configuration file.');
109
        }
110 13
111
        $paths = Util::globAll($paths);
112
113
        if (empty($paths)) {
114 13
            throw new Exception(
115
                'You probably used curly braces to define migration path in your Phinx configuration file, ' .
116
                'but no directories have been matched using this pattern. ' .
117 13
                'You need to create a migration directory manually.'
118
            );
119
        }
120
121 13
        // Only one path set, so select that:
122
        if (count($paths) === 1) {
123 13
            return array_shift($paths);
124
        }
125
126
        /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
127
        $helper = $this->getHelper('question');
128
        $question = $this->getSelectMigrationPathQuestion($paths);
129
130
        return $helper->ask($input, $output, $question);
131
    }
132 13
133 13
    /**
134
     * Create the new migration.
135
     *
136
     * @param \Symfony\Component\Console\Input\InputInterface $input Input
137
     * @param \Symfony\Component\Console\Output\OutputInterface $output Output
138
     *
139
     * @throws \RuntimeException
140
     * @throws \InvalidArgumentException
141
     *
142
     * @return int 0 on success
143
     */
144
    protected function execute(InputInterface $input, OutputInterface $output)
145
    {
146
        $this->bootstrap($input, $output);
147
148
        // get the migration path from the config
149
        $path = $this->getMigrationPath($input, $output);
150
151
        if (!file_exists($path)) {
152 13
            /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
153
            $helper = $this->getHelper('question');
154 13
            $question = $this->getCreateMigrationDirectoryQuestion();
155
156
            if ($helper->ask($input, $output, $question)) {
157 13
                mkdir($path, 0755, true);
158
            }
159 13
        }
160
161
        $this->verifyMigrationDirectory($path);
162
163
        $config = $this->getConfig();
164
        $namespace = $config instanceof NamespaceAwareInterface ? $config->getMigrationNamespaceByPath($path) : null;
165
166
        $path = realpath($path);
167
        $className = $input->getArgument('name');
168 13
        if ($className === null) {
169
            $currentTimestamp = Util::getCurrentTimestamp();
170 13
            $className = "V" . $currentTimestamp;
171 13
            $fileName = $currentTimestamp . '.php';
172
        } else {
173 13
            if (!Util::isValidPhinxClassName($className)) {
0 ignored issues
show
Bug introduced by
It seems like $className defined by $input->getArgument('name') on line 167 can also be of type array<integer,string>; however, Phinx\Util\Util::isValidPhinxClassName() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
174 13
                throw new InvalidArgumentException(sprintf(
175
                    'The migration class name "%s" is invalid. Please use CamelCase format.',
176 13
                    $className
177
                ));
178
            }
179
180
            // Compute the file path
181
            $fileName = Util::mapClassNameToFileName($className);
0 ignored issues
show
Bug introduced by
It seems like $className defined by $input->getArgument('name') on line 167 can also be of type array<integer,string>; however, Phinx\Util\Util::mapClassNameToFileName() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
182
        }
183 13
184 2
        if (!Util::isUniqueMigrationClassName($className, $path)) {
0 ignored issues
show
Bug introduced by
It seems like $className defined by $input->getArgument('name') on line 167 can also be of type array<integer,string>; however, Phinx\Util\Util::isUniqueMigrationClassName() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
185 2
            throw new InvalidArgumentException(sprintf(
186 2
                'The migration class name "%s%s" already exists',
187
                $namespace ? ($namespace . '\\') : '',
188 2
                $className
189
            ));
190
        }
191
192 13
        $filePath = $path . DIRECTORY_SEPARATOR . $fileName;
193 13
194
        if (is_file($filePath)) {
195 13
            throw new InvalidArgumentException(sprintf(
196
                'The file "%s" already exists',
197
                $filePath
198
            ));
199
        }
200
201
        // Get the alternative template and static class options from the config, but only allow one of them.
202
        $defaultAltTemplate = $this->getConfig()->getTemplateFile();
203 13
        $defaultCreationClassName = $this->getConfig()->getTemplateClass();
204 13
        if ($defaultAltTemplate && $defaultCreationClassName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $defaultAltTemplate of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $defaultCreationClassName of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
205 13
            throw new InvalidArgumentException('Cannot define template:class and template:file at the same time');
206 1
        }
207
208
        // Get the alternative template and static class options from the command line, but only allow one of them.
209
        $altTemplate = $input->getOption('template');
210 12
        $creationClassName = $input->getOption('class');
211 12
        if ($altTemplate && $creationClassName) {
212 12
            throw new InvalidArgumentException('Cannot use --template and --class at the same time');
213 1
        }
214
215
        // If no commandline options then use the defaults.
216
        if (!$altTemplate && !$creationClassName) {
217 11
            $altTemplate = $defaultAltTemplate;
218 5
            $creationClassName = $defaultCreationClassName;
219 5
        }
220 5
221
        // Verify the alternative template file's existence.
222
        if ($altTemplate && !is_file($altTemplate)) {
223 11
            throw new InvalidArgumentException(sprintf(
224
                'The alternative template file "%s" does not exist',
225
                $altTemplate
226
            ));
227
        }
228
229
        // Verify that the template creation class (or the aliased class) exists and that it implements the required interface.
230
        $aliasedClassName = null;
231 11
        if ($creationClassName) {
232 11
            // Supplied class does not exist, is it aliased?
233
            if (!class_exists($creationClassName)) {
234 9
                $aliasedClassName = $this->getConfig()->getAlias($creationClassName);
235 3
                if ($aliasedClassName && !class_exists($aliasedClassName)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $aliasedClassName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
236 3
                    throw new InvalidArgumentException(sprintf(
237
                        'The class "%s" via the alias "%s" does not exist',
238
                        $aliasedClassName,
239
                        $creationClassName
240
                    ));
241
                } elseif (!$aliasedClassName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $aliasedClassName of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
242 3
                    throw new InvalidArgumentException(sprintf(
243
                        'The class "%s" does not exist',
244
                        $creationClassName
245
                    ));
246
                }
247
            }
248 3
249
            // Does the class implement the required interface?
250
            if (!$aliasedClassName && !is_subclass_of($creationClassName, self::CREATION_INTERFACE)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $aliasedClassName of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if self::CREATION_INTERFACE can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
251 9
                throw new InvalidArgumentException(sprintf(
252 2
                    'The class "%s" does not implement the required interface "%s"',
253 2
                    $creationClassName,
254 2
                    self::CREATION_INTERFACE
255
                ));
256 2
            } elseif ($aliasedClassName && !is_subclass_of($aliasedClassName, self::CREATION_INTERFACE)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $aliasedClassName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if self::CREATION_INTERFACE can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
257 7
                throw new InvalidArgumentException(sprintf(
258 1
                    'The class "%s" via the alias "%s" does not implement the required interface "%s"',
259 1
                    $aliasedClassName,
260 1
                    $creationClassName,
261 1
                    self::CREATION_INTERFACE
262
                ));
263 1
            }
264
        }
265 6
266
        // Use the aliased class.
267
        $creationClassName = $aliasedClassName ?: $creationClassName;
268 8
269
        // Determine the appropriate mechanism to get the template
270
        if ($creationClassName) {
271 8
            // Get the template from the creation class
272
            $creationClass = new $creationClassName($input, $output);
273 6
            $contents = $creationClass->getMigrationTemplate();
274 6
        } else {
275 6
            // Load the alternative template if it is defined.
276
            $contents = file_get_contents($altTemplate ?: $this->getMigrationTemplateFilename());
277 2
        }
278
279
        // inject the class names appropriate to this migration
280
        $classes = [
281
            '$namespaceDefinition' => $namespace !== null ? (PHP_EOL . 'namespace ' . $namespace . ';' . PHP_EOL) : '',
282 8
            '$namespace' => $namespace,
283 8
            '$useClassName' => $this->getConfig()->getMigrationBaseClassName(false),
284 8
            '$className' => $className,
285 8
            '$version' => Util::getVersionFromFileName($fileName),
286 8
            '$baseClassName' => $this->getConfig()->getMigrationBaseClassName(true),
287 8
        ];
288 8
        $contents = strtr($contents, $classes);
289 8
290
        if (file_put_contents($filePath, $contents) === false) {
291 8
            throw new RuntimeException(sprintf(
292
                'The file "%s" could not be written to',
293
                $path
294
            ));
295
        }
296
297
        // Do we need to do the post creation call to the creation class?
298
        if (isset($creationClass)) {
299 8
            /** @var \Phinx\Migration\CreationInterface $creationClass */
300 6
            $creationClass->postMigrationCreation($filePath, $className, $this->getConfig()->getMigrationBaseClassName());
0 ignored issues
show
Bug introduced by
It seems like $className defined by $input->getArgument('name') on line 167 can also be of type array<integer,string>; however, Phinx\Migration\Creation...postMigrationCreation() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
301 6
        }
302
303 8
        $output->writeln('<info>using migration base class</info> ' . $classes['$useClassName']);
304
305 8
        if (!empty($altTemplate)) {
306
            $output->writeln('<info>using alternative template</info> ' . $altTemplate);
307 8
        } elseif (!empty($creationClassName)) {
308 6
            $output->writeln('<info>using template creation class</info> ' . $creationClassName);
309 6
        } else {
310 2
            $output->writeln('<info>using default template</info>');
311
        }
312
313 8
        $output->writeln('<info>created</info> ' . str_replace(getcwd() . DIRECTORY_SEPARATOR, '', $filePath));
314 8
315
        return self::CODE_SUCCESS;
316
    }
317
}
318