From 23c37d9e371ce0b44a07eb1720e58ca3b7158ba8 Mon Sep 17 00:00:00 2001 From: "Nathan J. Brauer" Date: Tue, 26 Mar 2024 21:22:15 -0700 Subject: [PATCH] NEW Added dev/task to initialise DataObjects for existing datasets New task is based on the original task built for SiteTree but is now extendable for any DataObject enabled with Fluent. To prevent duplicate code, the original task now extends the new task. --- .../InitialDataObjectLocalisationTask.php | 284 ++++++++++++++++++ src/Task/InitialPageLocalisationTask.php | 109 ++----- 2 files changed, 306 insertions(+), 87 deletions(-) create mode 100644 src/Task/InitialDataObjectLocalisationTask.php 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(); } + }