diff --git a/.config/shopware-extension.yml b/.config/shopware-extension.yml new file mode 100644 index 0000000..f6ab11f --- /dev/null +++ b/.config/shopware-extension.yml @@ -0,0 +1,21 @@ +# shopware-cli extension configuration (build/package/validate) +build: + zip: + pack: + excludes: + paths: + - tests + - docs + - scripts + - phpunit.xml + - docker-compose.yml + - skills-lock.json + - AGENTS.md + - CLAUDE.md + - QWEN.md + - .claude + - .scratch + - .wayfinder + - src/Resources/config/plugin-transparent.png +validation: + ignore: [] diff --git a/CLAUDE.md b/CLAUDE.md index 284ecc7..634eee2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,7 @@ the full build spec live under `.scratch/admin-dump-manager/` and `.scratch/dump - Plugin bootstrap class: `src/AeonDumpManager.php` (extends `Shopware\Core\Framework\Plugin`) — this is the entry point Shopware calls for install/uninstall/activate/deactivate/update lifecycle hooks. - Plugin class name is wired via `composer.json` → `extra.shopware-plugin-class`. -- Services are registered manually in `src/Resources/config/services.xml` (Symfony DI, XML format, +- Services are registered manually in `src/Resources/config/services.yaml` (Symfony DI, YAML format, autowiring is not configured — every new service/command/task must be added here explicitly with its tag, e.g. `console.command` or `shopware.scheduled.task`). - Plugin store-config UI fields (e.g. `maxDumps`, `retantionDays`) are defined in @@ -66,7 +66,7 @@ vendor/bin/phpunit tests/SomeTest.php # single file ## Adding functionality - New console commands go in `src/Command/`, new scheduled tasks in `src/ScheduledTask/` — both must - be registered in `src/Resources/config/services.xml` with the appropriate tag or Shopware will not + be registered in `src/Resources/config/services.yaml` with the appropriate tag or Shopware will not discover them. - Scheduled tasks need a matching handler (tagged `messenger.message_handler`) to actually run; a bare `ScheduledTask` subclass like `ExampleTask` only defines the schedule, not the behavior. diff --git a/composer.json b/composer.json index 2e90da2..2e2d71a 100755 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "source": "https://git.arthurerlich.de/haylan/AeonDumpManager" }, "require": { - "php": "^8.1", + "php": "^8.2", "shopware/core": "~6.6.0" }, "require-dev": { diff --git a/src/AeonDumpManager.php b/src/AeonDumpManager.php index 09ae91b..61710e2 100755 --- a/src/AeonDumpManager.php +++ b/src/AeonDumpManager.php @@ -4,17 +4,33 @@ namespace AeonDumpManager; use Doctrine\DBAL\Connection; use Shopware\Core\Framework\Plugin; -use Shopware\Core\Framework\Plugin\Context\ActivateContext; -use Shopware\Core\Framework\Plugin\Context\DeactivateContext; -use Shopware\Core\Framework\Plugin\Context\InstallContext; use Shopware\Core\Framework\Plugin\Context\UninstallContext; -use Shopware\Core\Framework\Plugin\Context\UpdateContext; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Loader\DelegatingLoader; +use Symfony\Component\Config\Loader\LoaderResolver; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\DirectoryLoader; +use Symfony\Component\DependencyInjection\Loader\GlobFileLoader; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; class AeonDumpManager extends Plugin { - public function install(InstallContext $installContext): void + public function build(ContainerBuilder $container): void { - // Do stuff such as creating a new payment method + parent::build($container); + + // Registers the dedicated monolog channel (Resources/config/packages/monolog.yaml) + $locator = new FileLocator('Resources/config'); + $resolver = new LoaderResolver([ + new YamlFileLoader($container, $locator), + new GlobFileLoader($container, $locator), + new DirectoryLoader($container, $locator), + ]); + + (new DelegatingLoader($resolver))->load( + \rtrim($this->getPath(), '/') . '/Resources/config/packages/*.yaml', + 'glob' + ); } public function uninstall(UninstallContext $uninstallContext): void @@ -29,29 +45,4 @@ class AeonDumpManager extends Plugin $connection = $this->container->get(Connection::class); $connection->executeStatement('DROP TABLE IF EXISTS `aeon_dump_manager_job`'); } - - public function activate(ActivateContext $activateContext): void - { - // Activate entities, such as a new payment method - // Or create new entities here, because now your plugin is installed and active for sure - } - - public function deactivate(DeactivateContext $deactivateContext): void - { - // Deactivate entities, such as a new payment method - // Or remove previously created entities - } - - public function update(UpdateContext $updateContext): void - { - // Update necessary stuff, mostly non-database related - } - - public function postInstall(InstallContext $installContext): void - { - } - - public function postUpdate(UpdateContext $updateContext): void - { - } } diff --git a/src/MessageQueue/Handler/CreateDumpHandler.php b/src/MessageQueue/Handler/CreateDumpHandler.php index 256df68..c9e6e47 100644 --- a/src/MessageQueue/Handler/CreateDumpHandler.php +++ b/src/MessageQueue/Handler/CreateDumpHandler.php @@ -5,6 +5,7 @@ namespace AeonDumpManager\MessageQueue\Handler; use AeonDumpManager\MessageQueue\Message\CreateDumpMessage; use AeonDumpManager\Service\DumpJobStatusService; use AeonDumpManager\Service\DumpService; +use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; #[AsMessageHandler] @@ -13,6 +14,7 @@ class CreateDumpHandler public function __construct( private readonly DumpService $dumpService, private readonly DumpJobStatusService $jobStatus, + private readonly LoggerInterface $logger, ) { } @@ -20,10 +22,12 @@ class CreateDumpHandler { $jobId = $message->getJobId(); $this->jobStatus->markRunning($jobId); + $this->logger->info('Dump job started.', ['jobId' => $jobId]); try { $this->dumpService->createAsync($jobId, $this->jobStatus); } catch (\Throwable $exception) { + $this->logger->error('Dump job failed.', ['jobId' => $jobId, 'exception' => $exception]); $this->jobStatus->markFailed($jobId, $exception->getMessage()); } } diff --git a/src/Resources/config/packages/monolog.yaml b/src/Resources/config/packages/monolog.yaml new file mode 100644 index 0000000..e59b78f --- /dev/null +++ b/src/Resources/config/packages/monolog.yaml @@ -0,0 +1,2 @@ +monolog: + channels: ['aeon_dump_manager'] diff --git a/src/Resources/config/plugin.png b/src/Resources/config/plugin.png new file mode 100644 index 0000000..cb3f9c9 Binary files /dev/null and b/src/Resources/config/plugin.png differ diff --git a/src/Resources/config/routes.php b/src/Resources/config/routes.php deleted file mode 100644 index 19ba0fe..0000000 --- a/src/Resources/config/routes.php +++ /dev/null @@ -1,7 +0,0 @@ -import(__DIR__ . '/../../Controller/*Controller.php', 'attribute'); -}; diff --git a/src/Resources/config/routes.yaml b/src/Resources/config/routes.yaml new file mode 100644 index 0000000..5930254 --- /dev/null +++ b/src/Resources/config/routes.yaml @@ -0,0 +1,3 @@ +aeon_dump_manager_controllers: + resource: ../../Controller/*Controller.php + type: attribute diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml deleted file mode 100755 index 1d8d433..0000000 --- a/src/Resources/config/services.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml new file mode 100644 index 0000000..0fc47e3 --- /dev/null +++ b/src/Resources/config/services.yaml @@ -0,0 +1,92 @@ +services: + Symfony\Component\Process\ExecutableFinder: ~ + + aeon_dump_manager.dump_filename_parser: + class: AeonDumpManager\Service\DumpFilenameParser + + aeon_dump_manager.dump_binary_locator: + class: AeonDumpManager\Service\DumpBinaryLocator + arguments: + - '@Doctrine\DBAL\Connection' + - '@Shopware\Core\System\SystemConfig\SystemConfigService' + - '@Symfony\Component\Process\ExecutableFinder' + + aeon_dump_manager.dump_job_status_service: + class: AeonDumpManager\Service\DumpJobStatusService + arguments: + - '@Doctrine\DBAL\Connection' + + aeon_dump_manager.dump_lister: + class: AeonDumpManager\Service\DumpLister + arguments: + - '@aeon_dump_manager.filesystem.private' + - '@aeon_dump_manager.dump_filename_parser' + + aeon_dump_manager.dump_service: + class: AeonDumpManager\Service\DumpService + arguments: + - '@aeon_dump_manager.dump_lister' + - '@aeon_dump_manager.filesystem.private' + - '@aeon_dump_manager.dump_binary_locator' + - '@Doctrine\DBAL\Connection' + - '@Shopware\Core\System\SystemConfig\SystemConfigService' + - '@aeon_dump_manager.dump_filename_parser' + - '@monolog.logger.aeon_dump_manager' + + # Async create job + aeon_dump_manager.message_queue.create_dump_handler: + class: AeonDumpManager\MessageQueue\Handler\CreateDumpHandler + arguments: + - '@aeon_dump_manager.dump_service' + - '@aeon_dump_manager.dump_job_status_service' + - '@monolog.logger.aeon_dump_manager' + tags: + - { name: messenger.message_handler } + + # Retention + aeon_dump_manager.scheduled_task.purge_dumps_task: + class: AeonDumpManager\ScheduledTask\PurgeDumpsTask + tags: + - { name: shopware.scheduled.task } + + aeon_dump_manager.scheduled_task.purge_dumps_task_handler: + class: AeonDumpManager\ScheduledTask\PurgeDumpsTaskHandler + arguments: + - '@scheduled_task.repository' + - '@monolog.logger.aeon_dump_manager' + - '@aeon_dump_manager.dump_service' + - '@aeon_dump_manager.dump_job_status_service' + - '@Shopware\Core\System\SystemConfig\SystemConfigService' + tags: + - { name: messenger.message_handler, handles: AeonDumpManager\ScheduledTask\PurgeDumpsTask } + + # Console commands + aeon_dump_manager.command.dump_list: + class: AeonDumpManager\Command\DumpListCommand + arguments: + - '@aeon_dump_manager.dump_lister' + tags: + - { name: console.command } + + aeon_dump_manager.command.dump_create: + class: AeonDumpManager\Command\DumpCreateCommand + arguments: + - '@aeon_dump_manager.dump_service' + tags: + - { name: console.command } + + aeon_dump_manager.command.dump_remove: + class: AeonDumpManager\Command\DumpRemoveCommand + arguments: + - '@aeon_dump_manager.dump_service' + tags: + - { name: console.command } + + # Admin API controller + AeonDumpManager\Controller\DumpController: + public: true + arguments: + - '@aeon_dump_manager.dump_lister' + - '@aeon_dump_manager.dump_service' + - '@aeon_dump_manager.dump_job_status_service' + - '@messenger.default_bus' diff --git a/src/Service/DumpService.php b/src/Service/DumpService.php index 8725be6..d0cb382 100644 --- a/src/Service/DumpService.php +++ b/src/Service/DumpService.php @@ -6,6 +6,7 @@ use AeonDumpManager\Service\Exception\DumpNotFoundException; use AeonDumpManager\Struct\DumpFile; use Doctrine\DBAL\Connection; use League\Flysystem\FilesystemOperator; +use Psr\Log\LoggerInterface; use Shopware\Core\System\SystemConfig\SystemConfigService; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; @@ -24,6 +25,7 @@ class DumpService private readonly Connection $connection, private readonly SystemConfigService $systemConfigService, private readonly DumpFilenameParser $filenameParser, + private readonly LoggerInterface $logger, ) { } @@ -69,6 +71,7 @@ class DumpService } $this->aeonDumpManagerFilesystemPrivate->delete($path); + $this->logger->info('Dump deleted.', ['filename' => $filename]); } /** @@ -100,6 +103,7 @@ class DumpService foreach ($excess as $dumpFile) { $this->aeonDumpManagerFilesystemPrivate->delete(DumpLister::path($dumpFile->filename)); + $this->logger->info('Dump removed: maxDumps exceeded.', ['filename' => $dumpFile->filename]); } } @@ -108,6 +112,7 @@ class DumpService foreach ($this->dumpLister->list() as $dumpFile) { if ($dumpFile->datetime < $threshold) { $this->aeonDumpManagerFilesystemPrivate->delete(DumpLister::path($dumpFile->filename)); + $this->logger->info('Dump removed: older than retention.', ['filename' => $dumpFile->filename]); } } } @@ -143,6 +148,7 @@ class DumpService if (!$process->isSuccessful()) { @unlink($tempPath); + $this->logger->error('Dump process failed.', ['exitCode' => $process->getExitCode()]); throw new ProcessFailedException($process); } @@ -153,6 +159,8 @@ class DumpService fclose($stream); @unlink($tempPath); + $this->logger->info('Dump created.', ['filename' => $filename, 'sizeBytes' => $sizeBytes]); + return new DumpFile($filename, $dateTime, $sizeBytes); }