diff --git a/docs/en/installation.md b/docs/en/installation.md index 1fbb4e83..29a2c5e2 100644 --- a/docs/en/installation.md +++ b/docs/en/installation.md @@ -7,7 +7,7 @@ with composer. composer require tractorcow/silverstripe-fluent ^5 ``` - * Run a `dev/build` to ensure all additional table fields have been generated + * Run `sake db:build --flush` to ensure all additional table fields have been generated * Configure your locales in the `/admin/locales` section * Publish pages in each of the locales you want them to be visible in diff --git a/docs/en/migrating-from-single-language.md b/docs/en/migrating-from-single-language.md index 5137e43d..f5d5f723 100644 --- a/docs/en/migrating-from-single-language.md +++ b/docs/en/migrating-from-single-language.md @@ -33,7 +33,7 @@ TractorCow\Fluent\Model\Locale: URLSegment: en ``` -When you run `dev/build?flush` again, this adds the records to the database if the locales table is still empty. +When you run `sake db:build --flush` again, this adds the records to the database if the locales table is still empty. ## Populating initial localised content for existing Pages and DataObjects in your default locale @@ -63,32 +63,33 @@ publish your `Versioned` data (including Pages) from the commandline or queued a 1. Example: Localise all Pages (default, without publishing) - ``` - dev/tasks/initial-page-localisation-task + ```sh + sake tasks:initial-page-localisation-task ``` 2. Example: Localise & publish all Pages - ``` - dev/tasks/initial-page-localisation-task publish=1 + ```sh + sake tasks:initial-page-localisation-task --publish ``` 3. Example: Localising Pages in batches can be done by using the `limit` option. This will localise & publish five pages on each run. - ``` - dev/tasks/initial-page-localisation-task publish=1&limit=5 + ```sh + sake tasks:initial-page-localisation-task --publish --limit=5 ``` 4. Example: All the same functionality is available for localising all DataObjects, including `Versioned` and non-Versioned classes + ```sh + sake tasks:initial-dataobject-localisation-task ``` - dev/tasks/initial-dataobject-localisation-task - ``` + or - ``` - dev/tasks/initial-dataobject-localisation-task publish=1&limit=5 + ```sh + sake tasks:initial-dataobject-localisation-task --publish --limit=5 ``` #### Customize your own initialisation dev task diff --git a/src/Task/ConvertTranslatableTask.php b/src/Task/ConvertTranslatableTask.php deleted file mode 100644 index 0542b49b..00000000 --- a/src/Task/ConvertTranslatableTask.php +++ /dev/null @@ -1,222 +0,0 @@ - Fluent Task"; - - protected $description = "Migrates site DB from SS3 Translatable DB format to SS4 Fluent."; - - private static $segment = 'ConvertTranslatableTask'; - - public function __construct() - { - parent::__construct(); - Deprecation::withSuppressedNotice(function () { - Deprecation::notice( - '7.3.0', - 'Will be removed without equivalent functionality to replace it', - Deprecation::SCOPE_CLASS - ); - }); - } - - /** - * Checks that fluent is configured correctly - * - * @throws ConvertTranslatableTask\Exception - */ - protected function checkInstalled() - { - // Assert that fluent is configured - $locales = Locale::getLocales(); - if (empty($locales)) { - throw new Exception("Please configure Fluent locales (in the CMS) prior to migrating from translatable"); - } - - $defaultLocale = Locale::getDefault(); - if (empty($defaultLocale)) { - throw new Exception( - "Please configure a Fluent default locale (in the CMS) prior to migrating from translatable" - ); - } - } - - /** - * Gets all classes with FluentExtension - * - * @return array Array of classes to migrate - */ - public function fluentClasses() - { - $classes = []; - $dataClasses = ClassInfo::subclassesFor(DataObject::class); - array_shift($dataClasses); - foreach ($dataClasses as $class) { - $base = DataObject::getSchema()->baseDataClass($class); - foreach (DataObject::get_extensions($base) as $extension) { - if (is_a($extension, FluentExtension::class, true)) { - $classes[] = $base; - break; - } - } - } - return array_unique($classes); - } - - public function run($request) - { - $this->checkInstalled(); - - // we may need some privileges for this to work - // without this, running under sake is a problem - // maybe sake could take care of it ... - Member::actAs( - DefaultAdminService::singleton()->findOrCreateDefaultAdmin(), - function () { - DB::get_conn()->withTransaction(function () { - Versioned::set_stage(Versioned::DRAFT); - $classes = $this->fluentClasses(); - $tables = DB::get_schema()->tableList(); - if (empty($classes)) { - Debug::message('No classes have Fluent enabled, so skipping.', false); - } - - foreach ($classes as $class) { - /** @var DataObject $class */ - - // Ensure that a translationgroup table exists for this class - $baseTable = DataObject::getSchema()->baseDataTable($class); - $groupTable = strtolower($baseTable . "_translationgroups"); - if (isset($tables[$groupTable])) { - $groupTable = $tables[$groupTable]; - } else { - Debug::message("Ignoring class without _translationgroups table $class", false); - continue; - } - - // Disable filter if it has been applied to the class - if (singleton($class)->hasMethod('has_extension') - && $class::has_extension(FluentFilteredExtension::class) - ) { - $class::remove_extension(FluentFilteredExtension::class); - } - - // Select all instances of this class in the base table, where the Locale field is not null. - // Translatable has a Locale column on the base table in SS3, but Fluent doesn't use it. Newly - // created records via SS4 Fluent will not set this column, but will set it in {$baseTable}_Localised - $instances = DataObject::get($class, sprintf( - '"%s"."Locale" IS NOT NULL', - $baseTable - )); - - foreach ($instances as $instance) { - // Get the Locale column directly from the base table, since the SS ORM will not include it - $instanceLocale = SQLSelect::create() - ->setFrom("\"{$baseTable}\"") - ->setSelect('"Locale"') - ->setWhere(["\"{$baseTable}\".\"ID\"" => $instance->ID]) - ->execute() - ->first(); - - // Ensure that we got the Locale out of the base table before continuing - if (empty($instanceLocale['Locale'])) { - Debug::message("Skipping {$instance->Title} with ID {$instance->ID} - couldn't find Locale"); - continue; - } - $instanceLocale = $instanceLocale['Locale']; - - // Check for obsolete classes that don't need to be handled any more - if ($instance->ObsoleteClassName) { - Debug::message( - "Skipping {$instance->ClassName} with ID {$instance->ID} because it from an obsolete class", - false - ); - continue; - } - - Debug::message( - "Updating {$instance->ClassName} {$instance->Title} ({$instance->ID}) with locale {$instanceLocale}", - false - ); - - FluentState::singleton() - ->withState(function (FluentState $state) use ($instance, $instanceLocale) { - // Use Fluent's ORM to write and/or publish the record into the correct locale - // from Translatable - $state->setLocale($instanceLocale); - - if (!$this->isPublished($instance)) { - $instance->write(); - Debug::message(" -- Saved to draft", false); - } elseif ($instance->publishRecursive() === false) { - Debug::message(" -- Publishing FAILED", false); - throw new Exception("Failed to publish"); - } else { - Debug::message(" -- Published", false); - } - }); - } - - // Drop the "Locale" column from the base table - Debug::message('Dropping "Locale" column from ' . $baseTable, false); - DB::query(sprintf('ALTER TABLE "%s" DROP COLUMN "Locale"', $baseTable)); - - // Drop the "_translationgroups" translatable table - Debug::message('Deleting Translatable table ' . $groupTable, false); - DB::query(sprintf('DROP TABLE IF EXISTS "%s"', $groupTable)); - } - }); - } - ); - } - - /** - * Determine whether the record has been published previously/is currently published - * - * @param DataObject $instance - * @return bool - */ - protected function isPublished(DataObject $instance) - { - $isPublished = false; - if ($instance->hasMethod('isPublished')) { - $isPublished = $instance->isPublished(); - } - return $isPublished; - } -} diff --git a/src/Task/ConvertTranslatableTask/Exception.php b/src/Task/ConvertTranslatableTask/Exception.php deleted file mode 100644 index 08956cf0..00000000 --- a/src/Task/ConvertTranslatableTask/Exception.php +++ /dev/null @@ -1,23 +0,0 @@ -' . PHP_EOL; - } + $output->writeForHtml('
');
 
