diff --git a/packages/admin/src/Charcoal/Admin/Action/Object/RevertRevisionAction.php b/packages/admin/src/Charcoal/Admin/Action/Object/RevertRevisionAction.php index 3b320b310..0f8b85f69 100644 --- a/packages/admin/src/Charcoal/Admin/Action/Object/RevertRevisionAction.php +++ b/packages/admin/src/Charcoal/Admin/Action/Object/RevertRevisionAction.php @@ -2,14 +2,15 @@ namespace Charcoal\Admin\Action\Object; +use Charcoal\Object\RevisionsManager; use Exception; use InvalidArgumentException; // From PSR-7 +use Pimple\Container; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; // From 'charcoal-object' use Charcoal\Object\ObjectRevisionInterface; -use Charcoal\Object\RevisionableInterface; // From 'charcoal-admin' use Charcoal\Admin\AdminAction; use Charcoal\Admin\Ui\ObjectContainerInterface; @@ -45,6 +46,15 @@ class RevertRevisionAction extends AdminAction implements ObjectContainerInterfa */ protected $revNum; + private RevisionsManager $revisionService; + + protected function setDependencies(Container $container) + { + parent::setDependencies($container); + + $this->revisionService = $container->get('revisions/manager'); + } + /** * Retrieve the list of parameters to extract from the HTTP request. * @@ -62,9 +72,9 @@ protected function validDataFromRequest() /** * Set the revision number to restore. * - * @param integer $revNum The revision number to load. - * @throws InvalidArgumentException If the given revision is invalid. + * @param integer $revNum The revision number to load. * @return ObjectContainerInterface Chainable + * @throws InvalidArgumentException If the given revision is invalid. */ protected function setRevNum($revNum) { @@ -91,8 +101,8 @@ public function revNum() } /** - * @param RequestInterface $request A PSR-7 compatible Request instance. - * @param ResponseInterface $response A PSR-7 compatible Response instance. + * @param RequestInterface $request A PSR-7 compatible Request instance. + * @param ResponseInterface $response A PSR-7 compatible Response instance. * @return ResponseInterface */ public function run(RequestInterface $request, ResponseInterface $response) @@ -107,19 +117,11 @@ public function run(RequestInterface $request, ResponseInterface $response) '{{ errorMessage }}' => $failMessage ]); - $obj = $this->obj(); - if (!($obj instanceof RevisionableInterface)) { - $this->setSuccess(false); - - $this->addFeedback('error', strtr('{{ model }} does not support revisions', [ - '{{ model }}' => $this->getSingularLabelFromObj($obj), - ])); - - return $response->withStatus(400); - } - + $obj = $this->obj(); $revNum = $this->revNum(); - $revision = $obj->revisionNum($revNum); + $this->revisionService->setModel($obj); + + $revision = $this->revisionService->revisionFromNumber($revNum); if (!$revision['id']) { $this->setSuccess(false); @@ -139,7 +141,7 @@ public function run(RequestInterface $request, ResponseInterface $response) return $response->withStatus(404); } - $result = $obj->revertToRevision($revNum); + $result = $this->revisionService->revertToRevision($revNum); if ($result) { $doneMessage = $translator->translate( diff --git a/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsInterface.php b/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsInterface.php index 846efab13..cb5262de4 100644 --- a/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsInterface.php +++ b/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsInterface.php @@ -12,5 +12,5 @@ interface ObjectRevisionsInterface /** * @return \Charcoal\Object\ObjectRevisionInterface[] */ - public function objectRevisions(); + public function objectRevisions(): array; } diff --git a/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsTrait.php b/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsTrait.php index f30af4628..45b33dfa5 100644 --- a/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsTrait.php +++ b/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsTrait.php @@ -6,6 +6,7 @@ use Charcoal\Factory\FactoryInterface; // From 'charcoal-object' use Charcoal\Object\ObjectRevisionInterface; +use Charcoal\Object\RevisionsManager; /** * An implementation, as Trait, of the {@see \Charcoal\Admin\Ui\ObjectRevisionsInterface}. @@ -15,7 +16,7 @@ trait ObjectRevisionsTrait /** * @return ObjectRevisionInterface[] */ - public function objectRevisions() + public function objectRevisions(): array { if (!$this->objType() || !$this->objId()) { return []; @@ -24,7 +25,9 @@ public function objectRevisions() $obj = $this->modelFactory()->create($this->objType()); $obj->setId($this->objId()); - $lastRevision = $obj->latestRevision(); + $this->revisionService()->setModel($obj); + + $lastRevision = $this->revisionService()->latestRevision(); $propLabel = '%2$s'; $callback = function (ObjectRevisionInterface &$revision) use ($lastRevision, $obj, $propLabel) { @@ -67,7 +70,7 @@ public function objectRevisions() $revision->allowRevert = ($lastRevision['revNum'] !== $revision['revNum']); }; - return $obj->allRevisions($callback); + return $this->revisionService()->allRevisions($callback); } /** @@ -88,4 +91,6 @@ abstract public function objId(); * @return FactoryInterface */ abstract protected function modelFactory(); + + abstract protected function revisionService(): RevisionsManager; } diff --git a/packages/admin/src/Charcoal/Admin/Widget/FormGroup/ObjectRevisionsFormGroup.php b/packages/admin/src/Charcoal/Admin/Widget/FormGroup/ObjectRevisionsFormGroup.php index 526d6d514..55bc1a978 100644 --- a/packages/admin/src/Charcoal/Admin/Widget/FormGroup/ObjectRevisionsFormGroup.php +++ b/packages/admin/src/Charcoal/Admin/Widget/FormGroup/ObjectRevisionsFormGroup.php @@ -3,6 +3,7 @@ namespace Charcoal\Admin\Widget\FormGroup; // From 'pimple/pimple' +use Charcoal\Object\RevisionsManager; use Pimple\Container; // From 'charcoal-core' use Charcoal\Model\ModelFactoryTrait; @@ -37,6 +38,12 @@ class ObjectRevisionsFormGroup extends AbstractFormGroup implements */ public $widgetId; + private RevisionsManager $revisionService; + + protected function revisionService(): RevisionsManager + { + return $this->revisionService; + } /** * @param string $widgetId The widget identifier. @@ -110,6 +117,7 @@ protected function setDependencies(Container $container) parent::setDependencies($container); $this->setModelFactory($container['model/factory']); + $this->revisionService = $container['revisions/manager']; $this->objType = $container['request']->getParam('obj_type'); $this->objId = $container['request']->getParam('obj_id'); diff --git a/packages/admin/src/Charcoal/Admin/Widget/FormSidebarWidget.php b/packages/admin/src/Charcoal/Admin/Widget/FormSidebarWidget.php index d2189706b..407fb886b 100644 --- a/packages/admin/src/Charcoal/Admin/Widget/FormSidebarWidget.php +++ b/packages/admin/src/Charcoal/Admin/Widget/FormSidebarWidget.php @@ -2,7 +2,7 @@ namespace Charcoal\Admin\Widget; -use Charcoal\Object\RevisionableInterface; +use Charcoal\Object\RevisionsManager; use Charcoal\User\AuthAwareInterface; use InvalidArgumentException; // From Pimple @@ -163,6 +163,8 @@ class FormSidebarWidget extends AdminWidget implements */ private $requiredGlobalAclPermissions = []; + private RevisionsManager $revisionService; + /** * @param array|ArrayInterface $data Class data. * @return FormSidebarWidget Chainable @@ -552,8 +554,10 @@ public function isObjRevisionable() return $this->isObjRevisionable; } - if ($obj instanceof RevisionableInterface && $obj['revisionEnabled']) { - $this->isObjRevisionable = !!count($obj->allRevisions()); + $this->revisionService->setModel($obj); + + if ($this->revisionService->revisionEnabled()) { + $this->isObjRevisionable = !!count($this->revisionService->allRevisions()); } } } @@ -894,4 +898,11 @@ protected function isAssoc(array $array) return !!array_filter($array, 'is_string', ARRAY_FILTER_USE_KEY); } + + protected function setDependencies(Container $container) + { + parent::setDependencies($container); + + $this->revisionService = $container['revisions/manager']; + } } diff --git a/packages/admin/src/Charcoal/Admin/Widget/ObjectRevisionsWidget.php b/packages/admin/src/Charcoal/Admin/Widget/ObjectRevisionsWidget.php index cb482ded4..8b52e753e 100644 --- a/packages/admin/src/Charcoal/Admin/Widget/ObjectRevisionsWidget.php +++ b/packages/admin/src/Charcoal/Admin/Widget/ObjectRevisionsWidget.php @@ -8,6 +8,8 @@ use Charcoal\Admin\AdminWidget; use Charcoal\Admin\Ui\ObjectRevisionsInterface; use Charcoal\Admin\Ui\ObjectRevisionsTrait; +use Charcoal\Object\RevisionsManager; +use Pimple\Container; /** * Class ObjectRevisionWidget @@ -27,6 +29,20 @@ class ObjectRevisionsWidget extends AdminWidget implements */ protected $objId; + private RevisionsManager $revisionService; + + protected function setDependencies(Container $container) + { + parent::setDependencies($container); + + $this->revisionService = $container['revisions/manager']; + } + + protected function revisionService(): RevisionsManager + { + return $this->revisionService; + } + /** * @return boolean */ diff --git a/packages/app/composer.json b/packages/app/composer.json index b5ec134ba..5571589bf 100644 --- a/packages/app/composer.json +++ b/packages/app/composer.json @@ -32,6 +32,7 @@ "charcoal/config": "^4.1", "charcoal/event": "^3.2", "charcoal/factory": "^4.1", + "charcoal/object": "^4.1", "charcoal/translator": "^4.1", "charcoal/view": "^4.1", "monolog/monolog": "^1.17", diff --git a/packages/app/src/Charcoal/App/ServiceProvider/AppServiceProvider.php b/packages/app/src/Charcoal/App/ServiceProvider/AppServiceProvider.php index 3859433ac..84ee46cd7 100644 --- a/packages/app/src/Charcoal/App/ServiceProvider/AppServiceProvider.php +++ b/packages/app/src/Charcoal/App/ServiceProvider/AppServiceProvider.php @@ -38,6 +38,7 @@ use Charcoal\App\Template\TemplateInterface; use Charcoal\App\Template\WidgetInterface; use Charcoal\App\Template\WidgetBuilder; +use Charcoal\Object\RevisionServiceProvider; use Charcoal\View\Twig\DebugHelpers as TwigDebugHelpers; use Charcoal\View\Twig\HelpersInterface as TwigHelpersInterface; use Charcoal\View\Twig\UrlHelpers as TwigUrlHelpers; @@ -78,6 +79,7 @@ public function register(Container $container) $container->register(new ScriptServiceProvider()); $container->register(new TranslatorServiceProvider()); $container->register(new ViewServiceProvider()); + $container->register(new RevisionServiceProvider()); $this->registerKernelServices($container); $this->registerHandlerServices($container); diff --git a/packages/object/metadata/admin/charcoal/object/object-revision.json b/packages/object/metadata/admin/charcoal/object/object-revision.json index 14fd32a23..f5f2954b2 100644 --- a/packages/object/metadata/admin/charcoal/object/object-revision.json +++ b/packages/object/metadata/admin/charcoal/object/object-revision.json @@ -39,7 +39,7 @@ "form": { "type": "charcoal/admin/widget/object-form", "form_ident": "default", - "target_type": "charcoal/object/object-revision" + "obj_type": "charcoal/object/object-revision" } }, "layout": { @@ -57,7 +57,7 @@ "form": { "type": "charcoal/admin/widget/table", "collection_ident": "default", - "target_type": "charcoal/object/object-revision" + "obj_type": "charcoal/object/object-revision" } }, "layout": { diff --git a/packages/object/src/Charcoal/Object/Content.php b/packages/object/src/Charcoal/Object/Content.php index 32cf80941..2b53f1bf0 100644 --- a/packages/object/src/Charcoal/Object/Content.php +++ b/packages/object/src/Charcoal/Object/Content.php @@ -15,8 +15,6 @@ use Charcoal\Object\ContentInterface; use Charcoal\Object\AuthorableInterface; use Charcoal\Object\AuthorableTrait; -use Charcoal\Object\RevisionableInterface; -use Charcoal\Object\RevisionableTrait; use Charcoal\Object\TimestampableInterface; use Charcoal\Object\TimestampableTrait; @@ -26,11 +24,9 @@ class Content extends AbstractModel implements AuthorableInterface, ContentInterface, - RevisionableInterface, TimestampableInterface { use AuthorableTrait; - use RevisionableTrait; use TranslatorAwareTrait; use TimestampableTrait; @@ -197,11 +193,6 @@ protected function preUpdate(array $properties = null) { parent::preUpdate($properties); - // Content is revisionable - if ($this['revisionEnabled']) { - $this->generateRevision(); - } - // Timestampable propertiees $this->setLastModified('now'); diff --git a/packages/object/src/Charcoal/Object/GenerateRevisionListener.php b/packages/object/src/Charcoal/Object/GenerateRevisionListener.php new file mode 100644 index 000000000..7abed4340 --- /dev/null +++ b/packages/object/src/Charcoal/Object/GenerateRevisionListener.php @@ -0,0 +1,30 @@ +getObject(); + + $this->revisionService->setModel($model)->generateRevision(); + } + + public function setDependencies(Container $container) + { + parent::setDependencies($container); + + $this->revisionService = $container->get('revisions/manager'); + } +} diff --git a/packages/object/src/Charcoal/Object/ObjectRevision.php b/packages/object/src/Charcoal/Object/ObjectRevision.php index 4f847c572..675ec5bba 100644 --- a/packages/object/src/Charcoal/Object/ObjectRevision.php +++ b/packages/object/src/Charcoal/Object/ObjectRevision.php @@ -2,6 +2,7 @@ namespace Charcoal\Object; +use Charcoal\Model\ModelInterface; use InvalidArgumentException; use DateTime; use DateTimeInterface; @@ -14,7 +15,6 @@ use Charcoal\Model\ModelFactoryTrait; // From 'charcoal-object' use Charcoal\Object\ObjectRevisionInterface; -use Charcoal\Object\RevisionableInterface; /** * Represents the changeset of an object. @@ -291,10 +291,11 @@ public function getDataDiff() * 2. Load the current item from DB * 3. Create diff from (1) and (2). * - * @param RevisionableInterface $obj The object to create the revision from. + * @param ModelInterface $obj The object to create the revision from. + * @param array|null $properties List of properties to revision. * @return ObjectRevision Chainable */ - public function createFromObject(RevisionableInterface $obj) + public function createFromObject(ModelInterface $obj, ?array $properties = null) { $prevRev = $this->lastObjectRevision($obj); @@ -307,7 +308,7 @@ public function createFromObject(RevisionableInterface $obj) $this->setRevUser($obj['lastModifiedBy']); } - $this->setDataObj($obj->data()); + $this->setDataObj($obj->data($properties)); $this->setDataPrev($prevRev->getDataObj()); $diff = $this->createDiff(); @@ -382,10 +383,10 @@ public function recursiveDiff(array $array1, array $array2) /** * @todo Should return NULL if source does not exist. * - * @param RevisionableInterface $obj The object to load the last revision of. + * @param ModelInterface $obj The object to load the last revision of. * @return ObjectRevision The last revision for the give object. */ - public function lastObjectRevision(RevisionableInterface $obj) + public function lastObjectRevision(ModelInterface $obj) { $rev = $this->modelFactory()->create(self::class); @@ -410,11 +411,11 @@ public function lastObjectRevision(RevisionableInterface $obj) * * @todo Should return NULL if source does not exist. * - * @param RevisionableInterface $obj Target object. + * @param ModelInterface $obj Target object. * @param integer $revNum The revision number to load. * @return ObjectRevision */ - public function objectRevisionNum(RevisionableInterface $obj, $revNum) + public function objectRevisionNum(ModelInterface $obj, $revNum) { $rev = $this->modelFactory()->create(self::class); diff --git a/packages/object/src/Charcoal/Object/ObjectRevisionInterface.php b/packages/object/src/Charcoal/Object/ObjectRevisionInterface.php index 9206ee66c..349c18108 100644 --- a/packages/object/src/Charcoal/Object/ObjectRevisionInterface.php +++ b/packages/object/src/Charcoal/Object/ObjectRevisionInterface.php @@ -2,8 +2,10 @@ namespace Charcoal\Object; +use Charcoal\Model\ModelInterface; + /** - * Defines a changeset of an object implementing {@see \Charcoal\Object\RevisionableInterface}. + * Defines a changeset of an object implementing {@see \Charcoal\Object\ModelInterface}. * * {@see \Charcoal\Object\ObjectRevision} for a basic implementation. */ @@ -104,10 +106,11 @@ public function getDataDiff(); * 2. Load the current item from DB * 3. Create diff from (1) and (2). * - * @param RevisionableInterface $obj The object to create the revision from. + * @param ModelInterface $obj The object to create the revision from. + * @param array|null $properties List of properties to revision. * @return ObjectRevision Chainable */ - public function createFromObject(RevisionableInterface $obj); + public function createFromObject(ModelInterface $obj, ?array $properties = null); /** * @param array $dataPrev Optional. The previous revision data. @@ -126,17 +129,17 @@ public function createDiff(array $dataPrev, array $dataObj); public function recursiveDiff(array $array1, array $array2); /** - * @param RevisionableInterface $obj The object to load the last revision of. + * @param ModelInterface $obj The object to load the last revision of. * @return ObjectRevision The last revision for the give object. */ - public function lastObjectRevision(RevisionableInterface $obj); + public function lastObjectRevision(ModelInterface $obj); /** * Retrieve a specific object revision, by revision number. * - * @param RevisionableInterface $obj Target object. + * @param ModelInterface $obj Target object. * @param integer $revNum The revision number to load. * @return ObjectRevision */ - public function objectRevisionNum(RevisionableInterface $obj, $revNum); + public function objectRevisionNum(ModelInterface $obj, $revNum); } diff --git a/packages/object/src/Charcoal/Object/RevisionModelConfig.php b/packages/object/src/Charcoal/Object/RevisionModelConfig.php new file mode 100644 index 000000000..d7c1c41ac --- /dev/null +++ b/packages/object/src/Charcoal/Object/RevisionModelConfig.php @@ -0,0 +1,80 @@ +enabled; + } + + /** + * @return string[] + */ + public function getProperties(): array + { + return $this->properties; + } + + public function hasProperties(): bool + { + return !!count($this->properties); + } + + /** + * @return string[] + */ + public function getExcludedProperties(): array + { + return $this->excludedProperties; + } + + public function hasExcludedProperties(): bool + { + return !!count($this->excludedProperties); + } + + /** + * @return string[] + */ + public function getIncludedProperties(): array + { + return $this->includedProperties; + } + + public function hasIncludedProperties(): bool + { + return (bool)$this->includedProperties; + } + + /** + * @return class-string + */ + public function getRevisionClass(): string + { + return $this->revisionClass; + } +} diff --git a/packages/object/src/Charcoal/Object/RevisionServiceProvider.php b/packages/object/src/Charcoal/Object/RevisionServiceProvider.php new file mode 100644 index 000000000..38adc231c --- /dev/null +++ b/packages/object/src/Charcoal/Object/RevisionServiceProvider.php @@ -0,0 +1,39 @@ +get('revisions'); + + // If the config data is a boolean, it means we only want to affect the enabled state. + if (is_bool($configData)) { + $configData = [ + 'enabled' => $configData, + ]; + } + + return new RevisionsConfig($configData); + }; + + $pimple['revisions/manager'] = function (Container $pimple): RevisionsManager { + $services = new ServiceLocator($pimple, [ + 'revisions/config', + 'model/factory', + 'logger' + ]); + + return new RevisionsManager($services); + }; + } +} diff --git a/packages/object/src/Charcoal/Object/RevisionableInterface.php b/packages/object/src/Charcoal/Object/RevisionableInterface.php deleted file mode 100644 index 6b60497ed..000000000 --- a/packages/object/src/Charcoal/Object/RevisionableInterface.php +++ /dev/null @@ -1,62 +0,0 @@ -revisionEnabled = !!$enabled; - return $this; - } - - /** - * @return boolean - */ - public function getRevisionEnabled() - { - return $this->revisionEnabled; - } - - /** - * Create a revision collection loader. - * - * @return CollectionLoader - */ - public function createRevisionObjectCollectionLoader() - { - $loader = new CollectionLoader([ - 'logger' => $this->logger, - 'factory' => $this->modelFactory(), - 'model' => $this->getRevisionObjectPrototype(), - ]); - - return $loader; - } - - /** - * Create a revision object. - * - * @return ObjectRevisionInterface - */ - public function createRevisionObject() - { - $rev = $this->modelFactory()->create($this->getObjectRevisionClass()); - - return $rev; - } - - /** - * Retrieve the revision object prototype. - * - * @return ObjectRevisionInterface - */ - public function getRevisionObjectPrototype() - { - $proto = $this->modelFactory()->get($this->getObjectRevisionClass()); - - return $proto; - } - - /** - * Set the class name of the object revision model. - * - * @param string $className The class name of the object revision model. - * @throws InvalidArgumentException If the class name is not a string. - * @return AbstractPropertyDisplay Chainable - */ - protected function setObjectRevisionClass($className) - { - if (!is_string($className)) { - throw new InvalidArgumentException( - 'Route class name must be a string.' - ); - } - - $this->objectRevisionClass = $className; - return $this; - } - - /** - * Retrieve the class name of the object revision model. - * - * @return string - */ - public function getObjectRevisionClass() - { - return $this->objectRevisionClass; - } - - /** - * Alias of {@see self::getObjectRevisionClass()}. - * - * @return string - */ - public function objectRevisionClass() - { - return $this->getObjectRevisionClass(); - } - - /** - * @see \Charcoal\Object\ObjectRevision::create_fromObject() - * @return ObjectRevision - */ - public function generateRevision() - { - $rev = $this->createRevisionObject(); - - $rev->createFromObject($this); - if (!empty($rev->getDataDiff())) { - $rev->save(); - } - - return $rev; - } - - /** - * @see \Charcoal\Object\ObejctRevision::lastObjectRevision - * @return ObjectRevision - */ - public function latestRevision() - { - $rev = $this->createRevisionObject(); - $rev = $rev->lastObjectRevision($this); - - return $rev; - } - - /** - * @see \Charcoal\Object\ObejctRevision::objectRevisionNum() - * - * @todo Should return NULL if source does not exist. - * - * @param integer $revNum The revision number. - * @return ObjectRevision - */ - public function revisionNum($revNum) - { - $rev = $this->createRevisionObject(); - $rev = $rev->objectRevisionNum($this, intval($revNum)); - - return $rev; - } - - /** - * Retrieves all revisions for the current objet - * - * @param callable $callback Optional object callback. - * @return array - */ - public function allRevisions(callable $callback = null) - { - $loader = $this->createRevisionObjectCollectionLoader(); - $loader - ->addOrder('revTs', 'desc') - ->addFilters([ - [ - 'property' => 'targetType', - 'value' => $this->objType(), - ], - [ - 'property' => 'targetId', - 'value' => $this->id(), - ], - ]); - - if ($callback !== null) { - $loader->setCallback($callback); - } - - $revisions = $loader->load(); - return $revisions->objects(); - } - - /** - * @param integer $revNum The revision number to revert to. - * @throws InvalidArgumentException If revision number is invalid. - * @return boolean Success / Failure. - */ - public function revertToRevision($revNum) - { - if (!$revNum) { - throw new InvalidArgumentException( - 'Invalid revision number' - ); - } - - $rev = $this->revisionNum(intval($revNum)); - - if (!$rev->id()) { - return false; - } - - if (isset($obj['lastModifiedBy'])) { - $obj['lastModifiedBy'] = $rev->getRevUser(); - } - - $this->setData($rev->getDataObj()); - $this->update(); - - return true; - } - - /** - * Retrieve the object model factory. - * - * @return \Charcoal\Factory\FactoryInterface - */ - abstract public function modelFactory(); - - /** - * @return \Charcoal\Model\MetadataInterface - */ - abstract public function metadata(); -} diff --git a/packages/object/src/Charcoal/Object/RevisionsConfig.php b/packages/object/src/Charcoal/Object/RevisionsConfig.php new file mode 100644 index 000000000..4d849c430 --- /dev/null +++ b/packages/object/src/Charcoal/Object/RevisionsConfig.php @@ -0,0 +1,158 @@ +prepareRevisionModelConfig($model, $models[$class]); + } + + // If the exact class is not defined in the revisions models key, try to find options from inheritance. + foreach ($this->models as $class => $revisionOptions) { + if ($model instanceof $class) { + return $this->prepareRevisionModelConfig($model, $revisionOptions); + } + } + + return null; + } + + /** + * @param ModelInterface $model + * @param array|boolean $revisionOptions + * @return RevisionModelConfig + */ + private function prepareRevisionModelConfig(ModelInterface $model, $revisionOptions): RevisionModelConfig + { + // If a config is a boolean instead of a data array, it means we only want to affect the enabled state. + if (is_bool($revisionOptions)) { + $revisionOptions = [ + 'enabled' => $revisionOptions, + ]; + } + + $extraOptions = [ + 'excludedProperties' => $this->getExcludedProperties(), + ]; + + // Extract excludedProperties options from the model's ancestors. + foreach ($this->models as $class => $modelConfig) { + if ($model instanceof $class) { + // keep only excludedProperties from ancestors + $modelConfig = array_intersect_key($modelConfig, array_flip(['excludedProperties'])); + $extraOptions = array_merge_recursive($extraOptions, $modelConfig); + } + } + + return new RevisionModelConfig(array_merge($revisionOptions, $extraOptions)); + } + + /** + * @return bool + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * @return string + */ + public function getRevisionClass(): string + { + return $this->revisionClass; + } + + /** + * @param string $revisionClass RevisionClass for RevisionConfig. + * @return self + */ + public function setRevisionClass(string $revisionClass): self + { + $this->revisionClass = $revisionClass; + + return $this; + } + + /** + * @return int|null + */ + public function getLimitPerModel(): ?int + { + return $this->limitPerModel; + } + + /** + * @param int|null $limitPerModel LimitPerModel for RevisionConfig. + * @return self + */ + public function setLimitPerModel(?int $limitPerModel): self + { + $this->limitPerModel = $limitPerModel; + + return $this; + } + + /** + * @return array + */ + public function getModels(): array + { + return $this->models; + } + + /** + * @param array $models Models for RevisionConfig. + * @return self + */ + public function setModels(array $models): self + { + $this->models = $models; + + return $this; + } + + /** + * @return array + */ + public function getExcludedProperties(): array + { + return $this->excludedProperties; + } + + /** + * @param array $excludedProperties ExcludedProperties for RevisionConfig. + * @return self + */ + public function setExcludedProperties(array $excludedProperties): self + { + $this->excludedProperties = $excludedProperties; + + return $this; + } +} diff --git a/packages/object/src/Charcoal/Object/RevisionsManager.php b/packages/object/src/Charcoal/Object/RevisionsManager.php new file mode 100644 index 000000000..24c5051ce --- /dev/null +++ b/packages/object/src/Charcoal/Object/RevisionsManager.php @@ -0,0 +1,227 @@ +revisionConfig = $locator->get('revisions/config'); + $this->setModelFactory($locator->get('model/factory')); + $this->setLogger($locator->get('logger')); + } + + public function __invoke(ModelInterface $model): self + { + $this->setModel($model); + + return $this; + } + + public function generateRevision(): ?ObjectRevisionInterface + { + $model = $this->getModel(); + + // Bail early + if ( + !$this->revisionConfig->isEnabled() || + !$this->revisionEnabled() + ) { + return null; + } + + $revisionProperties = $this->parseRevisionProperties(); + $revisionObject = $this->createRevisionObject(); + + $revisionObject->createFromObject($model, $revisionProperties); + + if (!empty($revisionObject->getDataDiff())) { + $revisionObject->save(); + } + + return $revisionObject; + } + + public function latestRevision(): ObjectRevisionInterface + { + $model = $this->getModel(); + $revision = $this->createRevisionObject(); + + return $revision->lastObjectRevision($model); + } + + /** + * @return ObjectRevisionInterface[] + */ + public function allRevisions(callable $callback = null): array + { + $model = $this->getModel(); + $loader = $this->createRevisionObjectCollectionLoader(); + + $loader + ->addOrder('revTs', 'desc') + ->addFilters([ + [ + 'property' => 'targetType', + 'value' => $model->objType(), + ], + [ + 'property' => 'targetId', + 'value' => $model->id(), + ], + ]); + + if ($callback !== null) { + $loader->setCallback($callback); + } + + $revisions = $loader->load(); + + return $revisions->objects(); + } + + public function revertToRevision(int $number): bool + { + $model = $this->getModel(); + $revision = $this->revisionFromNumber($number); + + if (!$revision->id()) { + return false; + } + + if (isset($model['lastModifiedBy'])) { + $model['lastModifiedBy'] = $revision->getRevUser(); + } + + $model->setData($revision->getDataObj()); + + return $model->update(); + } + + public function parseRevisionProperties(): array + { + $model = $this->getModel(); + $modelConfig = $this->getModelRevisionConfig($model); + $properties = array_keys($model->data()); + + if ($modelConfig->hasProperties()) { + return array_intersect($properties, $modelConfig->getProperties()); + } + + if ($modelConfig->hasExcludedProperties()) { + $excludedProperties = $modelConfig->getExcludedProperties(); + + if ($modelConfig->hasIncludedProperties()) { + $includedProperties = $modelConfig->getIncludedProperties(); + $excludedProperties = array_filter($excludedProperties, fn($e) => !in_array($e, $includedProperties)); + } + + return array_filter( + $properties, + fn($n) => !in_array($n, $excludedProperties) + ); + } + + return $properties; + } + + public function getObjectRevisionClass(): string + { + $modelConfig = $this->getModelRevisionConfig(); + + return $modelConfig->getRevisionClass(); + } + + public function createRevisionObjectCollectionLoader(): CollectionLoader + { + return new CollectionLoader([ + 'logger' => $this->logger, + 'factory' => $this->modelFactory(), + 'model' => $this->getRevisionObjectPrototype($this->getObjectRevisionClass()), + ]); + } + + public function getRevisionObjectPrototype(): ObjectRevisionInterface + { + return $this->modelFactory()->get($this->getObjectRevisionClass()); + } + + public function createRevisionObject(): ObjectRevisionInterface + { + return $this->modelFactory()->create($this->getObjectRevisionClass()); + } + + public function revisionFromNumber(int $number): ObjectRevisionInterface + { + return $this->createRevisionObject()->objectRevisionNum($this->getModel(), $number); + } + + public function revisionEnabled(): bool + { + $model = $this->getModel(); + $revisionConfig = $this->getModelRevisionConfig($model); + + // If we did not find a config of the value of the config is false, we don't want to revision. + if (!$revisionConfig) { + return false; + } + + return $revisionConfig->isEnabled(); + } + + private function getModelRevisionConfig(): ?RevisionModelConfig + { + $model = $this->getModel(); + + if (!isset($this->modelRevisionConfig[get_class($model)])) { + $this->modelRevisionConfig[get_class($model)] = $this->revisionConfig->buildModelConfig($model); + } + + return $this->modelRevisionConfig[get_class($model)]; + } + + public function getModel(): ModelInterface + { + if (!isset($this->model)) { + throw new \InvalidArgumentException('Setting a `ModelInterface` is imperative to use the RevisionService'); + } + + return $this->model; + } + + public function setModel(ModelInterface $model): self + { + $this->model = $model; + + return $this; + } +}