Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
`migrate:create` commands: remove redundant messages, replace `>>>` with cleaner output, and move "Database
connection" info to the top (@samdark, @vjik)
- Enh #333: Use `newMigrationPath` and `newMigrationNamespace` as source (@Tigrov)
- Bug #341: Fix migration namespaces and paths (@Tigrov)

## 2.0.1 December 20, 2025

Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"check-dependencies": "composer-require-checker",
"mutation": "infection",
"psalm": "psalm",
"test": "phpunit --testdox --no-interaction"
"test": "phpunit --testdox --no-interaction",
"cs-fix": "php-cs-fixer fix"
}
}
11 changes: 9 additions & 2 deletions src/Command/CreateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Yiisoft\Db\Migration\Command;

use LogicException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
Expand Down Expand Up @@ -144,16 +145,22 @@
$className = $this->migrationService->generateClassName($name);
$nameLimit = $this->migrator->getMigrationNameLimit();

if ($nameLimit !== 0 && strlen($className) > $nameLimit) {

Check warning on line 148 in src/Command/CreateCommand.php

View workflow job for this annotation

GitHub Actions / tests / PHP 8.5

Escaped Mutant for Mutator "GreaterThan": @@ @@ $className = $this->migrationService->generateClassName($name); $nameLimit = $this->migrator->getMigrationNameLimit(); - if ($nameLimit !== 0 && strlen($className) > $nameLimit) { + if ($nameLimit !== 0 && strlen($className) >= $nameLimit) { $io->error('The migration name is too long.'); return Command::INVALID;
$io->error('The migration name is too long.');

return Command::INVALID;
}

$migrationPath = $this->migrationService->findMigrationPath();
try {
$migrationPath = $this->migrationService->findMigrationPath();
} catch (LogicException $e) {
$io->error($e->getMessage());

return Command::INVALID;
}

if (!is_dir($migrationPath)) {
$io->error("Invalid path directory $migrationPath");
$io->error("Invalid path directory \"$migrationPath\"");

return Command::INVALID;
}
Expand Down Expand Up @@ -188,13 +195,13 @@
{
$result = '';

return match ($command) {

Check warning on line 198 in src/Command/CreateCommand.php

View workflow job for this annotation

GitHub Actions / tests / PHP 8.5

Escaped Mutant for Mutator "MatchArmRemoval": @@ @@ 'addColumn' => 'Add_Column_' . $name, 'dropColumn' => 'Drop_Column_' . $name, 'junction' => 'Junction_Table_For_' . $name . '_And_' . (string) $and . '_Tables', - default => $result, }; } }
'create' => $name,
'table' => 'Create_' . $name . '_Table',
'dropTable' => 'Drop_' . $name . '_Table',

Check warning on line 201 in src/Command/CreateCommand.php

View workflow job for this annotation

GitHub Actions / tests / PHP 8.5

Escaped Mutant for Mutator "Concat": @@ @@ return match ($command) { 'create' => $name, 'table' => 'Create_' . $name . '_Table', - 'dropTable' => 'Drop_' . $name . '_Table', + 'dropTable' => 'Drop_' . '_Table' . $name, 'addColumn' => 'Add_Column_' . $name, 'dropColumn' => 'Drop_Column_' . $name, 'junction' => 'Junction_Table_For_' . $name . '_And_' . (string) $and . '_Tables',

Check warning on line 201 in src/Command/CreateCommand.php

View workflow job for this annotation

GitHub Actions / tests / PHP 8.5

Escaped Mutant for Mutator "ConcatOperandRemoval": @@ @@ return match ($command) { 'create' => $name, 'table' => 'Create_' . $name . '_Table', - 'dropTable' => 'Drop_' . $name . '_Table', + 'dropTable' => $name . '_Table', 'addColumn' => 'Add_Column_' . $name, 'dropColumn' => 'Drop_Column_' . $name, 'junction' => 'Junction_Table_For_' . $name . '_And_' . (string) $and . '_Tables',
'addColumn' => 'Add_Column_' . $name,
'dropColumn' => 'Drop_Column_' . $name,

Check warning on line 203 in src/Command/CreateCommand.php

View workflow job for this annotation

GitHub Actions / tests / PHP 8.5

Escaped Mutant for Mutator "Concat": @@ @@ 'table' => 'Create_' . $name . '_Table', 'dropTable' => 'Drop_' . $name . '_Table', 'addColumn' => 'Add_Column_' . $name, - 'dropColumn' => 'Drop_Column_' . $name, + 'dropColumn' => $name . 'Drop_Column_', 'junction' => 'Junction_Table_For_' . $name . '_And_' . (string) $and . '_Tables', default => $result, };
'junction' => 'Junction_Table_For_' . $name . '_And_' . (string) $and . '_Tables',

Check warning on line 204 in src/Command/CreateCommand.php

View workflow job for this annotation

GitHub Actions / tests / PHP 8.5

Escaped Mutant for Mutator "Concat": @@ @@ 'dropTable' => 'Drop_' . $name . '_Table', 'addColumn' => 'Add_Column_' . $name, 'dropColumn' => 'Drop_Column_' . $name, - 'junction' => 'Junction_Table_For_' . $name . '_And_' . (string) $and . '_Tables', + 'junction' => 'Junction_Table_For_' . $name . (string) $and . '_And_' . '_Tables', default => $result, }; }

