diff --git a/src/Task/InitialDataObjectLocalisationTask.php b/src/Task/InitialDataObjectLocalisationTask.php new file mode 100644 index 00000000..5e2d206b --- /dev/null +++ b/src/Task/InitialDataObjectLocalisationTask.php @@ -0,0 +1,284 @@ +exclude_classes`. + * @var string[] + */ + protected array $include_only_classes = []; + + /** + * When extending this class, you may choose to exclude these specific classes. + * This is IGNORED if `$this->include_only_classes` is not empty. + * @var string[] + */ + protected array $exclude_classes = [ + SiteTree::class + ]; + + /** + * @param HTTPRequest $request + * @return void + * @throws \ReflectionException + * @throws \SilverStripe\ORM\ValidationException + */ + public function run($request) + { + if (!Director::is_cli()) { + echo '
' . PHP_EOL;
+        }
+
+        $publish = (bool)$request->getVar('publish');
+        $limit = (int)$request->getVar('limit');
+
+        $total_results = [
+            'localisable' => 0,
+            'localised' => 0,
+            'publishable' => 0,
+            'published' => 0,
+        ];
+
+        /** @var Locale $globalLocale */
+        $globalLocale = Locale::get()
+            ->filter(['IsGlobalDefault' => 1])
+            ->sort('ID', 'ASC')
+            ->first();
+
+        if (!$globalLocale) {
+            echo 'Please set global locale first!' . PHP_EOL;
+
+            return;
+        }
+
+        if ($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;
+                    unset($classesWithFluent[$key]);
+                }
+            }
+        } else {
+            $dataClasses = static::getDirectSubclassesRecursivelyFor(DataObject::class);
+            $classesWithFluent = $this->filterPermittedClassesRecursively($dataClasses);
+        }
+
+        foreach ($classesWithFluent as $classWithFluent) {
+            if (!$this->isClassNamePermitted($classWithFluent)) {
+                continue;
+            }
+
+            $results = $this->doLocaliseClass($classWithFluent, $globalLocale, $limit, $publish);
+            foreach ($results as $key => $value) {
+                $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;
+            if ($results['publishable']) {
+                echo sprintf('└─ Published %d of %d objects.', $results['published'], $results['publishable']) . PHP_EOL;
+            }
+        }
+
+        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;
+
+        if ($total_results['publishable']) {
+            echo sprintf('└─ Published %d of %d objects in total.', $total_results['published'], $total_results['publishable']) . PHP_EOL;
+            echo PHP_EOL;
+        }
+
+        if (!Director::is_cli()) {
+            echo '
'; + } + } + + /** + * @param $className + * @param $globalLocale + * @param $limit + * @param $publish + * @return array{localisable: int, localised: int, publishable: int, published: int} + * @throws \SilverStripe\ORM\ValidationException + */ + protected function doLocaliseClass($className, $globalLocale, $limit, $publish): array + { + $dataObjectIDs = FluentState::singleton()->withState(static function (FluentState $state) use ($className, $limit): array { + $state->setLocale(null); + $dataObjects = $className::get()->sort('ID', 'ASC'); + + if ($limit > 0) { + $dataObjects = $dataObjects->limit($limit); + } + + return $dataObjects->column('ID'); + }); + + return FluentState::singleton()->withState( + static function (FluentState $state) use ($className, $globalLocale, $publish, $dataObjectIDs): array { + $state->setLocale($globalLocale->Locale); + $return = [ + 'localisable' => 0, + 'localised' => 0, + 'publishable' => 0, + 'published' => 0, + ]; + + foreach ($dataObjectIDs as $dataObjectID) { + /** @var DataObject|FluentExtension $dataObject */ + $dataObject = $className::get()->byID($dataObjectID); + $return['localisable'] += 1; + + if (!$dataObject->hasExtension(FluentVersionedExtension::class)) { + if ($dataObject->existsInLocale()) { + continue; + } + $dataObject->write(); + $return['localised'] += 1; + continue; + } + + // We have versioned data, so start tracking how many have been published + $return['publishable'] += 1; + + /** @var DataObject|Versioned|FluentVersionedExtension $dataObject */ + if ($dataObject->isDraftedInLocale()) { + continue; + } + $dataObject->writeToStage(Versioned::DRAFT); + + $return['localised'] += 1; + + if (!$publish) { + continue; + } + + // Check if the base record was published - if not then we don't need to publish + // as this would leak draft content, we only want to publish pages which were published + // before Fluent module was added + $dataObjectID = $dataObject->ID; + $isBaseRecordPublished = FluentState::singleton()->withState( + static function (FluentState $state) use ($className, $dataObjectID): bool { + $state->setLocale(null); + $page = $className::get_by_id($dataObjectID); + + if ($page === null) { + return false; + } + + return $page->isPublished(); + } + ); + + if (!$isBaseRecordPublished) { + continue; + } + + $dataObject->publishRecursive(); + $return['published'] += 1; + } + + return $return; + } + ); + } + + /** + * @param string $className + * @return array[] + * @throws \ReflectionException + */ + protected static function getDirectSubclassesRecursivelyFor(string $className): array + { + $directSubclasses = []; + foreach (ClassInfo::subclassesFor($className, false) as $subclassName) { + $actualParentClass = get_parent_class($subclassName); + if ($className === $actualParentClass) { + $directSubclasses[$subclassName] = static::getDirectSubclassesRecursivelyFor($subclassName); + } + } + return $directSubclasses; + } + + /** + * @param array $classes + * @return array + */ + protected function filterPermittedClassesRecursively(array $classes): array + { + $permittedClasses = []; + foreach ($classes as $parentClassName => $subclassNames) { + if ($this->isClassNamePermitted($parentClassName)) { + $permittedClasses[] = $parentClassName; + // We will skip all subclasses since the ORM will automatically + // pull them in when this parent is referenced + continue; + } + + $permittedClasses = array_merge($permittedClasses, $this->filterPermittedClassesRecursively($subclassNames)); + } + return $permittedClasses; + } + + /** + * @param string $className + * @return bool + */ + protected function isClassNamePermitted(string $className): bool + { + // Do a simple (inexpensive) text comparison against the exclusion list before we create an object + if (!$this->include_only_classes && in_array($className, $this->exclude_classes)) { + return false; + } + + /** @var DataObject $dataObject */ + $dataObject = singleton($className); + + // Now we'll do a full comparison against the exclusion list + // This important step will, for example, match (refuse) a BlogPost if Page is listed as excluded + foreach ($this->exclude_classes as $excluded_class) { + if ($dataObject instanceof $excluded_class) { + return false; + } + } + + return $dataObject->hasExtension(FluentExtension::class); + } + +} diff --git a/src/Task/InitialPageLocalisationTask.php b/src/Task/InitialPageLocalisationTask.php index 07724630..408f2a23 100644 --- a/src/Task/InitialPageLocalisationTask.php +++ b/src/Task/InitialPageLocalisationTask.php @@ -3,14 +3,8 @@ namespace TractorCow\Fluent\Task; use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\Control\HTTPRequest; -use SilverStripe\Dev\BuildTask; -use SilverStripe\Versioned\Versioned; -use TractorCow\Fluent\Extension\FluentSiteTreeExtension; -use TractorCow\Fluent\Model\Locale; -use TractorCow\Fluent\State\FluentState; -class InitialPageLocalisationTask extends BuildTask +class InitialPageLocalisationTask extends InitialDataObjectLocalisationTask { /** * @var string @@ -20,94 +14,35 @@ class InitialPageLocalisationTask extends BuildTask /** * @var string */ - protected $title = 'Initial page localisation'; + protected $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.' . - ' Pages which are not published will not be published, only localised. Pages which are already localised will be skipped.'; + 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.'; /** - * @param HTTPRequest $request + * @var string[] */ - public function run($request) - { - $publish = (bool) $request->getVar('publish'); - $limit = (int) $request->getVar('limit'); - - /** @var Locale $globalLocale */ - $globalLocale = Locale::get() - ->filter(['IsGlobalDefault' => 1]) - ->sort('ID', 'ASC') - ->first(); - - if (!$globalLocale) { - echo 'Please set global locale first!' . PHP_EOL; - - return; - } - - $pageIds = FluentState::singleton()->withState(static function (FluentState $state) use ($limit): array { - $state->setLocale(null); - $pages = SiteTree::get()->sort('ID', 'ASC'); - - if ($limit > 0) { - $pages = $pages->limit($limit); - } - - return $pages->column('ID'); - }); - - $localised = FluentState::singleton()->withState( - static function (FluentState $state) use ($globalLocale, $pageIds, $publish): int { - $state->setLocale($globalLocale->Locale); - $localised = 0; - - foreach ($pageIds as $pageId) { - /** @var SiteTree|FluentSiteTreeExtension $page */ - $page = SiteTree::get()->byID($pageId); - - if ($page->isDraftedInLocale()) { - continue; - } - - $page->writeToStage(Versioned::DRAFT); - $localised += 1; - - if (!$publish) { - continue; - } + protected array $include_only_classes = [ + SiteTree::class + ]; - // Check if the base record was published - if not then we don't need to publish - // as this would leak draft content, we only want to publish pages which were published - // before Fluent module was added - $pageId = $page->ID; - $isBaseRecordPublished = FluentState::singleton()->withState( - static function (FluentState $state) use ($pageId): bool { - $state->setLocale(null); - $page = SiteTree::get_by_id($pageId); - - if ($page === null) { - return false; - } - - return $page->isPublished(); - } - ); - - if (!$isBaseRecordPublished) { - continue; - } - - $page->publishRecursive(); - } - - return $localised; - } - ); + /** + * @var string[] + */ + protected array $exclude_classes = []; - echo sprintf('Localised %d pages.', $localised) . PHP_EOL; + /** + * Soft dependency on CMS module + * @return bool + */ + function isEnabled(): bool + { + return class_exists(SiteTree::class) && parent::isEnabled(); } + }