-        $publish = (bool)$request->getVar('publish');
-        $limit = (int)$request->getVar('limit');
+        $publish = $input->getOption('publish');
+        $limit = (int)$input->getOption('limit');
 
         $total_results = [
             'localisable' => 0,
@@ -79,16 +66,16 @@ public function run($request)
             ->first();
 
         if (!$globalLocale) {
-            echo 'Please set global locale first!' . PHP_EOL;
-
-            return;
+            $output->writeln('Please set global locale first!');
+            $output->writeForHtml('
'); + return Command::INVALID; } if ($this->include_only_classes && is_array($this->include_only_classes)) { $classesWithFluent = $this->include_only_classes; foreach ($this->include_only_classes as $key => $dataClass) { if (!$this->isClassNamePermitted($dataClass)) { - echo sprintf('ERROR: `%s` does not have FluentExtension installed. Continuing without it...', $dataClass) . PHP_EOL; + $output->writeln(sprintf('ERROR: `%s` does not have FluentExtension installed. Continuing without it...', $dataClass)); unset($classesWithFluent[$key]); } } @@ -107,26 +94,33 @@ public function run($request) $total_results[$key] += $value; } - echo sprintf('Processing %s objects...', $classWithFluent) . PHP_EOL; - echo sprintf('└─ Localised %d of %d objects.', $results['localised'], $results['localisable']) . PHP_EOL; + $output->writeln(sprintf('Processing %s objects...', $classWithFluent)); + $output->writeln(sprintf('└─ Localised %d of %d objects.', $results['localised'], $results['localisable'])); if ($results['publishable']) { - echo sprintf('└─ Published %d of %d objects.', $results['published'], $results['publishable']) . PHP_EOL; + $output->writeln(sprintf('└─ Published %d of %d objects.', $results['published'], $results['publishable'])); } } - echo PHP_EOL; - echo sprintf('Completed %d classes.', count($classesWithFluent)) . PHP_EOL; - echo sprintf('└─ Localised %d of %d objects in total.', $total_results['localised'], $total_results['localisable']) . PHP_EOL; - echo PHP_EOL; + $output->writeln(''); + $output->writeln(sprintf('Completed %d classes.', count($classesWithFluent))); + $output->writeln(sprintf('└─ Localised %d of %d objects in total.', $total_results['localised'], $total_results['localisable'])); + $output->writeln(''); if ($total_results['publishable']) { - echo sprintf('└─ Published %d of %d objects in total.', $total_results['published'], $total_results['publishable']) . PHP_EOL; - echo PHP_EOL; + $output->writeln(sprintf('└─ Published %d of %d objects in total.', $total_results['published'], $total_results['publishable'])); + $output->writeln(''); } - if (!Director::is_cli()) { - echo ''; - } + $output->writeForHtml('
');
+        return Command::SUCCESS;
+    }
+
+    public function getOptions(): array
+    {
+        return [
+            new InputOption('publish', null, InputOption::VALUE_NONE, 'Publish pages after localising (if they were published beforehand)'),
+            new InputOption('limit', null, InputOption::VALUE_REQUIRED, 'Maximum number of records to localise at once', 1),
+        ];
     }
 
     /**
@@ -284,4 +278,17 @@ protected function isClassNamePermitted(string $className): bool
 
         return $dataObject->hasExtension(FluentExtension::class);
     }
+
+    public static function getHelp(): string
+    {
+        $isCli = Director::is_cli();
+        $limit = $isCli ? '--limit=N' : 'limit=N';
+        $publish = $isCli ? '--publish' : 'publish=1';
+        return <<$limit to limit number of records to localise. Pass $publish to enable publishing of localised Versioned DataObjects.
+        Regardless, Versioned DataObjects which were not already published will not be published, only localised. DataObjects which were already localised will always be skipped.
+        This class may be extended to create custom initialization tasks targeting or excluding specific classes.
+        TXT;
+    }
 }
diff --git a/src/Task/InitialPageLocalisationTask.php b/src/Task/InitialPageLocalisationTask.php
index d39bd7bc..24ca9668 100644
--- a/src/Task/InitialPageLocalisationTask.php
+++ b/src/Task/InitialPageLocalisationTask.php
@@ -3,26 +3,15 @@
 namespace TractorCow\Fluent\Task;
 
 use SilverStripe\CMS\Model\SiteTree;
+use SilverStripe\Control\Director;
 
 class InitialPageLocalisationTask extends InitialDataObjectLocalisationTask
 {
-    /**
-     * @var string
-     */
-    private static $segment = 'initial-page-localisation-task';
+    protected static string $commandName = 'initial-page-localisation-task';
 
-    /**
-     * @var string
-     */
-    protected $title = 'Initial SiteTree localisation';
+    protected string $title = 'Initial SiteTree localisation';
 
-    /**
-     * @var string
-     */
-    protected $description = 'Intended for projects which already have some Pages when Fluent module is added.' .
-    ' This dev task will localise / publish all Pages in the default locale. Locale setup has to be done before running this task.' .
-    ' Pass limit=N to limit number of records to localise. Pass publish=1 to force publishing of localised Pages.' .
-    ' Regardless, Pages which were not already published will not be published, only localised. Pages which were already localised will always be skipped.';
+    protected static string $description = 'Intended for projects which already have some Pages when Fluent module is added';
 
     /**
      * @var string[]
@@ -44,4 +33,16 @@ function isEnabled(): bool
     {
         return class_exists(SiteTree::class) && parent::isEnabled();
     }
+
+    public static function getHelp(): string
+    {
+        $isCli = Director::is_cli();
+        $limit = $isCli ? '--limit=N' : 'limit=N';
+        $publish = $isCli ? '--publish' : 'publish=1';
+        return <<$limit to limit number of records to localise. Pass $publish to force publishing of localised Pages.
+        Regardless, Pages which were not already published will not be published, only localised. Pages which were already localised will always be skipped.
+        TXT;
+    }
 }
diff --git a/tests/php/Task/InitialPageLocalisationTaskTest.php b/tests/php/Task/InitialPageLocalisationTaskTest.php
index bdb121f4..b54a01d9 100644
--- a/tests/php/Task/InitialPageLocalisationTaskTest.php
+++ b/tests/php/Task/InitialPageLocalisationTaskTest.php
@@ -3,11 +3,11 @@
 namespace TractorCow\Fluent\Tests\Task;
 
 use SilverStripe\CMS\Model\SiteTree;
-use SilverStripe\Control\HTTPRequest;
 use SilverStripe\Dev\SapphireTest;
-use SilverStripe\Core\Validation\ValidationException;
+use SilverStripe\PolyExecution\PolyOutput;
+use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Output\BufferedOutput;
 use TractorCow\Fluent\Extension\FluentSiteTreeExtension;
-use TractorCow\Fluent\Model\Locale;
 use TractorCow\Fluent\State\FluentState;
 use TractorCow\Fluent\Task\InitialPageLocalisationTask;
 use PHPUnit\Framework\Attributes\DataProvider;
@@ -75,7 +75,12 @@ public function testInitialPageLocalisation(bool $publish, int $limit, array $lo
         ];
 
         // Localise pages
-        InitialPageLocalisationTask::singleton()->run(new HTTPRequest('GET', '/', $getParams));
+        $task = InitialPageLocalisationTask::singleton();
+        $buffer = new BufferedOutput();
+        $output = new PolyOutput(PolyOutput::FORMAT_ANSI, wrappedOutput: $buffer);
+        $input = new ArrayInput($getParams);
+        $input->setInteractive(false);
+        $task->run($input, $output);
 
         // Check localised records (should have all pages now)
         $pages = $this->getLocalisedPages();