Check warning on line 204 in src/Command/CreateCommand.php

View workflow job for this annotation

GitHub Actions / tests / PHP 8.5

Escaped Mutant for Mutator "ConcatOperandRemoval": @@ @@ 'dropTable' => 'Drop_' . $name . '_Table', 'addColumn' => 'Add_Column_' . $name, 'dropColumn' => 'Drop_Column_' . $name, - 'junction' => 'Junction_Table_For_' . $name . '_And_' . (string) $and . '_Tables', + 'junction' => 'Junction_Table_For_' . $name . (string) $and . '_Tables', default => $result, }; }

Check warning on line 204 in src/Command/CreateCommand.php

View workflow job for this annotation

GitHub Actions / tests / PHP 8.5

Escaped Mutant for Mutator "Concat": @@ @@ 'dropTable' => 'Drop_' . $name . '_Table', 'addColumn' => 'Add_Column_' . $name, 'dropColumn' => 'Drop_Column_' . $name, - 'junction' => 'Junction_Table_For_' . $name . '_And_' . (string) $and . '_Tables', + 'junction' => $name . 'Junction_Table_For_' . '_And_' . (string) $and . '_Tables', default => $result, }; }
default => $result,
};
}
Expand Down
146 changes: 100 additions & 46 deletions src/Service/MigrationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
use Yiisoft\Db\Migration\Migrator;
use Yiisoft\Db\Migration\RevertibleMigrationInterface;

use function array_keys;
use function array_map;
use function array_merge;
use function array_unique;
use function array_values;
use function class_exists;
use function closedir;
use function dirname;
use function gmdate;
Expand All @@ -33,6 +36,7 @@
use function readdir;
use function realpath;
use function reset;
use function rtrim;
use function str_contains;
use function str_replace;
use function str_starts_with;
Expand Down Expand Up @@ -128,40 +132,8 @@ public function getNewMigrations(): array
$applied[trim($class, '\\')] = true;
}

$migrations = [];
$migrationPaths = $this->findSourcePaths();

foreach ($migrationPaths as [$sourcePath, $namespace]) {
if (!is_dir($sourcePath)) {
continue;
}

/** @var resource $handle */
$handle = opendir($sourcePath);
while (($file = readdir($handle)) !== false) {
if ($file === '.' || $file === '..') {
continue;
}

$path = $sourcePath . DIRECTORY_SEPARATOR . $file;

if (is_file($path) && preg_match('/^(M(\d{12}).*)\.php$/s', $file, $matches)) {
[, $class, $time] = $matches;

if (!empty($namespace)) {
$class = $namespace . '\\' . $class;
}

/** @psalm-var class-string $class */

if (!isset($applied[$class])) {
$migrations[$time . '\\' . $class] = $class;
}
}
}
closedir($handle);
}

$migrations = $this->loadMigrationClasses();
$migrations = array_filter($migrations, static fn(string $class): bool => !isset($applied[$class]));
ksort($migrations);
return array_values($migrations);
}
Expand Down Expand Up @@ -416,28 +388,33 @@ private function makeMigrationInstance(string $class): object
}

