diff --git a/src/bundle/RepositoryInstaller/Command/UpgradePlatformCommand.php b/src/bundle/RepositoryInstaller/Command/UpgradePlatformCommand.php
new file mode 100644
index 0000000000..1c4b231b81
--- /dev/null
+++ b/src/bundle/RepositoryInstaller/Command/UpgradePlatformCommand.php
@@ -0,0 +1,273 @@
+connection = $connection;
+ $this->installers = $installers;
+ $this->cachePool = $cachePool;
+ $this->environment = $environment;
+ $this->repositoryConfigurationProvider = $repositoryConfigurationProvider;
+ parent::__construct();
+ }
+
+ protected function configure()
+ {
+ $this->setName('ibexa:flavor-upgrade');
+ $this->setAliases($this->getDeprecatedAliases());
+ $this->addArgument(
+ 'type',
+ InputArgument::OPTIONAL,
+ 'The type of upgrade. Available options: ' . implode(', ', array_keys($this->installers)),
+ 'ibexa-content'
+ );
+ $this->addOption(
+ 'skip-indexing',
+ null,
+ InputOption::VALUE_NONE,
+ 'Skip indexing (ezplaform:reindex)'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->output = $output;
+ $this->checkPermissions();
+ $this->checkParameters();
+ $this->checkCreateDatabase($output);
+
+ $type = $input->getArgument('type');
+ $siteaccess = $input->getOption('siteaccess');
+ $installer = $this->getInstaller($type);
+ if ($installer === false) {
+ $output->writeln(
+ "Unknown install type '$type', available options in currently installed Ibexa package: " .
+ implode(', ', array_keys($this->installers))
+ );
+ exit(self::EXIT_UNKNOWN_INSTALL_TYPE);
+ }
+
+ $installer->setOutput($output);
+
+ $installer->importSchema();
+ $installer->importData();
+ $this->cacheClear($output);
+
+ if (!$input->getOption('skip-indexing')) {
+ $this->indexData($output, $siteaccess);
+ }
+
+ return 0;
+ }
+
+ private function checkPermissions()
+ {
+ // @todo should take var-dir etc. from composer config or fallback to flex directory scheme
+ if (!is_writable('public') && !is_writable('public/var')) {
+ $this->output->writeln('[public/ | public/var] is not writable');
+ exit(self::EXIT_MISSING_PERMISSIONS);
+ }
+ }
+
+ private function checkParameters()
+ {
+ // @todo doesn't make sense to check for parameters.yml in sf4 and flex
+ return;
+ $parametersFile = 'app/config/parameters.yml';
+ if (!is_file($parametersFile)) {
+ $this->output->writeln("Required configuration file '$parametersFile' not found");
+ exit(self::EXIT_PARAMETERS_NOT_FOUND);
+ }
+ }
+
+ private function checkCreateDatabase(OutputInterface $output)
+ {
+ $output->writeln(
+ sprintf(
+ 'Creating database %s if it does not exist, using doctrine:database:create --if-not-exists',
+ $this->connection->getDatabase()
+ )
+ );
+ try {
+ $bufferedOutput = new BufferedOutput();
+ $connectionName = $this->repositoryConfigurationProvider->getStorageConnectionName();
+ $command = sprintf('doctrine:database:create --if-not-exists --connection=%s', $connectionName);
+ $this->executeCommand($bufferedOutput, $command);
+ $output->writeln($bufferedOutput->fetch());
+ } catch (\RuntimeException $exception) {
+ $this->output->writeln(
+ sprintf(
+ "The configured database '%s' does not exist or cannot be created (%s).",
+ $this->connection->getDatabase(),
+ $exception->getMessage()
+ )
+ );
+ $this->output->writeln("Please check the database configuration in 'app/config/parameters.yml'");
+ exit(self::EXIT_GENERAL_DATABASE_ERROR);
+ }
+ }
+
+ /**
+ * Clear all content related cache (persistence cache).
+ *
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ */
+ private function cacheClear(OutputInterface $output)
+ {
+ $this->cachePool->clear();
+ }
+
+ /**
+ * Calls indexing commands.
+ *
+ * @todo This should not be needed once/if the Installer starts using API in the future.
+ * So temporary measure until it is not raw SQL based for the data itself (as opposed to the schema).
+ * This is done after cache clearing to make sure no cached data from before sql import is used.
+ *
+ * IMPORTANT: This is done using a command because config has change, so container and all services are different.
+ *
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @param string|null $siteaccess
+ */
+ private function indexData(OutputInterface $output, $siteaccess = null)
+ {
+ $output->writeln(
+ sprintf('Search engine re-indexing, executing command ibexa:reindex')
+ );
+
+ $command = 'ibexa:reindex';
+ if ($siteaccess) {
+ $command .= sprintf(' --siteaccess=%s', $siteaccess);
+ }
+
+ $this->executeCommand($output, $command);
+ }
+
+ /**
+ * @param $type
+ *
+ * @return \Ibexa\Bundle\RepositoryInstaller\Installer\Installer
+ */
+ private function getInstaller($type)
+ {
+ if (!isset($this->installers[$type])) {
+ return false;
+ }
+
+ return $this->installers[$type];
+ }
+
+ /**
+ * Executes a Symfony command in separate process.
+ *
+ * Typically useful when configuration has changed, or you are outside of Symfony context (Composer commands).
+ *
+ * Based on {@see \Sensio\Bundle\DistributionBundle\Composer\ScriptHandler::executeCommand}.
+ *
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @param string $cmd Ibexa command to execute, like 'ezplatform:solr_create_index'
+ * Escape any user provided arguments, like: 'assets:install '.escapeshellarg($webDir)
+ * @param int $timeout
+ */
+ private function executeCommand(OutputInterface $output, $cmd, $timeout = 300)
+ {
+ $phpFinder = new PhpExecutableFinder();
+ if (!$phpPath = $phpFinder->find(false)) {
+ throw new \RuntimeException('The php executable could not be found. Add it to your PATH environment variable and try again');
+ }
+
+ // We don't know which php arguments where used so we gather some to be on the safe side
+ $arguments = $phpFinder->findArguments();
+ if (false !== ($ini = php_ini_loaded_file())) {
+ $arguments[] = '--php-ini=' . $ini;
+ }
+
+ // Pass memory_limit in case this was specified as php argument, if not it will most likely be same as $ini.
+ if ($memoryLimit = ini_get('memory_limit')) {
+ $arguments[] = '-d memory_limit=' . $memoryLimit;
+ }
+
+ $phpArgs = implode(' ', array_map('escapeshellarg', $arguments));
+ $php = escapeshellarg($phpPath) . ($phpArgs ? ' ' . $phpArgs : '');
+
+ // Make sure to pass along relevant global Symfony options to console command
+ $console = escapeshellarg('bin/console');
+ if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) {
+ $console .= ' -' . str_repeat('v', $output->getVerbosity() - 1);
+ }
+
+ if ($output->isDecorated()) {
+ $console .= ' --ansi';
+ }
+
+ $console .= ' --env=' . escapeshellarg($this->environment);
+
+ $process = Process::fromShellCommandline(
+ implode(' ', [$php, $console, $cmd]),
+ null,
+ null,
+ null,
+ $timeout
+ );
+
+ $process->run(static function ($type, $buffer) use ($output) { $output->write($buffer, false); });
+ if (!$process->getExitCode() === 1) {
+ throw new \RuntimeException(sprintf('An error occurred when executing the "%s" command.', escapeshellarg($cmd)));
+ }
+ }
+
+ public function getDeprecatedAliases(): array
+ {
+ return ['ezplatform:upgrade'];
+ }
+}
+
+class_alias(UpgradePlatformCommand::class, 'EzSystems\PlatformInstallerBundle\Command\UpgradePlatformCommand');
diff --git a/src/bundle/RepositoryInstaller/DependencyInjection/Compiler/UpgraderTagPass.php b/src/bundle/RepositoryInstaller/DependencyInjection/Compiler/UpgraderTagPass.php
new file mode 100644
index 0000000000..2d2e85fd86
--- /dev/null
+++ b/src/bundle/RepositoryInstaller/DependencyInjection/Compiler/UpgraderTagPass.php
@@ -0,0 +1,52 @@
+hasDefinition(UpgradePlatformCommand::class)) {
+ return;
+ }
+
+ $installCommandDef = $container->findDefinition(UpgradePlatformCommand::class);
+ $installers = [];
+
+ foreach ($container->findTaggedServiceIds(self::INSTALLER_TAG) as $id => $tags) {
+ foreach ($tags as $tag) {
+ if (!isset($tag['type'])) {
+ throw new LogicException(
+ sprintf(
+ 'Service tag %s needs a "type" attribute to identify the installer. You need to provide a tag for %s.',
+ self::INSTALLER_TAG,
+ $id
+ )
+ );
+ }
+
+ $installers[$tag['type']] = new Reference($id);
+ }
+ }
+
+ $installCommandDef->replaceArgument('$installers', $installers);
+ }
+}
+
+class_alias(UpgraderTagPass::class, 'EzSystems\PlatformInstallerBundle\DependencyInjection\Compiler\UpgraderTagPass');
diff --git a/src/bundle/RepositoryInstaller/IbexaRepositoryInstallerBundle.php b/src/bundle/RepositoryInstaller/IbexaRepositoryInstallerBundle.php
index b5934d8080..cb419349a9 100644
--- a/src/bundle/RepositoryInstaller/IbexaRepositoryInstallerBundle.php
+++ b/src/bundle/RepositoryInstaller/IbexaRepositoryInstallerBundle.php
@@ -8,6 +8,7 @@
use Ibexa\Bundle\DoctrineSchema\DoctrineSchemaBundle;
use Ibexa\Bundle\RepositoryInstaller\DependencyInjection\Compiler\InstallerTagPass;
+use Ibexa\Bundle\RepositoryInstaller\DependencyInjection\Compiler\UpgraderTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -30,6 +31,7 @@ public function build(ContainerBuilder $container)
parent::build($container);
$container->addCompilerPass(new InstallerTagPass());
+ $container->addCompilerPass(new UpgraderTagPass());
}
}
diff --git a/src/bundle/RepositoryInstaller/Installer/CoreUpgrader.php b/src/bundle/RepositoryInstaller/Installer/CoreUpgrader.php
new file mode 100644
index 0000000000..a896fe99a5
--- /dev/null
+++ b/src/bundle/RepositoryInstaller/Installer/CoreUpgrader.php
@@ -0,0 +1,184 @@
+schemaImporter = $schemaImporter;
+ //$this->defaultTableOptions = $defaultTableOptions;
+ $this->previousType = $previousType;
+ $this->nextType = $nextType;
+ }
+
+ /**
+ * Import Schema using schema.yaml files from new bundles
+ *
+ * @throws \Ibexa\Contracts\DoctrineSchema\Exception\InvalidConfigurationException
+ * @throws \Doctrine\DBAL\DBALException
+ */
+ public function importSchema()
+ {
+ $schemaFiles = $this->findSchemaFiles();
+
+ $config = new SchemaConfig();
+ $config->setDefaultTableOptions($this->defaultTableOptions);
+
+ $schema = new Schema([], [], $config);
+
+ $this->output->writeln(
+ sprintf(
+ 'Import %d schema files',
+ count($schemaFiles)
+ )
+ );
+
+ foreach ($schemaFiles as $schemaFilePath) {
+ $this->schemaImporter->importFromFile($schemaFilePath, $schema);
+ }
+
+ $databasePlatform = $this->db->getDatabasePlatform();
+ $queries = array_map(function($query) {
+ return str_replace('CREATE TABLE', 'CREATE TABLE IF NOT EXISTS', $query);
+ }, $schema->toSql($databasePlatform));
+
+ $queriesCount = count($queries);
+ $this->output->writeln(
+ sprintf(
+ 'Executing %d queries on database %s (%s)',
+ $queriesCount,
+ $this->db->getDatabase(),
+ $databasePlatform->getName()
+ )
+ );
+ $progressBar = new ProgressBar($this->output);
+ $progressBar->start($queriesCount);
+
+ foreach ($queries as $query) {
+ try {
+ @$this->db->exec($query);
+ } catch (DriverException $driverException) {
+ if (false === strpos($query, 'ALTER TABLE')) {
+ throw $driverException;
+ }
+ }
+ $progressBar->advance(1);
+ }
+
+ $progressBar->finish();
+ // go to the next line after ProgressBar::finish and add one more extra blank line for readability
+ $this->output->writeln(PHP_EOL);
+ // clear any leftover progress bar parts in the output buffer
+ $progressBar->clear();
+ }
+
+ /**
+ * @todo Have a nicer way to retrieve those schemas than this dirty hack
+ *
+ * @return string[]
+ */
+ private function findSchemaFiles(): array {
+ $schemaFiles = [];
+
+ $previousEditionPackage = str_replace('-', '/', $this->previousType);
+ $version=null;
+ foreach(json_decode(file_get_contents('vendor/composer/installed.json'), true)['packages'] as $package) {
+ if ($package['name'] === $previousEditionPackage) {
+ $version = $package['version'];
+ break;
+ }
+ }
+ if (is_null($version)) {
+ return $schemaFiles; //TODO: Throw a proper error instead
+ }
+ $nextEditionPackage = str_replace('-', '/', $this->nextType);
+ $nextPackageList = [];
+ foreach (json_decode(shell_exec("curl -s https://raw.githubusercontent.com/$nextEditionPackage/$version/composer.json"), true)['require'] as $requiredPackage=>$requiredVersion) {
+ if (0 === strpos($requiredPackage, 'ibexa/') && $requiredPackage !== $nextEditionPackage) {
+ $nextPackageList[] = $requiredPackage;
+ }
+ }
+
+ foreach($nextPackageList as $package) {
+ foreach (['', 'storage/', 'storage/legacy/'] as $subDir) {
+ $schemaFile="vendor/$package/src/bundle/Resources/config/{$subDir}schema.yaml";
+ if (file_exists($schemaFile)) {
+ $schemaFiles[] = $schemaFile;
+ }
+ }
+ }
+
+ return $schemaFiles;
+ }
+
+ public function importData()
+ {
+ // Already imported during previous install
+ //$this->runQueriesFromFile($this->getKernelSQLFileForDBMS('cleandata.sql'));
+ }
+
+ /**
+ * @param \Doctrine\DBAL\Schema\Schema $newSchema
+ * @param \Doctrine\DBAL\Platforms\AbstractPlatform $databasePlatform
+ *
+ * @return string[]
+ */
+ protected function getDropSqlStatementsForExistingSchema(
+ Schema $newSchema,
+ AbstractPlatform $databasePlatform
+ ): array {
+ $existingSchema = $this->db->getSchemaManager()->createSchema();
+ $statements = [];
+ // reverse table order for clean-up (due to FKs)
+ $tables = array_reverse($newSchema->getTables());
+ // cleanup pre-existing database
+ foreach ($tables as $table) {
+ if ($existingSchema->hasTable($table->getName())) {
+ $statements[] = $databasePlatform->getDropTableSQL($table);
+ }
+ }
+
+ return $statements;
+ }
+
+ /**
+ * Handle optional import of binary files to var folder.
+ */
+ public function importBinaries()
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createConfiguration()
+ {
+ }
+}
+
+class_alias(CoreUpgrader::class, 'EzSystems\PlatformInstallerBundle\Installer\CoreUpgrader');
diff --git a/src/bundle/RepositoryInstaller/Resources/config/services.yml b/src/bundle/RepositoryInstaller/Resources/config/services.yml
index 965404750b..4fa5472a65 100644
--- a/src/bundle/RepositoryInstaller/Resources/config/services.yml
+++ b/src/bundle/RepositoryInstaller/Resources/config/services.yml
@@ -16,6 +16,14 @@ services:
tags:
- { name: ibexa.installer, type: ibexa-oss }
+ Ibexa\Bundle\RepositoryInstaller\Installer\CoreUpgrader:
+ autowire: true
+ parent: Ibexa\Bundle\RepositoryInstaller\Installer\DbBasedInstaller
+ #arguments:
+ # $defaultTableOptions: [] # '%ibexa.schema.default_table_options%'
+ tags:
+ - { name: ibexa.upgrader, type: ibexa-oss }
+
Ibexa\Bundle\RepositoryInstaller\Command\InstallPlatformCommand:
arguments:
$connection: '@ibexa.persistence.connection'
@@ -26,6 +34,16 @@ services:
tags:
- { name: console.command }
+ Ibexa\Bundle\RepositoryInstaller\Command\UpgradePlatformCommand:
+ arguments:
+ $connection: '@ibexa.persistence.connection'
+ $installers: []
+ $cachePool: '@ibexa.cache_pool'
+ $environment: "%kernel.environment%"
+ $repositoryConfigurationProvider: '@Ibexa\Bundle\Core\ApiLoader\RepositoryConfigurationProvider'
+ tags:
+ - { name: console.command }
+
Ibexa\Bundle\RepositoryInstaller\Command\ValidatePasswordHashesCommand:
arguments:
$userStorage: '@Ibexa\Core\FieldType\User\UserStorage'