diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index fb293b8..ade6932 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -36,7 +36,7 @@ jobs: run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - name: Cache dependencies installed with composer - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ env.COMPOSER_CACHE_DIR }} key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml index d341e2d..d12143d 100644 --- a/.github/workflows/qodana_code_quality.yml +++ b/.github/workflows/qodana_code_quality.yml @@ -15,9 +15,9 @@ jobs: checks: write strategy: matrix: - php-versions: [ '8.2', '8.3', '8.4'] + php-versions: [ '8.2', '8.3', '8.4' ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit fetch-depth: 0 # a full history is required for pull request analysis @@ -33,7 +33,7 @@ jobs: run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - name: Cache dependencies installed with composer - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ env.COMPOSER_CACHE_DIR }} key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index b9d4c50..40282ef 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -35,7 +35,7 @@ jobs: run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - name: Cache dependencies installed with composer - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ env.COMPOSER_CACHE_DIR }} key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} diff --git a/.gitignore b/.gitignore index 65d0b98..88d66c2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ /.phpunit.cache /clover.xml /coveralls-upload.json -/phpunit.xml /vendor/ .idea composer.lock +.phpunit.result.cache diff --git a/bin/composer-post-install-script.php b/bin/composer-post-install-script.php new file mode 100644 index 0000000..4d3abfe --- /dev/null +++ b/bin/composer-post-install-script.php @@ -0,0 +1,69 @@ +} $file + */ +function copyFile(array $file): void +{ + if (! in_array(getEnvironment(), $file['environment'])) { + echo "Skipping the copy of {$file['source']} due to environment settings." . PHP_EOL; + return; + } + + if (is_readable($file['destination'])) { + echo "File {$file['destination']} already exists. Skipping..." . PHP_EOL; + return; + } + + if (! copy($file['source'], $file['destination'])) { + echo "Cannot copy {$file['source']} file to {$file['destination']}" . PHP_EOL; + } else { + echo "File {$file['source']} copied successfully to {$file['destination']}." . PHP_EOL; + } +} + +function getEnvironment(): string +{ + return getenv('COMPOSER_DEV_MODE') === '1' ? ENVIRONMENT_DEVELOPMENT : ENVIRONMENT_PRODUCTION; +} + +/** + * When adding files to the below array: + * - `source` and `destination` paths must be relative to the project root folder + * - `environment` key will indicate on what environments the file will be copied + */ +$files = [ + [ + 'source' => 'config/autoload/local.php.dist', + 'destination' => 'config/autoload/local.php', + 'environment' => [ENVIRONMENT_DEVELOPMENT, ENVIRONMENT_PRODUCTION], + ], + [ + 'source' => 'config/autoload/log.local.php.dist', + 'destination' => 'config/autoload/log.local.php', + 'environment' => [ENVIRONMENT_DEVELOPMENT, ENVIRONMENT_PRODUCTION], + ], + [ + 'source' => 'config/autoload/messenger.local.php.dist', + 'destination' => 'config/autoload/messenger.local.php', + 'environment' => [ENVIRONMENT_DEVELOPMENT, ENVIRONMENT_PRODUCTION], + ], + [ + 'source' => 'config/autoload/swoole.local.php.dist', + 'destination' => 'config/autoload/swoole.local.php', + 'environment' => [ENVIRONMENT_DEVELOPMENT, ENVIRONMENT_PRODUCTION], + ], +]; + +echo "Using environment setting: " . getEnvironment() . PHP_EOL; + +array_walk($files, 'copyFile'); diff --git a/bin/doctrine b/bin/doctrine new file mode 100644 index 0000000..56d0bbc --- /dev/null +++ b/bin/doctrine @@ -0,0 +1,27 @@ +#!/usr/bin/env php +get(EntityManager::class); +$entityManager->getEventManager() + ->addEventListener(Events::loadClassMetadata, $container->get(TablePrefixEventListener::class)); + +$commands = [ + $container->get(ExecuteFixturesCommand::class), + $container->get(ListFixturesCommand::class), +]; + +ConsoleRunner::run(new SingleManagerProvider($entityManager), $commands); diff --git a/composer.json b/composer.json index 46ae5ed..d363d5d 100644 --- a/composer.json +++ b/composer.json @@ -44,27 +44,37 @@ }, "require": { "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "dotkernel/dot-cli": "^3.9", - "dotkernel/dot-dependency-injection": "^1.2", - "dotkernel/dot-errorhandler": "4.2.1", - "laminas/laminas-component-installer": "^3.5", - "laminas/laminas-config-aggregator": "^1.18", - "mezzio/mezzio": "^3.20", - "netglue/laminas-messenger": "^2.3.0", - "symfony/redis-messenger": "^v7.2.3" + "ext-redis": "*", + "dotkernel/dot-cache": "^4.4", + "dotkernel/dot-cli": "^3.10", + "dotkernel/dot-data-fixtures": "^1.5", + "dotkernel/dot-dependency-injection": "^1.3", + "dotkernel/dot-errorhandler": "4.4.0", + "laminas/laminas-component-installer": "^3.7", + "laminas/laminas-config-aggregator": "^1.19", + "mezzio/mezzio": "^3.27.0", + "netglue/laminas-messenger": "^2.5.0", + "ramsey/uuid": "^4.9.2", + "ramsey/uuid-doctrine": "^2.1", + "roave/psr-container-doctrine": "^5.2.2 || ^6.1.0", + "symfony/polyfill-php83": "^1.33", + "symfony/redis-messenger": "^7.4.6" }, "require-dev": { - "laminas/laminas-coding-standard": "^3.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-doctrine": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5.45", + "laminas/laminas-coding-standard": "^3.1", + "laminas/laminas-development-mode": "^3.15", + "phpstan/phpstan": "^2.1.40", + "phpstan/phpstan-doctrine": "^2.0.18", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpunit/phpunit": "^10.5.63", "roave/security-advisories": "dev-master", - "swoole/ide-helper": "~5.0.0" + "swoole/ide-helper": "~5.0.3" }, "autoload": { "psr-4": { - "Queue\\": "src/" + "Core\\App\\": "src/Core/src/App/src/", + "Queue\\App\\": "src/App/src/", + "Queue\\Swoole\\": "src/Swoole/src/" } }, "autoload-dev": { @@ -81,8 +91,13 @@ ], "cs-check": "phpcs", "cs-fix": "phpcbf", - "test": "phpunit --colors=always", - "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", - "static-analysis": "phpstan analyse --memory-limit 1G" + "development-disable": "laminas-development-mode disable", + "development-enable": "laminas-development-mode enable", + "development-status": "laminas-development-mode status", + "post-update-cmd": [ + "php ./bin/composer-post-install-script.php" + ], + "static-analysis": "phpstan analyse --memory-limit 1G", + "test": "phpunit --colors=always" } } diff --git a/config/autoload/cli.global.php b/config/autoload/cli.global.php index aefbeb9..3745e9d 100644 --- a/config/autoload/cli.global.php +++ b/config/autoload/cli.global.php @@ -16,13 +16,13 @@ 'version' => '1.0.0', 'name' => 'DotKernel CLI', 'commands' => [ - "swoole:start" => StartCommand::class, - "swoole:stop" => StopCommand::class, - "messenger:start" => ConsumeMessagesCommand::class, - "messenger:debug" => DebugCommand::class, - "processed" => GetProcessedMessagesCommand::class, - "failed" => GetFailedMessagesCommand::class, - "inventory" => GetQueuedMessagesCommand::class, + 'swoole:start' => StartCommand::class, + 'swoole:stop' => StopCommand::class, + 'messenger:start' => ConsumeMessagesCommand::class, + 'messenger:debug' => DebugCommand::class, + 'processed' => GetProcessedMessagesCommand::class, + 'failed' => GetFailedMessagesCommand::class, + 'inventory' => GetQueuedMessagesCommand::class, ], ], FileLockerInterface::class => [ diff --git a/config/autoload/dependencies.global.php b/config/autoload/dependencies.global.php index cc8b534..cea597b 100644 --- a/config/autoload/dependencies.global.php +++ b/config/autoload/dependencies.global.php @@ -2,25 +2,26 @@ declare(strict_types=1); +use Doctrine\Migrations\Tools\Console\Command\ExecuteCommand; +use Roave\PsrContainerDoctrine\Migrations\CommandFactory; + return [ // Provides application-wide services. - // We recommend using fully-qualified class names whenever possible as - // service names. + // We recommend using fully qualified class names whenever possible as service names. 'dependencies' => [ - // Use 'aliases' to alias a service name to another service. The - // key is the alias name, the value is the service to which it points. + // Use 'aliases' to alias a service name to another service. + // The key is the alias name, the value is the service to which it points. 'aliases' => [ // Fully\Qualified\ClassOrInterfaceName::class => Fully\Qualified\ClassName::class, ], - // Use 'invokables' for constructor-less services, or services that do - // not require arguments to the constructor. Map a service name to the - // class name. + // Use 'invokables' for constructorless services, or services that do not require arguments to the constructor. + // Map a service name to the class name. 'invokables' => [ // Fully\Qualified\InterfaceName::class => Fully\Qualified\ClassName::class, ], // Use 'factories' for services provided by callbacks/factory classes. 'factories' => [ - // Fully\Qualified\ClassName::class => Fully\Qualified\FactoryName::class, + ExecuteCommand::class => CommandFactory::class, ], ], ]; diff --git a/config/autoload/local.php.dist b/config/autoload/local.php.dist index 3de5fb1..e44a219 100644 --- a/config/autoload/local.php.dist +++ b/config/autoload/local.php.dist @@ -12,29 +12,45 @@ declare(strict_types=1); $baseUrl = 'https://queue.dotkernel.net'; $databases = [ - 'default' => [ - 'host' => '', - 'dbname' => '', - 'user' => '', - 'password' => '', - 'port' => 3306, - 'driver' => 'pdo_mysql', - 'charset' => 'utf8mb4', - 'collate' => 'utf8mb4_general_ci', + /** + * You can add more database connections to this array. + * Only one active connection is allowed at a time. + * By default, the application uses the 'mariadb' connection. + * You can switch to another connection by activating it under doctrine->connection->orm_default-->params. + */ + 'mariadb' => [ + 'host' => 'localhost', + 'dbname' => 'dotkernel', + 'user' => '', + 'password' => '', + 'port' => 3306, + 'driver' => 'pdo_mysql', + 'collation' => 'utf8mb4_general_ci', + 'table_prefix' => '', + ], + 'postgresql' => [ + 'host' => 'localhost', + 'dbname' => 'dotkernel', + 'user' => '', + 'password' => '', + 'port' => 5432, + 'driver' => 'pdo_pgsql', + 'collation' => 'utf8mb4_general_ci', + 'table_prefix' => '', ], - // you can add more database connections into this array ]; return [ 'application' => [ - 'name' => $app['name'] ?? '', + 'name' => 'Dotkernel Queue', 'url' => $baseUrl, ], 'databases' => $databases, 'doctrine' => [ 'connection' => [ 'orm_default' => [ - 'params' => $databases['default'], + 'params' => $databases['mariadb'], +// 'params' => $databases['postgresql'], ], ], ], diff --git a/config/autoload/messenger.local.php.dist b/config/autoload/messenger.local.php.dist index 78a2c4a..a61d1e4 100644 --- a/config/autoload/messenger.local.php.dist +++ b/config/autoload/messenger.local.php.dist @@ -8,8 +8,8 @@ use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface as SymfonySerializer; return [ - "symfony" => [ - "messenger" => [ + 'symfony' => [ + 'messenger' => [ 'transports' => [ 'redis_transport' => [ 'dsn' => 'redis://127.0.0.1:6379/messages', @@ -31,10 +31,10 @@ return [ 'failure_transport' => 'failed', ], ], - "dependencies" => [ - "factories" => [ - "redis_transport" => [TransportFactory::class, 'redis_transport'], - "failed" => [TransportFactory::class, 'failed'], + 'dependencies' => [ + 'factories' => [ + 'redis_transport' => [TransportFactory::class, 'redis_transport'], + 'failed' => [TransportFactory::class, 'failed'], SymfonySerializer::class => fn(ContainerInterface $container) => new PhpSerializer(), ], ], diff --git a/config/cli-config.php b/config/cli-config.php new file mode 100644 index 0000000..6115452 --- /dev/null +++ b/config/cli-config.php @@ -0,0 +1,36 @@ +get(EntityManager::class); + $entityManager->getEventManager() + ->addEventListener(Events::loadClassMetadata, $container->get(TablePrefixEventListener::class)); + $entityManager->getEventManager() + ->addEventSubscriber(new MigrationsMigratedSubscriber($container)); + + return DependencyFactory::fromEntityManager( + new ConfigurationArray($container->get('config')['doctrine']['migrations']), + new ExistingEntityManager($entityManager) + ); +} catch (ContainerExceptionInterface $exception) { + try { + /** @var Logger $logger */ + $logger = $container->get('dot-log.queue-log'); + $logger->error($exception->getMessage()); + } catch (ContainerExceptionInterface $exception) { + error_log($exception->getMessage()); + } +} diff --git a/config/config.php b/config/config.php index 60bc0c2..a7f3c4f 100644 --- a/config/config.php +++ b/config/config.php @@ -19,14 +19,16 @@ Netglue\PsrContainer\Messenger\ConfigProvider::class, // Default App module config + Core\App\ConfigProvider::class, Queue\App\ConfigProvider::class, Queue\Swoole\ConfigProvider::class, // Dotkernel packages - Dot\Log\ConfigProvider::class, Dot\Cli\ConfigProvider::class, Dot\DependencyInjection\ConfigProvider::class, + Dot\DataFixtures\ConfigProvider::class, Dot\ErrorHandler\ConfigProvider::class, + Dot\Log\ConfigProvider::class, // Load application config in a pre-defined order in such a way that local settings // overwrite global settings. (Loaded as first to last): diff --git a/phpstan.neon b/phpstan.neon index 9da6dbc..f1598f7 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,8 +4,13 @@ includes: parameters: level: 5 paths: + - bin - config - public - src - test treatPhpDocTypesAsCertain: false + ignoreErrors: + - '/^Trait Core\\App\\Entity\\NumericIdentifierTrait is used zero times and is not analysed.$/' + - '/^Trait Core\\App\\Entity\\TimestampsTrait is used zero times and is not analysed.$/' + - '/^Trait Core\\App\\Entity\\UuidIdentifierTrait is used zero times and is not analysed.$/' diff --git a/phpunit.xml.dist b/phpunit.xml similarity index 96% rename from phpunit.xml.dist rename to phpunit.xml index 7a397a0..3e2008e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml @@ -3,7 +3,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" - cacheDirectory=".phpunit.cache" displayDetailsOnIncompleteTests="true" displayDetailsOnSkippedTests="true" displayDetailsOnTestsThatTriggerDeprecations="true" diff --git a/src/App/ConfigProvider.php b/src/App/src/ConfigProvider.php similarity index 81% rename from src/App/ConfigProvider.php rename to src/App/src/ConfigProvider.php index 8040eb4..144d142 100644 --- a/src/App/ConfigProvider.php +++ b/src/App/src/ConfigProvider.php @@ -14,6 +14,33 @@ use Queue\App\Message\MessageHandler; use Symfony\Component\Messenger\MessageBusInterface; +/** + * @phpstan-type ConfigType array{ + * dependencies: DependenciesType, + * symfony: SymfonyType, + * } + * @phpstan-type DependenciesType array{ + * factories: array, + * aliases: array, + * } + * @phpstan-type SymfonyType array{ + * messenger: array{ + * buses: array{ + * message_bus: array{ + * allows_zero_handlers: bool, + * middleware: list, + * handler_locator: class-string, + * handlers: array{ + * Message::class: list, + * }, + * routes: array{ + * Message::class: list, + * }, + * }, + * }, + * }, + * } + */ class ConfigProvider { public function __invoke(): array diff --git a/src/App/Message/Message.php b/src/App/src/Message/Message.php similarity index 100% rename from src/App/Message/Message.php rename to src/App/src/Message/Message.php diff --git a/src/App/Message/MessageHandler.php b/src/App/src/Message/MessageHandler.php similarity index 100% rename from src/App/Message/MessageHandler.php rename to src/App/src/Message/MessageHandler.php diff --git a/src/Core/src/App/src/ConfigProvider.php b/src/Core/src/App/src/ConfigProvider.php new file mode 100644 index 0000000..a6b2253 --- /dev/null +++ b/src/Core/src/App/src/ConfigProvider.php @@ -0,0 +1,169 @@ +, + * }, + * filesystem: array{ + * class: class-string, + * directory: non-empty-string, + * namespace: non-empty-string, + * }, + * }, + * configuration: array{ + * orm_default: array{ + * entity_listener_resolver: class-string, + * result_cache: non-empty-string, + * metadata_cache: non-empty-string, + * query_cache: non-empty-string, + * hydration_cache: non-empty-string, + * typed_field_mapper: non-empty-string|null, + * second_level_cache: array{ + * enabled: bool, + * default_lifetime: int, + * default_lock_lifetime: int, + * file_lock_region_directory: string, + * regions: non-empty-string[], + * }, + * }, + * }, + * driver: array{ + * orm_default: array{ + * class: class-string, + * }, + * }, + * fixtures: non-empty-string, + * migrations: array{ + * table_storage: array{ + * table_name: non-empty-string, + * version_column_name: non-empty-string, + * version_column_length: int, + * executed_at_column_name: non-empty-string, + * execution_time_column_name: non-empty-string, + * }, + * migrations_paths: array, + * all_or_nothing: bool, + * check_database_platform: bool, + * }, + * types: array, + * } + * @phpstan-type DependenciesType array{ + * factories: array, + * aliases: array, + * } + */ +class ConfigProvider +{ + public function __invoke(): array + { + return [ + 'dependencies' => $this->getDependencies(), + 'doctrine' => $this->getDoctrineConfig(), + 'resultCacheLifetime' => 600, + ]; + } + + private function getDependencies(): array + { + return [ + 'factories' => [ + 'doctrine.entity_manager.orm_default' => EntityManagerFactory::class, + EntityListenerResolver::class => EntityListenerResolverFactory::class, + TablePrefixEventListener::class => AttributedServiceFactory::class, + ], + 'aliases' => [ + EntityManager::class => 'doctrine.entity_manager.orm_default', + EntityManagerInterface::class => 'doctrine.entity_manager.orm_default', + ], + ]; + } + + /** + * @return DoctrineConfigType + */ + private function getDoctrineConfig(): array + { + return [ + 'cache' => [ + 'array' => [ + 'class' => ArrayAdapter::class, + ], + 'filesystem' => [ + 'class' => FilesystemAdapter::class, + 'directory' => getcwd() . '/data/cache', + 'namespace' => 'doctrine', + ], + ], + 'configuration' => [ + 'orm_default' => [ + 'entity_listener_resolver' => EntityListenerResolver::class, + 'result_cache' => 'filesystem', + 'metadata_cache' => 'filesystem', + 'query_cache' => 'filesystem', + 'hydration_cache' => 'array', + 'typed_field_mapper' => null, + 'second_level_cache' => [ + 'enabled' => true, + 'default_lifetime' => 3600, + 'default_lock_lifetime' => 60, + 'file_lock_region_directory' => '', + 'regions' => [], + ], + ], + ], + 'driver' => [ + // The default metadata driver aggregates all other drivers into a single one. + // Override `orm_default` only if you know what you're doing. + 'orm_default' => [ + 'class' => MappingDriverChain::class, + ], + ], + 'fixtures' => getcwd() . '/src/Core/src/App/src/Fixture', + 'migrations' => [ + 'table_storage' => [ + 'table_name' => 'doctrine_migration_versions', + 'version_column_name' => 'version', + 'version_column_length' => 191, + 'executed_at_column_name' => 'executed_at', + 'execution_time_column_name' => 'execution_time', + ], + 'migrations_paths' => [ + 'Core\App\Migration' => 'src/Core/src/App/src/Migration', + ], + 'all_or_nothing' => true, + 'check_database_platform' => true, + ], + 'types' => [ + UuidType::NAME => UuidType::class, + ], + ]; + } +} diff --git a/src/Core/src/App/src/DBAL/Types/AbstractEnumType.php b/src/Core/src/App/src/DBAL/Types/AbstractEnumType.php new file mode 100644 index 0000000..4383697 --- /dev/null +++ b/src/Core/src/App/src/DBAL/Types/AbstractEnumType.php @@ -0,0 +1,78 @@ +getName(); + } + + if ($platform instanceof SQLitePlatform) { + return 'TEXT'; + } + + $values = array_map(fn($case) => "'$case->value'", $this->getEnumCases()); + + return sprintf('ENUM(%s)', implode(', ', $values)); + } + + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed + { + return $this->getValue($value); + } + + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed + { + return $this->getValue($value); + } + + /** + * @return class-string + */ + abstract public function getEnumClass(): string; + + /** + * @return non-empty-string + */ + abstract public function getName(): string; + + /** + * @return BackedEnum[] + */ + public function getEnumCases(): array + { + return $this->getEnumClass()::cases(); + } + + /** + * @return list + */ + public function getEnumValues(): array + { + return $this->getEnumClass()::values(); + } + + public function getValue(mixed $value): mixed + { + if (! $value instanceof BackedEnum) { + return $value; + } + + return $value->value; + } +} diff --git a/src/Core/src/App/src/DBAL/Types/UuidType.php b/src/Core/src/App/src/DBAL/Types/UuidType.php new file mode 100644 index 0000000..852e1d2 --- /dev/null +++ b/src/Core/src/App/src/DBAL/Types/UuidType.php @@ -0,0 +1,17 @@ +entityManager = $container->get('doctrine.entity_manager.orm_default'); + $this->connection = $this->entityManager->getConnection(); + } + + /** + * @throws Exception + */ + public function getSubscribedEvents(): array + { + if (! $this->connection->getDatabasePlatform() instanceof PostgreSQLPlatform) { + return []; + } + + return [ + Events::onMigrationsMigrating, + ]; + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws Exception + */ + public function onMigrationsMigrating(): void + { + $dbEnumTypes = $this->getCustomEnumTypesFromTheDatabase(); + $fsEnumTypes = $this->getCustomEnumTypesFromTheFileSystem(); + + $enumTypes = $this->mergeCustomEnumTypes($dbEnumTypes, $fsEnumTypes); + foreach ($enumTypes as $action => $enums) { + foreach ($enums as $type => $values) { + match ($action) { + 'create' => $this->createDatabaseType($type, $values), + 'delete' => $this->deleteDatabaseType($type), + 'update' => $this->updateDatabaseType($type, $values), + default => null, + }; + } + } + } + + /** + * @phpstan-return array + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function getCustomEnumTypesFromTheFileSystem(): array + { + $enumTypes = []; + + $customTypes = $this->container->get('config')['doctrine']['types'] ?? []; + foreach ($customTypes as $type => $class) { + $class = new $class(); + if (! $class instanceof AbstractEnumType) { + continue; + } + $enumTypes[$type] = $class; + } + + return $enumTypes; + } + + /** + * @phpstan-return list + * @throws Exception + */ + private function getDatabaseTypeValues(string $type): array + { + $results = $this->connection->executeQuery( + "SELECT e.enumlabel FROM pg_type t JOIN pg_enum e ON t.oid = e.enumtypid WHERE t.typname = '$type';" + )->fetchAllAssociative(); + + return array_column($results, 'enumlabel'); + } + + /** + * @return list + * @throws Exception + */ + private function getCustomEnumTypesFromTheDatabase(): array + { + return $this->connection->executeQuery( + 'SELECT t.typname FROM pg_type t JOIN pg_enum e ON t.oid = e.enumtypid GROUP BY t.typname' + )->fetchFirstColumn(); + } + + /** + * @param list $dbEnumTypes + * @param array $fsEnumTypes + * @return array{ + * create: array>, + * delete: array>, + * skip: array>, + * update: array> + * } + * @throws Exception + */ + private function mergeCustomEnumTypes(array $dbEnumTypes, array $fsEnumTypes): array + { + $enumTypes = [ + 'create' => [], + 'delete' => [], + 'skip' => [], + 'update' => [], + ]; + + /** @var AbstractEnumType $class */ + foreach ($fsEnumTypes as $type => $class) { + $fsTypeValues = $class->getEnumValues(); + if (in_array($type, $dbEnumTypes)) { + $dbTypeValues = $this->getDatabaseTypeValues($type); + if ($dbTypeValues === $fsTypeValues) { + $enumTypes['skip'][$type] = $fsTypeValues; + } else { + $enumTypes['update'][$type] = $fsTypeValues; + } + } else { + $enumTypes['create'][$type] = $fsTypeValues; + } + } + + foreach ($dbEnumTypes as $type) { + if (! array_key_exists($type, $fsEnumTypes)) { + $enumTypes['delete'][$type] = $this->getDatabaseTypeValues($type); + } + } + + return $enumTypes; + } + + /** + * @param non-empty-string $type + * @param list $values + * @throws Exception + */ + private function createDatabaseType(string $type, array $values): void + { + $this->connection->executeQuery( + sprintf("CREATE TYPE %s AS ENUM ('%s');", $type, implode("', '", $values)) + ); + } + + /** + * @throws Exception + */ + private function deleteDatabaseType(string $type): void + { + $this->connection->executeQuery( + sprintf('DROP TYPE %s;', $type) + ); + } + + /** + * @param non-empty-string $type + * @param list $values + * @throws Exception + */ + private function updateDatabaseType(string $type, array $values): void + { + $this->deleteDatabaseType($type); + $this->createDatabaseType($type, $values); + } +} diff --git a/src/Core/src/App/src/Entity/AbstractEntity.php b/src/Core/src/App/src/Entity/AbstractEntity.php new file mode 100644 index 0000000..dea496a --- /dev/null +++ b/src/Core/src/App/src/Entity/AbstractEntity.php @@ -0,0 +1,57 @@ +initId(); + } + + protected function initId(): void + { + } + + /** + * Override this method in soft-deletable entities + */ + public function isDeleted(): bool + { + return false; + } + + /** + * @param array $array + */ + public function exchangeArray(array $array): void + { + foreach ($array as $property => $values) { + if (is_array($values)) { + $method = 'add' . ucfirst($property); + if (! method_exists($this, $method)) { + continue; + } + foreach ($values as $value) { + $this->$method($value); + } + } else { + $method = 'set' . ucfirst($property); + if (! method_exists($this, $method)) { + continue; + } + $this->$method($values); + } + } + } +} diff --git a/src/Core/src/App/src/Entity/EntityInterface.php b/src/Core/src/App/src/Entity/EntityInterface.php new file mode 100644 index 0000000..d4091b8 --- /dev/null +++ b/src/Core/src/App/src/Entity/EntityInterface.php @@ -0,0 +1,23 @@ + true])] + #[ORM\GeneratedValue(strategy: 'AUTO')] + protected ?int $id; + + public function getId(): ?int + { + return $this->id; + } + + public function setId(int $id): static + { + $this->id = $id; + + return $this; + } +} diff --git a/src/Core/src/App/src/Entity/TimestampsTrait.php b/src/Core/src/App/src/Entity/TimestampsTrait.php new file mode 100644 index 0000000..4485bea --- /dev/null +++ b/src/Core/src/App/src/Entity/TimestampsTrait.php @@ -0,0 +1,49 @@ +created; + } + + public function getCreatedFormatted(string $dateFormat = 'Y-m-d H:i:s'): ?string + { + return $this->created?->format($dateFormat); + } + + public function getUpdated(): ?DateTimeImmutable + { + return $this->updated; + } + + public function getUpdatedFormatted(string $dateFormat = 'Y-m-d H:i:s'): ?string + { + return $this->updated?->format($dateFormat); + } + + #[ORM\PrePersist] + public function created(): void + { + $this->created = new DateTimeImmutable(); + } + + #[ORM\PreUpdate] + public function touch(): void + { + $this->updated = new DateTimeImmutable(); + } +} diff --git a/src/Core/src/App/src/Entity/UuidIdentifierTrait.php b/src/Core/src/App/src/Entity/UuidIdentifierTrait.php new file mode 100644 index 0000000..9e6f2ef --- /dev/null +++ b/src/Core/src/App/src/Entity/UuidIdentifierTrait.php @@ -0,0 +1,33 @@ +id; + } + + public function setId(UuidInterface $id): static + { + $this->id = $id; + + return $this; + } + + protected function initId(): void + { + $this->id = Uuid::uuid7(); + } +} diff --git a/src/Core/src/App/src/Event/TablePrefixEventListener.php b/src/Core/src/App/src/Event/TablePrefixEventListener.php new file mode 100644 index 0000000..68b5d2a --- /dev/null +++ b/src/Core/src/App/src/Event/TablePrefixEventListener.php @@ -0,0 +1,54 @@ + $config + */ + #[Inject( + 'config.doctrine.connection.orm_default.params', + )] + public function __construct(array $config) + { + if (array_key_exists('table_prefix', $config) && is_string($config['table_prefix'])) { + $this->prefix = $config['table_prefix']; + } + } + + public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs): void + { + if ($this->prefix === '') { + return; + } + + $classMetadata = $eventArgs->getClassMetadata(); + if ( + ! $classMetadata->isInheritanceTypeSingleTable() + || $classMetadata->getName() === $classMetadata->rootEntityName + ) { + $classMetadata->setPrimaryTable([ + 'name' => $this->prefix . $classMetadata->getTableName(), + ]); + } + + foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) { + if ($mapping['type'] === ClassMetadata::MANY_TO_MANY && $mapping['isOwningSide']) { + $classMetadata->associationMappings[$fieldName]['joinTable']['name'] = + $this->prefix . $mapping['joinTable']['name']; + } + } + } +} diff --git a/src/Core/src/App/src/Factory/EntityListenerResolverFactory.php b/src/Core/src/App/src/Factory/EntityListenerResolverFactory.php new file mode 100644 index 0000000..c826995 --- /dev/null +++ b/src/Core/src/App/src/Factory/EntityListenerResolverFactory.php @@ -0,0 +1,16 @@ + + */ +abstract class AbstractRepository extends EntityRepository +{ + public function deleteResource(EntityInterface $resource): void + { + $this->getEntityManager()->remove($resource); + $this->getEntityManager()->flush(); + } + + public function getQueryBuilder(): QueryBuilder + { + return $this->getEntityManager()->createQueryBuilder(); + } + + public function saveResource(EntityInterface $resource): void + { + $this->getEntityManager()->persist($resource); + $this->getEntityManager()->flush(); + } +} diff --git a/src/Core/src/App/src/Resolver/EntityListenerResolver.php b/src/Core/src/App/src/Resolver/EntityListenerResolver.php new file mode 100644 index 0000000..a842ee0 --- /dev/null +++ b/src/Core/src/App/src/Resolver/EntityListenerResolver.php @@ -0,0 +1,27 @@ +container->get($className); + } +} diff --git a/src/Swoole/Command/Factory/StartCommandFactory.php b/src/Swoole/src/Command/Factory/StartCommandFactory.php similarity index 100% rename from src/Swoole/Command/Factory/StartCommandFactory.php rename to src/Swoole/src/Command/Factory/StartCommandFactory.php diff --git a/src/Swoole/Command/Factory/StopCommandFactory.php b/src/Swoole/src/Command/Factory/StopCommandFactory.php similarity index 100% rename from src/Swoole/Command/Factory/StopCommandFactory.php rename to src/Swoole/src/Command/Factory/StopCommandFactory.php diff --git a/src/Swoole/Command/GetFailedMessagesCommand.php b/src/Swoole/src/Command/GetFailedMessagesCommand.php similarity index 98% rename from src/Swoole/Command/GetFailedMessagesCommand.php rename to src/Swoole/src/Command/GetFailedMessagesCommand.php index 02c6638..df071e1 100644 --- a/src/Swoole/Command/GetFailedMessagesCommand.php +++ b/src/Swoole/src/Command/GetFailedMessagesCommand.php @@ -87,7 +87,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $logPath = dirname(__DIR__, 3) . '/log/queue-log.log'; + $logPath = dirname(__DIR__, 4) . '/log/queue-log.log'; if (! file_exists($logPath)) { $output->writeln("Log file was not found: $logPath"); diff --git a/src/Swoole/Command/GetProcessedMessagesCommand.php b/src/Swoole/src/Command/GetProcessedMessagesCommand.php similarity index 98% rename from src/Swoole/Command/GetProcessedMessagesCommand.php rename to src/Swoole/src/Command/GetProcessedMessagesCommand.php index d92ff4b..9e604e1 100644 --- a/src/Swoole/Command/GetProcessedMessagesCommand.php +++ b/src/Swoole/src/Command/GetProcessedMessagesCommand.php @@ -87,7 +87,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $logPath = dirname(__DIR__, 3) . '/log/queue-log.log'; + $logPath = dirname(__DIR__, 4) . '/log/queue-log.log'; if (! file_exists($logPath)) { $output->writeln("Log file was not found: $logPath"); diff --git a/src/Swoole/Command/GetQueuedMessagesCommand.php b/src/Swoole/src/Command/GetQueuedMessagesCommand.php similarity index 100% rename from src/Swoole/Command/GetQueuedMessagesCommand.php rename to src/Swoole/src/Command/GetQueuedMessagesCommand.php diff --git a/src/Swoole/Command/IsRunningTrait.php b/src/Swoole/src/Command/IsRunningTrait.php similarity index 100% rename from src/Swoole/Command/IsRunningTrait.php rename to src/Swoole/src/Command/IsRunningTrait.php diff --git a/src/Swoole/Command/StartCommand.php b/src/Swoole/src/Command/StartCommand.php similarity index 100% rename from src/Swoole/Command/StartCommand.php rename to src/Swoole/src/Command/StartCommand.php diff --git a/src/Swoole/Command/StopCommand.php b/src/Swoole/src/Command/StopCommand.php similarity index 96% rename from src/Swoole/Command/StopCommand.php rename to src/Swoole/src/Command/StopCommand.php index b64d2b3..fc3bc96 100644 --- a/src/Swoole/Command/StopCommand.php +++ b/src/Swoole/src/Command/StopCommand.php @@ -7,6 +7,7 @@ use Closure; use Queue\Swoole\PidManager; use Swoole\Process as SwooleProcess; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -14,6 +15,7 @@ use function time; use function usleep; +#[AsCommand('queue:swoole:stop')] class StopCommand extends Command { use IsRunningTrait; diff --git a/src/Swoole/ConfigProvider.php b/src/Swoole/src/ConfigProvider.php similarity index 100% rename from src/Swoole/ConfigProvider.php rename to src/Swoole/src/ConfigProvider.php diff --git a/src/Swoole/Delegators/TCPServerDelegator.php b/src/Swoole/src/Delegators/TCPServerDelegator.php similarity index 100% rename from src/Swoole/Delegators/TCPServerDelegator.php rename to src/Swoole/src/Delegators/TCPServerDelegator.php diff --git a/src/Swoole/Exception/ExceptionInterface.php b/src/Swoole/src/Exception/ExceptionInterface.php similarity index 100% rename from src/Swoole/Exception/ExceptionInterface.php rename to src/Swoole/src/Exception/ExceptionInterface.php diff --git a/src/Swoole/Exception/ExtensionNotLoadedException.php b/src/Swoole/src/Exception/ExtensionNotLoadedException.php similarity index 100% rename from src/Swoole/Exception/ExtensionNotLoadedException.php rename to src/Swoole/src/Exception/ExtensionNotLoadedException.php diff --git a/src/Swoole/Exception/InvalidArgumentException.php b/src/Swoole/src/Exception/InvalidArgumentException.php similarity index 100% rename from src/Swoole/Exception/InvalidArgumentException.php rename to src/Swoole/src/Exception/InvalidArgumentException.php diff --git a/src/Swoole/Exception/InvalidConfigException.php b/src/Swoole/src/Exception/InvalidConfigException.php similarity index 100% rename from src/Swoole/Exception/InvalidConfigException.php rename to src/Swoole/src/Exception/InvalidConfigException.php diff --git a/src/Swoole/Exception/InvalidStaticResourceMiddlewareException.php b/src/Swoole/src/Exception/InvalidStaticResourceMiddlewareException.php similarity index 100% rename from src/Swoole/Exception/InvalidStaticResourceMiddlewareException.php rename to src/Swoole/src/Exception/InvalidStaticResourceMiddlewareException.php diff --git a/src/Swoole/Exception/RuntimeException.php b/src/Swoole/src/Exception/RuntimeException.php similarity index 100% rename from src/Swoole/Exception/RuntimeException.php rename to src/Swoole/src/Exception/RuntimeException.php diff --git a/src/Swoole/PidManager.php b/src/Swoole/src/PidManager.php similarity index 100% rename from src/Swoole/PidManager.php rename to src/Swoole/src/PidManager.php diff --git a/src/Swoole/PidManagerFactory.php b/src/Swoole/src/PidManagerFactory.php similarity index 100% rename from src/Swoole/PidManagerFactory.php rename to src/Swoole/src/PidManagerFactory.php diff --git a/src/Swoole/ServerFactory.php b/src/Swoole/src/ServerFactory.php similarity index 100% rename from src/Swoole/ServerFactory.php rename to src/Swoole/src/ServerFactory.php