diff --git a/analysis-baseline.toml b/analysis-baseline.toml index 4cde692..e8500e9 100644 --- a/analysis-baseline.toml +++ b/analysis-baseline.toml @@ -9,13 +9,13 @@ count = 1 [[issues]] file = "src/ConfigProvider.php" code = "invalid-return-statement" -message = '''Invalid return type for function `Webware\Log\ConfigProvider::__invoke`: expected `array{'dependencies': array{'aliases': array{Psr\EventDispatcher\EventDispatcherInterface::class: class-string, Psr\EventDispatcher\ListenerProviderInterface::class: class-string}, 'delegators': array{Laminas\Stratigility\Middleware\ErrorHandler::class: list}, 'factories': array{Phly\EventDispatcher\ListenerProvider\ListenerProviderAggregate::class: class-string, Psr\Log\LoggerInterface::class: class-string, Webware\Log\Handler\PhpDbHandler::class: class-string, Webware\Log\Http\Middleware\MonologMiddleware::class: class-string, Webware\Log\Listener\Psr3LogPsr14Listener::class: class-string, Webware\Log\Processor\LaminasI18nProcessor::class: class-string}, 'invokables': array{Phly\EventDispatcher\ListenerProvider\AttachableListenerProvider::class: class-string, Phly\EventDispatcher\ListenerProvider\PrioritizedListenerProvider::class: class-string, Webware\Log\Processor\RamseyUuidProcessor::class: class-string}}, 'listener_providers': array, 'listeners': array>, 'templates': array{'paths': array{'log': list}}, Psr\Log\LoggerInterface::class: array{'auth_attribute': string, 'channel': string, 'log_errors': bool, 'process_translation': bool, 'process_uuid': bool, 'table': string}}`, but found `array{'Psr\Log\LoggerInterface': array{'auth_attribute': string, 'channel': string, 'log_errors': bool, 'process_translation': bool, 'process_uuid': bool, 'table': string}, 'dependencies': array{'aliases': array{Psr\EventDispatcher\EventDispatcherInterface::class: class-string, Psr\EventDispatcher\ListenerProviderInterface::class: class-string}, 'delegators': array{Laminas\Stratigility\Middleware\ErrorHandler::class: list}, 'factories': array{Phly\EventDispatcher\ListenerProvider\ListenerProviderAggregate::class: class-string, Psr\Log\LoggerInterface::class: class-string, Webware\Log\Handler\PhpDbHandler::class: class-string, Webware\Log\Http\Middleware\MonologMiddleware::class: class-string, Webware\Log\Listener\Psr3LogPsr14Listener::class: class-string, Webware\Log\Processor\LaminasI18nProcessor::class: class-string}, 'invokables': array{Phly\EventDispatcher\ListenerProvider\AttachableListenerProvider::class: class-string, Phly\EventDispatcher\ListenerProvider\PrioritizedListenerProvider::class: class-string, Webware\Log\Processor\RamseyUuidProcessor::class: class-string}}, 'listener_providers': array{}, 'listeners': array>, 'templates': array{'paths': array{'log': list}}}`.''' +message = '''Invalid return type for function `Webware\Log\ConfigProvider::__invoke`: expected `array{'dependencies': array{'aliases': array{Psr\EventDispatcher\EventDispatcherInterface::class: class-string, Psr\EventDispatcher\ListenerProviderInterface::class: class-string}, 'delegators': array{Laminas\Stratigility\Middleware\ErrorHandler::class: list}, 'factories': array{Phly\EventDispatcher\ListenerProvider\ListenerProviderAggregate::class: class-string, Psr\Log\LoggerInterface::class: class-string, Webware\Log\Console\InitDbCommand::class: class-string, Webware\Log\Handler\PhpDbHandler::class: class-string, Webware\Log\Http\Middleware\MonologMiddleware::class: class-string, Webware\Log\Listener\Psr3LogPsr14Listener::class: class-string, Webware\Log\Processor\LaminasI18nProcessor::class: class-string}, 'invokables': array{Phly\EventDispatcher\ListenerProvider\AttachableListenerProvider::class: class-string, Phly\EventDispatcher\ListenerProvider\PrioritizedListenerProvider::class: class-string, Webware\Log\Processor\RamseyUuidProcessor::class: class-string}}, 'listener_providers': array, 'listeners': array>, 'templates': array{'paths': array{'log': list}}, Psr\Log\LoggerInterface::class: array{'auth_attribute': string, 'channel': string, 'log_errors': bool, 'process_translation': bool, 'process_uuid': bool, 'table': string}, Webware\Console\ConsoleInterface::class: array{'commands': array}}`, but found `array{'Psr\Log\LoggerInterface': array{'auth_attribute': string, 'channel': string, 'log_errors': bool, 'process_translation': bool, 'process_uuid': bool, 'table': string}, 'Webware\Console\ConsoleInterface': array{'commands': array{'log:init-db': class-string('Webware\Log\Console\InitDbCommand')}}, 'dependencies': array{'aliases': array{Psr\EventDispatcher\EventDispatcherInterface::class: class-string, Psr\EventDispatcher\ListenerProviderInterface::class: class-string}, 'delegators': array{Laminas\Stratigility\Middleware\ErrorHandler::class: list}, 'factories': array{Phly\EventDispatcher\ListenerProvider\ListenerProviderAggregate::class: class-string, Psr\Log\LoggerInterface::class: class-string, Webware\Log\Console\InitDbCommand::class: class-string, Webware\Log\Handler\PhpDbHandler::class: class-string, Webware\Log\Http\Middleware\MonologMiddleware::class: class-string, Webware\Log\Listener\Psr3LogPsr14Listener::class: class-string, Webware\Log\Processor\LaminasI18nProcessor::class: class-string}, 'invokables': array{Phly\EventDispatcher\ListenerProvider\AttachableListenerProvider::class: class-string, Phly\EventDispatcher\ListenerProvider\PrioritizedListenerProvider::class: class-string, Webware\Log\Processor\RamseyUuidProcessor::class: class-string}}, 'listener_providers': array{}, 'listeners': array>, 'templates': array{'paths': array{'log': list}}}`.''' count = 1 [[issues]] file = "src/ConfigProvider.php" code = "invalid-return-statement" -message = '''Invalid return type for function `Webware\Log\ConfigProvider::getDependencies`: expected `array{'aliases': array{Psr\EventDispatcher\EventDispatcherInterface::class: class-string, Psr\EventDispatcher\ListenerProviderInterface::class: class-string}, 'delegators': array{Laminas\Stratigility\Middleware\ErrorHandler::class: list}, 'factories': array{Phly\EventDispatcher\ListenerProvider\ListenerProviderAggregate::class: class-string, Psr\Log\LoggerInterface::class: class-string, Webware\Log\Handler\PhpDbHandler::class: class-string, Webware\Log\Http\Middleware\MonologMiddleware::class: class-string, Webware\Log\Listener\Psr3LogPsr14Listener::class: class-string, Webware\Log\Processor\LaminasI18nProcessor::class: class-string}, 'invokables': array{Phly\EventDispatcher\ListenerProvider\AttachableListenerProvider::class: class-string, Phly\EventDispatcher\ListenerProvider\PrioritizedListenerProvider::class: class-string, Webware\Log\Processor\RamseyUuidProcessor::class: class-string}}`, but found `array{'aliases': array{'Psr\EventDispatcher\EventDispatcherInterface': class-string('Phly\EventDispatcher\EventDispatcher'), 'Psr\EventDispatcher\ListenerProviderInterface': class-string('Phly\EventDispatcher\ListenerProvider\ListenerProviderAggregate')}, 'delegators': array{'Laminas\Stratigility\Middleware\ErrorHandler': list{class-string('Webware\Log\Container\MezzioErrorHandlerDelegator')}}, 'factories': array{'Phly\EventDispatcher\ListenerProvider\ListenerProviderAggregate': class-string('Webware\Log\Container\ListenerProviderAggregateFactory'), 'Psr\Log\LoggerInterface': class-string('Webware\Log\Container\LogFactory'), 'Webware\Log\Handler\PhpDbHandler': class-string('Webware\Log\Handler\PhpDbHandlerFactory'), 'Webware\Log\Http\Middleware\MonologMiddleware': class-string('Webware\Log\Http\Middleware\Container\MonologMiddlewareFactory'), 'Webware\Log\Listener\Psr3LogPsr14Listener': class-string('Webware\Log\Listener\Container\Psr3LogPsr14ListenerFactory'), 'Webware\Log\Processor\LaminasI18nProcessor': class-string('Webware\Log\Processor\LaminasI18nProcessorFactory')}, 'invokables': array{'Phly\EventDispatcher\ListenerProvider\AttachableListenerProvider': class-string('Phly\EventDispatcher\ListenerProvider\AttachableListenerProvider'), 'Phly\EventDispatcher\ListenerProvider\PrioritizedListenerProvider': class-string('Phly\EventDispatcher\ListenerProvider\PrioritizedListenerProvider'), 'Webware\Log\Processor\RamseyUuidProcessor': class-string('Webware\Log\Processor\RamseyUuidProcessor')}}`.''' +message = '''Invalid return type for function `Webware\Log\ConfigProvider::getDependencies`: expected `array{'aliases': array{Psr\EventDispatcher\EventDispatcherInterface::class: class-string, Psr\EventDispatcher\ListenerProviderInterface::class: class-string}, 'delegators': array{Laminas\Stratigility\Middleware\ErrorHandler::class: list}, 'factories': array{Phly\EventDispatcher\ListenerProvider\ListenerProviderAggregate::class: class-string, Psr\Log\LoggerInterface::class: class-string, Webware\Log\Console\InitDbCommand::class: class-string, Webware\Log\Handler\PhpDbHandler::class: class-string, Webware\Log\Http\Middleware\MonologMiddleware::class: class-string, Webware\Log\Listener\Psr3LogPsr14Listener::class: class-string, Webware\Log\Processor\LaminasI18nProcessor::class: class-string}, 'invokables': array{Phly\EventDispatcher\ListenerProvider\AttachableListenerProvider::class: class-string, Phly\EventDispatcher\ListenerProvider\PrioritizedListenerProvider::class: class-string, Webware\Log\Processor\RamseyUuidProcessor::class: class-string}}`, but found `array{'aliases': array{'Psr\EventDispatcher\EventDispatcherInterface': class-string('Phly\EventDispatcher\EventDispatcher'), 'Psr\EventDispatcher\ListenerProviderInterface': class-string('Phly\EventDispatcher\ListenerProvider\ListenerProviderAggregate')}, 'delegators': array{'Laminas\Stratigility\Middleware\ErrorHandler': list{class-string('Webware\Log\Container\MezzioErrorHandlerDelegator')}}, 'factories': array{'Phly\EventDispatcher\ListenerProvider\ListenerProviderAggregate': class-string('Webware\Log\Container\ListenerProviderAggregateFactory'), 'Psr\Log\LoggerInterface': class-string('Webware\Log\Container\LogFactory'), 'Webware\Log\Console\InitDbCommand': class-string('Webware\Log\Console\Container\InitDbCommandFactory'), 'Webware\Log\Handler\PhpDbHandler': class-string('Webware\Log\Handler\PhpDbHandlerFactory'), 'Webware\Log\Http\Middleware\MonologMiddleware': class-string('Webware\Log\Http\Middleware\Container\MonologMiddlewareFactory'), 'Webware\Log\Listener\Psr3LogPsr14Listener': class-string('Webware\Log\Listener\Container\Psr3LogPsr14ListenerFactory'), 'Webware\Log\Processor\LaminasI18nProcessor': class-string('Webware\Log\Processor\LaminasI18nProcessorFactory')}, 'invokables': array{'Phly\EventDispatcher\ListenerProvider\AttachableListenerProvider': class-string('Phly\EventDispatcher\ListenerProvider\AttachableListenerProvider'), 'Phly\EventDispatcher\ListenerProvider\PrioritizedListenerProvider': class-string('Phly\EventDispatcher\ListenerProvider\PrioritizedListenerProvider'), 'Webware\Log\Processor\RamseyUuidProcessor': class-string('Webware\Log\Processor\RamseyUuidProcessor')}}`.''' count = 1 [[issues]] diff --git a/composer.json b/composer.json index b460a18..18ce6ca 100644 --- a/composer.json +++ b/composer.json @@ -54,10 +54,12 @@ "phpunit/phpunit": "^13.3.0", "roave/backward-compatibility-check": "^8.21.0", "roave/security-advisories": "dev-master", + "webware/webware-console": "1.0.x-dev", "webware/webware-tools": "^1.0.0-beta.2" }, "suggest": { - "php-db/phpdb-mysql": "Required when using PhpDbHandler with a MySQL/MariaDB database" + "php-db/phpdb-mysql": "Required when using PhpDbHandler with a MySQL/MariaDB database", + "webware/webware-console": "Provides the console that runs log:init-db to create the log table" }, "extra": { "laminas": { diff --git a/composer.lock b/composer.lock index 69f882c..b1c7ffc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cc25ff80e3ba9b058ac012187b23610f", + "content-hash": "967d0a2564e1b6e2e4f6270251686893", "packages": [ { "name": "brick/math", @@ -2381,6 +2381,74 @@ }, "time": "2026-09-23T19:46:42+00:00" }, + { + "name": "laminas/laminas-config-aggregator", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-config-aggregator.git", + "reference": "75331b17a9ba72c5fa8679988ef08a588ddd86af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-config-aggregator/zipball/75331b17a9ba72c5fa8679988ef08a588ddd86af", + "reference": "75331b17a9ba72c5fa8679988ef08a588ddd86af", + "shasum": "" + }, + "require": { + "brick/varexporter": "^0.5.0 || ^0.4.0 || ^0.6.0 || ^0.7.0", + "laminas/laminas-stdlib": "^3.18.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "webimpress/safe-writer": "^2.2.0" + }, + "conflict": { + "nikic/php-parser": "<4.12", + "zendframework/zend-config-aggregator": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~3.1.0", + "laminas/laminas-config": "^3.10.1", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "suggest": { + "laminas/laminas-config": "Allows loading configuration from XML, INI, YAML, and JSON files", + "laminas/laminas-config-aggregator-modulemanager": "Allows loading configuration from laminas-mvc Module classes", + "laminas/laminas-config-aggregator-parameters": "Allows usage of templated parameters within your configuration" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\ConfigAggregator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Lightweight library for collecting and merging configuration from different sources", + "homepage": "https://laminas.dev", + "keywords": [ + "config-aggregator", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-config-aggregator/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-config-aggregator/issues", + "rss": "https://github.com/laminas/laminas-config-aggregator/releases.atom", + "source": "https://github.com/laminas/laminas-config-aggregator" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2026-06-01T15:37:55+00:00" + }, { "name": "laminas/laminas-diactoros", "version": "3.8.0", @@ -3866,6 +3934,71 @@ }, "time": "2026-09-04T04:45:13+00:00" }, + { + "name": "php-standard-library/ansi", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/ansi.git", + "reference": "c0e20cf6ec3caba4aaaa7502c7ad93b81f07d348" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/ansi/zipball/c0e20cf6ec3caba4aaaa7502c7ad93b81f07d348", + "reference": "c0e20cf6ec3caba4aaaa7502c7ad93b81f07d348", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Ansi\\": "src/Psl/Ansi/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Pure functions for constructing ANSI escape sequences - text styling, colors, cursor movement, and screen manipulation", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "ansi", + "colors", + "escape-sequences", + "terminal" + ], + "support": { + "source": "https://github.com/php-standard-library/ansi/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, { "name": "php-standard-library/async", "version": "6.3.0", @@ -5008,6 +5141,73 @@ }, "time": "2026-09-25T15:54:11+00:00" }, + { + "name": "php-standard-library/math", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/math.git", + "reference": "7ed71a7ed487a32d136b5f38ffe7cffc206bbdfe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/math/zipball/7ed71a7ed487a32d136b5f38ffe7cffc206bbdfe", + "reference": "7ed71a7ed487a32d136b5f38ffe7cffc206bbdfe", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/iter": "^6.0", + "php-standard-library/str": "^6.0", + "php-standard-library/vec": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Math\\": "src/Psl/Math/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Mathematical functions with strict typing and predictable error handling", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "math", + "mathematics" + ], + "support": { + "source": "https://github.com/php-standard-library/math/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, { "name": "php-standard-library/option", "version": "6.3.0", @@ -5074,6 +5274,70 @@ }, "time": "2026-09-25T15:54:11+00:00" }, + { + "name": "php-standard-library/os", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/os.git", + "reference": "602a4bd323e7f7d70c727b99759864d28f1058b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/os/zipball/602a4bd323e7f7d70c727b99759864d28f1058b9", + "reference": "602a4bd323e7f7d70c727b99759864d28f1058b9", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/default": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\OS\\": "src/Psl/OS/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Type-safe operating system detection via OperatingSystemFamily enum", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "operating-system", + "os", + "platform" + ], + "support": { + "source": "https://github.com/php-standard-library/os/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, { "name": "php-standard-library/process", "version": "6.3.0", @@ -5537,6 +5801,79 @@ }, "time": "2026-09-25T15:54:11+00:00" }, + { + "name": "php-standard-library/terminal", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/terminal.git", + "reference": "22ea2db5532d65d13f51dde97a4ee0d01f8083bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/terminal/zipball/22ea2db5532d65d13f51dde97a4ee0d01f8083bf", + "reference": "22ea2db5532d65d13f51dde97a4ee0d01f8083bf", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/ansi": "^6.0", + "php-standard-library/async": "^6.0", + "php-standard-library/date-time": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/io": "^6.0", + "php-standard-library/math": "^6.0", + "php-standard-library/os": "^6.0", + "php-standard-library/process": "^6.0", + "php-standard-library/str": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Terminal\\": "src/Psl/Terminal/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Full-featured TUI framework with widgets, events, and diff-based rendering", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "console", + "terminal", + "tui", + "widgets" + ], + "support": { + "source": "https://github.com/php-standard-library/terminal/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, { "name": "php-standard-library/type", "version": "6.3.0", @@ -11282,6 +11619,65 @@ ], "time": "2025-12-08T11:19:18+00:00" }, + { + "name": "webimpress/safe-writer", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/webimpress/safe-writer.git", + "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webimpress/safe-writer/zipball/9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", + "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5.4", + "vimeo/psalm": "^4.7", + "webimpress/coding-standard": "^1.2.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev", + "dev-develop": "2.3.x-dev", + "dev-release-1.0": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Webimpress\\SafeWriter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Tool to write files safely, to avoid race conditions", + "keywords": [ + "concurrent write", + "file writer", + "race condition", + "safe writer", + "webimpress" + ], + "support": { + "issues": "https://github.com/webimpress/safe-writer/issues", + "source": "https://github.com/webimpress/safe-writer/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2021-04-19T16:34:45+00:00" + }, { "name": "webmozart/glob", "version": "4.7.0", @@ -11331,6 +11727,67 @@ }, "time": "2024-03-07T20:33:40+00:00" }, + { + "name": "webware/webware-console", + "version": "1.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/webinertia/webware-console.git", + "reference": "242e30e31ec41d35cb1942772fea65bdc39b3b70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webinertia/webware-console/zipball/242e30e31ec41d35cb1942772fea65bdc39b3b70", + "reference": "242e30e31ec41d35cb1942772fea65bdc39b3b70", + "shasum": "" + }, + "require": { + "laminas/laminas-config-aggregator": "^1.20", + "laminas/laminas-servicemanager": "^4.5", + "php": "~8.4.1 || ~8.5.0", + "php-standard-library/terminal": "^6.2", + "symfony/console": "^7.4 || ^8.0" + }, + "require-dev": { + "infection/infection": "^0.35", + "phpunit/phpunit": "^13.3", + "webware/webware-tools": "^1.0.0-beta.2" + }, + "default-branch": true, + "bin": [ + "bin/webware" + ], + "type": "library", + "autoload": { + "psr-4": { + "Webware\\Console\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Joey Smith", + "email": "jsmith@webinertia.net", + "role": "Developer" + } + ], + "description": "Webware Console — TUI application providing a menu and better help for Webware and Mezzio CLI commands", + "keywords": [ + "cli", + "console", + "mezzio", + "tui", + "webware" + ], + "support": { + "issues": "https://github.com/webinertia/webware-console/issues", + "source": "https://github.com/webinertia/webware-console/tree/1.0.x" + }, + "time": "2026-09-29T05:26:25+00:00" + }, { "name": "webware/webware-tools", "version": "1.0.0-beta.5", @@ -11380,7 +11837,8 @@ "minimum-stability": "dev", "stability-flags": { "php-db/phpdb-mysql": 20, - "roave/security-advisories": 20 + "roave/security-advisories": 20, + "webware/webware-console": 20 }, "prefer-stable": true, "prefer-lowest": false, diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 3353f53..51b1d84 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -23,6 +23,7 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\ListenerProviderInterface; use Psr\Log\LoggerInterface; +use Webware\Console\ConsoleInterface; /** * @phpstan-type LogDefaults array{ @@ -46,6 +47,7 @@ * LoggerInterface::class: class-string, * Http\Middleware\MonologMiddleware::class: class-string, * Handler\PhpDbHandler::class: class-string, + * Console\InitDbCommand::class: class-string, * Processor\LaminasI18nProcessor::class: class-string, * } * @phpstan-type LogInvokables array{ @@ -68,6 +70,7 @@ * listener_providers: array, * templates: LogTemplatePaths, * LoggerInterface::class: LogDefaults, + * ConsoleInterface::class: array{commands: array}, * } */ class ConfigProvider @@ -109,6 +112,7 @@ public function getDependencies(): array LoggerInterface::class => Container\LogFactory::class, Http\Middleware\MonologMiddleware::class => Http\Middleware\Container\MonologMiddlewareFactory::class, Handler\PhpDbHandler::class => Handler\PhpDbHandlerFactory::class, + Console\InitDbCommand::class => Console\Container\InitDbCommandFactory::class, Processor\LaminasI18nProcessor::class => Processor\LaminasI18nProcessorFactory::class, ], 'invokables' => [ @@ -160,8 +164,13 @@ public function __invoke(): array self::LISTENER_KEY => $this->getListeners(), self::LISTENER_PROVIDER_KEY => [], // 'middleware_pipeline' => $this->getPipelineConfig(), - 'templates' => $this->getTemplates(), - LoggerInterface::class => $this->getConfigDefaults(), + 'templates' => $this->getTemplates(), + LoggerInterface::class => $this->getConfigDefaults(), + ConsoleInterface::class => [ + 'commands' => [ + 'log:init-db' => Console\InitDbCommand::class, + ], + ], ]; } } diff --git a/src/Console/Container/InitDbCommandFactory.php b/src/Console/Container/InitDbCommandFactory.php new file mode 100644 index 0000000..8ca0a5c --- /dev/null +++ b/src/Console/Container/InitDbCommandFactory.php @@ -0,0 +1,47 @@ + + * and contributors. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Webware\Log\Console\Container; + +use PhpDb\Adapter\AdapterInterface; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Exception\LogicException; +use Webware\Log\ConfigProvider; +use Webware\Log\Console\InitDbCommand; + +/** + * The table name is read the way the database handler reads it (the configured logger table, else + * the default), so the command creates the table the handler will write to. + */ +final readonly class InitDbCommandFactory +{ + /** + * @throws ContainerExceptionInterface + * @throws LogicException + * @throws NotFoundExceptionInterface + */ + public function __invoke(ContainerInterface $container): InitDbCommand + { + /** @var array $config */ + $config = $container->get('config'); + + return new InitDbCommand( + adapter: $container->get(AdapterInterface::class), + table : $config[LoggerInterface::class]['table'] ?? new ConfigProvider()->getConfigDefaults()['table'], + ); + } +} diff --git a/src/Console/Ddl/Column/LongText.php b/src/Console/Ddl/Column/LongText.php new file mode 100644 index 0000000..1d3b671 --- /dev/null +++ b/src/Console/Ddl/Column/LongText.php @@ -0,0 +1,27 @@ + + * and contributors. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Webware\Log\Console\Ddl\Column; + +use PhpDb\Sql\Ddl\Column\Text; + +/** + * phpdb ships TEXT but no LONGTEXT; the log message and context can exceed 64 KB. + * + * @internal + */ +final class LongText extends Text +{ + protected string $type = 'LONGTEXT'; +} diff --git a/src/Console/InitDbCommand.php b/src/Console/InitDbCommand.php new file mode 100644 index 0000000..9a36c1d --- /dev/null +++ b/src/Console/InitDbCommand.php @@ -0,0 +1,97 @@ + + * and contributors. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Webware\Log\Console; + +use Override; +use PhpDb\Adapter\AdapterInterface; +use PhpDb\Sql\Ddl\CreateTable; +use PhpDb\Sql\Ddl\DropTable; +use PhpDb\Sql\Exception\ExceptionInterface as SqlException; +use PhpDb\Sql\Sql; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\InvalidArgumentException as ConsoleInvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException as ConsoleLogicException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Webware\Log\Console\Schema\LogSchema; + +#[AsCommand( + name : 'log:init-db', + description: 'Create the table the database log handler writes to', +)] +final class InitDbCommand extends Command +{ + private readonly LogSchema $schema; + + /** + * @throws ConsoleLogicException + */ + public function __construct( + private readonly AdapterInterface $adapter, + private readonly string $table, + ) { + $this->schema = new LogSchema(); + + parent::__construct(); + } + + /** + * @throws ConsoleInvalidArgumentException + */ + #[Override] + protected function configure(): void + { + $this->addOption( + name : 'drop', + mode : InputOption::VALUE_NONE, + description: 'Drop the log table before recreating it', + ); + } + + /** + * @throws ConsoleInvalidArgumentException + * @throws SqlException + */ + #[Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + $sql = new Sql($this->adapter); + + if (true === $input->getOption('drop')) { + $output->writeln("Dropping table {$this->table}..."); + $this->executeDdl($sql, $this->schema->dropTable($this->table)); + } + + $output->writeln("Creating table {$this->table}..."); + $this->executeDdl($sql, $this->schema->logTable($this->table)); + + $output->writeln('Log table initialized.'); + + return Command::SUCCESS; + } + + /** + * @throws SqlException + */ + private function executeDdl(Sql $sql, CreateTable|DropTable $ddl): void + { + $this->adapter->query( + $sql->buildSqlString($ddl), + AdapterInterface::QUERY_MODE_EXECUTE, + ); + } +} diff --git a/src/Console/Schema/LogSchema.php b/src/Console/Schema/LogSchema.php new file mode 100644 index 0000000..229565f --- /dev/null +++ b/src/Console/Schema/LogSchema.php @@ -0,0 +1,110 @@ + + * and contributors. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Webware\Log\Console\Schema; + +use PhpDb\Sql\Ddl\Column\Char; +use PhpDb\Sql\Ddl\Column\Integer; +use PhpDb\Sql\Ddl\Column\Varchar; +use PhpDb\Sql\Ddl\Constraint\PrimaryKey; +use PhpDb\Sql\Ddl\Constraint\UniqueKey; +use PhpDb\Sql\Ddl\CreateTable; +use PhpDb\Sql\Ddl\DropTable; +use PhpDb\Sql\Ddl\Index\Index; +use PhpDb\Sql\Literal; +use Webware\Log\Console\Ddl\Column\LongText; + +/** + * Builds the table {@see \Webware\Log\Handler\PhpDbHandler} writes to. + * + * The columns are exactly the ones the handler inserts: channel, level, uuid, message, time, + * user_identifier and context. The table name is the caller's, because the handler takes it from + * configuration and the two must agree. + * + * @internal + */ +final class LogSchema +{ + public function dropTable(string $table): DropTable + { + return new DropTable(table: $table)->ifExists(); + } + + public function logTable(string $table): CreateTable + { + $createTable = new CreateTable(table: $table)->ifNotExists(); + + $createTable->addColumn( + new Integer( + name : 'id', + nullable: false, + )->setOptions(options: ['unsigned' => true, 'autoincrement' => true]), + ); + $createTable->addColumn(new Char( + name : 'uuid', + length : 36, + nullable: true, + default : null, + )); + $createTable->addColumn(new Varchar( + name : 'channel', + length : 255, + nullable: false, + )); + $createTable->addColumn(new Varchar( + name : 'level', + length : 9, + nullable: false, + )); + $createTable->addColumn(new Varchar( + name : 'user_identifier', + length : 320, + nullable: true, + default : null, + )); + $createTable->addColumn(new LongText( + name : 'message', + nullable: false, + )); + $createTable->addColumn( + new Integer( + name : 'time', + nullable: false, + )->setOptions(options: ['unsigned' => true]), + ); + $createTable->addColumn(new LongText( + name : 'context', + nullable: true, + default : null, + )); + + $createTable->addConstraint(new PrimaryKey(columns: 'id')); + $createTable->addConstraint(new UniqueKey( + columns: 'uuid', + name : 'uuid', + )); + $createTable->addConstraint(new Index( + columns: 'channel', + name : 'ChannelIndex', + )); + + $createTable->setOptions(options: [ + 'engine' => new Literal(literal: 'InnoDB'), + 'default charset' => new Literal(literal: 'utf8mb4'), + 'collate' => new Literal(literal: 'utf8mb4_general_ci'), + ]); + + return $createTable; + } +} diff --git a/test/integration/Console/InitDbCommandTest.php b/test/integration/Console/InitDbCommandTest.php new file mode 100644 index 0000000..9e54510 --- /dev/null +++ b/test/integration/Console/InitDbCommandTest.php @@ -0,0 +1,188 @@ + + * and contributors. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace WebwareTestIntegration\Log\Console; + +use DateTimeImmutable; +use Monolog\Level; +use Monolog\LogRecord; +use Override; +use PDO; +use PhpDb\Adapter\Adapter; +use PhpDb\Adapter\AdapterInterface; +use PhpDb\Adapter\Driver\Pdo\Result; +use PhpDb\Adapter\Driver\Pdo\Statement; +use PhpDb\Mysql\AdapterPlatform; +use PhpDb\Mysql\Pdo\Connection; +use PhpDb\Mysql\Pdo\Driver; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Tester\CommandTester; +use Webware\Log\Console\InitDbCommand; +use Webware\Log\Handler\PhpDbHandler; + +use function getenv; +use function sprintf; + +#[CoversClass(InitDbCommand::class)] +#[CoversMethod(InitDbCommand::class, 'execute')] +#[RequiresPhpExtension('pdo_mysql')] +final class InitDbCommandTest extends TestCase +{ + private const string TABLE = 'init_db_probe'; + + private AdapterInterface $adapter; + + private PDO $pdo; + + #[Test] + public function createdTableMatchesTheFixtureTheHandlerIsTestedAgainst(): void + { + $this->runCommand(['--drop' => true]); + + self::assertSame( + $this->describe('log'), + $this->describe(self::TABLE), + ); + } + + #[Test] + public function handlerWritesToTheCreatedTable(): void + { + $this->runCommand(['--drop' => true]); + + new PhpDbHandler($this->adapter, self::TABLE)->handle(new LogRecord( + datetime : new DateTimeImmutable(), + channel : 'app', + level : Level::Info, + message : 'written to the created table', + formatted: 'written to the created table', + )); + + self::assertSame( + '1', + (string) $this->pdo->query(sprintf('SELECT COUNT(*) FROM `%s`', self::TABLE))?->fetchColumn(), + ); + } + + #[Test] + public function runningWithDropEmptiesTheTable(): void + { + $this->runCommand(['--drop' => true]); + $this->pdo->exec(sprintf( + "INSERT INTO `%s` (channel, level, message, time) VALUES ('app', 'INFO', 'gone', 1)", + self::TABLE, + )); + + $this->runCommand(['--drop' => true]); + + self::assertSame( + '0', + (string) $this->pdo->query(sprintf('SELECT COUNT(*) FROM `%s`', self::TABLE))?->fetchColumn(), + ); + } + + #[Test] + public function runningWithoutDropKeepsExistingRows(): void + { + $this->runCommand(['--drop' => true]); + $this->pdo->exec(sprintf( + "INSERT INTO `%s` (channel, level, message, time) VALUES ('app', 'INFO', 'kept', 1)", + self::TABLE, + )); + + self::assertSame(Command::SUCCESS, $this->runCommand([])); + self::assertSame( + '1', + (string) $this->pdo->query(sprintf('SELECT COUNT(*) FROM `%s`', self::TABLE))?->fetchColumn(), + ); + } + + #[Override] + protected function setUp(): void + { + $hostname = $this->env('TESTS_ADAPTER_MYSQL_HOSTNAME', 'localhost'); + $username = $this->env('TESTS_ADAPTER_MYSQL_USERNAME', 'root'); + $password = $this->env('TESTS_ADAPTER_MYSQL_PASSWORD', ''); + $database = $this->env('TESTS_ADAPTER_MYSQL_DATABASE', 'webware_log_test'); + $port = (int) $this->env('TESTS_ADAPTER_MYSQL_PORT', '3306'); + + $driver = new Driver( + new Connection([ + 'hostname' => $hostname, + 'port' => $port, + 'username' => $username, + 'password' => $password, + 'database' => $database, + ]), + new Statement(), + new Result(), + ); + $this->adapter = new Adapter($driver, new AdapterPlatform($driver)); + + $this->pdo = new PDO( + sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $hostname, $port, $database), + $username, + $password, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION], + ); + } + + #[Override] + protected function tearDown(): void + { + $this->pdo->exec(sprintf('DROP TABLE IF EXISTS `%s`', self::TABLE)); + } + + /** + * @return list> + */ + private function describe(string $table): array + { + $statement = $this->pdo->prepare( + 'SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY, EXTRA, COLLATION_NAME' + . ' FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?' + . ' ORDER BY ORDINAL_POSITION', + ); + $statement->execute([$table]); + + /** @var list> */ + return $statement->fetchAll(PDO::FETCH_ASSOC); + } + + private function env(string $name, string $default): string + { + $value = getenv($name); + + return false === $value ? $default : $value; + } + + /** + * @param array $input + */ + private function runCommand(array $input): int + { + $tester = new CommandTester(new InitDbCommand( + adapter: $this->adapter, + table : self::TABLE, + )); + $tester->execute($input); + + return $tester->getStatusCode(); + } +} diff --git a/test/unit/ConfigProviderTest.php b/test/unit/ConfigProviderTest.php index 94e9c6d..8d8d4d5 100644 --- a/test/unit/ConfigProviderTest.php +++ b/test/unit/ConfigProviderTest.php @@ -22,7 +22,10 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\ListenerProviderInterface; use Psr\Log\LoggerInterface; +use Webware\Console\ConsoleInterface; use Webware\Log\ConfigProvider; +use Webware\Log\Console\Container\InitDbCommandFactory; +use Webware\Log\Console\InitDbCommand; use Webware\Log\Event\LogEvent; use Webware\Log\Listener\Psr3LogPsr14Listener; use Webware\Log\LogChannel; @@ -108,6 +111,17 @@ public function getDependenciesAliasesListenerProviderInterface(): void $this->assertArrayHasKey(ListenerProviderInterface::class, $deps['aliases']); } + /** + * @throws \PHPUnit\Exception + */ + #[Test] + public function getDependenciesRegistersTheInitDbCommandFactory(): void + { + $deps = $this->provider->getDependencies(); + + $this->assertSame(InitDbCommandFactory::class, $deps['factories'][InitDbCommand::class]); + } + /** * @throws \PHPUnit\Exception */ @@ -187,6 +201,20 @@ public function invokeNestedLoggerConfigMatchesGetConfigDefaults(): void $this->assertSame($this->provider->getConfigDefaults(), $config[LoggerInterface::class]); } + /** + * @throws \PHPUnit\Exception + */ + #[Test] + public function invokeRegistersTheLogInitDbCommandForConsoleDiscovery(): void + { + $config = ($this->provider)(); + + $this->assertSame( + ['commands' => ['log:init-db' => InitDbCommand::class]], + $config[ConsoleInterface::class], + ); + } + /** * @throws \PHPUnit\Exception */ diff --git a/test/unit/Console/Container/InitDbCommandFactoryTest.php b/test/unit/Console/Container/InitDbCommandFactoryTest.php new file mode 100644 index 0000000..d1b15ce --- /dev/null +++ b/test/unit/Console/Container/InitDbCommandFactoryTest.php @@ -0,0 +1,73 @@ +container( + config : [], + executed: $executed, + )); + + self::assertInstanceOf(InitDbCommand::class, $command); + + new CommandTester($command)->execute([]); + + self::assertStringContainsString('`log`', $executed[0]); + } + + #[Test] + public function invokeUsesTheTableTheHandlerIsConfiguredWith(): void + { + $executed = []; + $command = (new InitDbCommandFactory())($this->container( + config : [LoggerInterface::class => ['table' => 'app_log']], + executed: $executed, + )); + + new CommandTester($command)->execute([]); + + self::assertStringContainsString('`app_log`', $executed[0]); + } + + /** + * @param array $config + * @param list $executed + */ + private function container(array $config, array &$executed): ContainerInterface + { + $adapter = $this->ddlAdapter($executed); + $container = $this->createStub(ContainerInterface::class); + $container->method('get') + ->willReturnCallback( + static fn(string $id): mixed => match ($id) { + 'config' => $config, + AdapterInterface::class => $adapter, + }, + ); + + return $container; + } +} diff --git a/test/unit/Console/Ddl/Column/LongTextTest.php b/test/unit/Console/Ddl/Column/LongTextTest.php new file mode 100644 index 0000000..80b88f3 --- /dev/null +++ b/test/unit/Console/Ddl/Column/LongTextTest.php @@ -0,0 +1,41 @@ +addColumn(new LongText( + name : 'message', + nullable: false, + )); + + $driver = $this->createStub(DriverInterface::class); + $connection = $this->createStub(ConnectionInterface::class); + $driver->method('getConnection')->willReturn($connection); + + $platform = new Platform(); + $platform->setSubject($table); + + self::assertStringContainsString( + '`message` LONGTEXT NOT NULL', + $platform->getSqlString(new AdapterPlatform($driver)), + ); + } +} diff --git a/test/unit/Console/InitDbCommandTest.php b/test/unit/Console/InitDbCommandTest.php new file mode 100644 index 0000000..ad1470e --- /dev/null +++ b/test/unit/Console/InitDbCommandTest.php @@ -0,0 +1,91 @@ +ddlAdapter($executed), + table : 'log', + ); + + self::assertSame('log:init-db', $command->getName()); + self::assertTrue($command->getDefinition()->hasOption('drop')); + } + + #[Test] + public function executeCreatesTheLogTable(): void + { + $executed = []; + $command = new InitDbCommand( + adapter: $this->ddlAdapter($executed), + table : 'log', + ); + + $tester = new CommandTester($command); + $tester->execute([]); + + self::assertSame(Command::SUCCESS, $tester->getStatusCode()); + self::assertCount(1, $executed); + self::assertStringContainsString('CREATE TABLE IF NOT EXISTS `log`', $executed[0]); + self::assertStringContainsString('Creating table log...', $tester->getDisplay()); + self::assertStringContainsString('Log table initialized.', $tester->getDisplay()); + self::assertStringNotContainsString('Dropping table', $tester->getDisplay()); + } + + #[Test] + public function executeDropsTheTableFirstWhenRequested(): void + { + $executed = []; + $command = new InitDbCommand( + adapter: $this->ddlAdapter($executed), + table : 'log', + ); + + $tester = new CommandTester($command); + $tester->execute(['--drop' => true]); + + self::assertSame(Command::SUCCESS, $tester->getStatusCode()); + self::assertCount(2, $executed); + self::assertStringContainsString('DROP TABLE IF EXISTS `log`', $executed[0]); + self::assertStringContainsString('CREATE TABLE IF NOT EXISTS `log`', $executed[1]); + self::assertStringContainsString('Dropping table log...', $tester->getDisplay()); + } + + #[Test] + public function executeUsesTheConfiguredTableName(): void + { + $executed = []; + $command = new InitDbCommand( + adapter: $this->ddlAdapter($executed), + table : 'app_log', + ); + + new CommandTester($command)->execute([]); + + self::assertSame(1, count($executed)); + self::assertStringContainsString('`app_log`', $executed[0]); + } +} diff --git a/test/unit/Console/Schema/LogSchemaTest.php b/test/unit/Console/Schema/LogSchemaTest.php new file mode 100644 index 0000000..c8dc2f1 --- /dev/null +++ b/test/unit/Console/Schema/LogSchemaTest.php @@ -0,0 +1,166 @@ +renderSql(new LogSchema()->dropTable('log')); + + self::assertStringContainsString('DROP TABLE IF EXISTS `log`', $sql); + } + + #[Test] + public function logTableCarriesTheKeysAndTheChannelIndex(): void + { + $constraints = $this->constraints(new LogSchema()->logTable('log')); + + self::assertCount(3, $constraints); + self::assertInstanceOf(PrimaryKey::class, $constraints[0]); + self::assertInstanceOf(UniqueKey::class, $constraints[1]); + self::assertSame('uuid', $constraints[1]->getName()); + self::assertInstanceOf(Index::class, $constraints[2]); + self::assertSame('ChannelIndex', $constraints[2]->getName()); + } + + #[Test] + public function logTableHasExactlyTheColumnsTheHandlerInserts(): void + { + $names = array_map( + static fn(ColumnInterface $column): string => $column->getName(), + $this->columns(new LogSchema()->logTable('log')), + ); + + self::assertSame( + ['id', 'uuid', 'channel', 'level', 'user_identifier', 'message', 'time', 'context'], + $names, + ); + } + + #[Test] + public function logTableOptionsSetEngineCharsetAndCollation(): void + { + $options = new LogSchema()->logTable('log') + ->getOptions(); + + self::assertSame(['engine', 'default charset', 'collate'], array_keys($options)); + self::assertInstanceOf(Literal::class, $options['engine']); + self::assertSame('InnoDB', $options['engine']->getLiteral()); + self::assertSame('utf8mb4', $options['default charset']->getLiteral()); + self::assertSame('utf8mb4_general_ci', $options['collate']->getLiteral()); + } + + #[Test] + public function logTableRendersTheExactStatement(): void + { + $sql = (string) preg_replace( + pattern : '/\s+/', + replacement: ' ', + subject : $this->renderSql(new LogSchema()->logTable('log')), + ); + + self::assertSame( + 'CREATE TABLE IF NOT EXISTS `log` ( ' + . '`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, ' + . '`uuid` CHAR(36) NULL DEFAULT NULL, ' + . '`channel` VARCHAR(255) NOT NULL, ' + . '`level` VARCHAR(9) NOT NULL, ' + . '`user_identifier` VARCHAR(320) NULL DEFAULT NULL, ' + . '`message` LONGTEXT NOT NULL, ' + . '`time` INTEGER UNSIGNED NOT NULL, ' + . '`context` LONGTEXT NULL DEFAULT NULL , ' + . 'PRIMARY KEY (`id`), ' + . 'CONSTRAINT `uuid` UNIQUE (`uuid`), ' + . 'INDEX `ChannelIndex`(`channel`) ' + . ') ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci', + $sql, + ); + } + + #[Test] + public function logTableRendersValidMysqlDdl(): void + { + $sql = $this->renderSql(new LogSchema()->logTable('log')); + + self::assertStringContainsString('CREATE TABLE IF NOT EXISTS `log`', $sql); + self::assertStringContainsString('UNSIGNED', $sql); + self::assertStringContainsString('AUTO_INCREMENT', $sql); + self::assertStringContainsString('CHAR(36)', $sql); + self::assertStringContainsString('VARCHAR(255)', $sql); + self::assertStringContainsString('VARCHAR(9)', $sql); + self::assertStringContainsString('VARCHAR(320)', $sql); + self::assertStringContainsString('LONGTEXT', $sql); + self::assertStringContainsString('PRIMARY KEY', $sql); + self::assertStringContainsString('UNIQUE', $sql); + self::assertStringContainsString('ChannelIndex', $sql); + self::assertStringContainsString('ENGINE = InnoDB', $sql); + self::assertStringContainsString('utf8mb4_general_ci', $sql); + } + + #[Test] + public function logTableUsesTheTableNameItIsGiven(): void + { + $sql = $this->renderSql(new LogSchema()->logTable('app_log')); + + self::assertStringContainsString('`app_log`', $sql); + } + + /** + * @return list + */ + private function columns(CreateTable $table): array + { + /** @var list */ + return $table->getRawState('columns'); + } + + /** + * @return list + */ + private function constraints(CreateTable $table): array + { + /** @var list */ + return $table->getRawState('constraints'); + } + + private function renderSql(CreateTable|DropTable $sql): string + { + $driver = $this->createStub(DriverInterface::class); + $connection = $this->createStub(ConnectionInterface::class); + $driver->method('getConnection')->willReturn($connection); + + $platform = new Platform(); + $platform->setSubject($sql); + + return $platform->getSqlString(new AdapterPlatform($driver)); + } +} diff --git a/test/unit/Support/DdlAdapterTrait.php b/test/unit/Support/DdlAdapterTrait.php new file mode 100644 index 0000000..8f5dbd3 --- /dev/null +++ b/test/unit/Support/DdlAdapterTrait.php @@ -0,0 +1,42 @@ + $executed filled with each statement the adapter is asked to run + */ + private function ddlAdapter(array &$executed): AdapterInterface + { + $driver = $this->createStub(DriverInterface::class); + $connection = $this->createStub(ConnectionInterface::class); + $driver->method('getConnection')->willReturn($connection); + + $adapter = $this->createStub(AdapterInterface::class); + $adapter->method('getDriver')->willReturn($driver); + $adapter->method('getPlatform')->willReturn(new AdapterPlatform($driver)); + $adapter->method('query') + ->willReturnCallback( + function (string $sql) use (&$executed): ResultInterface { + $executed[] = $sql; + + return $this->createStub(ResultInterface::class); + }, + ); + + return $adapter; + } +}