diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4a27d2a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 120 + +[{docker-compose.yml,docker-compose.override.yml}] +indent_size = 2 + +# markdown uses two trailing spaces for explicit line breaks +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.github/workflows/test-application.yaml b/.github/workflows/test-application.yaml new file mode 100644 index 0000000..d8ee319 --- /dev/null +++ b/.github/workflows/test-application.yaml @@ -0,0 +1,89 @@ +name: Test application + +on: + pull_request: + push: + branches: + - '[0-9]+.x' + - '[0-9]+.[0-9]+' + +jobs: + test: + name: 'PHP ${{ matrix.php-version }} (${{ matrix.dependency-versions }})' + runs-on: ubuntu-latest + + env: + APP_ENV: test + DATABASE_URL: mysql://root:root@127.0.0.1:3306/su_content_test?serverVersion=5.7.32 + DATABASE_CHARSET: utf8mb4 + DATABASE_COLLATE: utf8mb4_unicode_ci + + strategy: + fail-fast: false + matrix: + include: + - php-version: '8.1' + dependency-versions: 'lowest' + env: + SYMFONY_DEPRECATIONS_HELPER: weak + + - php-version: '8.2' + dependency-versions: 'highest' + env: + SYMFONY_DEPRECATIONS_HELPER: weak + + - php-version: '8.3' + dependency-versions: 'highest' + env: + SYMFONY_DEPRECATIONS_HELPER: weak + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 + + steps: + - name: Checkout project + uses: actions/checkout@v2 + + - name: Install and configure PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: ctype, iconv, mysql + tools: 'composer:v2' + + - name: Install composer dependencies + uses: ramsey/composer-install@v2 + with: + dependency-versions: ${{ matrix.dependency-versions }} + composer-options: ${{ matrix.composer-options }} + + - name: Execute test cases + run: composer test + + lint: + name: "PHP Lint" + runs-on: ubuntu-latest + + steps: + - name: Checkout project + uses: actions/checkout@v2 + + - name: Install and configure PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + extensions: ctype, iconv, mysql + + - name: Install composer dependencies + uses: ramsey/composer-install@v2 + with: + dependency-versions: highest + + - name: Lint Code + run: composer lint diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb19384 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# composer +/composer.phar +composer.lock +/vendor + +.php-cs-fixer.cache +.eslintcache +.env.local + +# phpunit +.phpunit +phpunit.xml +.phpunit.result.cache + +# IDEs +/.idea +*.iml +*~ +.web-server-pid + +# System files +.DS_Store + +# Styleguide +/styleguide + +# IDEs +.idea +*.iml + +# Jackrabbit +jackrabbit-standalone* +jackrabbit/* + +# Symfony CLI +# https://symfony.com/doc/current/setup/symfony_server.html#different-php-settings-per-project +/php.ini +/.php-version + +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/public/bundles/ +/var/ +/vendor/ +/Tests/Application/var +###< symfony/framework-bundle ### + +###> phpunit/phpunit ### +/phpunit.xml +###< phpunit/phpunit ### + +###> symfony/phpunit-bridge ### +.phpunit +/phpunit.xml +###< symfony/phpunit-bridge ### + +###> symfony/web-server-bundle ### +/.web-server-pid +###< symfony/web-server-bundle ### + +###> qossmic/deptrac ### +/.deptrac.cache +###< qossmic/deptrac ### diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..649058a --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,50 @@ +exclude(['var/cache', 'tests/Resources/cache', 'node_modules']) + ->in(__DIR__); + +$config = new PhpCsFixer\Config(); +$config->setRiskyAllowed(true) + ->setRules([ + '@Symfony' => true, + 'array_syntax' => ['syntax' => 'short'], + 'class_definition' => false, + 'concat_space' => ['spacing' => 'one'], + 'function_declaration' => ['closure_function_spacing' => 'none'], + 'header_comment' => ['header' => $header], + 'native_constant_invocation' => true, + 'native_function_casing' => true, + 'native_function_invocation' => ['include' => ['@internal']], + 'global_namespace_import' => ['import_classes' => false, 'import_constants' => false, 'import_functions' => false], + 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true, 'remove_inheritdoc' => true], + 'ordered_imports' => true, + 'phpdoc_align' => ['align' => 'left'], + 'phpdoc_types_order' => false, + 'single_line_throw' => false, + 'single_line_comment_spacing' => false, + 'phpdoc_to_comment' => [ + 'ignored_tags' => ['todo', 'var'], + ], + 'phpdoc_separation' => [ + 'groups' => [ + ['Serializer\\*', 'VirtualProperty', 'Accessor', 'Type', 'Groups', 'Expose', 'Exclude', 'SerializedName', 'Inline', 'ExclusionPolicy'], + ], + ], + 'get_class_to_class_keyword' => false, // should be enabled as soon as support for php < 8 is dropped + 'nullable_type_declaration_for_default_null_value' => true, + 'no_null_property_initialization' => false, + 'fully_qualified_strict_types' => false, + ]) + ->setFinder($finder); + +return $config; \ No newline at end of file diff --git a/README.md b/README.md index 834d129..41bc2c7 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ We are committed to a fully transparent development process and **highly appreci In case you have questions, we are happy to welcome you in our official [Slack channel](https://sulu.io/services-and-support). If you found a bug or miss a specific feature, feel free to **file a new issue** with a respective title and description -on the the [sulu/SuluHeadlessBundle](https://github.com/sulu/SuluHeadlessBundle) repository. +on the [sulu/SuluPHPCRMigrationBundle](https://github.com/sulu/SuluPHPCRMigrationBundle) repository. ## 📘  License diff --git a/Resources/config/command.xml b/Resources/config/command.xml new file mode 100644 index 0000000..039df9a --- /dev/null +++ b/Resources/config/command.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/Resources/config/session.xml b/Resources/config/session.xml new file mode 100644 index 0000000..0888f23 --- /dev/null +++ b/Resources/config/session.xml @@ -0,0 +1,14 @@ + + + + + + %sulu_phpcr_migration.configuration% + + + + + diff --git a/Tests/Application/.env b/Tests/Application/.env new file mode 100644 index 0000000..afd9352 --- /dev/null +++ b/Tests/Application/.env @@ -0,0 +1 @@ +APP_ENV=test diff --git a/Tests/Application/config/bootstrap.php b/Tests/Application/config/bootstrap.php new file mode 100644 index 0000000..36801a1 --- /dev/null +++ b/Tests/Application/config/bootstrap.php @@ -0,0 +1,39 @@ +=1.2) +if (\is_array($env = @include \dirname(__DIR__) . '/.env.local.php')) { + $_SERVER += $env; + $_ENV += $env; +} elseif (!\class_exists(Dotenv::class)) { + throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); +} else { + $path = \dirname(__DIR__) . '/.env'; + $dotenv = new Dotenv(); + $dotenv->loadEnv($path); +} + +$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; +$_SERVER['APP_DEBUG'] ??= $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; +$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || \filter_var($_SERVER['APP_DEBUG'], \FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; diff --git a/Tests/Functional/.gitkeep b/Tests/Functional/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Unit/.gitkeep b/Tests/Unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Tests/test-bootstrap.php b/Tests/test-bootstrap.php new file mode 100644 index 0000000..42cbea5 --- /dev/null +++ b/Tests/test-bootstrap.php @@ -0,0 +1,16 @@ + + + + + + + + + + + + + ./Tests/Unit + + + + ./Tests/Functional + + + diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..60374f6 --- /dev/null +++ b/rector.php @@ -0,0 +1,33 @@ +paths([__DIR__ . '/src', __DIR__ . '/Tests']); + + $rectorConfig->phpstanConfigs([ + __DIR__ . '/phpstan.neon', + ]); + + // basic rules + $rectorConfig->importNames(); + $rectorConfig->importShortClasses(false); + + $rectorConfig->sets([ + SetList::CODE_QUALITY, + LevelSetList::UP_TO_PHP_81, + ]); +}; diff --git a/src/Application/Session/SessionManager.php b/src/Application/Session/SessionManager.php new file mode 100644 index 0000000..7ee6a1a --- /dev/null +++ b/src/Application/Session/SessionManager.php @@ -0,0 +1,73 @@ +workspace = $configuration['workspace']['default']; + $this->workspaceLive = $configuration['workspace']['live']; + } + + public function getDefaultSession(): SessionInterface + { + return $this->getSession($this->workspace); + } + + public function getLiveSession(): SessionInterface + { + return $this->getSession($this->workspaceLive); + } + + private function getSession(string $workspace): SessionInterface + { + $factory = $this->connection instanceof Connection ? new RepositoryFactoryDoctrineDBAL() : new RepositoryFactoryJackrabbit(); + $repository = $factory->getRepository(\array_filter([ + 'jackalope.doctrine_dbal_connection' => $this->connection, + 'jackalope.jackrabbit_uri' => $this->configuration['connection']['url'] ?? null, + ])); + + $credentials = new SimpleCredentials( + $this->configuration['connection']['user'] ?? 'dummy', + $this->configuration['connection']['password'] ?? 'dummy' + ); + + return $repository->login($credentials, $workspace); + } +} diff --git a/src/SuluPhpcrMigrationBundle.php b/src/SuluPhpcrMigrationBundle.php new file mode 100644 index 0000000..b20ff18 --- /dev/null +++ b/src/SuluPhpcrMigrationBundle.php @@ -0,0 +1,128 @@ +load('session.xml'); + $loader->load('command.xml'); + } + + public function configure(DefinitionConfigurator $definition): void + { + /** @var ArrayNodeDefinition $rootNode */ + $rootNode = $definition->rootNode(); + $rootNode + ->children() + ->scalarNode('DSN')->isRequired()->end() + ->end(); + } + + public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void + { + /** @var array{'DSN': string}[] $config */ + $config = $builder->getExtensionConfig('sulu_phpcr_migration'); + + $dsn = $config[0]['DSN']; + $builder->setParameter('sulu_phpcr_migration.dsn', $dsn); + + $configuration = $this->getConnectionConfiguration($dsn); + $builder->setParameter('sulu_phpcr_migration.configuration', $configuration); + + if ('dbal' === $configuration['connection']['type'] && $name = ($configuration['connection']['name'] ?? null)) { + $builder->setAlias('sulu_phpcr_migration.connection', \sprintf('doctrine.dbal.%s_connection', $name)); + + return; + } + } + + /** + * @return array{ + * connection: array{ + * type: string, + * name?: string, + * url?: string, + * user?: string|null, + * password?: string|null + * }, + * workspace: array{ + * default: string, + * live: string + * } + * } + */ + private function getConnectionConfiguration(string $dsn): array + { + /** @var array{ + * scheme: string, + * host?: string, + * port?: string, + * path?: string, + * query: string, + * user?: string, + * pass?: string + * } $parts + */ + $parts = \parse_url($dsn); + \parse_str($parts['query'], $query); + + /** @var string|null $workspace */ + $workspace = $query['workspace'] ?? ''; + unset($query['workspace']); + + if (!$workspace) { + throw new \InvalidArgumentException('Workspace is missing in DSN'); + } + + $result = [ + 'connection' => [ + 'type' => $parts['scheme'], + ], + 'workspace' => [ + 'default' => $workspace, + 'live' => $workspace . '_live', + ], + ]; + + if ('dbal' === $parts['scheme'] && ($host = $parts['host'] ?? null)) { + $result['connection']['name'] = $host; + + return $result; + } + + $result['connection']['url'] = \sprintf( + '%s:%s%s%s', + $parts['host'] ?? '', + $parts['port'] ?? '', + $parts['path'] ?? '', + $query ? '?' . \http_build_query($query) : '', + ); + $result['connection']['user'] = $parts['user'] ?? null; + $result['connection']['password'] = $parts['pass'] ?? null; + + return $result; + } +} diff --git a/src/UserInterface/Command/MigratePhpcrCommand.php b/src/UserInterface/Command/MigratePhpcrCommand.php new file mode 100644 index 0000000..174355a --- /dev/null +++ b/src/UserInterface/Command/MigratePhpcrCommand.php @@ -0,0 +1,35 @@ +sessionManager->getDefaultSession(); + $liveSession = $this->sessionManager->getLiveSession(); + + return Command::SUCCESS; + } +}