Skip to content

Commit 184b920

Browse files
authored
Improve collision handling on multiple create calls (#2325)
1 parent 33631e8 commit 184b920

File tree

4 files changed

+76
-7
lines changed

4 files changed

+76
-7
lines changed

src/Phinx/Console/Command/Create.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
167167

168168
$path = realpath($path);
169169
$className = $input->getArgument('name');
170+
$offset = 0;
171+
do {
172+
$timestamp = Util::getCurrentTimestamp($offset++);
173+
} while (!Util::isUniqueTimestamp($path, $timestamp));
174+
170175
if ($className === null) {
171-
$currentTimestamp = Util::getCurrentTimestamp();
172-
$className = 'V' . $currentTimestamp;
173-
$fileName = $currentTimestamp . '.php';
176+
$className = 'V' . $timestamp;
177+
$fileName = '';
174178
} else {
175179
if (!Util::isValidPhinxClassName($className)) {
176180
throw new InvalidArgumentException(sprintf(
@@ -179,9 +183,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
179183
));
180184
}
181185

182-
// Compute the file path
183-
$fileName = Util::mapClassNameToFileName($className);
186+
$fileName = Util::toSnakeCase($className);
184187
}
188+
$fileName = $timestamp . $fileName . '.php';
185189

186190
if (!Util::isUniqueMigrationClassName($className, $path)) {
187191
throw new InvalidArgumentException(sprintf(

src/Phinx/Util/Util.php

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,28 @@ class Util
4747
*
4848
* @return string
4949
*/
50-
public static function getCurrentTimestamp(): string
50+
public static function getCurrentTimestamp(?int $offset = null): string
5151
{
5252
$dt = new DateTime('now', new DateTimeZone('UTC'));
53+
if ($offset) {
54+
$dt->modify('+' . $offset . ' seconds');
55+
}
5356

5457
return $dt->format(static::DATE_FORMAT);
5558
}
5659

60+
/**
61+
* Checks that the given timestamp is a unique prefix for any files in the given path.
62+
*
63+
* @param string $path Path to check
64+
* @param string $timestamp Timestamp to check
65+
* @return bool
66+
*/
67+
public static function isUniqueTimestamp(string $path, string $timestamp): bool
68+
{
69+
return !count(static::glob($path . DIRECTORY_SEPARATOR . $timestamp . '*.php'));
70+
}
71+
5772
/**
5873
* Gets an array of all the existing migration class names.
5974
*
@@ -99,11 +114,27 @@ public static function getVersionFromFileName(string $fileName): int
99114
return $value;
100115
}
101116

117+
/**
118+
* Given a string, convert it to snake_case.
119+
*
120+
* @param string $string String to convert
121+
* @return string
122+
*/
123+
public static function toSnakeCase(string $string): string
124+
{
125+
$snake = function ($matches) {
126+
return '_' . strtolower($matches[0]);
127+
};
128+
129+
return preg_replace_callback('/\d+|[A-Z]/', $snake, $string);
130+
}
131+
102132
/**
103133
* Turn migration names like 'CreateUserTable' into file names like
104134
* '12345678901234_create_user_table.php' or 'LimitResourceNamesTo30Chars' into
105135
* '12345678901234_limit_resource_names_to_30_chars.php'.
106136
*
137+
* @deprecated Will be removed in 0.17.0
107138
* @param string $className Class Name
108139
* @return string
109140
*/

tests/Phinx/Console/Command/CreateTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Phinx\Console\Command\AbstractCommand;
1010
use Phinx\Console\Command\Create;
1111
use Phinx\Console\PhinxApplication;
12+
use Phinx\Util\Util;
1213
use Symfony\Component\Console\Input\ArrayInput;
1314
use Symfony\Component\Console\Output\StreamOutput;
1415
use Symfony\Component\Console\Tester\CommandTester;
@@ -561,6 +562,33 @@ public function testCreateMigrationWithUpDownStyleAsFlag(): void
561562
$this->assertStringNotContainsString('public function change()', $migrationContents);
562563
}
563564

565+
public function testCreateMigrationWithExistingTimestamp(): void
566+
{
567+
$application = new PhinxApplication();
568+
$application->add(new Create());
569+
570+
/** @var Create $command */
571+
$command = $application->find('create');
572+
573+
/** @var \Phinx\Migration\Manager $managerStub mock the manager class */
574+
$managerStub = $this->getMockBuilder('\Phinx\Migration\Manager')
575+
->setConstructorArgs([$this->config, $this->input, $this->output])
576+
->getMock();
577+
578+
$command->setConfig($this->config);
579+
$command->setManager($managerStub);
580+
581+
$commandTester = new CommandTester($command);
582+
$commandTester->execute(['command' => $command->getName(), 'name' => 'Foo']);
583+
$commandTester->execute(['command' => $command->getName(), 'name' => 'Bar']);
584+
585+
$files = array_map(fn ($file) => basename($file), Util::getFiles($this->config->getMigrationPaths()));
586+
sort($files);
587+
$timestamp = explode('_', $files[0])[0];
588+
$secondTimestamp = (float)$timestamp + (str_ends_with($timestamp, '59') ? 41 : 1);
589+
$this->assertEquals([$timestamp . '_foo.php', $secondTimestamp . '_bar.php'], $files);
590+
}
591+
564592
public function testCreateMigrationWithInvalidStyleFlagThrows(): void
565593
{
566594
$application = new PhinxApplication();

tests/Phinx/Util/UtilTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ public function testGetCurrentTimestamp()
5151
$this->assertLessThanOrEqual($expected + 2, $current);
5252
}
5353

54+
public function testIsUniqueTimestamp(): void
55+
{
56+
$this->assertFalse(Util::isUniqueTimestamp(__DIR__ . '/_files/migrations', '20120111235330'));
57+
$this->assertTrue(Util::isUniqueTimestamp(__DIR__ . '/_files/migrations', '20120111235301'));
58+
}
59+
5460
public function testGetVersionFromFileName(): void
5561
{
5662
$this->assertSame(20221130101652, Util::getVersionFromFileName('20221130101652_test.php'));
@@ -62,7 +68,7 @@ public function testGetVersionFromFileNameErrorNoVersion(): void
6268
Util::getVersionFromFileName('foo.php');
6369
}
6470

65-
public function testGetVersionFromFileNameErrorZeroVersion(): VoidCommand
71+
public function testGetVersionFromFileNameErrorZeroVersion(): void
6672
{
6773
$this->expectException(RuntimeException::class);
6874
Util::getVersionFromFileName('0_foo.php');

0 commit comments

Comments
 (0)