/**
* Returns the migration paths with namespaces if they are specified.
* Returns the migration paths with namespaces.
*
* @return array<array{0: string, 1: string}>
* @return true[][]
* @psalm-return array<string, array<string, true>>
*/
private function findSourcePaths(): array
{
$paths = [];

if ($this->newMigrationPath !== '') {
$paths[] = [$this->newMigrationPath, ''];
$newMigrationPath = $this->normalizePath($this->newMigrationPath);
$newMigrationNamespaces = $this->getNamespacesFromPath($newMigrationPath);
$paths[$newMigrationPath] = array_fill_keys($newMigrationNamespaces, true);
} elseif ($this->newMigrationNamespace !== '') {
$newMigrationPath = $this->getNamespacePath($this->newMigrationNamespace);
$paths[] = [$newMigrationPath, $this->newMigrationNamespace];
$paths[$newMigrationPath][$this->newMigrationNamespace] = true;
}

foreach ($this->sourcePaths as $sourcePaths) {
$paths[] = [$sourcePaths, ''];
foreach ($this->sourcePaths as $sourcePath) {
$sourcePath = $this->normalizePath($sourcePath);
$sourceNamespaces = $this->getNamespacesFromPath($sourcePath);
$paths[$sourcePath] = ($paths[$sourcePath] ?? []) + array_fill_keys($sourceNamespaces, true);
}

foreach ($this->sourceNamespaces as $namespace) {
$sourcePath = $this->getNamespacePath($namespace);
$paths[] = [$sourcePath, $namespace];
foreach ($this->sourceNamespaces as $sourceNamespace) {
$sourcePath = $this->getNamespacePath($sourceNamespace);
$paths[$sourcePath][$sourceNamespace] = true;
}

return $paths;
Expand All @@ -448,6 +425,8 @@ private function findSourcePaths(): array
*
* @param string $namespace Namespace.
*
* @throws LogicException If the namespace is invalid.
*
* @return string File path.
*/
private function getNamespacePath(string $namespace): string
Expand All @@ -462,11 +441,15 @@ private function getNamespacePath(string $namespace): string
if (str_starts_with($namespace, trim($mapNamespace, '\\'))) {
/** @var string $mapDirectory */
$mapDirectory = reset($mapDirectories);
return $mapDirectory . '/' . str_replace('\\', '/', substr($namespace, strlen($mapNamespace)));
$path = $mapDirectory . '/' . str_replace('\\', '/', substr($namespace, strlen($mapNamespace)));

if (is_dir($path)) {
return $this->normalizePath($path);
}
}
}

throw new LogicException("Invalid namespace: \"$namespace\".");
throw new LogicException("Invalid namespace \"$namespace\"");
}

/**
Expand All @@ -475,6 +458,7 @@ private function getNamespacePath(string $namespace): string
* @param string $path File path.
*
* @return string[] Namespaces.
* @psalm-return list<string>
*/
private function getNamespacesFromPath(string $path): array
{
Expand Down Expand Up @@ -513,12 +497,82 @@ private function getNamespacesFromPath(string $path): array

krsort($namespaces);

return array_values(reset($namespaces));
/** @psalm-var list<string> */
return array_values(array_unique(array_merge(...$namespaces)));
}

private function getVendorDir(): string
{
$class = new ReflectionClass(ClassLoader::class);
return dirname($class->getFileName(), 2);
}

/**
* Loads migration classes.
*
* @return string[] List of migration classes indexed by their time stamp and file path.
* @psalm-return class-string[]
*/
private function loadMigrationClasses(): array
{
$migrationPaths = $this->findSourcePaths();

$migrations = [];

foreach ($migrationPaths as $path => $namespaces) {
/** @var resource $handle */
$handle = opendir($path);
while (($file = readdir($handle)) !== false) {
if ($file === '.' || $file === '..') {
continue;
}

$filePath = "$path/$file";

if (!is_file($filePath)) {
continue;
}

if (preg_match('/^(M(\d{12}).*)\.php$/s', $file, $matches) !== 1) {
continue;
}

[, $class, $time] = $matches;

$sortKey = "$time/$filePath";

if (isset($migrations[$sortKey])) {
continue;
}

require_once $filePath;

foreach (array_keys($namespaces) as $namespace) {
if (class_exists($namespace . '\\' . $class, false)) {
/** @psalm-var class-string */
$migrations[$sortKey] = $namespace . '\\' . $class;
break;
}

if (class_exists($class, false)) {
/** @psalm-var class-string $class */
$migrations[$sortKey] = $class;
break;
}
}
}
closedir($handle);
}

return $migrations;
}

private function normalizePath(string $path): string
{
if (!is_dir($path)) {
throw new LogicException("Invalid path directory \"$path\"");
}

return rtrim(str_replace('\\', '/', $path), '/');
}
}
2 changes: 1 addition & 1 deletion tests/Common/Command/AbstractCreateCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,7 @@ public function testIncorrectNewMigrationNamespace(): void
$output = preg_replace('/(\R|\s)+/', ' ', $command->getDisplay(true));

$this->assertSame(Command::INVALID, $exitCode);
$this->assertStringContainsString('Invalid path directory', $output);
$this->assertStringContainsString('Invalid namespace "Yiisoft\Db\Migration\TestsRuntime\NotExists"', $output);
}

public function testWithoutNewMigrationNamespace(): void
Expand Down
Loading
Loading