diff --git a/composer.json b/composer.json index 7ca1aa1df..e1e45ab1f 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,7 @@ "doctrine/orm": "^2.13 || ^3", "doctrine/persistence": "^2 || ^3", "doctrine/sql-formatter": "^1.0", + "fig/log-test": "^1", "phpstan/phpstan": "^1.10", "phpstan/phpstan-deprecation-rules": "^1.1", "phpstan/phpstan-phpunit": "^1.3", diff --git a/phpcs.xml.dist b/phpcs.xml.dist index fbb09a1c7..1f53d5c6b 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -38,8 +38,7 @@ - tests/TestLogger.php - src/Tools/Console/ConsoleLogger.php + tests/LogUtil.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 6d86b7af3..a455f7dff 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -17,9 +17,6 @@ parameters: - message: '~^Call to function is_bool\(\) with bool will always evaluate to true\.$~' path: src/InlineParameterFormatter.php - - - message: '~^Call to an undefined method Symfony\\Component\\Console\\Output\\OutputInterface\:\:getErrorOutput\(\)\.$~' - path: src/Tools/Console/ConsoleLogger.php # https://github.com/phpstan/phpstan/issues/5982 - diff --git a/src/Tools/Console/Command/DoctrineCommand.php b/src/Tools/Console/Command/DoctrineCommand.php index 661f30d54..c1003bce7 100644 --- a/src/Tools/Console/Command/DoctrineCommand.php +++ b/src/Tools/Console/Command/DoctrineCommand.php @@ -7,14 +7,15 @@ use Doctrine\Migrations\Configuration\Connection\ConfigurationFile; use Doctrine\Migrations\Configuration\Migration\ConfigurationFileWithFallback; use Doctrine\Migrations\DependencyFactory; -use Doctrine\Migrations\Tools\Console\ConsoleLogger; use Doctrine\Migrations\Tools\Console\Exception\DependenciesNotSatisfied; use Doctrine\Migrations\Tools\Console\Exception\InvalidOptionUsage; use Exception; use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Style\StyleInterface; @@ -104,7 +105,16 @@ protected function initialize(InputInterface $input, OutputInterface $output): v return; } - $logger = new ConsoleLogger($output); + $logger = new ConsoleLogger($output, [ + LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL, + LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL, + LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL, + LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL, + LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL, + LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL, + LogLevel::INFO => OutputInterface::VERBOSITY_VERBOSE, + LogLevel::DEBUG => OutputInterface::VERBOSITY_VERY_VERBOSE, + ]); $dependencyFactory->setService(LoggerInterface::class, $logger); $dependencyFactory->freeze(); } diff --git a/src/Tools/Console/ConsoleLogger.php b/src/Tools/Console/ConsoleLogger.php deleted file mode 100644 index 1cba5a322..000000000 --- a/src/Tools/Console/ConsoleLogger.php +++ /dev/null @@ -1,133 +0,0 @@ - */ - private array $verbosityLevelMap = [ - LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL, - LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL, - LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL, - LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL, - LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL, - LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL, - LogLevel::INFO => OutputInterface::VERBOSITY_VERBOSE, - LogLevel::DEBUG => OutputInterface::VERBOSITY_VERY_VERBOSE, - ]; - /** @var array */ - private array $formatLevelMap = [ - LogLevel::EMERGENCY => self::ERROR, - LogLevel::ALERT => self::ERROR, - LogLevel::CRITICAL => self::ERROR, - LogLevel::ERROR => self::ERROR, - LogLevel::WARNING => self::INFO, - LogLevel::NOTICE => self::INFO, - LogLevel::INFO => self::INFO, - LogLevel::DEBUG => self::INFO, - ]; - - /** - * @param array $verbosityLevelMap - * @param array $formatLevelMap - */ - public function __construct( - private readonly OutputInterface $output, - array $verbosityLevelMap = [], - array $formatLevelMap = [], - ) { - $this->verbosityLevelMap = $verbosityLevelMap + $this->verbosityLevelMap; - $this->formatLevelMap = $formatLevelMap + $this->formatLevelMap; - } - - /** - * {@inheritDoc} - * - * @param mixed[] $context - */ - public function log(mixed $level, string|Stringable $message, array $context = []): void - { - if (! isset($this->verbosityLevelMap[$level])) { - throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level)); - } - - $output = $this->output; - - // Write to the error output if necessary and available - if ($this->formatLevelMap[$level] === self::ERROR) { - if ($this->output instanceof ConsoleOutputInterface) { - $output = $output->getErrorOutput(); - } - } - - // the if condition check isn't necessary -- it's the same one that $output will do internally anyway. - // We only do it for efficiency here as the message formatting is relatively expensive. - if ($output->getVerbosity() < $this->verbosityLevelMap[$level]) { - return; - } - - $output->writeln(sprintf('<%1$s>[%2$s] %3$s', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context)), $this->verbosityLevelMap[$level]); - } - - /** - * Interpolates context values into the message placeholders. - * - * @param mixed[] $context - */ - private function interpolate(string|Stringable $message, array $context): string - { - $message = (string) $message; - if (! str_contains($message, '{')) { - return $message; - } - - $replacements = []; - foreach ($context as $key => $val) { - if ($val === null || is_scalar($val) || $val instanceof Stringable) { - $replacements["{{$key}}"] = $val; - } elseif ($val instanceof DateTimeInterface) { - $replacements["{{$key}}"] = $val->format(DateTime::RFC3339); - } elseif (is_object($val)) { - $replacements["{{$key}}"] = '[object ' . $val::class . ']'; - } else { - $replacements["{{$key}}"] = '[' . gettype($val) . ']'; - } - - if (! isset($replacements["{{$key}}"])) { - continue; - } - - $replacements["{{$key}}"] = '' . $replacements["{{$key}}"] . ''; - } - - return strtr($message, $replacements); - } -} diff --git a/tests/AbstractMigrationTest.php b/tests/AbstractMigrationTest.php index 24d952b58..25e58bf3d 100644 --- a/tests/AbstractMigrationTest.php +++ b/tests/AbstractMigrationTest.php @@ -12,9 +12,12 @@ use Doctrine\Migrations\Query\Query; use Doctrine\Migrations\Tests\Stub\AbstractMigrationStub; use Doctrine\Migrations\Tests\Stub\AbstractMigrationWithoutDownStub; +use Psr\Log\Test\TestLogger; class AbstractMigrationTest extends MigrationTestCase { + use LogUtil; + private AbstractMigrationStub $migration; private TestLogger $logger; @@ -73,7 +76,7 @@ public function testWarnIfAddDefaultMessage(): void public function testWarnIfDontOutputMessageIfFalse(): void { $this->migration->warnIf(false, 'trallala'); - self::assertSame('', $this->getLogOutput($this->logger)); + self::assertSame([], $this->logger->records); } public function testWriteInvokesOutputWriter(): void diff --git a/tests/TestLogger.php b/tests/LogUtil.php similarity index 59% rename from tests/TestLogger.php rename to tests/LogUtil.php index 7b2e5c6ca..86e1a617d 100644 --- a/tests/TestLogger.php +++ b/tests/LogUtil.php @@ -4,50 +4,49 @@ namespace Doctrine\Migrations\Tests; -use DateTime; use DateTimeInterface; -use Psr\Log\AbstractLogger; +use Psr\Log\Test\TestLogger; use Stringable; +use function array_map; use function gettype; +use function implode; use function is_object; use function is_scalar; use function str_contains; use function strtr; -class TestLogger extends AbstractLogger +trait LogUtil { - /** @var string[] */ - public array $logs = []; + private function getLogOutput(TestLogger $logger): string + { + return implode("\n", $this->getInterpolatedLogRecords($logger)); + } - /** - * {@inheritDoc} - * - * @param mixed[] $context - */ - public function log(mixed $level, string|Stringable $message, array $context = []): void + /** @return list */ + private function getInterpolatedLogRecords(TestLogger $logger): array { - $this->logs[] = $this->interpolate($message, $context); + return array_map($this->interpolate(...), $logger->records); } /** * Interpolates context values into the message placeholders. * - * @param mixed[] $context + * @param array{level: mixed, message: string|Stringable, context: mixed[]} $record */ - private function interpolate(string|Stringable $message, array $context): string + private function interpolate(array $record): string { - $message = (string) $message; + $message = (string) $record['message']; if (! str_contains($message, '{')) { return $message; } $replacements = []; - foreach ($context as $key => $val) { + foreach ($record['context'] as $key => $val) { if ($val === null || is_scalar($val) || $val instanceof Stringable) { $replacements["{{$key}}"] = $val; } elseif ($val instanceof DateTimeInterface) { - $replacements["{{$key}}"] = $val->format(DateTime::RFC3339); + $replacements["{{$key}}"] = $val->format(DateTimeInterface::RFC3339); } elseif (is_object($val)) { $replacements["{{$key}}"] = '[object ' . $val::class . ']'; } else { diff --git a/tests/Metadata/Storage/DebugLogger.php b/tests/Metadata/Storage/DebugLogger.php deleted file mode 100644 index 30ad3b286..000000000 --- a/tests/Metadata/Storage/DebugLogger.php +++ /dev/null @@ -1,23 +0,0 @@ -count++; - } -} diff --git a/tests/Metadata/Storage/TableMetadataStorageTest.php b/tests/Metadata/Storage/TableMetadataStorageTest.php index 8e1d772bf..4fcb0a5ae 100644 --- a/tests/Metadata/Storage/TableMetadataStorageTest.php +++ b/tests/Metadata/Storage/TableMetadataStorageTest.php @@ -26,6 +26,7 @@ use Doctrine\Migrations\Version\ExecutionResult; use Doctrine\Migrations\Version\Version; use PHPUnit\Framework\TestCase; +use Psr\Log\Test\TestLogger; use function sprintf; @@ -42,7 +43,7 @@ class TableMetadataStorageTest extends TestCase /** @var AbstractSchemaManager */ private AbstractSchemaManager $schemaManager; - private DebugLogger $debugLogger; + private TestLogger $testLogger; private function getSqliteConnection(Configuration|null $configuration = null): Connection { @@ -54,8 +55,8 @@ private function getSqliteConnection(Configuration|null $configuration = null): public function setUp(): void { $this->connectionConfig = new Configuration(); - $this->debugLogger = new DebugLogger(); - $this->connectionConfig->setMiddlewares([new Middleware($this->debugLogger)]); + $this->testLogger = new TestLogger(); + $this->connectionConfig->setMiddlewares([new Middleware($this->testLogger)]); $this->connection = $this->getSqliteConnection($this->connectionConfig); $this->schemaManager = $this->connection->createSchemaManager(); @@ -67,13 +68,12 @@ public function testSchemaIntrospectionExecutedOnlyOnce(): void { $this->storage->ensureInitialized(); - $oldQueryCount = $this->debugLogger->count; + $this->testLogger->reset(); $this->storage->ensureInitialized(); - self::assertSame(0, $this->debugLogger->count - $oldQueryCount); + self::assertCount(0, $this->testLogger->records); - $oldQueryCount = $this->debugLogger->count; $this->storage->getExecutedMigrations(); - self::assertSame(1, $this->debugLogger->count - $oldQueryCount); + self::assertCount(1, $this->testLogger->records); } public function testDifferentTableNotUpdatedOnRead(): void diff --git a/tests/MigrationTestCase.php b/tests/MigrationTestCase.php index 28b14c48a..32f2e091d 100644 --- a/tests/MigrationTestCase.php +++ b/tests/MigrationTestCase.php @@ -8,8 +8,6 @@ use Doctrine\DBAL\DriverManager; use PHPUnit\Framework\TestCase; -use function implode; - abstract class MigrationTestCase extends TestCase { public function getSqliteConnection(): Connection @@ -18,9 +16,4 @@ public function getSqliteConnection(): Connection return DriverManager::getConnection($params); } - - public function getLogOutput(TestLogger $logger): string - { - return implode("\n", $logger->logs); - } } diff --git a/tests/MigratorTest.php b/tests/MigratorTest.php index bfe910d8c..58b492f19 100644 --- a/tests/MigratorTest.php +++ b/tests/MigratorTest.php @@ -23,6 +23,7 @@ use Doctrine\Migrations\Version\Direction; use Doctrine\Migrations\Version\Version; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\Test\TestLogger; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Stopwatch\Stopwatch; use Throwable; @@ -79,8 +80,8 @@ public function testEmptyPlanShowsMessage(): void $planList = new MigrationPlanList([], Direction::UP); $migrator->migrate($planList, $this->migratorConfiguration); - self::assertCount(1, $this->logger->logs, 'should output the no migrations message'); - self::assertStringContainsString('No migrations', $this->logger->logs[0]); + self::assertCount(1, $this->logger->records, 'should output the no migrations message'); + self::assertStringContainsString('No migrations', $this->logger->records[0]['message']); } protected function createTestMigrator(): DbalMigrator diff --git a/tests/Tools/Console/Command/MigrationVersionTest.php b/tests/Tools/Console/Command/MigrationVersionTest.php index 13b303e26..a57209ddd 100644 --- a/tests/Tools/Console/Command/MigrationVersionTest.php +++ b/tests/Tools/Console/Command/MigrationVersionTest.php @@ -13,12 +13,12 @@ use Doctrine\Migrations\MigrationsRepository; use Doctrine\Migrations\Tests\Helper; use Doctrine\Migrations\Tests\MigrationTestCase; -use Doctrine\Migrations\Tests\TestLogger; use Doctrine\Migrations\Tools\Console\Command\VersionCommand; use Doctrine\Migrations\Version\Direction; use Doctrine\Migrations\Version\ExecutionResult; use Doctrine\Migrations\Version\Version; use InvalidArgumentException; +use Psr\Log\NullLogger; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Tester\CommandTester; @@ -40,13 +40,12 @@ protected function setUp(): void $configuration = new Configuration(); $configuration->addMigrationsDirectory('DoctrineMigrations', sys_get_temp_dir()); - $conn = $this->getSqliteConnection(); - $logger = new TestLogger(); + $conn = $this->getSqliteConnection(); $dependencyFactory = DependencyFactory::fromConnection( new ExistingConfiguration($configuration), new ExistingConnection($conn), - $logger, + new NullLogger(), ); $this->migrationRepository = $dependencyFactory->getMigrationRepository(); diff --git a/tests/Tools/Console/ConsoleLoggerTest.php b/tests/Tools/Console/ConsoleLoggerTest.php deleted file mode 100644 index b8aac2d91..000000000 --- a/tests/Tools/Console/ConsoleLoggerTest.php +++ /dev/null @@ -1,74 +0,0 @@ -output = new BufferedOutput(); - } - - public function testNoInfoAndDebugAsDefault(): void - { - $logger = new ConsoleLogger($this->output); - $logger->info('foo'); - $logger->debug('bar'); - - self::assertSame('', $this->output->fetch()); - } - - public function testLevelCanBeChanged(): void - { - $logger = new ConsoleLogger($this->output, [LogLevel::INFO => OutputInterface::VERBOSITY_NORMAL]); - $logger->info('foo'); - $logger->debug('bar'); - - self::assertSame('[info] foo' . PHP_EOL, $this->output->fetch()); - } - - public function testVerbosityIsREspected(): void - { - $this->output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); - - $logger = new ConsoleLogger($this->output); - $logger->info('foo'); - $logger->debug('bar'); - - self::assertSame( - '[info] foo' . PHP_EOL . '[debug] bar' . PHP_EOL, - $this->output->fetch(), - ); - } - - public function testInterpolation(): void - { - $logger = new ConsoleLogger($this->output); - $logger->error('foo {number} {date} {object} {resource} {missing} bar', [ - 'number' => 1, - 'date' => new DateTime('2010-01-01 00:08:09+00:00'), - 'object' => new stdClass(), - 'resource' => fopen('php://output', 'w'), - ]); - self::assertSame( - '[error] foo 1 2010-01-01T00:08:09+00:00 [object stdClass] [resource] {missing} bar' . PHP_EOL, - $this->output->fetch(), - ); - } -} diff --git a/tests/Version/ExecutorTest.php b/tests/Version/ExecutorTest.php index 31acf865d..cf5d8746d 100644 --- a/tests/Version/ExecutorTest.php +++ b/tests/Version/ExecutorTest.php @@ -14,7 +14,7 @@ use Doctrine\Migrations\ParameterFormatter; use Doctrine\Migrations\Provider\SchemaDiffProvider; use Doctrine\Migrations\Query\Query; -use Doctrine\Migrations\Tests\TestLogger; +use Doctrine\Migrations\Tests\LogUtil; use Doctrine\Migrations\Tests\Version\Fixture\EmptyTestMigration; use Doctrine\Migrations\Tests\Version\Fixture\VersionExecutorTestMigration; use Doctrine\Migrations\Version\DbalExecutor; @@ -25,6 +25,7 @@ use Exception; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\Test\TestLogger; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\Stopwatch\StopwatchEvent; use Symfony\Component\Stopwatch\StopwatchPeriod; @@ -32,6 +33,8 @@ class ExecutorTest extends TestCase { + use LogUtil; + /** @var Connection&MockObject */ private Connection $connection; @@ -90,7 +93,7 @@ public function testExecuteWithNoQueries(): void '++ migrating xx', 'Migration xx was executed but did not result in any SQL statements.', 'Migration xx migrated (took 100ms, used 100 memory)', - ], $this->logger->logs); + ], $this->getInterpolatedLogRecords($this->logger)); } public function testExecuteUp(): void @@ -137,7 +140,7 @@ public function testExecuteUp(): void 3 => 'SELECT 2 ', 4 => 'Query took 100ms', 5 => 'Migration test migrated (took 100ms, used 100 memory)', - ], $this->logger->logs); + ], $this->getInterpolatedLogRecords($this->logger)); } /** @test */ @@ -151,7 +154,7 @@ public function executeUpShouldAppendDescriptionWhenItIsNotEmpty(): void $this->versionExecutor->execute($plan, $migratorConfiguration); - self::assertSame('++ migrating test (testing)', $this->logger->logs[0]); + self::assertSame('++ migrating test (testing)', $this->getInterpolatedLogRecords($this->logger)[0]); } public function testExecuteDown(): void @@ -198,7 +201,7 @@ public function testExecuteDown(): void 3 => 'SELECT 4 ', 4 => 'Query took 100ms', 5 => 'Migration test reverted (took 100ms, used 100 memory)', - ], $this->logger->logs); + ], $this->getInterpolatedLogRecords($this->logger)); } public function testExecuteDryRun(): void @@ -261,7 +264,7 @@ public function testExecuteDryRun(): void 'SELECT 1 ', 'SELECT 2 ', 'Migration test migrated (took 100ms, used 100 memory)', - ], $this->logger->logs); + ], $this->getInterpolatedLogRecords($this->logger)); } /** @test */ @@ -498,7 +501,7 @@ public function executeDownShouldAppendDescriptionWhenItIsNotEmpty(): void $migratorConfiguration, ); - self::assertSame('++ reverting test', $this->logger->logs[0]); + self::assertSame('++ reverting test', $this->getInterpolatedLogRecords($this->logger)[0]); } protected function setUp(): void