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