From e307b95771a7b31d35997b54dc417744b88fab3c Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Wed, 8 Jan 2020 17:06:15 +1300 Subject: [PATCH 01/16] First cut --- _config/config.yml | 124 +++++++-- src/Dispatch/Context.php | 25 ++ src/Dispatch/Dispatcher.php | 73 +++++ src/Handler/CMSMain/ActionHandler.php | 63 +++++ src/Handler/Form/FormSubmissionHandler.php | 94 +++++++ src/Handler/Form/SaveHandler.php | 35 +++ src/Handler/GraphQL/MiddlewareHandler.php | 98 +++++++ src/Handler/GraphQL/MutationHandler.php | 99 +++++++ src/Handler/GridField/ActionHandler.php | 71 +++++ src/Handler/GridField/AlterationHandler.php | 97 +++++++ src/Handler/HandlerAbstract.php | 25 ++ src/Handler/HandlerInterface.php | 12 + src/Listener/CMSMainActionListener.php | 46 ++++ src/Listener/CurrentPage.php | 2 +- .../Form/SubmissionListenerAbstract.php | 49 ---- src/Listener/FormSubmissionListener.php | 46 ++++ src/Listener/GraphQL/CustomAction.php | 120 --------- .../GraphQL/CustomActionListenerAbstract.php | 55 ---- src/Listener/GraphQL/GenericAction.php | 124 --------- .../GraphQL/GenericActionListenerAbstract.php | 56 ---- src/Listener/GraphQLMiddlewareListener.php | 48 ++++ src/Listener/GraphQLMutationListener.php | 51 ++++ src/Listener/GridField/AlterAction.php | 136 ---------- .../GridField/AlterActionListenerAbstract.php | 53 ---- .../UrlHandlerActionListenerAbstract.php | 47 ---- src/Listener/GridFieldAlterationListener.php | 52 ++++ ...lerAction.php => GridFieldURLListener.php} | 52 +--- src/Listener/Page/CMSMainAction.php | 81 ------ src/Listener/Page/CMSMainListenerAbstract.php | 43 --- src/Snapshot.php | 249 +----------------- src/SnapshotHasher.php | 14 +- 31 files changed, 1070 insertions(+), 1070 deletions(-) create mode 100644 src/Dispatch/Context.php create mode 100644 src/Dispatch/Dispatcher.php create mode 100644 src/Handler/CMSMain/ActionHandler.php create mode 100644 src/Handler/Form/FormSubmissionHandler.php create mode 100644 src/Handler/Form/SaveHandler.php create mode 100644 src/Handler/GraphQL/MiddlewareHandler.php create mode 100644 src/Handler/GraphQL/MutationHandler.php create mode 100644 src/Handler/GridField/ActionHandler.php create mode 100644 src/Handler/GridField/AlterationHandler.php create mode 100644 src/Handler/HandlerAbstract.php create mode 100644 src/Handler/HandlerInterface.php create mode 100644 src/Listener/CMSMainActionListener.php delete mode 100644 src/Listener/Form/SubmissionListenerAbstract.php create mode 100644 src/Listener/FormSubmissionListener.php delete mode 100644 src/Listener/GraphQL/CustomAction.php delete mode 100644 src/Listener/GraphQL/CustomActionListenerAbstract.php delete mode 100644 src/Listener/GraphQL/GenericAction.php delete mode 100644 src/Listener/GraphQL/GenericActionListenerAbstract.php create mode 100644 src/Listener/GraphQLMiddlewareListener.php create mode 100644 src/Listener/GraphQLMutationListener.php delete mode 100644 src/Listener/GridField/AlterAction.php delete mode 100644 src/Listener/GridField/AlterActionListenerAbstract.php delete mode 100644 src/Listener/GridField/UrlHandlerActionListenerAbstract.php create mode 100644 src/Listener/GridFieldAlterationListener.php rename src/Listener/{GridField/URLHandlerAction.php => GridFieldURLListener.php} (51%) delete mode 100644 src/Listener/Page/CMSMainAction.php delete mode 100644 src/Listener/Page/CMSMainListenerAbstract.php diff --git a/_config/config.yml b/_config/config.yml index 394f9fe..e70ebfb 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -57,25 +57,105 @@ SilverStripe\GraphQL\Manager: --- Name: snapshot-actions --- -SilverStripe\Snapshots\Snapshot: - actions: - # page edit form - 'save': 'Save page' - 'publish': 'Publish' - 'unpublish': 'Unpublish' - # site tree - 'savetreenode': 'Changed site tree position' - # grid field item edit form - 'doSave': 'Save item' - 'doDelete': 'Delete item' - # grid field actions (via standard action) - 'deleterecord': 'Delete item' - # grid field actions (via form submission) - 'handleReorder': 'Changed items order' - # linkable edit form - 'doSaveLink': 'Link Edit' - 'doRemoveLink': 'Link Delete' - # GraphQL CRUD - 'graphql_crud_create': 'GraphQL create' - 'graphql_crud_delete': 'GraphQL delete' - 'graphql_crud_update': 'GraphQL update' +SilverStripe\Core\Injector\Injector: + # Form submission handlers + SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.publish: + class: SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler + constructor: + message: 'Publish page' + handlerName: 'publish' + SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.unpublish: + class: SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler + constructor: + message: 'Unpublish page' + handlerName: 'unpublish' + SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.gridFieldSave: + class: SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler + constructor: + message: 'Save item' + handlerName: 'doSave' + SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.gridFieldDelete: + class: SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler + constructor: + message: 'Delete item' + handlerName: 'doDelete' + + # CMS action handlers + SilverStripe\Snapshots\Handler\CMSMain\ActionHandler.savetreenode: + class: SilverStripe\Snapshots\Handler\CMSMain\ActionHandler + constructor: + action: 'savetreenode' + message: 'Changed site tree position' + + # GridField action handlers + SilverStripe\Snapshots\Handler\GridField\ActionHandler.deleterecord: + class: SilverStripe\Snapshots\Handler\GridField\ActionHandler + constructor: + action: 'deleterecord' + message: 'Delete item' + + # GridField manipulator handlers + SilverStripe\Snapshots\Handler\GridField\AlterationHandler.handleReorder: + class: SilverStripe\Snapshots\Handler\GridField\AlterationHandler + constructor: + action: 'handleReorder' + message: 'Changed items order' + + # GraphQL mutation handlers + SilverStripe\Snapshots\Handler\GraphQL\MutationHandler.create: + class: SilverStripe\Snapshots\Handler\GraphQL\MutationHandler + constructor: + mutationType: 'create' + SilverStripe\Snapshots\Handler\GraphQL\MutationHandler.update: + class: SilverStripe\Snapshots\Handler\GraphQL\MutationHandler + constructor: + mutationType: 'update' + SilverStripe\Snapshots\Handler\GraphQL\MutationHandler.delete: + class: SilverStripe\Snapshots\Handler\GraphQL\MutationHandler + constructor: + mutationType: 'delete' + + + SilverStripe\Snapshots\Dispatch\Dispatcher: + properties: + handlers: + # Form submission handlers + saveHandler: + event: 'formSubmitted' + handler: %$SilverStripe\Snapshots\Handler\Form\SaveHandler + publishHandler: + event: 'formSubmitted' + handler: %$SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.publish + unpublishHandler: + event: 'formSubmitted' + handler: %$SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.unpublish + gridFieldSaveHandler: + event: 'formSubmitted' + handler: %$SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.gridFieldSave + gridFieldDeleteHandler: + event: 'formSubmitted' + handler: %$SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.gridFieldDelete + + # CMSMain actions + reorderSiteTreeHandler: + event: 'cmsAction' + handler: %$SilverStripe\Snapshots\Handler\CMSMain\ActionHandler.savetreenode + + # GridField URL providers + deleteRecordHandler: + event: 'gridFieldAction' + handler: %$SilverStripe\Snapshots\Handler\GridField\ActionHandler.deleterecord + + # GridField action providers + reorderHandler: + event: 'gridFieldAlteration' + handler: %$SilverStripe\Snapshots\Handler\GridField\AlterationHandler.handleReorder + graphqlCreate: + event: 'graphqlMutation' + handler: %$SilverStripe\Snapshots\Handler\GraphQL\MutationHandler.create + graphqlUpdate: + event: 'graphqlMutation' + handler: %$SilverStripe\Snapshots\Handler\GraphQL\MutationHandler.update + graphqlDelete: + event: 'graphqlMutation' + handler: %$SilverStripe\Snapshots\Handler\GraphQL\MutationHandler.delete diff --git a/src/Dispatch/Context.php b/src/Dispatch/Context.php new file mode 100644 index 0000000..37f6984 --- /dev/null +++ b/src/Dispatch/Context.php @@ -0,0 +1,25 @@ +data = $data; + } + + public function __get(string $name) + { + return $this->get(name); + } + + public function get(string $name) + { + return $this->data[$name] ?? null; + } +} diff --git a/src/Dispatch/Dispatcher.php b/src/Dispatch/Dispatcher.php new file mode 100644 index 0000000..d8f25ff --- /dev/null +++ b/src/Dispatch/Dispatcher.php @@ -0,0 +1,73 @@ +addListener($event, $handler); + } + } + + public function addListener(string $event, HandlerInterface $handler): self + { + if (!isset($this->handlers[$event])) { + $this->handlers[$event] = []; + } + + foreach ($this->handlers[$event] as $existing) { + if ($existing === $handler) { + throw new Exception(sprintf( + 'Handler for %s has already been added', + $event + )); + } + } + $this->handlers[$event][] = $handler; + + return $this; + } + + public function removeListener(string $event, HandlerInterface $handler): self + { + $handlers = $this->handlers[$event] ?? []; + /* @var HandlerInterface $handler */ + $this->handlers = array_filter(function ($existing) use ($handler) { + return $handler !== $existing; + }, $this->handlers); + + return $this; + } + + public function trigger(string $event, Context $context): void + { + $handlers = $this->handlers[$event] ?? []; + /* @var HandlerInterface $handler */ + foreach ($handlers as $handler) { + if ($handler->shouldFire($context)) { + $handler->fire($context); + } + } + } +} diff --git a/src/Handler/CMSMain/ActionHandler.php b/src/Handler/CMSMain/ActionHandler.php new file mode 100644 index 0000000..785c906 --- /dev/null +++ b/src/Handler/CMSMain/ActionHandler.php @@ -0,0 +1,63 @@ +action = $action; + $this->message = $message; + } + + public function shouldFire(Context $context): bool + { + return ( + parent::shouldFire($context) && + $context->action === $this->action + ); + } + + public function fire(Context $context): void + { + $message = $this->getMessage(); + + if (!$context->result instanceof HTTPResponse) { + return; + } + + if ((int) $context->result->getStatusCode() !== 200) { + return; + } + + $className = $context->treeClass; + $id = (int) $context->id; + + if (!$id) { + return; + } + + /** @var SiteTree $page */ + $page = DataObject::get_by_id($className, $id); + + if ($page === null) { + return; + } + + $snapshot->createSnapshotFromAction($page, null, $message); + } +} diff --git a/src/Handler/Form/FormSubmissionHandler.php b/src/Handler/Form/FormSubmissionHandler.php new file mode 100644 index 0000000..c9e701c --- /dev/null +++ b/src/Handler/Form/FormSubmissionHandler.php @@ -0,0 +1,94 @@ +message = $message; + $this->formHandlerName = $formHandlerName; + $this->formName = $formName; + $this->controllerClass = $controllerClass; + } + + public function getMessage(): string + { + return $this->message; + } + + /** + * @param Context $context + * @return bool + */ + public function shouldFire(Context $context): bool + { + return ( + parent::shouldFire($context) && + $context->form->getController() instanceof $this->controllerClass && + $context->form->getName() === $this->formName && + $context->handlerName == $this->formHandlerName + ); + } + + /** + * @param Context $context + */ + public function fire(Context $context): void + { + $message = $this->getMessage(); + $record = $context->form->getRecord(); + + if ($record === null) { + return; + } + + $url = $context->request->getURL(); + $page = $this->getCurrentPageFromRequestUrl($url); + + if ($page === null) { + return; + } + + $snapshot->createSnapshotFromAction($page, $record, $message); + } +} diff --git a/src/Handler/Form/SaveHandler.php b/src/Handler/Form/SaveHandler.php new file mode 100644 index 0000000..a923a03 --- /dev/null +++ b/src/Handler/Form/SaveHandler.php @@ -0,0 +1,35 @@ +request->getURL(); + $page = $this->getCurrentPageFromRequestUrl($url); + + return parent::shouldFire($context) && $page && $page->isModifiedOnDraft(); + } +} diff --git a/src/Handler/GraphQL/MiddlewareHandler.php b/src/Handler/GraphQL/MiddlewareHandler.php new file mode 100644 index 0000000..eeb8386 --- /dev/null +++ b/src/Handler/GraphQL/MiddlewareHandler.php @@ -0,0 +1,98 @@ +actionType = $actionType; + } + + /** + * @param Context $context + * @return bool + */ + public function shouldFire(Context $context): bool + { + $action = $this->getActionType($context->query); + return ( + parent::shouldFire($context) && + $action && + $action === $this->actionType + ); + } + + /** + * @param Context $context + * @throws \SilverStripe\ORM\ValidationException + */ + public function fire(Context $context): void + { + $message = $this->getMessage(); + $controller = Controller::curr(); + + if (!$controller) { + return; + } + + $request = $controller->getRequest(); + + if (!$request) { + return; + } + + $url = $request->getHeader('referer'); + $url = parse_url($url, PHP_URL_PATH); + $url = ltrim($url, '/'); + $page = $this->getCurrentPageFromRequestUrl($url); + + if ($page === null) { + return; + } + + $snapshot->createSnapshotFromAction($page, null, $message); + } + + /** + * Extract action type from query + * + * @param string $query + * @return string|null + */ + private function getActionType(string $query): ?string + { + $action = explode('(', $query); + + if (count($action) === 0) { + return null; + } + + $action = array_shift($action); + + if (!$action) { + return null; + } + + $action = str_replace(' ', '_', $action); + + return $action; + } + +} diff --git a/src/Handler/GraphQL/MutationHandler.php b/src/Handler/GraphQL/MutationHandler.php new file mode 100644 index 0000000..4437805 --- /dev/null +++ b/src/Handler/GraphQL/MutationHandler.php @@ -0,0 +1,99 @@ +mutationType = $mutationType; + } + + public function shouldFire(Context $context): bool + { + $type = $this->getActionType($context->mutation); + + return ( + parent::shouldFire($context) && + $type && + $type === $this->mutationType + ); + } + + public function fire(Context $context): void + { + $action = static::ACTION_PREFIX . $type; + $message = $this->getMessage(); + + $controller = Controller::curr(); + + if (!$controller) { + return; + } + + $request = $controller->getRequest(); + + if (!$request) { + return; + } + + $url = $request->getHeader('referer'); + $url = parse_url($url, PHP_URL_PATH); + $url = ltrim($url, '/'); + $page = $this->getCurrentPageFromRequestUrl($url); + + if ($page === null) { + return; + } + + $snapshot->createSnapshotFromAction($page, null, $message); + } + + /** + * @param OperationScaffolder $scaffolder + * @return string|null + */ + private function getActionType(OperationScaffolder $scaffolder): ?string + { + if ($scaffolder instanceof Create) { + return static::TYPE_CREATE; + } + + if ($scaffolder instanceof Delete) { + return static::TYPE_DELETE; + } + + if ($scaffolder instanceof Update) { + return static::TYPE_UPDATE; + } + + return null; + } + +} diff --git a/src/Handler/GridField/ActionHandler.php b/src/Handler/GridField/ActionHandler.php new file mode 100644 index 0000000..bdf5d59 --- /dev/null +++ b/src/Handler/GridField/ActionHandler.php @@ -0,0 +1,71 @@ +action === $context->action + ); + } + + public function __construct(string $action, $message) + { + $this->action = $action; + $this->message = $message; + } + + public function getMessage(): string + { + return $this->message; + } + + public function fire(Context $context): void + { + $message = $this->getMessage(); + $form = $owner->getForm(); + + if (!$form) { + return; + } + + $record = $form->getRecord(); + + if (!$record) { + return; + } + + $page = $this->getCurrentPageFromController($form); + + if ($page === null) { + return; + } + + // attempt to create a custom snapshot first + $customSnapshot = $snapshot->gridFieldUrlActionSnapshot($page, $action, $message, $owner); + + if ($customSnapshot) { + return; + } + + // fall back to default snapshot + $snapshot->createSnapshotFromAction($page, $record, $message); + + } +} diff --git a/src/Handler/GridField/AlterationHandler.php b/src/Handler/GridField/AlterationHandler.php new file mode 100644 index 0000000..b6d5938 --- /dev/null +++ b/src/Handler/GridField/AlterationHandler.php @@ -0,0 +1,97 @@ +requestVars(); + $actionData = $this->getActionData($requestData, $context->gridField); + + if ($actionData === null) { + return; + } + + list ($identifier, $arguments, $data) = $actionData; + + $message = $this->getMessage(); + $form = $owner->getForm(); + + if (!$form) { + return; + } + + $record = $form->getRecord(); + + if (!$record) { + return; + } + + $page = $this->getCurrentPageFromController($form); + + if ($page === null) { + return; + } + + $snapshot->createSnapshotFromAction($page, $record, $message); + } + + /** + * @param array $data + * @return array|null + */ + private function getActionData(array $data, GridField $gridField): ?array + { + // Fetch the store for the "state" of actions (not the GridField) + /** @var StateStore $store */ + $store = Injector::inst()->create(StateStore::class . '.' . $gridField->getName()); + + foreach ($data as $dataKey => $dataValue) { + if (!preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) { + continue; + } + + $stateChange = $store->load($matches[1]); + + $actionName = $stateChange['actionName']; + $arguments = array_key_exists('args', $stateChange) ? $stateChange['args'] : []; + $arguments = is_array($arguments) ? $arguments : []; + + if ($actionName) { + return [ + $actionName, + $arguments, + $data, + ]; + } + } + + return null; + } + +} diff --git a/src/Handler/HandlerAbstract.php b/src/Handler/HandlerAbstract.php new file mode 100644 index 0000000..fc1e058 --- /dev/null +++ b/src/Handler/HandlerAbstract.php @@ -0,0 +1,25 @@ +isActionTriggerActive(); + } +} diff --git a/src/Handler/HandlerInterface.php b/src/Handler/HandlerInterface.php new file mode 100644 index 0000000..063d315 --- /dev/null +++ b/src/Handler/HandlerInterface.php @@ -0,0 +1,12 @@ +trigger('cmsAction', new Context([ + 'action' => $action, + 'result' => $result, + 'treeClass' => $this->owner->config()->get('tree_class'), + 'id' => $request->requestVar('ID'), + ])); + } +} diff --git a/src/Listener/CurrentPage.php b/src/Listener/CurrentPage.php index 0b71346..d7f9068 100644 --- a/src/Listener/CurrentPage.php +++ b/src/Listener/CurrentPage.php @@ -54,7 +54,7 @@ private function getCurrentPageFromController($controller): ?Page * @param string|null $url * @return Page|null */ - private function getCurrentPageFromRequestUrl(?string $url): ?Page + protected function getCurrentPageFromRequestUrl(?string $url): ?Page { $url = trim($url); diff --git a/src/Listener/Form/SubmissionListenerAbstract.php b/src/Listener/Form/SubmissionListenerAbstract.php deleted file mode 100644 index 7fbcafe..0000000 --- a/src/Listener/Form/SubmissionListenerAbstract.php +++ /dev/null @@ -1,49 +0,0 @@ -getActionName()) { - return false; - } - - return $this->processAction($form, $request, $page, $action, $message); - } - - abstract protected function getActionName(): string; - - abstract protected function processAction( - Form $form, - HTTPRequest $request, - Page $page, - string $action, - string $message - ): bool; -} diff --git a/src/Listener/FormSubmissionListener.php b/src/Listener/FormSubmissionListener.php new file mode 100644 index 0000000..9c603c4 --- /dev/null +++ b/src/Listener/FormSubmissionListener.php @@ -0,0 +1,46 @@ +trigger('formSubmitted', new Context([ + 'handlerName' => $funcName, + 'form' => $form, + 'request' => $request + ]); + } +} diff --git a/src/Listener/GraphQL/CustomAction.php b/src/Listener/GraphQL/CustomAction.php deleted file mode 100644 index 8ec810c..0000000 --- a/src/Listener/GraphQL/CustomAction.php +++ /dev/null @@ -1,120 +0,0 @@ -isActionTriggerActive()) { - return; - } - - $action = $this->getActionType($query); - - if (!$action) { - return; - } - - $message = $snapshot->getActionMessage($action); - - if ($message === null) { - return; - } - - $controller = Controller::curr(); - - if (!$controller) { - return; - } - - $request = $controller->getRequest(); - - if (!$request) { - return; - } - - $url = $request->getHeader('referer'); - $url = parse_url($url, PHP_URL_PATH); - $url = ltrim($url, '/'); - $page = $this->getCurrentPageFromRequestUrl($url); - - if ($page === null) { - return; - } - - // attempt to create a custom snapshot first - $customSnapshot = $snapshot->graphQLCustomActionSnapshot( - $page, - $action, - $message, - $schema, - $query, - $context, - $params - ); - - if ($customSnapshot) { - return; - } - - // fall back to default snapshot - $snapshot->createSnapshotFromAction($page, null, $message); - } - - /** - * Extract action type from query - * - * @param string $query - * @return string|null - */ - private function getActionType(string $query): ?string - { - $action = explode('(', $query); - - if (count($action) === 0) { - return null; - } - - $action = array_shift($action); - - if (!$action) { - return null; - } - - $action = str_replace(' ', '_', $action); - - return $action; - } -} diff --git a/src/Listener/GraphQL/CustomActionListenerAbstract.php b/src/Listener/GraphQL/CustomActionListenerAbstract.php deleted file mode 100644 index 8f29e0c..0000000 --- a/src/Listener/GraphQL/CustomActionListenerAbstract.php +++ /dev/null @@ -1,55 +0,0 @@ -getActionName()) { - return false; - } - - return $this->processAction($page, $action, $message, $schema, $query, $context, $params); - } - - abstract protected function getActionName(): string; - - abstract protected function processAction( - Page $page, - string $action, - string $message, - Schema $schema, - string $query, - array $context, - array $params - ): bool; -} diff --git a/src/Listener/GraphQL/GenericAction.php b/src/Listener/GraphQL/GenericAction.php deleted file mode 100644 index 6f025af..0000000 --- a/src/Listener/GraphQL/GenericAction.php +++ /dev/null @@ -1,124 +0,0 @@ -isActionTriggerActive()) { - return; - } - - $type = $this->getActionType(); - - if (!$type) { - return; - } - - $action = static::ACTION_PREFIX . $type; - $message = $snapshot->getActionMessage($action); - - if ($message === null) { - return; - } - - $controller = Controller::curr(); - - if (!$controller) { - return; - } - - $request = $controller->getRequest(); - - if (!$request) { - return; - } - - $url = $request->getHeader('referer'); - $url = parse_url($url, PHP_URL_PATH); - $url = ltrim($url, '/'); - $page = $this->getCurrentPageFromRequestUrl($url); - - if ($page === null) { - return; - } - - // attempt to create a custom snapshot first - $customSnapshot = $snapshot->graphQLGenericActionSnapshot( - $page, - $action, - $message, - $recordOrList, - $args, - $context, - $info - ); - - if ($customSnapshot) { - return; - } - - // fall back to default snapshot - $snapshot->createSnapshotFromAction($page, null, $message); - } - - private function getActionType(): ?string - { - $owner = $this->owner; - - if ($owner instanceof Create) { - return static::TYPE_CREATE; - } - - if ($owner instanceof Delete) { - return static::TYPE_DELETE; - } - - if ($owner instanceof Update) { - return static::TYPE_UPDATE; - } - - return null; - } -} diff --git a/src/Listener/GraphQL/GenericActionListenerAbstract.php b/src/Listener/GraphQL/GenericActionListenerAbstract.php deleted file mode 100644 index 451243c..0000000 --- a/src/Listener/GraphQL/GenericActionListenerAbstract.php +++ /dev/null @@ -1,56 +0,0 @@ -getActionName()) { - return false; - } - - return $this->processAction($page, $action, $message, $recordOrList, $args, $context, $info); - } - - abstract protected function getActionName(): string; - - abstract protected function processAction( - Page $page, - string $action, - string $message, - $recordOrList, - array $args, - $context, - ResolveInfo $info - ): bool; -} diff --git a/src/Listener/GraphQLMiddlewareListener.php b/src/Listener/GraphQLMiddlewareListener.php new file mode 100644 index 0000000..189aa50 --- /dev/null +++ b/src/Listener/GraphQLMiddlewareListener.php @@ -0,0 +1,48 @@ +trigger('graphqlMiddleware', new Context([ + 'schema' => $schema, + 'query' => $query, + 'context' => $context, + 'params' => $params, + ])); + } + +} diff --git a/src/Listener/GraphQLMutationListener.php b/src/Listener/GraphQLMutationListener.php new file mode 100644 index 0000000..63aeef5 --- /dev/null +++ b/src/Listener/GraphQLMutationListener.php @@ -0,0 +1,51 @@ +trigger('graphqlMutation', new Context([ + 'list' => $recordOrList instanceof SS_List ? $recordOrList : null, + 'record' => !$recordOrList instanceof SS_List ? $recordOrList : null, + 'args' => $arge, + 'context' => $context, + 'resolveInfo' => $info, + 'mutation' => $this->owner, + ])); + } +} diff --git a/src/Listener/GridField/AlterAction.php b/src/Listener/GridField/AlterAction.php deleted file mode 100644 index 67bb805..0000000 --- a/src/Listener/GridField/AlterAction.php +++ /dev/null @@ -1,136 +0,0 @@ -owner; - $snapshot = Snapshot::singleton(); - - if (!$snapshot->isActionTriggerActive()) { - return; - } - - $requestData = $request->requestVars(); - $actionData = $this->getActionData($requestData); - - if ($actionData === null) { - return; - } - - $identifier = array_shift($actionData); - $arguments = array_shift($actionData); - $data = array_shift($actionData); - - $message = $snapshot->getActionMessage($identifier); - - if ($message === null) { - return; - } - - $form = $owner->getForm(); - - if (!$form) { - return; - } - - $record = $form->getRecord(); - - if (!$record) { - return; - } - - $page = $this->getCurrentPageFromController($form); - - if ($page === null) { - return; - } - - // attempt to create a custom snapshot first - $customSnapshot = $snapshot->gridFieldAlterActionSnapshot( - $page, - $identifier, - $message, - $owner, - $arguments, - $data - ); - - if ($customSnapshot) { - return; - } - - // fall back to default snapshot - $snapshot->createSnapshotFromAction($page, $record, $message); - } - - private function getActionData(array $data): ?array - { - $gridField = $this->owner; - - // Fetch the store for the "state" of actions (not the GridField) - /** @var StateStore $store */ - $store = Injector::inst()->create(StateStore::class . '.' . $gridField->getName()); - - foreach ($data as $dataKey => $dataValue) { - if (!preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) { - continue; - } - - $stateChange = $store->load($matches[1]); - - $actionName = $stateChange['actionName']; - $arguments = array_key_exists('args', $stateChange) ? $stateChange['args'] : []; - $arguments = is_array($arguments) ? $arguments : []; - - if ($actionName) { - return [ - $actionName, - $arguments, - $data, - ]; - } - } - - return null; - } -} diff --git a/src/Listener/GridField/AlterActionListenerAbstract.php b/src/Listener/GridField/AlterActionListenerAbstract.php deleted file mode 100644 index da98763..0000000 --- a/src/Listener/GridField/AlterActionListenerAbstract.php +++ /dev/null @@ -1,53 +0,0 @@ -getActionName()) { - return false; - } - - return $this->processAction($page, $action, $message, $gridField, $arguments, $data); - } - - abstract protected function getActionName(): string; - - abstract protected function processAction( - Page $page, - string $action, - string $message, - GridField $gridField, - array $arguments, - array $data - ): bool; -} diff --git a/src/Listener/GridField/UrlHandlerActionListenerAbstract.php b/src/Listener/GridField/UrlHandlerActionListenerAbstract.php deleted file mode 100644 index f3a3507..0000000 --- a/src/Listener/GridField/UrlHandlerActionListenerAbstract.php +++ /dev/null @@ -1,47 +0,0 @@ -getActionName()) { - return false; - } - - return $this->processAction($page, $action, $message, $gridField); - } - - abstract protected function getActionName(): string; - - abstract protected function processAction( - Page $page, - string $action, - string $message, - GridField $gridField - ): bool; -} diff --git a/src/Listener/GridFieldAlterationListener.php b/src/Listener/GridFieldAlterationListener.php new file mode 100644 index 0000000..13f163b --- /dev/null +++ b/src/Listener/GridFieldAlterationListener.php @@ -0,0 +1,52 @@ +trigger('gridFieldAlteration', new Context([ + 'action' => $action, + 'request' => $request, + 'result' => $result, + 'gridField' => $this->owner, + ])); + } + +} diff --git a/src/Listener/GridField/URLHandlerAction.php b/src/Listener/GridFieldURLListener.php similarity index 51% rename from src/Listener/GridField/URLHandlerAction.php rename to src/Listener/GridFieldURLListener.php index 6e14f7f..3385281 100644 --- a/src/Listener/GridField/URLHandlerAction.php +++ b/src/Listener/GridFieldURLListener.php @@ -1,6 +1,6 @@ owner; - $snapshot = Snapshot::singleton(); - - if (!$snapshot->isActionTriggerActive()) { - return; - } - - $message = $snapshot->getActionMessage($action); - - if ($message === null) { - return; - } - - $form = $owner->getForm(); - - if (!$form) { - return; - } - - $record = $form->getRecord(); - - if (!$record) { - return; - } - - $page = $this->getCurrentPageFromController($form); - - if ($page === null) { - return; - } - - // attempt to create a custom snapshot first - $customSnapshot = $snapshot->gridFieldUrlActionSnapshot($page, $action, $message, $owner); - - if ($customSnapshot) { - return; - } - - // fall back to default snapshot - $snapshot->createSnapshotFromAction($page, $record, $message); + Dispatcher::singleton()->trigger('gridFieldAction', new Context([ + 'action' => $action, + 'result' => $result, + 'request' => $request, + 'gridField' => $this->owner, + ])); } } diff --git a/src/Listener/Page/CMSMainAction.php b/src/Listener/Page/CMSMainAction.php deleted file mode 100644 index 77f18e9..0000000 --- a/src/Listener/Page/CMSMainAction.php +++ /dev/null @@ -1,81 +0,0 @@ -isActionTriggerActive()) { - return; - } - - $message = $snapshot->getActionMessage($action); - - if ($message === null) { - return; - } - - if (!$result instanceof HTTPResponse) { - return; - } - - if ((int) $result->getStatusCode() !== 200) { - return; - } - - $className = $this->owner->config()->get('tree_class'); - $id = (int) $request->requestVar('ID'); - - if (!$id) { - return; - } - - /** @var SiteTree $page */ - $page = DataObject::get_by_id($className, $id); - - if ($page === null) { - return; - } - - // attempt to create a custom snapshot first - $customSnapshot = $snapshot->CMSMainActionSnapshot($page, $action, $message); - - if ($customSnapshot) { - return; - } - - // fall back to default snapshot - $snapshot->createSnapshotFromAction($page, null, $message); - } -} diff --git a/src/Listener/Page/CMSMainListenerAbstract.php b/src/Listener/Page/CMSMainListenerAbstract.php deleted file mode 100644 index 77f97a4..0000000 --- a/src/Listener/Page/CMSMainListenerAbstract.php +++ /dev/null @@ -1,43 +0,0 @@ -getActionName()) { - return false; - } - - return $this->processAction($page, $action, $message); - } - - abstract protected function getActionName(): string; - - abstract protected function processAction( - Page $page, - string $action, - string $message - ): bool; -} diff --git a/src/Snapshot.php b/src/Snapshot.php index 6c46756..6f77cdc 100644 --- a/src/Snapshot.php +++ b/src/Snapshot.php @@ -231,47 +231,23 @@ public function onBeforeWrite() $this->OriginHash = static::hashForSnapshot($this->OriginClass, $this->OriginID); } - public function isActionTriggerActive() + /** + * @return bool + */ + public function isActionTriggerActive(): bool { return $this->config()->get('trigger') === static::TRIGGER_ACTION; } - public function isModelTriggerActive() - { - return $this->config()->get('trigger') === static::TRIGGER_MODEL; - } - /** - * Singleton access only - * - * Valid values: - * NULL - action will not be processed into snapshot - * empty string - action will be processed into snapshot but no message will be displayed - * string - action will be processed into snapshot, message will be displayed - * - * This function is the ideal place to put action identifier logging when trying to determine - * which action identifier needs to be used in your configuration - * example logging code (place as the first line of the function) - * error_log($identifier); - * run your action in the CMS and look for your identifier in the log - * - * @param string|null $identifier - * @return string|null + * @return bool */ - public function getActionMessage($identifier): ?string + public function isModelTriggerActive(): bool { - $actions = $this->config()->get('actions'); - $actions = is_array($actions) ? $actions : []; - - if (!array_key_exists($identifier, $actions)) { - return null; - } - - return $actions[$identifier]; + return $this->config()->get('trigger') === static::TRIGGER_MODEL; } /** - * Singleton access only * * @param DataObject $owner * @param DataObject|null $origin @@ -305,7 +281,7 @@ public function createSnapshotFromAction( : sprintf( '%s %s', $message, - $origin->config()->get('singular_name') + $origin->singular_name() ); $origin = $event; @@ -315,7 +291,7 @@ public function createSnapshotFromAction( // no need to add the origin to the list of objects as it's already there $origin = $owner; } - } elseif ($origin->ClassName === $owner->ClassName && (int) $origin->ID === (int) $owner->ID) { + } elseif (static::hashSnapshotCompare($origin, $owner)) { // case 2: origin is same as the owner // no need to add the origin to the list of objects as it's already there $origin = $owner; @@ -352,210 +328,5 @@ public function createSnapshotFromAction( return $snapshot; } - - /** - * Singleton access only - * - * Use this extension point if you want to override default Snpashot creation for generic GraphQL actions - * - * @param Page $page - * @param string $action - * @param string $message - * @param $recordOrList - * @param array $args - * @param $context - * @param ResolveInfo $info - * @return bool - */ - public function graphQLGenericActionSnapshot( - Page $page, - string $action, - string $message, - $recordOrList, - array $args, - $context, - ResolveInfo $info - ): bool { - $results = $this->extend( - 'overrideGraphQLGenericActionSnapshot', - $page, - $action, - $message, - $recordOrList, - $args, - $context, - $info - ); - - return in_array(true, $results); - } - - /** - * Singleton access only - * - * Use this extension point if you want to override default Snpashot creation for custom GraphQL actions - * - * @param Page $page - * @param string $action - * @param string $message - * @param Schema $schema - * @param string $query - * @param array $context - * @param array $params - * @return bool - */ - public function graphQLCustomActionSnapshot( - Page $page, - string $action, - string $message, - Schema $schema, - string $query, - array $context, - array $params - ): bool { - $results = $this->extend( - 'overrideGraphQLCustomActionSnapshot', - $page, - $action, - $message, - $schema, - $query, - $context, - $params - ); - - return in_array(true, $results); - } - - /** - * Singleton access only - * - * Use this extension point if you want to override default Snpashot creation for GridField alter actions - * - * @param Page $page - * @param string $action - * @param string $message - * @param GridField $gridField - * @param array $arguments - * @param array $data - * @return bool - */ - public function gridFieldAlterActionSnapshot( - Page $page, - string $action, - string $message, - GridField $gridField, - array $arguments, - array $data - ): bool { - $results = $this->extend( - 'overrideGridFieldAlterActionSnapshot', - $page, - $action, - $message, - $gridField, - $arguments, - $data - ); - - return in_array(true, $results); - } - - /** - * Singleton access only - * - * Use this extension point if you want to override default Snapshot creation for GridField URL handler actions - * - * @param Page $page - * @param string $action - * @param string $message - * @param GridField $gridField - * @return bool - */ - public function gridFieldUrlActionSnapshot( - Page $page, - string $action, - string $message, - GridField $gridField - ): bool { - $results = $this->extend( - 'overrideGridFieldUrlActionSnapshot', - $page, - $action, - $message, - $gridField - ); - - return in_array(true, $results); - } - - /** - * Singleton access only - * - * Use this extension point if you want to override default Snpashot creation for Form submision actions - * - * @param Form $form - * @param HTTPRequest $request - * @param Page $page - * @param string $action - * @param string $message - * @return bool - */ - public function formSubmissionSnapshot( - Form $form, - HTTPRequest $request, - Page $page, - string $action, - string $message - ): bool { - $results = $this->extend( - 'overrideFormSubmissionSnapshot', - $form, - $request, - $page, - $action, - $message - ); - - return in_array(true, $results); - } - - /** - * Singleton access only - * - * Use this extension point if you want to override default Snpashot creation for CMS main actions - * - * @param Page $page - * @param string $action - * @param string $message - * @return bool - */ - public function CMSMainActionSnapshot( - Page $page, - string $action, - string $message - ): bool { - $results = $this->extend( - 'overrideCMSMainActionSnapshot', - $page, - $action, - $message - ); - - return in_array(true, $results); - } - - /** - * sets the related snapshot items to not modified - * - * items with modifications are used to determine the owner's modification - * status (eg in site tree's status flags) - */ - public function markNoModifications(): void - { - foreach ($this->Items() as $item) { - $item->Modification = false; - $item->write(); - } - } + } diff --git a/src/SnapshotHasher.php b/src/SnapshotHasher.php index b10343a..e93ef6e 100644 --- a/src/SnapshotHasher.php +++ b/src/SnapshotHasher.php @@ -16,7 +16,7 @@ trait SnapshotHasher * @param $id * @return string */ - public static function hashForSnapshot($class, $id) + public static function hashForSnapshot($class, $id): string { return base64_encode(hash('sha256', sprintf('%s:%s', $class, $id), true)); } @@ -27,8 +27,18 @@ public static function hashForSnapshot($class, $id) * @param DataObject $obj * @return string */ - public static function hashObjectForSnapshot(DataObject $obj) + public static function hashObjectForSnapshot(DataObject $obj): string { return static::hashForSnapshot($obj->baseClass(), $obj->ID); } + + /** + * @param DataObject $obj1 + * @param DataObject $obj2 + * @return bool + */ + public static function hashSnapshotCompare(DataObject $obj1, DataObject $obj2): bool + { + return static::hashObjectForSnapshot($obj1) === static::hashObjectForSnapshot($obj2); + } } From efa659282cb7b10d85d25d39a995e571cb5c0019 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Fri, 10 Jan 2020 15:02:41 +1300 Subject: [PATCH 02/16] It's working. README updated --- README.md | 360 +++++++----------- _config/config.yml | 135 ++----- lang/en.yml | 20 + src/Dispatch/Context.php | 25 -- src/Dispatch/Dispatcher.php | 87 ++++- src/Dispatch/EventHandlerLoader.php | 10 + src/Handler/CMSMain/ActionHandler.php | 59 ++- src/Handler/Form/FormSubmissionHandler.php | 98 ++--- src/Handler/Form/PublishHandler.php | 29 ++ src/Handler/Form/SaveHandler.php | 34 +- src/Handler/GraphQL/GenericHandler.php | 35 ++ src/Handler/GraphQL/MiddlewareHandler.php | 98 ----- src/Handler/GraphQL/MutationHandler.php | 91 +---- src/Handler/GridField/ActionHandler.php | 71 ---- src/Handler/GridField/AlterationHandler.php | 83 +--- src/Handler/GridField/URLActionHandler.php | 44 +++ src/Handler/HandlerAbstract.php | 37 +- src/Handler/HandlerInterface.php | 5 +- .../{ => CMSMain}/CMSMainActionListener.php | 25 +- src/Listener/CMSMain/CMSMainContext.php | 76 ++++ src/Listener/CurrentPage.php | 28 +- src/Listener/Form/FormContext.php | 81 ++++ .../{ => Form}/FormSubmissionListener.php | 17 +- .../GraphQL/GraphQLMiddlewareContext.php | 113 ++++++ .../GraphQL/GraphQLMiddlewareListener.php | 36 ++ .../GraphQL/GraphQLMutationContext.php | 145 +++++++ .../{ => GraphQL}/GraphQLMutationListener.php | 28 +- src/Listener/GraphQLMiddlewareListener.php | 48 --- .../GridField/GridFieldAlterationContext.php | 60 +++ .../GridField/GridFieldAlterationListener.php | 39 ++ src/Listener/GridField/GridFieldContext.php | 81 ++++ .../GridField/GridFieldURLListener.php | 35 ++ src/Listener/GridFieldAlterationListener.php | 52 --- src/Listener/GridFieldURLListener.php | 51 --- src/Listener/ListenerContext.php | 24 ++ src/Snapshot.php | 57 +-- src/SnapshotPublishable.php | 107 +----- 37 files changed, 1263 insertions(+), 1161 deletions(-) create mode 100644 lang/en.yml delete mode 100644 src/Dispatch/Context.php create mode 100644 src/Dispatch/EventHandlerLoader.php create mode 100644 src/Handler/Form/PublishHandler.php create mode 100644 src/Handler/GraphQL/GenericHandler.php delete mode 100644 src/Handler/GraphQL/MiddlewareHandler.php delete mode 100644 src/Handler/GridField/ActionHandler.php create mode 100644 src/Handler/GridField/URLActionHandler.php rename src/Listener/{ => CMSMain}/CMSMainActionListener.php (56%) create mode 100644 src/Listener/CMSMain/CMSMainContext.php create mode 100644 src/Listener/Form/FormContext.php rename src/Listener/{ => Form}/FormSubmissionListener.php (59%) create mode 100644 src/Listener/GraphQL/GraphQLMiddlewareContext.php create mode 100644 src/Listener/GraphQL/GraphQLMiddlewareListener.php create mode 100644 src/Listener/GraphQL/GraphQLMutationContext.php rename src/Listener/{ => GraphQL}/GraphQLMutationListener.php (57%) delete mode 100644 src/Listener/GraphQLMiddlewareListener.php create mode 100644 src/Listener/GridField/GridFieldAlterationContext.php create mode 100644 src/Listener/GridField/GridFieldAlterationListener.php create mode 100644 src/Listener/GridField/GridFieldContext.php create mode 100644 src/Listener/GridField/GridFieldURLListener.php delete mode 100644 src/Listener/GridFieldAlterationListener.php delete mode 100644 src/Listener/GridFieldURLListener.php create mode 100644 src/Listener/ListenerContext.php diff --git a/README.md b/README.md index 49e62d1..fd994af 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ which is particularly visible in [content blocks](https://github.com/dnadesign/s This module enables the data model. To take full advantage of its core offering, you should install [silverstripe/versioned-snapshot-admin](https://github.com/silverstripe/silverstripe-versioned-snapshot-admin) to expose these snapshots through the "History" tab of the CMS. -WARNING: This module is experimental, and not considered stable. +WARNING: This module is experimental, and not considered stable. ## Installation @@ -38,7 +38,7 @@ BlockPage Ownership between each of those nodes affords publication of the entire graph through one commmand (or click of a button). But it is not apparent to the user what owned content, if any, will -be published. If the Gallery is modified, `BlockPage` will not show a modified state. +be published. If the Gallery is modified, `BlockPage` will not show a modified state. This module aims to make these modification states and implicit edit history more transparent. @@ -75,7 +75,7 @@ on a template to create a human-readable activity feed. Returns an array of `Act The snapshot functionality is provided through the `SnapshotPublishable` extension, which is a drop-in replacement for `RecursivePublishable`. By default, this module will replace -`RecursivePublishable`, which is added to all dataobjects by `silverstripe-versioned`, with +`RecursivePublishable`, which is added to all dataobjects by `silverstripe-versioned`, with this custom subclass. For CMS views, use the `SnapshotSiteTreeExtension` to provide notifications about @@ -83,251 +83,168 @@ owned modification state (WORK IN PROGRESS, POC ONLY) ## How it works -This module comes with two very different work flows. - -* CMS action work flow (default) - trigger is on user action, actions are opt-in with some core actions already available -* model work flow - trigger is on after model write, actions are opt-out - -### CMS action work flow - -#### Static configuration - -This module comes with some CMS actions already provided. This configuration is located in `config.yml` under `snapshot-actions`. -The format is very simple: - -'`identifier`': '`message`' - -Where `identifier` is the action identifier (this is internal name from the component which is responsible for handling the action). -For example for page edit form we have the following rule: - -'`save`': '`Save page`' - -This means each time a user saves a page via page edit form a snapshot will be created with a context message `Save page`. - -This configuration can be overridden via standard configuration API means. - -**I want to add more actions** - -Create following configuration in your project `_config` folder: - -``` -Name: snapshot-custom-actions -After: - - '#snapshot-actions' ---- -SilverStripe\Snapshots\Snapshot: - actions: - # grid field actions (via standard action) - 'togglelayoutwidth': 'Toggle layout width' -``` - -This will add a new action for the `togglelayoutwidth` action and the snapshot message for this action will be `Toggle layout width`. - -**I want to disable a default action** - -``` -Name: snapshot-custom-actions -After: - - '#snapshot-actions' ---- -SilverStripe\Snapshots\Snapshot: - actions: - # GraphQL CRUD - disable default - 'graphql_crud_create': null -``` - -This will disable the action `graphql_crud_create` so no snapshot will be created when this action is executed. - -**I want to add a action but with no message** - -``` -Name: snapshot-custom-actions -After: - - '#snapshot-actions' ---- -SilverStripe\Snapshots\Snapshot: - actions: - # grid field actions (via standard action) - 'togglelayoutwidth': '' -``` - -This will still create a snapshot for the action but no snapshot message will be displayed. - -**I want to change message of existing action** - -``` -Name: snapshot-custom-actions -After: - - '#snapshot-actions' ---- -SilverStripe\Snapshots\Snapshot: - actions: - # GraphQL CRUD - disable default - 'graphql_crud_create': 'My custom message' -``` - -This will create snapshot for the action with your custom message. -Setting empty string as a message will still create the snapshot but with no message. +Snapshots are created in response to user events in the CMS. These events are driven by a simple +pub/sub API in the `SilverStripe\Snapshots\Dispatch\Dispatcher` API. + +### CMS Events and the Dispatcher + +By default, there are a handful +of broad-based CMS events that will trigger handlers that create snapshots. A suite of listeners are added via +extensions to key classes in the admin that then trigger these events through the +`Dispatcher` API, e.g. `Dispatcher::singleton()->trigger('formSubmitted')`. + +#### Event: formSubmitted +* **Description**: Any form submitted in the CMS +* **Example**: save, publish, unpublish, delete +* **Listener**: `SilverStripe\Snapshots\Listener\Form\FormSubmissionListener` +* **Handler**: `SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler` + +#### Event: cmsAction +* **Description**: A `CMSMain` controller action +* **Example**: `savetreenode` (reorder site tree) +* **Listener**: `SilverStripe\Snapshots\Listener\CMSMain\CMSMainActionListener` +* **Handler**: `SilverStripe\Snapshots\Handler\CMSMain\ActionHandler` + +#### Event: gridFieldAlteration +* **Description**: A standard GridField action (`GridField_ActionProvider`) +* **Example**: `handleReorder` (reorder items) +* **Listener**: `SilverStripe\Snapshots\Listener\GridField\GridFieldAlterationListener` +* **Handler**: `SilverStripe\Snapshots\Handler\GridField\AlterationHandler` + +#### Event: gridFieldAction +* **Description**: A GridField action invoked via a URL (`GridField_URLHandler`) +* **Example**: `deleterecord` +* **Listener**: `SilverStripe\Snapshots\Listener\GridField\GridFieldURLListener` +* **Handler**: `SilverStripe\Snapshots\Handler\GridField\URLActionHandler` + +#### Event: graphqlMutation +* **Description**: A scaffolded GraphQL mutation +* **Example**: `mutation createMyDataObject(Input: $Input)` +* **Listener**: `SilverStripe\Snapshots\Listener\GraphQL\GraphQLMutationListener` +* **Handler**: `SilverStripe\Snapshots\Handler\GraphQL\MutationHandler` + +#### Event: graphqlOperation +* **Description**: Any generic GraphQL operation +* **Example**: `mutation publishAllFiles`, `query allTheThings` +* **Listener**: `SilverStripe\Snapshots\Listener\GraphQL\GraphQLMiddlewareListener` +* **Handler**: `SilverStripe\Snapshots\Handler\GraphQL\GenericHandler` + +### Action identifiers + +Each of these handlers is passed a context object that exposes an **action identifier**. This is a string that +provides specific information about what happened in the event that the handler can then use in its implementation. +For instance, if a form was submitted, and the function that handles the form is`doSave($data, $form)`, the action +identifier is `doSave`. Likewise, controller actions, GridField actions, and GraphQL operations are all action +identifiers. + +Events are always called with `eventName.`. For instance `formSubmitted.doSave`, allowing +the subscribers to only react to a specific subset of events. #### How to find your action identifier -Common case is where you want to add a new action configuration but you don't know what your action identifier is. -This really depends on what the component responsible for handling the action is. -The most basic approach is to add temporary logging to start of `SilverStripe\Snapshots\Snapshot::getActionMessage()`. -Every action which is covered by this module (regardless of the configuration) flows through this function. +In the above example, we subscribe to the main event `formSubitted`, but we've added more specificity with `myFormHandler`. +This is the name of the `action` provided in the context of the event. + +The easiest way to debug events is to put breakpoints or logging into the `Dispatcher::trigger()` function. This +will provide all the detail you need about what events are triggered when, and with what context. ``` -public function getActionMessage($identifier): ?string +public function trigger(string $event, ListenerContext $context): void { - error_log($identifier); + error_log($event); + error_log($context->getAction()); ``` When the logging is in place you just go to the CMS and perform the action you are interested in. This should narrow the list of identifier down to a much smaller subset. -#### Runtime overrides - -In case static configuration in not enough, runtime overrides are available. This module comes with following types of listeners: +### Customising the snapshot messages -* Form submissions - actions that comes via form submissions (for example page edit form) -* GraphQL general - actions executed via GraphQL CRUD (for example standard model mutation) -* GraphQL custom - actions executed via GraphQL API (for example custom mutation) -* GridField alter - actions which are implemented via `GridField_ActionProvider` (for example delete item via GridField) -* GridField URL handler - actions which are implemented via `GridField_URLHandler` -* Page `CMSMain` actions - this covers page actions which are now handled by form submissions +By default, these events will trigger the message defined in the language file, e.g. +`_t('SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.HANDLER_publish', 'Publish page')`. However, if you want +to customise this message at the configuration level, simply override the message on the handler class. -Each type of listener provides an extension point which allows the override of the default module behaviour. - -To apply your override you need to first know which listener is handling your action. -Sometimes you can guess based on the action category but using logging may help you determine the listener type more easily. - -Form submissions - `Form\Submission::processAction` - -GraphQL custom - `GraphQL\CustomAction::onAfterCallMiddleware` - -GraphQL general - `GraphQL\GenericAction::afterMutation` +```yaml +SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler: + messages: + publish: 'My publish message' +``` -GridField alter - `GridField\AlterAction::afterCallActionHandler` +In this case "publish" is the **action identifier** (the function that handles the form). -GridField URL handler - `GridField\UrlHandlerAction::afterCallActionURLHandler` +### Customising existing snapshot creators -Page `CMSMain` actions - `Page\CMSMainAction::afterCallActionHandler` +All of the handlers are registered with injector, so the simplest way to customise them is to override their +definitions in the configuration. -Once you know listener type and the action identifier you need to create an extension which is a subclass of one of the abstract listener handlers. -Abstract listener depends on your listener type. +For instance, if you have something custom you with a snapshot when a page is saved: -Form submissions - `Form\SubmissionListenerAbstract` +```php +use SilverStripe\Snapshots\Handler\Form\SaveHandler; +use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Snapshot; -GraphQL custom - `GraphQL\CustomActionListenerAbstract` +class MySaveHandler extends SaveHandler +{ + protected function createSnapshot(ListenerContext $context): ?Snapshot + { + //... + } +} +``` -GraphQL general - `GraphQL\GenericActionListenerAbstract` +```yaml +SilverStripe\Core\Injector\Injector: + SilverStripe\Snapshots\Handler\Form\SaveHandler: + class: MyProject\MySaveHandler +``` -GridField alter - `GridField\AlterActionListenerAbstract` +### Adding snapshot creators -GridField URL handler - `GridField\UrlHandlerActionListenerAbstract` +If you have custom actions or form handlers you've added to the CMS, you might want to either ensure their tracked +by the default snapshot creators, or maybe even build your own snapshot creator for them. In this case, you can +use the declarative API on `Dispatcher` to subscribe to the events you need. -Page `CMSMain` actions - `Page\CMSMainListenerAbstract` +Let's say we have a form that submits to a function: `public function myFormHandler($data, $form)`. -**Example implementation** +```yaml +SilverStripe\Core\Injector\Injector: + SilverStripe\Snapshots\Dispatch\Dispatcher: + properties: + handlers: + - + on: [ formSubmitted.myFormHandler ] + handler: %$MyProject\Handlers\MyHandler +``` -config +#### Removing snapshot creators -``` -SilverStripe\Snapshots\Snapshot: - extensions: - - App\Snapshots\Listener\MutationUpdateLayoutBlockGroup -``` +The configuration API doesn't make it easy to remove items from arrays, so this is best done procedurally. -extension +You can register a `EventHandlerLoader` implementation with `Dispatcher` to procedurally register and unregister +events. +```yaml +SilverStripe\Core\Injector\Injector: + SilverStripe\Snapshots\Dispatch\Dispatcher: + constructor: + myLoader: %$MyProject\MyEventLoader ``` -exists()) { - return false; - } - - Snapshot::singleton()->createSnapshotFromAction($page, $block, $message); - - return true; + $dispatcher->removeListenerByClassName('formSubmitted.save', SaveHandler::class); } } - ``` -`getActionName` is the action identifier - -`CustomActionListenerAbstract` is the parent class because this action is a custom mutation - -Returning `false` inside `processAction` makes the module fallback to default behaviour. - -Returning `true` inside `processAction` makes the module skip the default behaviour. - -If you return `true` it's up to you to create the snapshot. -This covers the case where the action uses custom data and it's impossible for the module to figure out the origin object. -Use this approach when you are unhappy with the default behaviour and you know the way how to find the origin object from the data. -Note that the context data available is different for each listener type as the context is different. - #### Snapshot creation API To cover all cases, this module allows you to invoke snapshot creation in any part of your code outside of normal action flow. @@ -384,27 +301,6 @@ Passing the layout block through allows the layout block to display it's own ver This feature may have marginal use and it's ok to skip it. -### Model work flow - -When a dataobject is written, an `onAfterWrite` handler opens a snapshot by writing -a new `VersionedSnapshot` record. As long as this snapshot is open, any successive dataobject -writes will add themselves to the open snapshot, on the `VersionedSnapshotItem` table. The dataobject -that opens the snapshot is stored as the `Origin` on the `VersionedSnapshot` table (a polymorphic `has_one`). -It then looks up the ownership chain using `findOwners()` and puts each of its owners into the snapshot. - -Each snapshot item contains its version, class, and ID at the time of the snapshot. This -provides enough information to query what snapshots a given dataobject was involved in since -a given version or date. - -For the most part, the snapshot tables are considered immutable historical records, but there -are a few cases when snapshots are retroactively updated - -* When changes are reverted to live, any snapshots those changes made are deleted. -* When the ownership structure is changed, the previous owners are surgically removed -from the graph and the new ones stitched in. - - - ## Versioning This library follows [Semver](http://semver.org). According to Semver, diff --git a/_config/config.yml b/_config/config.yml index e70ebfb..11d7da9 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -27,135 +27,58 @@ Name: snapshot-listeners --- SilverStripe\CMS\Controllers\CMSMain: extensions: - - SilverStripe\Snapshots\Listener\Page\CMSMainAction + - SilverStripe\Snapshots\Listener\CMSMain\CMSMainActionListener SilverStripe\Forms\GridField\GridField: extensions: - - SilverStripe\Snapshots\Listener\GridField\AlterAction - - SilverStripe\Snapshots\Listener\GridField\UrlHandlerAction + - SilverStripe\Snapshots\Listener\GridField\GridFieldAlterationListener + - SilverStripe\Snapshots\Listener\GridField\GridFieldURLListener SilverStripe\Forms\FormRequestHandler: extensions: - - SilverStripe\Snapshots\Listener\Form\Submission + - SilverStripe\Snapshots\Listener\Form\FormSubmissionListener SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Create: extensions: - - SilverStripe\Snapshots\Listener\GraphQL\GenericAction + - SilverStripe\Snapshots\Listener\GraphQL\GraphQLMutationListener SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Delete: extensions: - - SilverStripe\Snapshots\Listener\GraphQL\GenericAction + - SilverStripe\Snapshots\Listener\GraphQL\GraphQLMutationListener SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Update: extensions: - - SilverStripe\Snapshots\Listener\GraphQL\GenericAction + - SilverStripe\Snapshots\Listener\GraphQL\GraphQLMutationListener SilverStripe\GraphQL\Manager: extensions: - - SilverStripe\Snapshots\Listener\GraphQL\CustomAction + - SilverStripe\Snapshots\Listener\GraphQL\GraphQLMiddlewareListener --- -Name: snapshot-actions +Name: snapshot-events --- SilverStripe\Core\Injector\Injector: - # Form submission handlers - SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.publish: - class: SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler - constructor: - message: 'Publish page' - handlerName: 'publish' - SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.unpublish: - class: SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler - constructor: - message: 'Unpublish page' - handlerName: 'unpublish' - SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.gridFieldSave: - class: SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler - constructor: - message: 'Save item' - handlerName: 'doSave' - SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.gridFieldDelete: - class: SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler - constructor: - message: 'Delete item' - handlerName: 'doDelete' - - # CMS action handlers - SilverStripe\Snapshots\Handler\CMSMain\ActionHandler.savetreenode: - class: SilverStripe\Snapshots\Handler\CMSMain\ActionHandler - constructor: - action: 'savetreenode' - message: 'Changed site tree position' - - # GridField action handlers - SilverStripe\Snapshots\Handler\GridField\ActionHandler.deleterecord: - class: SilverStripe\Snapshots\Handler\GridField\ActionHandler - constructor: - action: 'deleterecord' - message: 'Delete item' - - # GridField manipulator handlers - SilverStripe\Snapshots\Handler\GridField\AlterationHandler.handleReorder: - class: SilverStripe\Snapshots\Handler\GridField\AlterationHandler - constructor: - action: 'handleReorder' - message: 'Changed items order' - - # GraphQL mutation handlers - SilverStripe\Snapshots\Handler\GraphQL\MutationHandler.create: - class: SilverStripe\Snapshots\Handler\GraphQL\MutationHandler - constructor: - mutationType: 'create' - SilverStripe\Snapshots\Handler\GraphQL\MutationHandler.update: - class: SilverStripe\Snapshots\Handler\GraphQL\MutationHandler - constructor: - mutationType: 'update' - SilverStripe\Snapshots\Handler\GraphQL\MutationHandler.delete: - class: SilverStripe\Snapshots\Handler\GraphQL\MutationHandler - constructor: - mutationType: 'delete' - - SilverStripe\Snapshots\Dispatch\Dispatcher: properties: handlers: - # Form submission handlers - saveHandler: - event: 'formSubmitted' + - + on: [ formSubmitted.unpublish, formSubmitted.doSave, formSubmitted.doDelete] + handler: %$SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler + - + on: [ formSubmitted.save ] handler: %$SilverStripe\Snapshots\Handler\Form\SaveHandler - publishHandler: - event: 'formSubmitted' - handler: %$SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.publish - unpublishHandler: - event: 'formSubmitted' - handler: %$SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.unpublish - gridFieldSaveHandler: - event: 'formSubmitted' - handler: %$SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.gridFieldSave - gridFieldDeleteHandler: - event: 'formSubmitted' - handler: %$SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler.gridFieldDelete - - # CMSMain actions - reorderSiteTreeHandler: - event: 'cmsAction' - handler: %$SilverStripe\Snapshots\Handler\CMSMain\ActionHandler.savetreenode - - # GridField URL providers - deleteRecordHandler: - event: 'gridFieldAction' - handler: %$SilverStripe\Snapshots\Handler\GridField\ActionHandler.deleterecord - - # GridField action providers - reorderHandler: - event: 'gridFieldAlteration' - handler: %$SilverStripe\Snapshots\Handler\GridField\AlterationHandler.handleReorder - graphqlCreate: - event: 'graphqlMutation' - handler: %$SilverStripe\Snapshots\Handler\GraphQL\MutationHandler.create - graphqlUpdate: - event: 'graphqlMutation' - handler: %$SilverStripe\Snapshots\Handler\GraphQL\MutationHandler.update - graphqlDelete: - event: 'graphqlMutation' - handler: %$SilverStripe\Snapshots\Handler\GraphQL\MutationHandler.delete + - + on: [ formSubmitted.publish ] + handler: %$SilverStripe\Snapshots\Handler\Form\PublishHandler + - + on: [ cmsAction.savetreenode ] + handler: %$SilverStripe\Snapshots\Handler\CMSMain\ActionHandler + - + on: [ gridFieldAction.deleterecord ] + handler: %$SilverStripe\Snapshots\Handler\GridField\URLActionHandler + - + on: [ gridFieldAlteration.handleReorder ] + handler: %$SilverStripe\Snapshots\Handler\GridField\AlterationHandler + - + on: [ graphqlMutation.create, graphqlMutation.update, graphqlMutation.delete ] + handler: %$SilverStripe\Snapshots\Handler\GraphQL\MutationHandler diff --git a/lang/en.yml b/lang/en.yml new file mode 100644 index 0000000..90cb807 --- /dev/null +++ b/lang/en.yml @@ -0,0 +1,20 @@ +en: + SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler: + HANDLER_publish: 'Publish page' + HANDLER_unpublish: 'Unpublish page' + HANDLER_doSave: 'Save item' + HANDLER_doDelete: 'Delete item' + SilverStripe\Snapshots\Handler\Form\SaveHandler: + HANDLER_save: 'Save page' + SilverStripe\Snapshots\Handler\CMSMain\ActionHandler: + HANDLER_savetreenode: 'Reordered site tree' + HANDLER_publish: 'Publish page' + HANDLER_unpublish: 'Unpublish page' + SilverStripe\Snapshots\Handler\GraphQL\MutationHandler: + HANDLER_graphql_crud_create: 'GraphQL create' + HANDLER_graphql_crud_update: 'GraphQL update' + HANDLER_graphql_crud_delete: 'GraphQL delete' + SilverStripe\Snapshots\Handler\GridField\ActionHandler: + HANDLER_deleterecord: 'Delete record' + SilverStripe\Snapshots\Handler\GridField\AlterationHandler: + HANDLER_handleReorder: 'Reorder items' diff --git a/src/Dispatch/Context.php b/src/Dispatch/Context.php deleted file mode 100644 index 37f6984..0000000 --- a/src/Dispatch/Context.php +++ /dev/null @@ -1,25 +0,0 @@ -data = $data; - } - - public function __get(string $name) - { - return $this->get(name); - } - - public function get(string $name) - { - return $this->data[$name] ?? null; - } -} diff --git a/src/Dispatch/Dispatcher.php b/src/Dispatch/Dispatcher.php index d8f25ff..9ded3dd 100644 --- a/src/Dispatch/Dispatcher.php +++ b/src/Dispatch/Dispatcher.php @@ -2,34 +2,73 @@ namespace SilverStripe\Snapshots\Dispatch; +use SilverStripe\Core\Injector\Injectable; use SilverStripe\Snapshots\Handler\HandlerInterface; +use InvalidArgumentException; +use Exception; +use SilverStripe\Snapshots\Listener\ListenerContext; class Dispatcher { + use Injectable; + /** * @var array HandlerInterface[] */ private $handlers = []; + /** + * Dispatcher constructor. + * @param EventHandlerLoader[] $loaders + */ + public function __construct($loaders = []) + { + foreach ($loaders as $loader) { + if (!$loader instanceof EventHandlerLoader) { + throw new InvalidArgumentException(sprintf( + '%s not passed an instance of %s', + __CLASS__, + EventHandlerLoader::class + )); + } + + $loader->addToDispatcher($this); + } + } + /** * @param array $handlers + * @throws Exception */ public function setHandlers(array $handlers) { foreach ($handlers as $spec) { - list ($eventName, $handler) = $spec; + if (!isset($spec['handler']) || !isset($spec['on'])) { + throw new InvalidArgumentException('Event handlers must have a "on" and "handler" nodes'); + } + $on = is_array($spec['on']) ? $spec['on'] : [$spec['on']]; + $handler = $spec['handler']; + if (!$handler instanceof HandlerInterface) { throw new InvalidArgumentException(sprintf( 'Handler for %s is not an instance of %s', - $event, + implode(', ', $on), HandlerInterface::class )); } - $this->addListener($event, $handler); + foreach ($on as $eventName) { + $this->addListener($eventName, $handler); + } } } + /** + * @param string $event + * @param HandlerInterface $handler + * @return $this + * @throws Exception + */ public function addListener(string $event, HandlerInterface $handler): self { if (!isset($this->handlers[$event])) { @@ -49,23 +88,49 @@ public function addListener(string $event, HandlerInterface $handler): self return $this; } + /** + * @param string $event + * @param HandlerInterface $handler + * @return $this + */ public function removeListener(string $event, HandlerInterface $handler): self { $handlers = $this->handlers[$event] ?? []; - /* @var HandlerInterface $handler */ - $this->handlers = array_filter(function ($existing) use ($handler) { - return $handler !== $existing; - }, $this->handlers); + $this->handlers = array_filter($handlers, function ($existing) use ($handler) { + return $existing !== $handler; + }); return $this; } - public function trigger(string $event, Context $context): void + /** + * @param string $event + * @param string $className + * @return $this + */ + public function removeListenerByClassName(string $event, string $className): self { $handlers = $this->handlers[$event] ?? []; - /* @var HandlerInterface $handler */ - foreach ($handlers as $handler) { - if ($handler->shouldFire($context)) { + $this->handlers = array_filter($handlers, function ($existing) use ($className) { + return get_class($existing) !== $className; + }); + + return $this; + } + + /** + * @param string $event + * @param ListenerContext $context + */ + public function trigger(string $event, ListenerContext $context): void + { + $action = $context->getAction(); + // First fire listeners to , then just fire generic listeners + $eventsToFire = [ $event . '.' . $action, $event]; + foreach ($eventsToFire as $event) { + $handlers = $this->handlers[$event] ?? []; + /* @var HandlerInterface $handler */ + foreach ($handlers as $handler) { $handler->fire($context); } } diff --git a/src/Dispatch/EventHandlerLoader.php b/src/Dispatch/EventHandlerLoader.php new file mode 100644 index 0000000..15ddc26 --- /dev/null +++ b/src/Dispatch/EventHandlerLoader.php @@ -0,0 +1,10 @@ +action = $action; - $this->message = $message; - } - - public function shouldFire(Context $context): bool + /** + * @param ListenerContext $context + * @return Snapshot|null + * @throws ValidationException + */ + protected function createSnapshot(ListenerContext $context): ?Snapshot { - return ( - parent::shouldFire($context) && - $context->action === $this->action - ); - } - - public function fire(Context $context): void - { - $message = $this->getMessage(); - - if (!$context->result instanceof HTTPResponse) { - return; + /* @var CMSMainContext $context */ + $action = $context->getAction(); + /* @var HTTPResponse $result */ + $result = $context->getResult(); + if (!$result instanceof HTTPResponse) { + return null; } - - if ((int) $context->result->getStatusCode() !== 200) { - return; + if ((int) $result->getStatusCode() !== 200) { + return null; } - $className = $context->treeClass; - $id = (int) $context->id; + $className = $context->getTreeClass(); + $id = (int) $context->getId(); if (!$id) { - return; + return null; } /** @var SiteTree $page */ $page = DataObject::get_by_id($className, $id); if ($page === null) { - return; + return null; } - $snapshot->createSnapshotFromAction($page, null, $message); + $message = $this->getMessage($action); + + return Snapshot::singleton()->createSnapshotFromAction($page, null, $message); } } diff --git a/src/Handler/Form/FormSubmissionHandler.php b/src/Handler/Form/FormSubmissionHandler.php index c9e701c..90d739b 100644 --- a/src/Handler/Form/FormSubmissionHandler.php +++ b/src/Handler/Form/FormSubmissionHandler.php @@ -4,91 +4,43 @@ namespace SilverStripe\Snapshots\Handler\Form; -use SilverStripe\Admin\LeftAndMain; -use SilverStripe\Snapshots\Dispatch\Context; +use SilverStripe\CMS\Model\SiteTree; +use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; -use SilverStripe\Snapshots\Listener\CurrentPage; +use SilverStripe\Snapshots\Listener\Form\FormContext; +use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Snapshot; -class FormSubmissionHandler extends HandlerAbstract implements HandlerInterface +class FormSubmissionHandler extends HandlerAbstract { - - /** - * @var string - */ - protected $message; - - /** - * @var string - */ - protected $formHandlerName; - /** - * @var string + * @param ListenerContext $context + * @return Snapshot|null + * @throws ValidationException */ - protected $formName = 'EditForm'; - - /** - * @var string - */ - protected $controllerClass = LeftAndMain::class; + protected function createSnapshot(ListenerContext $context): ?Snapshot + { + /* @var FormContext $context */ + $action = $context->getAction(); + $page = $this->getPage($context); + $record = $context->getForm()->getRecord(); - /** - * FormSubmissionSnapshotHandler constructor. - * @param string $message - * @param string $formHandlerName - * @param string $formName - * @param string $controllerClass - */ - public function __construct( - string $message, - string $formHandlerName, - string $formName = 'EditForm', - string $controllerClass = LeftAndMain::class - ) { - $this->message = $message; - $this->formHandlerName = $formHandlerName; - $this->formName = $formName; - $this->controllerClass = $controllerClass; - } + if ($page === null || $record === null) { + return null; + } - public function getMessage(): string - { - return $this->message; - } + $message = $this->getMessage($action); - /** - * @param Context $context - * @return bool - */ - public function shouldFire(Context $context): bool - { - return ( - parent::shouldFire($context) && - $context->form->getController() instanceof $this->controllerClass && - $context->form->getName() === $this->formName && - $context->handlerName == $this->formHandlerName - ); + return Snapshot::singleton()->createSnapshotFromAction($page, $record, $message); } /** - * @param Context $context + * @param FormContext $context + * @return SiteTree|null */ - public function fire(Context $context): void + protected function getPage(FormContext $context): ?SiteTree { - $message = $this->getMessage(); - $record = $context->form->getRecord(); - - if ($record === null) { - return; - } - - $url = $context->request->getURL(); - $page = $this->getCurrentPageFromRequestUrl($url); - - if ($page === null) { - return; - } - - $snapshot->createSnapshotFromAction($page, $record, $message); + $url = $context->getRequest()->getURL(); + return $this->getCurrentPageFromRequestUrl($url); } } diff --git a/src/Handler/Form/PublishHandler.php b/src/Handler/Form/PublishHandler.php new file mode 100644 index 0000000..a33db0c --- /dev/null +++ b/src/Handler/Form/PublishHandler.php @@ -0,0 +1,29 @@ +getForm()->getName() === 'EditForm') { + foreach ($snapshot->Items() as $item) { + $item->WasPublished = true; + $item->write(); + } + } + } + + return $snapshot; + } +} diff --git a/src/Handler/Form/SaveHandler.php b/src/Handler/Form/SaveHandler.php index a923a03..c91064a 100644 --- a/src/Handler/Form/SaveHandler.php +++ b/src/Handler/Form/SaveHandler.php @@ -4,32 +4,26 @@ namespace SilverStripe\Snapshots\Handler\Form; -use SilverStripe\Snapshots\Dispatch\Context; +use SilverStripe\ORM\ValidationException; +use SilverStripe\Snapshots\Listener\Form\FormContext; +use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Snapshot; class SaveHandler extends FormSubmissionHandler { /** - * @var string + * @param ListenerContext $context + * @return Snapshot|null + * @throws ValidationException */ - protected $formHandlerName = 'save'; - - /** - * @return string - */ - protected function getMessage(): string - { - return _t('Snapshots.HANDLER_SAVE', 'Save page'); - } - - /** - * @param Context $context - * @return bool - */ - public function shouldFire(Context $context): bool + protected function createSnapshot(ListenerContext $context): ?Snapshot { - $url = $context->request->getURL(); - $page = $this->getCurrentPageFromRequestUrl($url); + /* @var FormContext $context */ + $page = $this->getPage($context); + if (!$page || !$page->isModifiedOnDraft()) { + return null; + } - return parent::shouldFire($context) && $page && $page->isModifiedOnDraft(); + return parent::createSnapshot($context); } } diff --git a/src/Handler/GraphQL/GenericHandler.php b/src/Handler/GraphQL/GenericHandler.php new file mode 100644 index 0000000..8fad6b4 --- /dev/null +++ b/src/Handler/GraphQL/GenericHandler.php @@ -0,0 +1,35 @@ +getAction(); + $message = $this->getMessage($action); + $page = $this->getPageFromReferrer(); + + if ($page === null) { + return null; + } + + return Snapshot::singleton()->createSnapshotFromAction($page, null, $message); + } + + +} diff --git a/src/Handler/GraphQL/MiddlewareHandler.php b/src/Handler/GraphQL/MiddlewareHandler.php deleted file mode 100644 index eeb8386..0000000 --- a/src/Handler/GraphQL/MiddlewareHandler.php +++ /dev/null @@ -1,98 +0,0 @@ -actionType = $actionType; - } - - /** - * @param Context $context - * @return bool - */ - public function shouldFire(Context $context): bool - { - $action = $this->getActionType($context->query); - return ( - parent::shouldFire($context) && - $action && - $action === $this->actionType - ); - } - - /** - * @param Context $context - * @throws \SilverStripe\ORM\ValidationException - */ - public function fire(Context $context): void - { - $message = $this->getMessage(); - $controller = Controller::curr(); - - if (!$controller) { - return; - } - - $request = $controller->getRequest(); - - if (!$request) { - return; - } - - $url = $request->getHeader('referer'); - $url = parse_url($url, PHP_URL_PATH); - $url = ltrim($url, '/'); - $page = $this->getCurrentPageFromRequestUrl($url); - - if ($page === null) { - return; - } - - $snapshot->createSnapshotFromAction($page, null, $message); - } - - /** - * Extract action type from query - * - * @param string $query - * @return string|null - */ - private function getActionType(string $query): ?string - { - $action = explode('(', $query); - - if (count($action) === 0) { - return null; - } - - $action = array_shift($action); - - if (!$action) { - return null; - } - - $action = str_replace(' ', '_', $action); - - return $action; - } - -} diff --git a/src/Handler/GraphQL/MutationHandler.php b/src/Handler/GraphQL/MutationHandler.php index 4437805..ae14635 100644 --- a/src/Handler/GraphQL/MutationHandler.php +++ b/src/Handler/GraphQL/MutationHandler.php @@ -4,96 +4,35 @@ namespace SilverStripe\Snapshots\Handler\GraphQL; -use SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Create; -use SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Delete; -use SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Update; -use SilverStripe\GraphQL\Scaffolding\Scaffolders\OperationScaffolder; -use SilverStripe\Snapshots\Dispatch\Context; +use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; -use SilverStripe\Snapshots\Handler\HandlerInterface; -use SilverStripe\Snapshots\Listener\CurrentPage; +use SilverStripe\Snapshots\Listener\GraphQL\GraphQLMutationContext; +use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Snapshot; -class MutationHandler extends HandlerAbstract implements HandlerInterface +class MutationHandler extends HandlerAbstract { - - const TYPE_CREATE = 'create'; - const TYPE_DELETE = 'delete'; - const TYPE_UPDATE = 'update'; const ACTION_PREFIX = 'graphql_crud_'; /** - * @var string - */ - protected $mutationType; - - /** - * MutationHandler constructor. - * @param string $mutationType + * @param ListenerContext $context + * @return Snapshot|null + * @throws ValidationException */ - public function __construct(string $mutationType) - { - $this->mutationType = $mutationType; - } - - public function shouldFire(Context $context): bool - { - $type = $this->getActionType($context->mutation); - - return ( - parent::shouldFire($context) && - $type && - $type === $this->mutationType - ); - } - - public function fire(Context $context): void + protected function createSnapshot(ListenerContext $context): ?Snapshot { + /* @var GraphQLMutationContext $context */ + $type = $context->getAction(); $action = static::ACTION_PREFIX . $type; - $message = $this->getMessage(); - - $controller = Controller::curr(); - - if (!$controller) { - return; - } - - $request = $controller->getRequest(); - - if (!$request) { - return; - } - - $url = $request->getHeader('referer'); - $url = parse_url($url, PHP_URL_PATH); - $url = ltrim($url, '/'); - $page = $this->getCurrentPageFromRequestUrl($url); + $message = $this->getMessage($action); + $page = $this->getPageFromReferrer(); if ($page === null) { - return; + return null; } - $snapshot->createSnapshotFromAction($page, null, $message); + return Snapshot::singleton()->createSnapshotFromAction($page, null, $message); } - /** - * @param OperationScaffolder $scaffolder - * @return string|null - */ - private function getActionType(OperationScaffolder $scaffolder): ?string - { - if ($scaffolder instanceof Create) { - return static::TYPE_CREATE; - } - - if ($scaffolder instanceof Delete) { - return static::TYPE_DELETE; - } - - if ($scaffolder instanceof Update) { - return static::TYPE_UPDATE; - } - - return null; - } } diff --git a/src/Handler/GridField/ActionHandler.php b/src/Handler/GridField/ActionHandler.php deleted file mode 100644 index bdf5d59..0000000 --- a/src/Handler/GridField/ActionHandler.php +++ /dev/null @@ -1,71 +0,0 @@ -action === $context->action - ); - } - - public function __construct(string $action, $message) - { - $this->action = $action; - $this->message = $message; - } - - public function getMessage(): string - { - return $this->message; - } - - public function fire(Context $context): void - { - $message = $this->getMessage(); - $form = $owner->getForm(); - - if (!$form) { - return; - } - - $record = $form->getRecord(); - - if (!$record) { - return; - } - - $page = $this->getCurrentPageFromController($form); - - if ($page === null) { - return; - } - - // attempt to create a custom snapshot first - $customSnapshot = $snapshot->gridFieldUrlActionSnapshot($page, $action, $message, $owner); - - if ($customSnapshot) { - return; - } - - // fall back to default snapshot - $snapshot->createSnapshotFromAction($page, $record, $message); - - } -} diff --git a/src/Handler/GridField/AlterationHandler.php b/src/Handler/GridField/AlterationHandler.php index b6d5938..88ad551 100644 --- a/src/Handler/GridField/AlterationHandler.php +++ b/src/Handler/GridField/AlterationHandler.php @@ -3,95 +3,44 @@ namespace SilverStripe\Snapshots\Handler\GridField; - -use SilverStripe\Core\Injector\Injector; -use SilverStripe\Forms\GridField\FormAction\StateStore; -use SilverStripe\Forms\GridField\GridField; -use SilverStripe\Snapshots\Dispatch\Context; +use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; -use SilverStripe\Snapshots\Handler\HandlerInterface; -use SilverStripe\Snapshots\Listener\CurrentPage; +use SilverStripe\Snapshots\Listener\GridField\GridFieldContext; +use SilverStripe\Snapshots\Listener\ListenerContext; use SilverStripe\Snapshots\Snapshot; -class AlterationHandler extends HandlerAbstract implements HandlerInterface +class AlterationHandler extends HandlerAbstract { - /** - * @param Context $context - * @return bool + * @param ListenerContext $context + * @return Snapshot|null + * @throws ValidationException */ - public function shouldFire(Context $context): bool - { - return ( - parent::shouldFire($context) && - in_array($action, ['index', 'gridFieldAlterAction']) - ); - } - - public function fire(Context $context): void + protected function createSnapshot(ListenerContext $context): ?Snapshot { - $requestData = $request->requestVars(); - $actionData = $this->getActionData($requestData, $context->gridField); - - if ($actionData === null) { - return; - } - - list ($identifier, $arguments, $data) = $actionData; - - $message = $this->getMessage(); - $form = $owner->getForm(); + /* @var GridFieldContext $context */ + $action = $context->getAction(); + $message = $this->getMessage($action); + $form = $context->getGridField()->getForm(); if (!$form) { - return; + return null; } $record = $form->getRecord(); if (!$record) { - return; + return null; } $page = $this->getCurrentPageFromController($form); if ($page === null) { - return; + return null; } - $snapshot->createSnapshotFromAction($page, $record, $message); + return Snapshot::singleton()->createSnapshotFromAction($page, $record, $message); } - /** - * @param array $data - * @return array|null - */ - private function getActionData(array $data, GridField $gridField): ?array - { - // Fetch the store for the "state" of actions (not the GridField) - /** @var StateStore $store */ - $store = Injector::inst()->create(StateStore::class . '.' . $gridField->getName()); - - foreach ($data as $dataKey => $dataValue) { - if (!preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) { - continue; - } - - $stateChange = $store->load($matches[1]); - - $actionName = $stateChange['actionName']; - $arguments = array_key_exists('args', $stateChange) ? $stateChange['args'] : []; - $arguments = is_array($arguments) ? $arguments : []; - - if ($actionName) { - return [ - $actionName, - $arguments, - $data, - ]; - } - } - - return null; - } } diff --git a/src/Handler/GridField/URLActionHandler.php b/src/Handler/GridField/URLActionHandler.php new file mode 100644 index 0000000..e8f0c98 --- /dev/null +++ b/src/Handler/GridField/URLActionHandler.php @@ -0,0 +1,44 @@ +getAction(); + $message = $this->getMessage($action); + $form = $context->getGridField()->getForm(); + + if (!$form) { + return null; + } + + $record = $form->getRecord(); + + if (!$record) { + return null; + } + + $page = $this->getCurrentPageFromController($form); + + if ($page === null) { + return null; + } + + return Snapshot::singleton()->createSnapshotFromAction($page, $record, $message); + } +} diff --git a/src/Handler/HandlerAbstract.php b/src/Handler/HandlerAbstract.php index fc1e058..3961e09 100644 --- a/src/Handler/HandlerAbstract.php +++ b/src/Handler/HandlerAbstract.php @@ -4,22 +4,47 @@ namespace SilverStripe\Snapshots\Handler; +use SilverStripe\Core\Config\Config; +use SilverStripe\Core\Config\Configurable; use SilverStripe\Snapshots\Dispatch\Context; use SilverStripe\Snapshots\Listener\CurrentPage; +use SilverStripe\Snapshots\Listener\ListenerContext; use SilverStripe\Snapshots\Snapshot; -abstract class HandlerAbstract +abstract class HandlerAbstract implements HandlerInterface { use CurrentPage; + use Configurable; - abstract public function getMessage(): string; + /** + * @var array + * @config + */ + private static $messages = []; /** - * @param Context $context - * @return bool + * @param string $action + * @return string */ - public function shouldFire(Context $context): bool + protected function getMessage(string $action): string { - return Snapshot::singleton()->isActionTriggerActive(); + $messages = $this->config()->get('messages'); + if (isset($messages[$action])) { + return $messages[$action]; + } + + $key = static::class . '.HANDLER_' . $action; + return _t($key, $action); } + + public function fire(ListenerContext $context): void + { + $this->createSnapshot($context); + } + + /** + * @param ListenerContext $context + * @return Snapshot|null + */ + abstract protected function createSnapshot(ListenerContext $context): ?Snapshot; } diff --git a/src/Handler/HandlerInterface.php b/src/Handler/HandlerInterface.php index 063d315..9321fba 100644 --- a/src/Handler/HandlerInterface.php +++ b/src/Handler/HandlerInterface.php @@ -4,9 +4,10 @@ namespace SilverStripe\Snapshots\Handler; -use SilverStripe\Snapshots\Dispatch\Context; +use SilverStripe\Snapshots\Listener\ListenerContext; interface HandlerInterface { - public function fire(Context $context): void; + public function fire(ListenerContext $context): void; + } diff --git a/src/Listener/CMSMainActionListener.php b/src/Listener/CMSMain/CMSMainActionListener.php similarity index 56% rename from src/Listener/CMSMainActionListener.php rename to src/Listener/CMSMain/CMSMainActionListener.php index 5d8597c..c6874bd 100644 --- a/src/Listener/CMSMainActionListener.php +++ b/src/Listener/CMSMain/CMSMainActionListener.php @@ -1,6 +1,6 @@ trigger('cmsAction', new Context([ - 'action' => $action, - 'result' => $result, - 'treeClass' => $this->owner->config()->get('tree_class'), - 'id' => $request->requestVar('ID'), - ])); + public function afterCallActionHandler(HTTPRequest $request, $action, $result): void { + Dispatcher::singleton()->trigger( + 'cmsAction', + new CMSMainContext( + $action, + $result, + $this->owner->config()->get('tree_class'), + $request->requestVar('ID') + ) + ); } } diff --git a/src/Listener/CMSMain/CMSMainContext.php b/src/Listener/CMSMain/CMSMainContext.php new file mode 100644 index 0000000..8a1a914 --- /dev/null +++ b/src/Listener/CMSMain/CMSMainContext.php @@ -0,0 +1,76 @@ +action = $action; + $this->result = $result; + $this->treeClass = $treeClass; + $this->id = $id; + } + + /** + * @return string + */ + public function getAction(): string + { + return $this->action; + } + + /** + * @return null + */ + public function getResult() + { + return $this->result; + } + + /** + * @return string|null + */ + public function getTreeClass(): ?string + { + return $this->treeClass; + } + + /** + * @return string|null + */ + public function getId(): ?string + { + return $this->id; + } +} diff --git a/src/Listener/CurrentPage.php b/src/Listener/CurrentPage.php index d7f9068..f9386e2 100644 --- a/src/Listener/CurrentPage.php +++ b/src/Listener/CurrentPage.php @@ -6,6 +6,9 @@ use SilverStripe\Admin\AdminRootController; use SilverStripe\CMS\Controllers\CMSMain; use SilverStripe\CMS\Controllers\CMSPageEditController; +use SilverStripe\CMS\Model\SiteTree; +use SilverStripe\Control\Controller; +use SilverStripe\Control\HTTPRequest; use SilverStripe\Forms\Form; use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest; use SilverStripe\ORM\DataObject; @@ -24,7 +27,7 @@ trait CurrentPage * @param mixed $controller * @return Page|null */ - private function getCurrentPageFromController($controller): ?Page + protected function getCurrentPageFromController($controller): ?Page { while ($controller && ($controller instanceof Form || $controller instanceof GridFieldDetailForm_ItemRequest)) { $controller = $controller->getController(); @@ -101,4 +104,27 @@ protected function getCurrentPageFromRequestUrl(?string $url): ?Page return $page; } + + /** + * @return HTTPRequest|null + */ + protected function getCurrentRequest(): ?HTTPRequest + { + return Controller::has_curr() ? Controller::curr()->getRequest() : null; + } + + /** + * @return SiteTree|null + */ + protected function getPageFromReferrer(): ?SiteTree + { + $request = $this->getCurrentRequest(); + if (!$request) { + return null; + } + $url = $request->getHeader('referer'); + $url = parse_url($url, PHP_URL_PATH); + $url = ltrim($url, '/'); + return $this->getCurrentPageFromRequestUrl($url); + } } diff --git a/src/Listener/Form/FormContext.php b/src/Listener/Form/FormContext.php new file mode 100644 index 0000000..8de230e --- /dev/null +++ b/src/Listener/Form/FormContext.php @@ -0,0 +1,81 @@ +action = $action; + $this->form = $form; + $this->request = $request; + $this->vars = $vars; + } + + /** + * @return string + */ + public function getAction(): string + { + return $this->action; + } + + /** + * @return Form|null + */ + public function getForm(): ?Form + { + return $this->form; + } + + /** + * @return HTTPRequest|null + */ + public function getRequest(): ?HTTPRequest + { + return $this->request; + } + + /** + * @return array + */ + public function getVars(): array + { + return $this->vars; + } + + +} diff --git a/src/Listener/FormSubmissionListener.php b/src/Listener/Form/FormSubmissionListener.php similarity index 59% rename from src/Listener/FormSubmissionListener.php rename to src/Listener/Form/FormSubmissionListener.php index 9c603c4..9b77801 100644 --- a/src/Listener/FormSubmissionListener.php +++ b/src/Listener/Form/FormSubmissionListener.php @@ -1,16 +1,12 @@ trigger('formSubmitted', new Context([ - 'handlerName' => $funcName, - 'form' => $form, - 'request' => $request - ]); + Dispatcher::singleton()->trigger('formSubmitted', new FormContext($funcName, $form, $request, $vars)); } } diff --git a/src/Listener/GraphQL/GraphQLMiddlewareContext.php b/src/Listener/GraphQL/GraphQLMiddlewareContext.php new file mode 100644 index 0000000..c14b68e --- /dev/null +++ b/src/Listener/GraphQL/GraphQLMiddlewareContext.php @@ -0,0 +1,113 @@ +query = $query; + $this->schema = $schema; + $this->graphqlContext = $graphqlContext; + $this->params = $params; + } + + /** + * @return string + */ + public function getQuery(): string + { + return $this->query; + } + + /** + * @return Schema|null + */ + public function getSchema(): ?Schema + { + return $this->schema; + } + + /** + * @return array + */ + public function getGraphqlContext(): array + { + return $this->graphqlContext; + } + + /** + * @return array + */ + public function getParams(): array + { + return $this->params; + } + + /** + * @return string + */ + public function getAction(): string + { + $document = Parser::parse(new Source($this->getQuery() ?: 'GraphQL')); + $defs = $document->definitions; + foreach ($defs as $statement) { + $options = [ + NodeKind::OPERATION_DEFINITION, + NodeKind::OPERATION_TYPE_DEFINITION + ]; + if (!in_array($statement->kind, $options, true)) { + continue; + } + if (in_array($statement->operation, [Manager::MUTATION_ROOT, Manager::QUERY_ROOT])) { + $selectionSet = $statement->selectionSet; + if ($selectionSet) { + $selections = $selectionSet->selections; + if (!empty($selections)) { + $firstField = $selections[0]; + + return $firstField->name->value; + } + } + return $statement->operation; + } + } + return 'graphql'; + } +} diff --git a/src/Listener/GraphQL/GraphQLMiddlewareListener.php b/src/Listener/GraphQL/GraphQLMiddlewareListener.php new file mode 100644 index 0000000..aee98b5 --- /dev/null +++ b/src/Listener/GraphQL/GraphQLMiddlewareListener.php @@ -0,0 +1,36 @@ +trigger( + 'graphqlOperation', + new GraphQLMiddlewareContext($query, $schema, $context, $params) + ); + } + +} diff --git a/src/Listener/GraphQL/GraphQLMutationContext.php b/src/Listener/GraphQL/GraphQLMutationContext.php new file mode 100644 index 0000000..47f4d64 --- /dev/null +++ b/src/Listener/GraphQL/GraphQLMutationContext.php @@ -0,0 +1,145 @@ +mutation = $mutation; + $this->list = $list; + $this->record = $record; + $this->args = $args; + $this->graphqlContext = $graphqlContext; + $this->info = $info; + } + + /** + * @return MutationScaffolder + */ + public function getMutation(): MutationScaffolder + { + return $this->mutation; + } + + /** + * @return SS_List|null + */ + public function getList(): ?SS_List + { + return $this->list; + } + + /** + * @return ViewableData|null + */ + public function getRecord(): ?ViewableData + { + return $this->record; + } + + /** + * @return array + */ + public function getArgs(): array + { + return $this->args; + } + + /** + * @return array + */ + public function getGraphqlContext(): array + { + return $this->graphqlContext; + } + + /** + * @return ResolveInfo|null + */ + public function getInfo(): ?ResolveInfo + { + return $this->info; + } + + public function getAction(): string + { + $scaffolder = $this->getMutation(); + if ($scaffolder instanceof Create) { + return static::TYPE_CREATE; + } + + if ($scaffolder instanceof Delete) { + return static::TYPE_DELETE; + } + + if ($scaffolder instanceof Update) { + return static::TYPE_UPDATE; + } + + return null; + } +} diff --git a/src/Listener/GraphQLMutationListener.php b/src/Listener/GraphQL/GraphQLMutationListener.php similarity index 57% rename from src/Listener/GraphQLMutationListener.php rename to src/Listener/GraphQL/GraphQLMutationListener.php index 63aeef5..5c84f5a 100644 --- a/src/Listener/GraphQLMutationListener.php +++ b/src/Listener/GraphQL/GraphQLMutationListener.php @@ -1,19 +1,14 @@ trigger('graphqlMutation', new Context([ - 'list' => $recordOrList instanceof SS_List ? $recordOrList : null, - 'record' => !$recordOrList instanceof SS_List ? $recordOrList : null, - 'args' => $arge, - 'context' => $context, - 'resolveInfo' => $info, - 'mutation' => $this->owner, - ])); + Dispatcher::singleton()->trigger( + 'graphqlMutation', + new GraphQLMutationContext( + $this->owner, + $recordOrList instanceof SS_List ? $recordOrList : null, + !$recordOrList instanceof SS_List ? $recordOrList : null, + $args, + $context, + $info + ) + ); } } diff --git a/src/Listener/GraphQLMiddlewareListener.php b/src/Listener/GraphQLMiddlewareListener.php deleted file mode 100644 index 189aa50..0000000 --- a/src/Listener/GraphQLMiddlewareListener.php +++ /dev/null @@ -1,48 +0,0 @@ -trigger('graphqlMiddleware', new Context([ - 'schema' => $schema, - 'query' => $query, - 'context' => $context, - 'params' => $params, - ])); - } - -} diff --git a/src/Listener/GridField/GridFieldAlterationContext.php b/src/Listener/GridField/GridFieldAlterationContext.php new file mode 100644 index 0000000..4013948 --- /dev/null +++ b/src/Listener/GridField/GridFieldAlterationContext.php @@ -0,0 +1,60 @@ +getActionData( + $this->getRequest()->requestVars(), + $this->getGridField() + ); + if (!$actionData) { + return null; + } + + return array_shift($actionData); + } + + /** + * @param array $data + * @param GridField $gridField + * @return array|null + */ + private function getActionData(array $data, GridField $gridField): ?array + { + // Fetch the store for the "state" of actions (not the GridField) + /** @var StateStore $store */ + $store = Injector::inst()->create(StateStore::class . '.' . $gridField->getName()); + + foreach ($data as $dataKey => $dataValue) { + if (!preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) { + continue; + } + + $stateChange = $store->load($matches[1]); + + $actionName = $stateChange['actionName']; + $arguments = array_key_exists('args', $stateChange) ? $stateChange['args'] : []; + $arguments = is_array($arguments) ? $arguments : []; + + if ($actionName) { + return [ + $actionName, + $arguments, + $data, + ]; + } + } + + return null; + } + +} diff --git a/src/Listener/GridField/GridFieldAlterationListener.php b/src/Listener/GridField/GridFieldAlterationListener.php new file mode 100644 index 0000000..0629204 --- /dev/null +++ b/src/Listener/GridField/GridFieldAlterationListener.php @@ -0,0 +1,39 @@ +trigger( + 'gridFieldAlteration', + new GridFieldContext($action, $request, $result, $this->owner + )); + } + +} diff --git a/src/Listener/GridField/GridFieldContext.php b/src/Listener/GridField/GridFieldContext.php new file mode 100644 index 0000000..6b50dab --- /dev/null +++ b/src/Listener/GridField/GridFieldContext.php @@ -0,0 +1,81 @@ +action = $action; + $this->request = $request; + $this->result = $result; + $this->gridField = $gridField; + } + + /** + * @return string + */ + public function getAction(): string + { + return $this->action; + } + + /** + * @return HTTPRequest|null + */ + public function getRequest(): ?HTTPRequest + { + return $this->request; + } + + /** + * @return null + */ + public function getResult() + { + return $this->result; + } + + /** + * @return GridField|null + */ + public function getGridField(): ?GridField + { + return $this->gridField; + } + + +} diff --git a/src/Listener/GridField/GridFieldURLListener.php b/src/Listener/GridField/GridFieldURLListener.php new file mode 100644 index 0000000..4d426b7 --- /dev/null +++ b/src/Listener/GridField/GridFieldURLListener.php @@ -0,0 +1,35 @@ +trigger( + 'gridFieldAction', + new GridFieldContext($action, $request, $result, $this->owner) + ); + } +} diff --git a/src/Listener/GridFieldAlterationListener.php b/src/Listener/GridFieldAlterationListener.php deleted file mode 100644 index 13f163b..0000000 --- a/src/Listener/GridFieldAlterationListener.php +++ /dev/null @@ -1,52 +0,0 @@ -trigger('gridFieldAlteration', new Context([ - 'action' => $action, - 'request' => $request, - 'result' => $result, - 'gridField' => $this->owner, - ])); - } - -} diff --git a/src/Listener/GridFieldURLListener.php b/src/Listener/GridFieldURLListener.php deleted file mode 100644 index 3385281..0000000 --- a/src/Listener/GridFieldURLListener.php +++ /dev/null @@ -1,51 +0,0 @@ -trigger('gridFieldAction', new Context([ - 'action' => $action, - 'result' => $result, - 'request' => $request, - 'gridField' => $this->owner, - ])); - } -} diff --git a/src/Listener/ListenerContext.php b/src/Listener/ListenerContext.php new file mode 100644 index 0000000..e875f9f --- /dev/null +++ b/src/Listener/ListenerContext.php @@ -0,0 +1,24 @@ +$method(); + } + + return null; + } +} diff --git a/src/Snapshot.php b/src/Snapshot.php index 6f77cdc..8829fe6 100644 --- a/src/Snapshot.php +++ b/src/Snapshot.php @@ -37,24 +37,6 @@ class Snapshot extends DataObject const TRIGGER_ACTION = 'action'; const TRIGGER_MODEL = 'model'; - /** - * Specifies which type of snapshot creation trigger is used - * valid values - * action - snapshot will be created via CMS actions, trigger is opt-in - * model - snapshot will be created via model writes, trigger is opt-out - * - * @config - * @var string - */ - private static $trigger = self::TRIGGER_ACTION; - - /** - * Whitelist of CMS actions which will create a snapshot - * - * @var array - */ - private static $actions = []; - /** * @var array */ @@ -128,7 +110,6 @@ public function getOriginItem() public function getOriginVersion() { $originItem = $this->getOriginItem(); - if ($originItem) { return Versioned::get_version( $originItem->ObjectClass, @@ -154,10 +135,8 @@ public function getDate() public function getActivityDescription() { $item = $this->getOriginItem(); - if ($item) { $activity = ActivityEntry::createFromSnapshotItem($item); - return ucfirst(sprintf( '%s "%s"', $activity->Subject->singular_name(), @@ -174,7 +153,6 @@ public function getActivityDescription() public function getActivityType() { $item = $this->getOriginItem(); - if ($item) { $activity = ActivityEntry::createFromSnapshotItem($item); @@ -231,28 +209,13 @@ public function onBeforeWrite() $this->OriginHash = static::hashForSnapshot($this->OriginClass, $this->OriginID); } - /** - * @return bool - */ - public function isActionTriggerActive(): bool - { - return $this->config()->get('trigger') === static::TRIGGER_ACTION; - } - - /** - * @return bool - */ - public function isModelTriggerActive(): bool - { - return $this->config()->get('trigger') === static::TRIGGER_MODEL; - } - /** * * @param DataObject $owner * @param DataObject|null $origin * @param string $message * @param array $objects + * @return Snapshot|null * @throws ValidationException */ public function createSnapshotFromAction( @@ -276,7 +239,7 @@ public function createSnapshotFromAction( $event->Title = $message; $event->write(); - $message = $origin === null + $message = ($origin === null) ? $message : sprintf( '%s %s', @@ -328,5 +291,19 @@ public function createSnapshotFromAction( return $snapshot; } - + + /** + * sets the related snapshot items to not modified + * + * items with modifications are used to determine the owner's modification + * status (eg in site tree's status flags) + */ + public function markNoModifications(): void + { + foreach ($this->Items() as $item) { + $item->Modification = false; + $item->write(); + } + } + } diff --git a/src/SnapshotPublishable.php b/src/SnapshotPublishable.php index c2d3273..f964a7c 100644 --- a/src/SnapshotPublishable.php +++ b/src/SnapshotPublishable.php @@ -61,52 +61,14 @@ public static function get_at_snapshot($class, $id, $snapshot) $list = DataList::create($baseClass) ->setDataQueryParam([ - 'Versioned.mode' => 'archive', - 'Versioned.date' => $snapshotDate, - 'Versioned.stage' => Versioned::DRAFT, + 'Versioned.mode' => 'archive', + 'Versioned.date' => $snapshotDate, + 'Versioned.stage' => Versioned::DRAFT, ]); return $list->byID($id); } - /** - * @return bool - */ - public function publishRecursive() - { - if (!Snapshot::singleton()->isModelTriggerActive()) { - return parent::publishRecursive(); - } - - if (!self::$active) { - return parent::publishRecursive(); - } - - $this->openSnapshot(); - $result = parent::publishRecursive(); - $this->closeSnapshot(); - - return $result; - } - - /** - * @param int|string $version - */ - public function rollbackRelations($version) - { - if (!Snapshot::singleton()->isModelTriggerActive()) { - return parent::rollbackRelations($version); - } - - if (!self::$active) { - return parent::rollbackRelations($version); - } - - $this->openSnapshot(); - parent::rollbackRelations($version); - $this->closeSnapshot(); - } - /** * @return DataList */ @@ -236,7 +198,6 @@ public function getSnapshotsBetweenVersionsFilters($min, $max = null, $includeAl '"ObjectHash"' => $hash, 'NOT ("Version" = ? AND "WasPublished" = 1)' => $min, ]; - if (!$includeAll) { $condtionStatement[] = 'Modification = 1'; } @@ -293,7 +254,7 @@ public function getActivity() 'SnapshotID' => $snapShotIDs, ]) ->where( - // Only get the items that were the subject of a user's action + // Only get the items that were the subject of a user's action "\"$snapshotTable\" . \"OriginHash\" = \"$itemTable\".\"ObjectHash\"" ) ->sort([ @@ -418,41 +379,6 @@ public function getAtSnapshot($snapshot) return static::get_at_snapshot($this->owner->baseClass(), $this->owner->ID, $snapshot); } - /** - * @return void - */ - public function onAfterWrite() - { - if (!Snapshot::singleton()->isModelTriggerActive()) { - return; - } - - if (!$this->requiresSnapshot()) { - return; - } - - $this->doSnapshot(); - - $changes = $this->getChangedOwnership(); - if (!empty($changes)) { - $this->reconcileOwnershipChanges($changes); - } - } - - /** - * @throws Exception - */ - public function onAfterVersionDelete() - { - if (!Snapshot::singleton()->isModelTriggerActive()) { - return; - } - - if ($this->requiresSnapshot()) { - $this->doSnapshot(); - } - } - /** * @return SnapshotItem */ @@ -483,31 +409,6 @@ public function createSnapshotItem() return SnapshotItem::create($record); } - public function onAfterPublish() - { - if (!Snapshot::singleton()->isModelTriggerActive()) { - return; - } - - if ($this->activeSnapshot) { - $item = $this->owner->createSnapshotItem(); - $item->WasPublished = true; - $this->activeSnapshot->Items()->add($item); - } - } - - public function onBeforeRevertToLive() - { - if (!Snapshot::singleton()->isModelTriggerActive()) { - return; - } - - if ($this->requiresSnapshot()) { - $this->openSnapshot(); - $this->doSnapshot(); - } - } - /** * Tidy up all the irrelevant snapshot records now that the changes have been reverted. */ From 3e1cc8c80823039222cfdb9f66a4e4977027f261 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Fri, 10 Jan 2020 16:23:33 +1300 Subject: [PATCH 03/16] Debugging, refinements --- README.md | 27 ++++++++++--------- _config/config.yml | 6 ++--- lang/en.yml | 8 +++--- src/ActivityEntry.php | 11 +++++++- .../GridField/GridFieldAlterationListener.php | 4 +-- 5 files changed, 33 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index fd994af..4694ddb 100644 --- a/README.md +++ b/README.md @@ -105,18 +105,18 @@ extensions to key classes in the admin that then trigger these events through th * **Listener**: `SilverStripe\Snapshots\Listener\CMSMain\CMSMainActionListener` * **Handler**: `SilverStripe\Snapshots\Handler\CMSMain\ActionHandler` -#### Event: gridFieldAlteration -* **Description**: A standard GridField action (`GridField_ActionProvider`) -* **Example**: `handleReorder` (reorder items) -* **Listener**: `SilverStripe\Snapshots\Listener\GridField\GridFieldAlterationListener` -* **Handler**: `SilverStripe\Snapshots\Handler\GridField\AlterationHandler` - #### Event: gridFieldAction -* **Description**: A GridField action invoked via a URL (`GridField_URLHandler`) -* **Example**: `deleterecord` +* **Description**: A standard GridField action invoked via a URL (`GridField_URLHandler`) +* **Example**: `handleReorder` (reorder items) * **Listener**: `SilverStripe\Snapshots\Listener\GridField\GridFieldURLListener` * **Handler**: `SilverStripe\Snapshots\Handler\GridField\URLActionHandler` +#### Event: gridFieldAlteration +* **Description**: A GridField action invoked via a URL (`GridField_ActionProvider`) +* **Example**: `deleterecord`, `archiverecord` +* **Listener**: `SilverStripe\Snapshots\Listener\GridField\GridFieldAlterationListener` +* **Handler**: `SilverStripe\Snapshots\Handler\GridField\AlterationHandler` + #### Event: graphqlMutation * **Description**: A scaffolded GraphQL mutation * **Example**: `mutation createMyDataObject(Input: $Input)` @@ -133,7 +133,7 @@ extensions to key classes in the admin that then trigger these events through th Each of these handlers is passed a context object that exposes an **action identifier**. This is a string that provides specific information about what happened in the event that the handler can then use in its implementation. -For instance, if a form was submitted, and the function that handles the form is`doSave($data, $form)`, the action +For instance, if a form was submitted, and the function that handles the form is `doSave($data, $form)`, the action identifier is `doSave`. Likewise, controller actions, GridField actions, and GraphQL operations are all action identifiers. @@ -142,17 +142,18 @@ the subscribers to only react to a specific subset of events. #### How to find your action identifier -In the above example, we subscribe to the main event `formSubitted`, but we've added more specificity with `myFormHandler`. +In the above example, we subscribe to the main event `formSubmitted`, but we've added more specificity with `myFormHandler`. This is the name of the `action` provided in the context of the event. The easiest way to debug events is to put breakpoints or logging into the `Dispatcher::trigger()` function. This will provide all the detail you need about what events are triggered when, and with what context. -``` +```php public function trigger(string $event, ListenerContext $context): void { error_log($event); error_log($context->getAction()); + // ... ``` When the logging is in place you just go to the CMS and perform the action you are interested in. @@ -217,7 +218,7 @@ SilverStripe\Core\Injector\Injector: handler: %$MyProject\Handlers\MyHandler ``` -#### Removing snapshot creators +### Removing snapshot creators The configuration API doesn't make it easy to remove items from arrays, so this is best done procedurally. @@ -245,7 +246,7 @@ class MyEventLoader implements EventHandlerLoader } ``` -#### Snapshot creation API +### Snapshot creation API To cover all cases, this module allows you to invoke snapshot creation in any part of your code outside of normal action flow. diff --git a/_config/config.yml b/_config/config.yml index 11d7da9..61f2831 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -74,11 +74,11 @@ SilverStripe\Core\Injector\Injector: on: [ cmsAction.savetreenode ] handler: %$SilverStripe\Snapshots\Handler\CMSMain\ActionHandler - - on: [ gridFieldAction.deleterecord ] + on: [ gridFieldAlteration.deleterecord, gridFieldAlteration.archiverecord ] handler: %$SilverStripe\Snapshots\Handler\GridField\URLActionHandler - - on: [ gridFieldAlteration.handleReorder ] - handler: %$SilverStripe\Snapshots\Handler\GridField\AlterationHandler + on: [ gridFieldAction.handleReorder ] + handler: %$SilverStripe\Snapshots\Handler\GridField\URLActionHandler - on: [ graphqlMutation.create, graphqlMutation.update, graphqlMutation.delete ] handler: %$SilverStripe\Snapshots\Handler\GraphQL\MutationHandler diff --git a/lang/en.yml b/lang/en.yml index 90cb807..97b117d 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -1,20 +1,20 @@ en: SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler: - HANDLER_publish: 'Publish page' HANDLER_unpublish: 'Unpublish page' HANDLER_doSave: 'Save item' HANDLER_doDelete: 'Delete item' SilverStripe\Snapshots\Handler\Form\SaveHandler: HANDLER_save: 'Save page' + SilverStripe\Snapshots\Handler\Form\PublishHandler: + HANDLER_publish: 'Publish page' SilverStripe\Snapshots\Handler\CMSMain\ActionHandler: HANDLER_savetreenode: 'Reordered site tree' - HANDLER_publish: 'Publish page' - HANDLER_unpublish: 'Unpublish page' SilverStripe\Snapshots\Handler\GraphQL\MutationHandler: HANDLER_graphql_crud_create: 'GraphQL create' HANDLER_graphql_crud_update: 'GraphQL update' HANDLER_graphql_crud_delete: 'GraphQL delete' - SilverStripe\Snapshots\Handler\GridField\ActionHandler: + SilverStripe\Snapshots\Handler\GridField\URLActionHandler: HANDLER_deleterecord: 'Delete record' + HANDLER_archiverecord: 'Archive record' SilverStripe\Snapshots\Handler\GridField\AlterationHandler: HANDLER_handleReorder: 'Reorder items' diff --git a/src/ActivityEntry.php b/src/ActivityEntry.php index 0ba2d6a..684c0fb 100644 --- a/src/ActivityEntry.php +++ b/src/ActivityEntry.php @@ -4,6 +4,7 @@ use SilverStripe\Versioned\Versioned; use SilverStripe\View\ArrayData; +use Exception; class ActivityEntry extends ArrayData { @@ -49,7 +50,7 @@ public static function createFromSnapshotItem(SnapshotItem $item) $previousVersion = Versioned::get_all_versions($item->ObjectClass, $item->ObjectID) ->sort('Version', 'DESC') ->first(); - if ($previousVersion->exists()) { + if ($previousVersion && $previousVersion->exists()) { $itemObj = $item->getItem($previousVersion->Version); // This is to deal with the case in which there is no previous version // it's better to give a faulty snapshot point than break the app @@ -58,6 +59,14 @@ public static function createFromSnapshotItem(SnapshotItem $item) } } + if (!$itemObj) { + throw new Exception(sprintf( + 'Could not resolve SnapshotItem %s to a previous %s version', + $item->ID, + $item->ObjectClass + )); + } + return new static([ 'Subject' => $itemObj, 'Action' => $flag, diff --git a/src/Listener/GridField/GridFieldAlterationListener.php b/src/Listener/GridField/GridFieldAlterationListener.php index 0629204..3a6ce95 100644 --- a/src/Listener/GridField/GridFieldAlterationListener.php +++ b/src/Listener/GridField/GridFieldAlterationListener.php @@ -32,8 +32,8 @@ public function afterCallActionHandler(HTTPRequest $request, $action, $result): } Dispatcher::singleton()->trigger( 'gridFieldAlteration', - new GridFieldContext($action, $request, $result, $this->owner - )); + new GridFieldAlterationContext($action, $request, $result, $this->owner) + ); } } From 914614b0e9cd62eafd0255484c2f6a26461fac87 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Sat, 11 Jan 2020 11:04:54 +1300 Subject: [PATCH 04/16] Change ListenerContext to EventContext --- README.md | 6 +++--- src/Dispatch/Dispatcher.php | 6 +++--- src/Handler/CMSMain/ActionHandler.php | 6 +++--- src/Handler/Form/FormSubmissionHandler.php | 6 +++--- src/Handler/Form/PublishHandler.php | 4 ++-- src/Handler/Form/SaveHandler.php | 6 +++--- src/Handler/GraphQL/GenericHandler.php | 6 +++--- src/Handler/GraphQL/MutationHandler.php | 6 +++--- src/Handler/GridField/AlterationHandler.php | 6 +++--- src/Handler/GridField/URLActionHandler.php | 6 +++--- src/Handler/HandlerAbstract.php | 8 ++++---- src/Handler/HandlerInterface.php | 4 ++-- src/Listener/CMSMain/CMSMainContext.php | 4 ++-- src/Listener/{ListenerContext.php => EventContext.php} | 2 +- src/Listener/Form/FormContext.php | 4 ++-- src/Listener/GraphQL/GraphQLMiddlewareContext.php | 4 ++-- src/Listener/GraphQL/GraphQLMutationContext.php | 4 ++-- src/Listener/GridField/GridFieldContext.php | 4 ++-- 18 files changed, 46 insertions(+), 46 deletions(-) rename src/Listener/{ListenerContext.php => EventContext.php} (92%) diff --git a/README.md b/README.md index 4694ddb..8242dfd 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ The easiest way to debug events is to put breakpoints or logging into the `Dispa will provide all the detail you need about what events are triggered when, and with what context. ```php -public function trigger(string $event, ListenerContext $context): void +public function trigger(string $event, EventContext $context): void { error_log($event); error_log($context->getAction()); @@ -182,12 +182,12 @@ For instance, if you have something custom you with a snapshot when a page is sa ```php use SilverStripe\Snapshots\Handler\Form\SaveHandler; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class MySaveHandler extends SaveHandler { - protected function createSnapshot(ListenerContext $context): ?Snapshot + protected function createSnapshot(EventContext $context): ?Snapshot { //... } diff --git a/src/Dispatch/Dispatcher.php b/src/Dispatch/Dispatcher.php index 9ded3dd..3b75838 100644 --- a/src/Dispatch/Dispatcher.php +++ b/src/Dispatch/Dispatcher.php @@ -6,7 +6,7 @@ use SilverStripe\Snapshots\Handler\HandlerInterface; use InvalidArgumentException; use Exception; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; class Dispatcher { @@ -120,9 +120,9 @@ public function removeListenerByClassName(string $event, string $className): sel /** * @param string $event - * @param ListenerContext $context + * @param EventContext $context */ - public function trigger(string $event, ListenerContext $context): void + public function trigger(string $event, EventContext $context): void { $action = $context->getAction(); // First fire listeners to , then just fire generic listeners diff --git a/src/Handler/CMSMain/ActionHandler.php b/src/Handler/CMSMain/ActionHandler.php index 9067c49..4c48fe2 100644 --- a/src/Handler/CMSMain/ActionHandler.php +++ b/src/Handler/CMSMain/ActionHandler.php @@ -10,17 +10,17 @@ use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; use SilverStripe\Snapshots\Listener\CMSMain\CMSMainContext; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class ActionHandler extends HandlerAbstract { /** - * @param ListenerContext $context + * @param EventContext $context * @return Snapshot|null * @throws ValidationException */ - protected function createSnapshot(ListenerContext $context): ?Snapshot + protected function createSnapshot(EventContext $context): ?Snapshot { /* @var CMSMainContext $context */ $action = $context->getAction(); diff --git a/src/Handler/Form/FormSubmissionHandler.php b/src/Handler/Form/FormSubmissionHandler.php index 90d739b..b6663cf 100644 --- a/src/Handler/Form/FormSubmissionHandler.php +++ b/src/Handler/Form/FormSubmissionHandler.php @@ -8,17 +8,17 @@ use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; use SilverStripe\Snapshots\Listener\Form\FormContext; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class FormSubmissionHandler extends HandlerAbstract { /** - * @param ListenerContext $context + * @param EventContext $context * @return Snapshot|null * @throws ValidationException */ - protected function createSnapshot(ListenerContext $context): ?Snapshot + protected function createSnapshot(EventContext $context): ?Snapshot { /* @var FormContext $context */ $action = $context->getAction(); diff --git a/src/Handler/Form/PublishHandler.php b/src/Handler/Form/PublishHandler.php index a33db0c..47275c9 100644 --- a/src/Handler/Form/PublishHandler.php +++ b/src/Handler/Form/PublishHandler.php @@ -5,12 +5,12 @@ use SilverStripe\Snapshots\Listener\Form\FormContext; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class PublishHandler extends FormSubmissionHandler { - protected function createSnapshot(ListenerContext $context): ?Snapshot + protected function createSnapshot(EventContext $context): ?Snapshot { /* @var FormContext $context */ $snapshot = parent::createSnapshot($context); diff --git a/src/Handler/Form/SaveHandler.php b/src/Handler/Form/SaveHandler.php index c91064a..abdd31f 100644 --- a/src/Handler/Form/SaveHandler.php +++ b/src/Handler/Form/SaveHandler.php @@ -6,17 +6,17 @@ use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Listener\Form\FormContext; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class SaveHandler extends FormSubmissionHandler { /** - * @param ListenerContext $context + * @param EventContext $context * @return Snapshot|null * @throws ValidationException */ - protected function createSnapshot(ListenerContext $context): ?Snapshot + protected function createSnapshot(EventContext $context): ?Snapshot { /* @var FormContext $context */ $page = $this->getPage($context); diff --git a/src/Handler/GraphQL/GenericHandler.php b/src/Handler/GraphQL/GenericHandler.php index 8fad6b4..2e49183 100644 --- a/src/Handler/GraphQL/GenericHandler.php +++ b/src/Handler/GraphQL/GenericHandler.php @@ -7,17 +7,17 @@ use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; use SilverStripe\Snapshots\Listener\GraphQL\GraphQLMiddlewareContext; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class GenericHandler extends HandlerAbstract { /** - * @param ListenerContext $context + * @param EventContext $context * @return Snapshot|null * @throws ValidationException */ - protected function createSnapshot(ListenerContext $context): ?Snapshot + protected function createSnapshot(EventContext $context): ?Snapshot { /* @var GraphQLMiddlewareContext $context */ $action = $context->getAction(); diff --git a/src/Handler/GraphQL/MutationHandler.php b/src/Handler/GraphQL/MutationHandler.php index ae14635..068e6fc 100644 --- a/src/Handler/GraphQL/MutationHandler.php +++ b/src/Handler/GraphQL/MutationHandler.php @@ -7,7 +7,7 @@ use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; use SilverStripe\Snapshots\Listener\GraphQL\GraphQLMutationContext; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class MutationHandler extends HandlerAbstract @@ -15,11 +15,11 @@ class MutationHandler extends HandlerAbstract const ACTION_PREFIX = 'graphql_crud_'; /** - * @param ListenerContext $context + * @param EventContext $context * @return Snapshot|null * @throws ValidationException */ - protected function createSnapshot(ListenerContext $context): ?Snapshot + protected function createSnapshot(EventContext $context): ?Snapshot { /* @var GraphQLMutationContext $context */ $type = $context->getAction(); diff --git a/src/Handler/GridField/AlterationHandler.php b/src/Handler/GridField/AlterationHandler.php index 88ad551..f54adc1 100644 --- a/src/Handler/GridField/AlterationHandler.php +++ b/src/Handler/GridField/AlterationHandler.php @@ -6,17 +6,17 @@ use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; use SilverStripe\Snapshots\Listener\GridField\GridFieldContext; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class AlterationHandler extends HandlerAbstract { /** - * @param ListenerContext $context + * @param EventContext $context * @return Snapshot|null * @throws ValidationException */ - protected function createSnapshot(ListenerContext $context): ?Snapshot + protected function createSnapshot(EventContext $context): ?Snapshot { /* @var GridFieldContext $context */ $action = $context->getAction(); diff --git a/src/Handler/GridField/URLActionHandler.php b/src/Handler/GridField/URLActionHandler.php index e8f0c98..33b4504 100644 --- a/src/Handler/GridField/URLActionHandler.php +++ b/src/Handler/GridField/URLActionHandler.php @@ -6,17 +6,17 @@ use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; use SilverStripe\Snapshots\Listener\GridField\GridFieldContext; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class URLActionHandler extends HandlerAbstract { /** - * @param ListenerContext $context + * @param EventContext $context * @return Snapshot|null * @throws ValidationException */ - protected function createSnapshot(ListenerContext $context): ?Snapshot + protected function createSnapshot(EventContext $context): ?Snapshot { /* @var GridFieldContext $context */ $action = $context->getAction(); diff --git a/src/Handler/HandlerAbstract.php b/src/Handler/HandlerAbstract.php index 3961e09..0efa45a 100644 --- a/src/Handler/HandlerAbstract.php +++ b/src/Handler/HandlerAbstract.php @@ -8,7 +8,7 @@ use SilverStripe\Core\Config\Configurable; use SilverStripe\Snapshots\Dispatch\Context; use SilverStripe\Snapshots\Listener\CurrentPage; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; abstract class HandlerAbstract implements HandlerInterface @@ -37,14 +37,14 @@ protected function getMessage(string $action): string return _t($key, $action); } - public function fire(ListenerContext $context): void + public function fire(EventContext $context): void { $this->createSnapshot($context); } /** - * @param ListenerContext $context + * @param EventContext $context * @return Snapshot|null */ - abstract protected function createSnapshot(ListenerContext $context): ?Snapshot; + abstract protected function createSnapshot(EventContext $context): ?Snapshot; } diff --git a/src/Handler/HandlerInterface.php b/src/Handler/HandlerInterface.php index 9321fba..b31512a 100644 --- a/src/Handler/HandlerInterface.php +++ b/src/Handler/HandlerInterface.php @@ -4,10 +4,10 @@ namespace SilverStripe\Snapshots\Handler; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; interface HandlerInterface { - public function fire(ListenerContext $context): void; + public function fire(EventContext $context): void; } diff --git a/src/Listener/CMSMain/CMSMainContext.php b/src/Listener/CMSMain/CMSMainContext.php index 8a1a914..4c99fa8 100644 --- a/src/Listener/CMSMain/CMSMainContext.php +++ b/src/Listener/CMSMain/CMSMainContext.php @@ -4,9 +4,9 @@ namespace SilverStripe\Snapshots\Listener\CMSMain; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; -class CMSMainContext extends ListenerContext +class CMSMainContext extends EventContext { /** * @var string diff --git a/src/Listener/ListenerContext.php b/src/Listener/EventContext.php similarity index 92% rename from src/Listener/ListenerContext.php rename to src/Listener/EventContext.php index e875f9f..4ecdf0a 100644 --- a/src/Listener/ListenerContext.php +++ b/src/Listener/EventContext.php @@ -4,7 +4,7 @@ namespace SilverStripe\Snapshots\Listener; -abstract class ListenerContext +abstract class EventContext { abstract public function getAction(): string; diff --git a/src/Listener/Form/FormContext.php b/src/Listener/Form/FormContext.php index 8de230e..5914026 100644 --- a/src/Listener/Form/FormContext.php +++ b/src/Listener/Form/FormContext.php @@ -6,9 +6,9 @@ use SilverStripe\Control\HTTPRequest; use SilverStripe\Forms\Form; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; -class FormContext extends ListenerContext +class FormContext extends EventContext { /** * @var string diff --git a/src/Listener/GraphQL/GraphQLMiddlewareContext.php b/src/Listener/GraphQL/GraphQLMiddlewareContext.php index c14b68e..fb71055 100644 --- a/src/Listener/GraphQL/GraphQLMiddlewareContext.php +++ b/src/Listener/GraphQL/GraphQLMiddlewareContext.php @@ -9,9 +9,9 @@ use GraphQL\Language\Source; use GraphQL\Type\Schema; use SilverStripe\GraphQL\Manager; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; -class GraphQLMiddlewareContext extends ListenerContext +class GraphQLMiddlewareContext extends EventContext { /** * @var string diff --git a/src/Listener/GraphQL/GraphQLMutationContext.php b/src/Listener/GraphQL/GraphQLMutationContext.php index 47f4d64..c11d175 100644 --- a/src/Listener/GraphQL/GraphQLMutationContext.php +++ b/src/Listener/GraphQL/GraphQLMutationContext.php @@ -10,10 +10,10 @@ use SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Update; use SilverStripe\GraphQL\Scaffolding\Scaffolders\MutationScaffolder; use SilverStripe\ORM\SS_List; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\View\ViewableData; -class GraphQLMutationContext extends ListenerContext +class GraphQLMutationContext extends EventContext { diff --git a/src/Listener/GridField/GridFieldContext.php b/src/Listener/GridField/GridFieldContext.php index 6b50dab..99f9e4b 100644 --- a/src/Listener/GridField/GridFieldContext.php +++ b/src/Listener/GridField/GridFieldContext.php @@ -6,9 +6,9 @@ use SilverStripe\Control\HTTPRequest; use SilverStripe\Forms\GridField\GridField; -use SilverStripe\Snapshots\Listener\ListenerContext; +use SilverStripe\Snapshots\Listener\EventContext; -class GridFieldContext extends ListenerContext +class GridFieldContext extends EventContext { /** * @var string From 5838c8034f1037532b83364f6e8c380f9d4a3091 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Mon, 13 Jan 2020 13:36:59 +1300 Subject: [PATCH 05/16] Remove custom event contexts. Use one class --- lang/en.yml | 1 - src/Handler/CMSMain/ActionHandler.php | 8 +- src/Handler/Form/FormSubmissionHandler.php | 16 +- src/Handler/Form/PublishHandler.php | 7 +- src/Handler/Form/SaveHandler.php | 3 +- src/Handler/GraphQL/GenericHandler.php | 2 - src/Handler/GraphQL/MutationHandler.php | 2 - src/Handler/GridField/AlterationHandler.php | 6 +- src/Handler/GridField/URLActionHandler.php | 6 +- src/Handler/HandlerAbstract.php | 3 - .../CMSMain/CMSMainActionListener.php | 17 +- src/Listener/CMSMain/CMSMainContext.php | 76 -------- src/Listener/EventContext.php | 52 +++++- src/Listener/Form/FormContext.php | 81 --------- src/Listener/Form/FormSubmissionListener.php | 13 +- src/Listener/Form/Submission.php | 166 ------------------ .../GraphQL/GraphQLMiddlewareContext.php | 113 ------------ .../GraphQL/GraphQLMiddlewareListener.php | 48 ++++- .../GraphQL/GraphQLMutationContext.php | 145 --------------- .../GraphQL/GraphQLMutationListener.php | 40 ++++- .../GridField/GridFieldAlterationContext.php | 60 ------- .../GridField/GridFieldAlterationListener.php | 57 +++++- src/Listener/GridField/GridFieldContext.php | 81 --------- .../GridField/GridFieldURLListener.php | 10 +- 24 files changed, 231 insertions(+), 782 deletions(-) delete mode 100644 src/Listener/CMSMain/CMSMainContext.php delete mode 100644 src/Listener/Form/FormContext.php delete mode 100644 src/Listener/Form/Submission.php delete mode 100644 src/Listener/GraphQL/GraphQLMiddlewareContext.php delete mode 100644 src/Listener/GraphQL/GraphQLMutationContext.php delete mode 100644 src/Listener/GridField/GridFieldAlterationContext.php delete mode 100644 src/Listener/GridField/GridFieldContext.php diff --git a/lang/en.yml b/lang/en.yml index 97b117d..59c538f 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -16,5 +16,4 @@ en: SilverStripe\Snapshots\Handler\GridField\URLActionHandler: HANDLER_deleterecord: 'Delete record' HANDLER_archiverecord: 'Archive record' - SilverStripe\Snapshots\Handler\GridField\AlterationHandler: HANDLER_handleReorder: 'Reorder items' diff --git a/src/Handler/CMSMain/ActionHandler.php b/src/Handler/CMSMain/ActionHandler.php index 4c48fe2..589ddfb 100644 --- a/src/Handler/CMSMain/ActionHandler.php +++ b/src/Handler/CMSMain/ActionHandler.php @@ -9,7 +9,6 @@ use SilverStripe\ORM\DataObject; use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; -use SilverStripe\Snapshots\Listener\CMSMain\CMSMainContext; use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; @@ -22,10 +21,9 @@ class ActionHandler extends HandlerAbstract */ protected function createSnapshot(EventContext $context): ?Snapshot { - /* @var CMSMainContext $context */ $action = $context->getAction(); /* @var HTTPResponse $result */ - $result = $context->getResult(); + $result = $context->get('result'); if (!$result instanceof HTTPResponse) { return null; } @@ -33,8 +31,8 @@ protected function createSnapshot(EventContext $context): ?Snapshot return null; } - $className = $context->getTreeClass(); - $id = (int) $context->getId(); + $className = $context->get('treeClass'); + $id = (int) $context->get('id'); if (!$id) { return null; diff --git a/src/Handler/Form/FormSubmissionHandler.php b/src/Handler/Form/FormSubmissionHandler.php index b6663cf..379c413 100644 --- a/src/Handler/Form/FormSubmissionHandler.php +++ b/src/Handler/Form/FormSubmissionHandler.php @@ -5,9 +5,9 @@ use SilverStripe\CMS\Model\SiteTree; +use SilverStripe\Control\HTTPRequest; use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; -use SilverStripe\Snapshots\Listener\Form\FormContext; use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; @@ -20,10 +20,12 @@ class FormSubmissionHandler extends HandlerAbstract */ protected function createSnapshot(EventContext $context): ?Snapshot { - /* @var FormContext $context */ $action = $context->getAction(); $page = $this->getPage($context); - $record = $context->getForm()->getRecord(); + $record = null; + if ($form = $context->get('form')) { + $record = $form->getRecord(); + } if ($page === null || $record === null) { return null; @@ -35,12 +37,14 @@ protected function createSnapshot(EventContext $context): ?Snapshot } /** - * @param FormContext $context + * @param EventContext $context * @return SiteTree|null */ - protected function getPage(FormContext $context): ?SiteTree + protected function getPage(EventContext $context): ?SiteTree { - $url = $context->getRequest()->getURL(); + /* @var HTTPRequest $request */ + $request = $context->get('request'); + $url = $request->getURL(); return $this->getCurrentPageFromRequestUrl($url); } } diff --git a/src/Handler/Form/PublishHandler.php b/src/Handler/Form/PublishHandler.php index 47275c9..a51c749 100644 --- a/src/Handler/Form/PublishHandler.php +++ b/src/Handler/Form/PublishHandler.php @@ -4,7 +4,7 @@ namespace SilverStripe\Snapshots\Handler\Form; -use SilverStripe\Snapshots\Listener\Form\FormContext; +use SilverStripe\Forms\Form; use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; @@ -12,11 +12,12 @@ class PublishHandler extends FormSubmissionHandler { protected function createSnapshot(EventContext $context): ?Snapshot { - /* @var FormContext $context */ $snapshot = parent::createSnapshot($context); if ($snapshot) { // mark publish actions as WasPublished - the status flags rely on this being set correctly - if ($context->getForm()->getName() === 'EditForm') { + /* @var Form $form */ + $form = $context->get('form'); + if ($form->getName() === 'EditForm') { foreach ($snapshot->Items() as $item) { $item->WasPublished = true; $item->write(); diff --git a/src/Handler/Form/SaveHandler.php b/src/Handler/Form/SaveHandler.php index abdd31f..98378f9 100644 --- a/src/Handler/Form/SaveHandler.php +++ b/src/Handler/Form/SaveHandler.php @@ -5,7 +5,6 @@ use SilverStripe\ORM\ValidationException; -use SilverStripe\Snapshots\Listener\Form\FormContext; use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; @@ -18,7 +17,7 @@ class SaveHandler extends FormSubmissionHandler */ protected function createSnapshot(EventContext $context): ?Snapshot { - /* @var FormContext $context */ + $page = $this->getPage($context); if (!$page || !$page->isModifiedOnDraft()) { return null; diff --git a/src/Handler/GraphQL/GenericHandler.php b/src/Handler/GraphQL/GenericHandler.php index 2e49183..46f8d41 100644 --- a/src/Handler/GraphQL/GenericHandler.php +++ b/src/Handler/GraphQL/GenericHandler.php @@ -6,7 +6,6 @@ use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; -use SilverStripe\Snapshots\Listener\GraphQL\GraphQLMiddlewareContext; use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; @@ -19,7 +18,6 @@ class GenericHandler extends HandlerAbstract */ protected function createSnapshot(EventContext $context): ?Snapshot { - /* @var GraphQLMiddlewareContext $context */ $action = $context->getAction(); $message = $this->getMessage($action); $page = $this->getPageFromReferrer(); diff --git a/src/Handler/GraphQL/MutationHandler.php b/src/Handler/GraphQL/MutationHandler.php index 068e6fc..d99bc06 100644 --- a/src/Handler/GraphQL/MutationHandler.php +++ b/src/Handler/GraphQL/MutationHandler.php @@ -6,7 +6,6 @@ use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; -use SilverStripe\Snapshots\Listener\GraphQL\GraphQLMutationContext; use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; @@ -21,7 +20,6 @@ class MutationHandler extends HandlerAbstract */ protected function createSnapshot(EventContext $context): ?Snapshot { - /* @var GraphQLMutationContext $context */ $type = $context->getAction(); $action = static::ACTION_PREFIX . $type; $message = $this->getMessage($action); diff --git a/src/Handler/GridField/AlterationHandler.php b/src/Handler/GridField/AlterationHandler.php index f54adc1..7ec5ab2 100644 --- a/src/Handler/GridField/AlterationHandler.php +++ b/src/Handler/GridField/AlterationHandler.php @@ -3,9 +3,9 @@ namespace SilverStripe\Snapshots\Handler\GridField; +use SilverStripe\Forms\Form; use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; -use SilverStripe\Snapshots\Listener\GridField\GridFieldContext; use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; @@ -18,10 +18,10 @@ class AlterationHandler extends HandlerAbstract */ protected function createSnapshot(EventContext $context): ?Snapshot { - /* @var GridFieldContext $context */ $action = $context->getAction(); $message = $this->getMessage($action); - $form = $context->getGridField()->getForm(); + /* @var Form $form */ + $form = $context->get('gridField')->getForm(); if (!$form) { return null; diff --git a/src/Handler/GridField/URLActionHandler.php b/src/Handler/GridField/URLActionHandler.php index 33b4504..062b342 100644 --- a/src/Handler/GridField/URLActionHandler.php +++ b/src/Handler/GridField/URLActionHandler.php @@ -3,9 +3,9 @@ namespace SilverStripe\Snapshots\Handler\GridField; +use SilverStripe\Forms\Form; use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; -use SilverStripe\Snapshots\Listener\GridField\GridFieldContext; use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; @@ -18,10 +18,10 @@ class URLActionHandler extends HandlerAbstract */ protected function createSnapshot(EventContext $context): ?Snapshot { - /* @var GridFieldContext $context */ $action = $context->getAction(); $message = $this->getMessage($action); - $form = $context->getGridField()->getForm(); + /* @var Form $form */ + $form = $context->get('gridField')->getForm(); if (!$form) { return null; diff --git a/src/Handler/HandlerAbstract.php b/src/Handler/HandlerAbstract.php index 0efa45a..0f33cc3 100644 --- a/src/Handler/HandlerAbstract.php +++ b/src/Handler/HandlerAbstract.php @@ -3,10 +3,7 @@ namespace SilverStripe\Snapshots\Handler; - -use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Configurable; -use SilverStripe\Snapshots\Dispatch\Context; use SilverStripe\Snapshots\Listener\CurrentPage; use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; diff --git a/src/Listener/CMSMain/CMSMainActionListener.php b/src/Listener/CMSMain/CMSMainActionListener.php index c6874bd..cacffac 100644 --- a/src/Listener/CMSMain/CMSMainActionListener.php +++ b/src/Listener/CMSMain/CMSMainActionListener.php @@ -3,15 +3,10 @@ namespace SilverStripe\Snapshots\Listener\CMSMain; use SilverStripe\CMS\Controllers\CMSMain; -use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Control\HTTPRequest; -use SilverStripe\Control\HTTPResponse; use SilverStripe\Core\Extension; -use SilverStripe\ORM\DataObject; -use SilverStripe\ORM\ValidationException; -use SilverStripe\Snapshots\Dispatch\Context; use SilverStripe\Snapshots\Dispatch\Dispatcher; -use SilverStripe\Snapshots\Snapshot; +use SilverStripe\Snapshots\Listener\EventContext; /** * Class CMSMainAction @@ -32,11 +27,13 @@ class CMSMainActionListener extends Extension public function afterCallActionHandler(HTTPRequest $request, $action, $result): void { Dispatcher::singleton()->trigger( 'cmsAction', - new CMSMainContext( + new EventContext( $action, - $result, - $this->owner->config()->get('tree_class'), - $request->requestVar('ID') + [ + 'result' => $result, + 'treeClass' => $this->owner->config()->get('tree_class'), + 'id' => $request->requestVar('ID'), + ] ) ); } diff --git a/src/Listener/CMSMain/CMSMainContext.php b/src/Listener/CMSMain/CMSMainContext.php deleted file mode 100644 index 4c99fa8..0000000 --- a/src/Listener/CMSMain/CMSMainContext.php +++ /dev/null @@ -1,76 +0,0 @@ -action = $action; - $this->result = $result; - $this->treeClass = $treeClass; - $this->id = $id; - } - - /** - * @return string - */ - public function getAction(): string - { - return $this->action; - } - - /** - * @return null - */ - public function getResult() - { - return $this->result; - } - - /** - * @return string|null - */ - public function getTreeClass(): ?string - { - return $this->treeClass; - } - - /** - * @return string|null - */ - public function getId(): ?string - { - return $this->id; - } -} diff --git a/src/Listener/EventContext.php b/src/Listener/EventContext.php index 4ecdf0a..6cce51a 100644 --- a/src/Listener/EventContext.php +++ b/src/Listener/EventContext.php @@ -4,21 +4,55 @@ namespace SilverStripe\Snapshots\Listener; -abstract class EventContext +use phpDocumentor\Reflection\Types\Scalar; + +class EventContext { - abstract public function getAction(): string; + /** + * @var string + */ + private $action; + + /** + * @var array + */ + private $meta = []; + + /** + * EventContext constructor. + * @param string $action + * @param array $meta + */ + public function __construct(string $action, array $meta = []) + { + $this->action = $action; + $this->meta = $meta; + } + + /** + * @return string + */ + public function getAction(): string + { + return $this->action; + } + + /** + * @param string $name + * @return string|int|bool|float|null + */ + public function get(string $name) + { + return $this->meta[$name] ?? null; + } /** * @param $name - * @return |null + * @return string|int|bool|float|null */ public function __get($name) { - $method = 'get' . ucfirst($name); - if (method_exists($this, $method)) { - return $this->$method(); - } - - return null; + return $this->get($name); } + } diff --git a/src/Listener/Form/FormContext.php b/src/Listener/Form/FormContext.php deleted file mode 100644 index 5914026..0000000 --- a/src/Listener/Form/FormContext.php +++ /dev/null @@ -1,81 +0,0 @@ -action = $action; - $this->form = $form; - $this->request = $request; - $this->vars = $vars; - } - - /** - * @return string - */ - public function getAction(): string - { - return $this->action; - } - - /** - * @return Form|null - */ - public function getForm(): ?Form - { - return $this->form; - } - - /** - * @return HTTPRequest|null - */ - public function getRequest(): ?HTTPRequest - { - return $this->request; - } - - /** - * @return array - */ - public function getVars(): array - { - return $this->vars; - } - - -} diff --git a/src/Listener/Form/FormSubmissionListener.php b/src/Listener/Form/FormSubmissionListener.php index 9b77801..37587f1 100644 --- a/src/Listener/Form/FormSubmissionListener.php +++ b/src/Listener/Form/FormSubmissionListener.php @@ -7,6 +7,7 @@ use SilverStripe\Forms\Form; use SilverStripe\Forms\FormRequestHandler; use SilverStripe\Snapshots\Dispatch\Dispatcher; +use SilverStripe\Snapshots\Listener\EventContext; /** * Class Submission @@ -28,6 +29,16 @@ class FormSubmissionListener extends Extension */ public function afterCallFormHandler (HTTPRequest $request, $funcName, $vars, $form): void { - Dispatcher::singleton()->trigger('formSubmitted', new FormContext($funcName, $form, $request, $vars)); + Dispatcher::singleton()->trigger( + 'formSubmitted', + new EventContext( + $funcName, + [ + 'form' => $form, + 'request' => $request, + 'vars' => $vars + ] + ) + ); } } diff --git a/src/Listener/Form/Submission.php b/src/Listener/Form/Submission.php deleted file mode 100644 index dd5e699..0000000 --- a/src/Listener/Form/Submission.php +++ /dev/null @@ -1,166 +0,0 @@ -processAction($action, $form, $request); - } - - /** - * Extension point in @see FormRequestHandler::httpSubmission - * form handler action via form submission action - * - * @param HTTPRequest $request - * @param $action - * @param $vars - * @param $form - * @param $result - * @throws ValidationException - */ - public function afterCallFormHandlerMethod( // phpcs:ignore SlevomatCodingStandard.TypeHints - HTTPRequest $request, - $action, - $vars, - $form, - $result - ): void { - $this->processAction($action, $form, $request); - } - - /** - * Extension point in @see FormRequestHandler::httpSubmission - * form method action via form submission action - * - * @param HTTPRequest $request - * @param $action - * @param $vars - * @param $form - * @param $result - * @throws ValidationException - */ - public function afterCallFormHandlerFormMethod( // phpcs:ignore SlevomatCodingStandard.TypeHints - HTTPRequest $request, - $action, - $vars, - $form, - $result - ): void { - $this->processAction($action, $form, $request); - } - - /** - * Extension point in @see FormRequestHandler::httpSubmission - * form field method action via form submission action - * - * @param HTTPRequest $request - * @param $action - * @param $vars - * @param $form - * @param $result - * @throws ValidationException - */ - public function afterCallFormHandlerFieldMethod( // phpcs:ignore SlevomatCodingStandard.TypeHints - HTTPRequest $request, - $action, - $vars, - $form, - $result - ): void { - $this->processAction($action, $form, $request); - } - - /** - * @param string|null $action - * @param Form $form - * @param HTTPRequest $request - * @throws ValidationException - */ - private function processAction(?string $action, Form $form, HTTPRequest $request): void - { - $snapshot = Snapshot::singleton(); - - if (!$snapshot->isActionTriggerActive()) { - return; - } - - $message = $snapshot->getActionMessage($action); - - if ($message === null) { - return; - } - - $record = $form->getRecord(); - - if ($record === null) { - return; - } - - $url = $request->getURL(); - $page = $this->getCurrentPageFromRequestUrl($url); - - if ($page === null) { - return; - } - - // avoid recording useless save actions to prevent multiple snapshots of the same version - if ($form->getName() === 'EditForm' && $action === 'save' && !$page->isModifiedOnDraft()) { - return; - } - - // attempt to create a custom snapshot first - $customSnapshot = $snapshot->formSubmissionSnapshot($form, $request, $page, $action, $message); - - if ($customSnapshot) { - return; - } - - // fall back to default snapshot - $snapshotInstance = $snapshot->createSnapshotFromAction($page, $record, $message); - - // mark publish actions as WasPublished - the status flags rely on this being set correctly - if ($form->getName() === 'EditForm' && $action === 'publish') { - foreach ($snapshotInstance->Items() as $item) { - $item->WasPublished = true; - $item->write(); - } - } - } -} diff --git a/src/Listener/GraphQL/GraphQLMiddlewareContext.php b/src/Listener/GraphQL/GraphQLMiddlewareContext.php deleted file mode 100644 index fb71055..0000000 --- a/src/Listener/GraphQL/GraphQLMiddlewareContext.php +++ /dev/null @@ -1,113 +0,0 @@ -query = $query; - $this->schema = $schema; - $this->graphqlContext = $graphqlContext; - $this->params = $params; - } - - /** - * @return string - */ - public function getQuery(): string - { - return $this->query; - } - - /** - * @return Schema|null - */ - public function getSchema(): ?Schema - { - return $this->schema; - } - - /** - * @return array - */ - public function getGraphqlContext(): array - { - return $this->graphqlContext; - } - - /** - * @return array - */ - public function getParams(): array - { - return $this->params; - } - - /** - * @return string - */ - public function getAction(): string - { - $document = Parser::parse(new Source($this->getQuery() ?: 'GraphQL')); - $defs = $document->definitions; - foreach ($defs as $statement) { - $options = [ - NodeKind::OPERATION_DEFINITION, - NodeKind::OPERATION_TYPE_DEFINITION - ]; - if (!in_array($statement->kind, $options, true)) { - continue; - } - if (in_array($statement->operation, [Manager::MUTATION_ROOT, Manager::QUERY_ROOT])) { - $selectionSet = $statement->selectionSet; - if ($selectionSet) { - $selections = $selectionSet->selections; - if (!empty($selections)) { - $firstField = $selections[0]; - - return $firstField->name->value; - } - } - return $statement->operation; - } - } - return 'graphql'; - } -} diff --git a/src/Listener/GraphQL/GraphQLMiddlewareListener.php b/src/Listener/GraphQL/GraphQLMiddlewareListener.php index aee98b5..b992349 100644 --- a/src/Listener/GraphQL/GraphQLMiddlewareListener.php +++ b/src/Listener/GraphQL/GraphQLMiddlewareListener.php @@ -2,10 +2,15 @@ namespace SilverStripe\Snapshots\Listener\GraphQL; +use GraphQL\Error\SyntaxError; +use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\Parser; +use GraphQL\Language\Source; use GraphQL\Type\Schema; use SilverStripe\Core\Extension; use SilverStripe\GraphQL\Manager; use SilverStripe\Snapshots\Dispatch\Dispatcher; +use SilverStripe\Snapshots\Listener\EventContext; /** * Class CustomAction @@ -24,13 +29,54 @@ class GraphQLMiddlewareListener extends Extension * @param string $query * @param array $context * @param array|null $params + * @throws SyntaxError */ public function onAfterCallMiddleware(Schema $schema, string $query, array $context, $params): void { Dispatcher::singleton()->trigger( 'graphqlOperation', - new GraphQLMiddlewareContext($query, $schema, $context, $params) + new EventContext( + $this->getActionFromQuery($query), + [ + 'schema' => $schema, + 'context' => $context, + 'params' => $params, + ] + ) ); } + /** + * @param string|null $query + * @return string + * @throws SyntaxError + */ + private function getActionFromQuery(?string $query = null): string + { + $document = Parser::parse(new Source($query ?: 'GraphQL')); + $defs = $document->definitions; + foreach ($defs as $statement) { + $options = [ + NodeKind::OPERATION_DEFINITION, + NodeKind::OPERATION_TYPE_DEFINITION + ]; + if (!in_array($statement->kind, $options, true)) { + continue; + } + if (in_array($statement->operation, [Manager::MUTATION_ROOT, Manager::QUERY_ROOT])) { + $selectionSet = $statement->selectionSet; + if ($selectionSet) { + $selections = $selectionSet->selections; + if (!empty($selections)) { + $firstField = $selections[0]; + + return $firstField->name->value; + } + } + return $statement->operation; + } + } + return 'graphql'; + } + } diff --git a/src/Listener/GraphQL/GraphQLMutationContext.php b/src/Listener/GraphQL/GraphQLMutationContext.php deleted file mode 100644 index c11d175..0000000 --- a/src/Listener/GraphQL/GraphQLMutationContext.php +++ /dev/null @@ -1,145 +0,0 @@ -mutation = $mutation; - $this->list = $list; - $this->record = $record; - $this->args = $args; - $this->graphqlContext = $graphqlContext; - $this->info = $info; - } - - /** - * @return MutationScaffolder - */ - public function getMutation(): MutationScaffolder - { - return $this->mutation; - } - - /** - * @return SS_List|null - */ - public function getList(): ?SS_List - { - return $this->list; - } - - /** - * @return ViewableData|null - */ - public function getRecord(): ?ViewableData - { - return $this->record; - } - - /** - * @return array - */ - public function getArgs(): array - { - return $this->args; - } - - /** - * @return array - */ - public function getGraphqlContext(): array - { - return $this->graphqlContext; - } - - /** - * @return ResolveInfo|null - */ - public function getInfo(): ?ResolveInfo - { - return $this->info; - } - - public function getAction(): string - { - $scaffolder = $this->getMutation(); - if ($scaffolder instanceof Create) { - return static::TYPE_CREATE; - } - - if ($scaffolder instanceof Delete) { - return static::TYPE_DELETE; - } - - if ($scaffolder instanceof Update) { - return static::TYPE_UPDATE; - } - - return null; - } -} diff --git a/src/Listener/GraphQL/GraphQLMutationListener.php b/src/Listener/GraphQL/GraphQLMutationListener.php index 5c84f5a..5c395a6 100644 --- a/src/Listener/GraphQL/GraphQLMutationListener.php +++ b/src/Listener/GraphQL/GraphQLMutationListener.php @@ -4,11 +4,13 @@ use GraphQL\Type\Definition\ResolveInfo; use SilverStripe\Core\Extension; +use SilverStripe\GraphQL\OperationResolver; use SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Create; use SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Delete; use SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Update; use SilverStripe\ORM\SS_List; use SilverStripe\Snapshots\Dispatch\Dispatcher; +use SilverStripe\Snapshots\Listener\EventContext; /** * Class GenericAction @@ -19,6 +21,11 @@ */ class GraphQLMutationListener extends Extension { + + const TYPE_CREATE = 'create'; + const TYPE_DELETE = 'delete'; + const TYPE_UPDATE = 'update'; + /** * Extension point in @see Create::resolve * Extension point in @see Delete::resolve @@ -34,14 +41,33 @@ public function afterMutation($recordOrList, array $args, $context, ResolveInfo { Dispatcher::singleton()->trigger( 'graphqlMutation', - new GraphQLMutationContext( - $this->owner, - $recordOrList instanceof SS_List ? $recordOrList : null, - !$recordOrList instanceof SS_List ? $recordOrList : null, - $args, - $context, - $info + new EventContext( + $this->getActionFromScaffolder($this->owner), + [ + 'list' => $recordOrList instanceof SS_List ? $recordOrList : null, + 'record' => !$recordOrList instanceof SS_List ? $recordOrList : null, + 'args' => $args, + 'context' => $context, + 'info' => $info, + ] ) ); } + + private function getActionFromScaffolder(OperationResolver $scaffolder): ?string + { + if ($scaffolder instanceof Create) { + return static::TYPE_CREATE; + } + + if ($scaffolder instanceof Delete) { + return static::TYPE_DELETE; + } + + if ($scaffolder instanceof Update) { + return static::TYPE_UPDATE; + } + + return null; + } } diff --git a/src/Listener/GridField/GridFieldAlterationContext.php b/src/Listener/GridField/GridFieldAlterationContext.php deleted file mode 100644 index 4013948..0000000 --- a/src/Listener/GridField/GridFieldAlterationContext.php +++ /dev/null @@ -1,60 +0,0 @@ -getActionData( - $this->getRequest()->requestVars(), - $this->getGridField() - ); - if (!$actionData) { - return null; - } - - return array_shift($actionData); - } - - /** - * @param array $data - * @param GridField $gridField - * @return array|null - */ - private function getActionData(array $data, GridField $gridField): ?array - { - // Fetch the store for the "state" of actions (not the GridField) - /** @var StateStore $store */ - $store = Injector::inst()->create(StateStore::class . '.' . $gridField->getName()); - - foreach ($data as $dataKey => $dataValue) { - if (!preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) { - continue; - } - - $stateChange = $store->load($matches[1]); - - $actionName = $stateChange['actionName']; - $arguments = array_key_exists('args', $stateChange) ? $stateChange['args'] : []; - $arguments = is_array($arguments) ? $arguments : []; - - if ($actionName) { - return [ - $actionName, - $arguments, - $data, - ]; - } - } - - return null; - } - -} diff --git a/src/Listener/GridField/GridFieldAlterationListener.php b/src/Listener/GridField/GridFieldAlterationListener.php index 3a6ce95..c7cb79f 100644 --- a/src/Listener/GridField/GridFieldAlterationListener.php +++ b/src/Listener/GridField/GridFieldAlterationListener.php @@ -4,8 +4,11 @@ use SilverStripe\Control\HTTPRequest; use SilverStripe\Core\Extension; +use SilverStripe\Core\Injector\Injector; +use SilverStripe\Forms\GridField\FormAction\StateStore; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Snapshots\Dispatch\Dispatcher; +use SilverStripe\Snapshots\Listener\EventContext; /** * Class AlterAction @@ -30,10 +33,62 @@ public function afterCallActionHandler(HTTPRequest $request, $action, $result): if (!in_array($action, ['index', 'gridFieldAlterAction'])) { return; } + $actionName = null; + $arguments = []; + $actionData = $this->getActionData($request->requestVars(), $this->owner); + if ($actionData) { + list ($actionName, $arguments) = $actionData; + } + if (!$actionName === null) { + return; + } Dispatcher::singleton()->trigger( 'gridFieldAlteration', - new GridFieldAlterationContext($action, $request, $result, $this->owner) + new EventContext( + $actionName, + [ + 'request' => $request, + 'result' => $result, + 'gridField' => $this->owner, + 'args' => $arguments, + ] + ) ); } + /** + * @param array $data + * @param GridField $gridField + * @return array|null + */ + private function getActionData(array $data, GridField $gridField): ?array + { + // Fetch the store for the "state" of actions (not the GridField) + /** @var StateStore $store */ + $store = Injector::inst()->create(StateStore::class . '.' . $gridField->getName()); + + foreach ($data as $dataKey => $dataValue) { + if (!preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) { + continue; + } + + $stateChange = $store->load($matches[1]); + + $actionName = $stateChange['actionName']; + $arguments = array_key_exists('args', $stateChange) ? $stateChange['args'] : []; + $arguments = is_array($arguments) ? $arguments : []; + + if ($actionName) { + return [ + $actionName, + $arguments, + $data, + ]; + } + } + + return null; + } + + } diff --git a/src/Listener/GridField/GridFieldContext.php b/src/Listener/GridField/GridFieldContext.php deleted file mode 100644 index 99f9e4b..0000000 --- a/src/Listener/GridField/GridFieldContext.php +++ /dev/null @@ -1,81 +0,0 @@ -action = $action; - $this->request = $request; - $this->result = $result; - $this->gridField = $gridField; - } - - /** - * @return string - */ - public function getAction(): string - { - return $this->action; - } - - /** - * @return HTTPRequest|null - */ - public function getRequest(): ?HTTPRequest - { - return $this->request; - } - - /** - * @return null - */ - public function getResult() - { - return $this->result; - } - - /** - * @return GridField|null - */ - public function getGridField(): ?GridField - { - return $this->gridField; - } - - -} diff --git a/src/Listener/GridField/GridFieldURLListener.php b/src/Listener/GridField/GridFieldURLListener.php index 4d426b7..e0451a8 100644 --- a/src/Listener/GridField/GridFieldURLListener.php +++ b/src/Listener/GridField/GridFieldURLListener.php @@ -6,6 +6,7 @@ use SilverStripe\Core\Extension; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Snapshots\Dispatch\Dispatcher; +use SilverStripe\Snapshots\Listener\EventContext; /** * Class UrlHandlerAction @@ -29,7 +30,14 @@ class GridFieldURLListener extends Extension public function afterCallActionURLHandler(HTTPRequest $request, $action, $result): void { Dispatcher::singleton()->trigger( 'gridFieldAction', - new GridFieldContext($action, $request, $result, $this->owner) + new EventContext( + $action, + [ + 'request' => $request, + 'result' => $result, + 'gridField' => $this->owner + ] + ) ); } } From 8a10e51c8ec16047e2246811e5d9abe7eabbba00 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Mon, 13 Jan 2020 17:08:42 +1300 Subject: [PATCH 06/16] Revisions per Mojmir --- README.md | 51 ++++++++++----- _config/config.yml | 63 +++++++++++-------- src/Dispatch/Dispatcher.php | 45 +++++++++++-- src/Dispatch/EventHandlerLoader.php | 1 - .../{ActionHandler.php => Handler.php} | 7 ++- ...{FormSubmissionHandler.php => Handler.php} | 7 ++- src/Handler/Form/PublishHandler.php | 23 +++---- src/Handler/Form/SaveHandler.php | 3 +- .../Handler.php} | 11 ++-- .../Handler.php} | 11 ++-- .../Handler.php} | 10 +-- .../Handler.php} | 8 ++- src/Handler/HandlerInterface.php | 2 - ...CMSMainActionListener.php => Listener.php} | 5 +- src/Listener/EventContext.php | 14 ++--- ...ormSubmissionListener.php => Listener.php} | 4 +- .../Listener.php} | 5 +- .../Listener.php} | 4 +- .../Listener.php} | 7 ++- .../Listener.php} | 9 ++- src/Snapshot.php | 1 - src/SnapshotPublishable.php | 2 +- 22 files changed, 179 insertions(+), 114 deletions(-) rename src/Handler/CMSMain/{ActionHandler.php => Handler.php} (92%) rename src/Handler/Form/{FormSubmissionHandler.php => Handler.php} (92%) rename src/Handler/GraphQL/{GenericHandler.php => Middleware/Handler.php} (80%) rename src/Handler/GraphQL/{MutationHandler.php => Mutation/Handler.php} (82%) rename src/Handler/GridField/{AlterationHandler.php => Action/Handler.php} (85%) rename src/Handler/GridField/{URLActionHandler.php => Alteration/Handler.php} (85%) rename src/Listener/CMSMain/{CMSMainActionListener.php => Listener.php} (93%) rename src/Listener/Form/{FormSubmissionListener.php => Listener.php} (87%) rename src/Listener/GraphQL/{GraphQLMiddlewareListener.php => Middleware/Listener.php} (95%) rename src/Listener/GraphQL/{GraphQLMutationListener.php => Mutation/Listener.php} (95%) rename src/Listener/GridField/{GridFieldURLListener.php => Action/Listener.php} (88%) rename src/Listener/GridField/{GridFieldAlterationListener.php => Alteration/Listener.php} (95%) diff --git a/README.md b/README.md index 8242dfd..3221227 100644 --- a/README.md +++ b/README.md @@ -96,38 +96,38 @@ extensions to key classes in the admin that then trigger these events through th #### Event: formSubmitted * **Description**: Any form submitted in the CMS * **Example**: save, publish, unpublish, delete -* **Listener**: `SilverStripe\Snapshots\Listener\Form\FormSubmissionListener` -* **Handler**: `SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler` +* **Listener**: `SilverStripe\Snapshots\Listener\Form\Listener` +* **Handler**: `SilverStripe\Snapshots\Handler\Form\Handler` #### Event: cmsAction * **Description**: A `CMSMain` controller action * **Example**: `savetreenode` (reorder site tree) -* **Listener**: `SilverStripe\Snapshots\Listener\CMSMain\CMSMainActionListener` -* **Handler**: `SilverStripe\Snapshots\Handler\CMSMain\ActionHandler` +* **Listener**: `SilverStripe\Snapshots\Listener\CMSMain\Listener` +* **Handler**: `SilverStripe\Snapshots\Handler\CMSMain\Handler` #### Event: gridFieldAction * **Description**: A standard GridField action invoked via a URL (`GridField_URLHandler`) * **Example**: `handleReorder` (reorder items) -* **Listener**: `SilverStripe\Snapshots\Listener\GridField\GridFieldURLListener` -* **Handler**: `SilverStripe\Snapshots\Handler\GridField\URLActionHandler` +* **Listener**: `SilverStripe\Snapshots\Listener\GridField\Action\Listener` +* **Handler**: `SilverStripe\Snapshots\Handler\GridField\Action\Handler` #### Event: gridFieldAlteration * **Description**: A GridField action invoked via a URL (`GridField_ActionProvider`) * **Example**: `deleterecord`, `archiverecord` -* **Listener**: `SilverStripe\Snapshots\Listener\GridField\GridFieldAlterationListener` -* **Handler**: `SilverStripe\Snapshots\Handler\GridField\AlterationHandler` +* **Listener**: `SilverStripe\Snapshots\Listener\GridField\Alteration\Listener` +* **Handler**: `SilverStripe\Snapshots\Handler\GridField\Alteration\Handler` #### Event: graphqlMutation * **Description**: A scaffolded GraphQL mutation * **Example**: `mutation createMyDataObject(Input: $Input)` -* **Listener**: `SilverStripe\Snapshots\Listener\GraphQL\GraphQLMutationListener` -* **Handler**: `SilverStripe\Snapshots\Handler\GraphQL\MutationHandler` +* **Listener**: `SilverStripe\Snapshots\Listener\GraphQL\Mutation\Listener` +* **Handler**: `SilverStripe\Snapshots\Handler\GraphQL\Mutation\Handler` #### Event: graphqlOperation * **Description**: Any generic GraphQL operation * **Example**: `mutation publishAllFiles`, `query allTheThings` -* **Listener**: `SilverStripe\Snapshots\Listener\GraphQL\GraphQLMiddlewareListener` -* **Handler**: `SilverStripe\Snapshots\Handler\GraphQL\GenericHandler` +* **Listener**: `SilverStripe\Snapshots\Listener\GraphQL\Middleware\Listener` +* **Handler**: `SilverStripe\Snapshots\Handler\GraphQL\Middleware\Handler` ### Action identifiers @@ -213,14 +213,30 @@ SilverStripe\Core\Injector\Injector: SilverStripe\Snapshots\Dispatch\Dispatcher: properties: handlers: - - - on: [ formSubmitted.myFormHandler ] + myForm: + on: + 'formSubmitted.myFormHandler': true handler: %$MyProject\Handlers\MyHandler ``` +Notice that the event name is in the key of the configuration. This makes it possible for another layer of +configuration to disable it. See below. + ### Removing snapshot creators -The configuration API doesn't make it easy to remove items from arrays, so this is best done procedurally. +To remove an event from a handler, simply set its value to `false`. + +```yaml +SilverStripe\Core\Injector\Injector: + SilverStripe\Snapshots\Dispatch\Dispatcher: + properties: + handlers: + myForm: + on: + 'formSubmitted.myFormHandler': false +``` + +### Procedurally adding event handlers You can register a `EventHandlerLoader` implementation with `Dispatcher` to procedurally register and unregister events. @@ -228,8 +244,9 @@ events. ```yaml SilverStripe\Core\Injector\Injector: SilverStripe\Snapshots\Dispatch\Dispatcher: - constructor: - myLoader: %$MyProject\MyEventLoader + properties: + loaders: + myLoader: %$MyProject\MyEventLoader ``` ```php diff --git a/_config/config.yml b/_config/config.yml index 61f2831..97a1880 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -27,32 +27,32 @@ Name: snapshot-listeners --- SilverStripe\CMS\Controllers\CMSMain: extensions: - - SilverStripe\Snapshots\Listener\CMSMain\CMSMainActionListener + - SilverStripe\Snapshots\Listener\CMSMain\Listener SilverStripe\Forms\GridField\GridField: extensions: - - SilverStripe\Snapshots\Listener\GridField\GridFieldAlterationListener - - SilverStripe\Snapshots\Listener\GridField\GridFieldURLListener + - SilverStripe\Snapshots\Listener\GridField\Alteration\Listener + - SilverStripe\Snapshots\Listener\GridField\Action\Listener SilverStripe\Forms\FormRequestHandler: extensions: - - SilverStripe\Snapshots\Listener\Form\FormSubmissionListener + - SilverStripe\Snapshots\Listener\Form\Listener SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Create: extensions: - - SilverStripe\Snapshots\Listener\GraphQL\GraphQLMutationListener + - SilverStripe\Snapshots\Listener\GraphQL\Mutation\Listener SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Delete: extensions: - - SilverStripe\Snapshots\Listener\GraphQL\GraphQLMutationListener + - SilverStripe\Snapshots\Listener\GraphQL\Mutation\Listener SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Update: extensions: - - SilverStripe\Snapshots\Listener\GraphQL\GraphQLMutationListener + - SilverStripe\Snapshots\Listener\GraphQL\Mutation\Listener SilverStripe\GraphQL\Manager: extensions: - - SilverStripe\Snapshots\Listener\GraphQL\GraphQLMiddlewareListener + - SilverStripe\Snapshots\Listener\GraphQL\Middleware\Listener --- Name: snapshot-events @@ -61,24 +61,33 @@ SilverStripe\Core\Injector\Injector: SilverStripe\Snapshots\Dispatch\Dispatcher: properties: handlers: - - - on: [ formSubmitted.unpublish, formSubmitted.doSave, formSubmitted.doDelete] - handler: %$SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler - - - on: [ formSubmitted.save ] + formGeneric: + on: + 'formSubmitted.unpublish': true + 'formSubmitted.doSave': true + 'formSubmitted.doDelete': true + handler: %$SilverStripe\Snapshots\Handler\Form\Handler + save: + on: + 'formSubmitted.save': true handler: %$SilverStripe\Snapshots\Handler\Form\SaveHandler - - - on: [ formSubmitted.publish ] + publish: + on: + 'formSubmitted.publish': true handler: %$SilverStripe\Snapshots\Handler\Form\PublishHandler - - - on: [ cmsAction.savetreenode ] - handler: %$SilverStripe\Snapshots\Handler\CMSMain\ActionHandler - - - on: [ gridFieldAlteration.deleterecord, gridFieldAlteration.archiverecord ] - handler: %$SilverStripe\Snapshots\Handler\GridField\URLActionHandler - - - on: [ gridFieldAction.handleReorder ] - handler: %$SilverStripe\Snapshots\Handler\GridField\URLActionHandler - - - on: [ graphqlMutation.create, graphqlMutation.update, graphqlMutation.delete ] - handler: %$SilverStripe\Snapshots\Handler\GraphQL\MutationHandler + cmsMain: + on: + 'cmsAction.savetreenode': true + handler: %$SilverStripe\Snapshots\Handler\CMSMain\Handler + gridFieldAction: + on: + 'gridFieldAction.handleReorder': true + 'gridFieldAlteration.deleterecord': true + 'gridFieldAlteration.archiverecord': true + handler: %$SilverStripe\Snapshots\Handler\GridField\Action\Handler + graphqlMutation: + on: + 'graphqlMutation.create': true + 'graphqlMutation.update': true + 'graphqlMutation.delete': true + handler: %$SilverStripe\Snapshots\Handler\GraphQL\Mutation\Handler diff --git a/src/Dispatch/Dispatcher.php b/src/Dispatch/Dispatcher.php index 3b75838..d0fa192 100644 --- a/src/Dispatch/Dispatcher.php +++ b/src/Dispatch/Dispatcher.php @@ -12,16 +12,26 @@ class Dispatcher { use Injectable; + /** + * @var EventHandlerLoader[] + */ + private $loaders = []; + /** * @var array HandlerInterface[] */ private $handlers = []; /** - * Dispatcher constructor. + * @var bool + */ + private $initialised = false; + + /** * @param EventHandlerLoader[] $loaders + * @return $this */ - public function __construct($loaders = []) + public function setLoaders($loaders = []) { foreach ($loaders as $loader) { if (!$loader instanceof EventHandlerLoader) { @@ -31,9 +41,10 @@ public function __construct($loaders = []) EventHandlerLoader::class )); } - - $loader->addToDispatcher($this); } + $this->loaders = $loaders; + + return $this; } /** @@ -57,8 +68,10 @@ public function setHandlers(array $handlers) )); } - foreach ($on as $eventName) { - $this->addListener($eventName, $handler); + foreach ($on as $eventName => $shouldInclude) { + if ($shouldInclude) { + $this->addListener($eventName, $handler); + } } } } @@ -124,7 +137,15 @@ public function removeListenerByClassName(string $event, string $className): sel */ public function trigger(string $event, EventContext $context): void { + // TODO: This could be moved to procedural code in something like _config.php, + // or add a new class that bootstraps the dispatcher. + $this->initialise(); + $action = $context->getAction(); + if ($action === null) { + return; + } + // First fire listeners to , then just fire generic listeners $eventsToFire = [ $event . '.' . $action, $event]; foreach ($eventsToFire as $event) { @@ -135,4 +156,16 @@ public function trigger(string $event, EventContext $context): void } } } + + private function initialise(): void + { + if ($this->initialised) { + return; + } + + foreach ($this->loaders as $loader) { + $loader->addToDispatcher($this); + } + $this->initialised = true; + } } diff --git a/src/Dispatch/EventHandlerLoader.php b/src/Dispatch/EventHandlerLoader.php index 15ddc26..4bf08b9 100644 --- a/src/Dispatch/EventHandlerLoader.php +++ b/src/Dispatch/EventHandlerLoader.php @@ -3,7 +3,6 @@ namespace SilverStripe\Snapshots\Dispatch; - interface EventHandlerLoader { public function addToDispatcher(Dispatcher $dispatcher): void; diff --git a/src/Handler/CMSMain/ActionHandler.php b/src/Handler/CMSMain/Handler.php similarity index 92% rename from src/Handler/CMSMain/ActionHandler.php rename to src/Handler/CMSMain/Handler.php index 589ddfb..6436f29 100644 --- a/src/Handler/CMSMain/ActionHandler.php +++ b/src/Handler/CMSMain/Handler.php @@ -3,7 +3,6 @@ namespace SilverStripe\Snapshots\Handler\CMSMain; - use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Control\HTTPResponse; use SilverStripe\ORM\DataObject; @@ -12,7 +11,7 @@ use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; -class ActionHandler extends HandlerAbstract +class Handler extends HandlerAbstract { /** * @param EventContext $context @@ -22,6 +21,10 @@ class ActionHandler extends HandlerAbstract protected function createSnapshot(EventContext $context): ?Snapshot { $action = $context->getAction(); + if ($action === null) { + return null; + } + /* @var HTTPResponse $result */ $result = $context->get('result'); if (!$result instanceof HTTPResponse) { diff --git a/src/Handler/Form/FormSubmissionHandler.php b/src/Handler/Form/Handler.php similarity index 92% rename from src/Handler/Form/FormSubmissionHandler.php rename to src/Handler/Form/Handler.php index 379c413..44f9a50 100644 --- a/src/Handler/Form/FormSubmissionHandler.php +++ b/src/Handler/Form/Handler.php @@ -3,7 +3,6 @@ namespace SilverStripe\Snapshots\Handler\Form; - use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Control\HTTPRequest; use SilverStripe\ORM\ValidationException; @@ -11,7 +10,7 @@ use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; -class FormSubmissionHandler extends HandlerAbstract +class Handler extends HandlerAbstract { /** * @param EventContext $context @@ -21,6 +20,10 @@ class FormSubmissionHandler extends HandlerAbstract protected function createSnapshot(EventContext $context): ?Snapshot { $action = $context->getAction(); + if ($action === null) { + return null; + } + $page = $this->getPage($context); $record = null; if ($form = $context->get('form')) { diff --git a/src/Handler/Form/PublishHandler.php b/src/Handler/Form/PublishHandler.php index a51c749..ef0c411 100644 --- a/src/Handler/Form/PublishHandler.php +++ b/src/Handler/Form/PublishHandler.php @@ -3,25 +3,26 @@ namespace SilverStripe\Snapshots\Handler\Form; - use SilverStripe\Forms\Form; use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; -class PublishHandler extends FormSubmissionHandler +class PublishHandler extends Handler { protected function createSnapshot(EventContext $context): ?Snapshot { $snapshot = parent::createSnapshot($context); - if ($snapshot) { - // mark publish actions as WasPublished - the status flags rely on this being set correctly - /* @var Form $form */ - $form = $context->get('form'); - if ($form->getName() === 'EditForm') { - foreach ($snapshot->Items() as $item) { - $item->WasPublished = true; - $item->write(); - } + if (!$snapshot) { + return null; + } + + // mark publish actions as WasPublished - the status flags rely on this being set correctly + /* @var Form $form */ + $form = $context->get('form'); + if ($form->getName() === 'EditForm') { + foreach ($snapshot->Items() as $item) { + $item->WasPublished = true; + $item->write(); } } diff --git a/src/Handler/Form/SaveHandler.php b/src/Handler/Form/SaveHandler.php index 98378f9..af35b01 100644 --- a/src/Handler/Form/SaveHandler.php +++ b/src/Handler/Form/SaveHandler.php @@ -3,12 +3,11 @@ namespace SilverStripe\Snapshots\Handler\Form; - use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; -class SaveHandler extends FormSubmissionHandler +class SaveHandler extends Handler { /** * @param EventContext $context diff --git a/src/Handler/GraphQL/GenericHandler.php b/src/Handler/GraphQL/Middleware/Handler.php similarity index 80% rename from src/Handler/GraphQL/GenericHandler.php rename to src/Handler/GraphQL/Middleware/Handler.php index 46f8d41..251f3e1 100644 --- a/src/Handler/GraphQL/GenericHandler.php +++ b/src/Handler/GraphQL/Middleware/Handler.php @@ -1,15 +1,14 @@ getAction(); + if ($action === null) { + return null; + } + $message = $this->getMessage($action); $page = $this->getPageFromReferrer(); @@ -28,6 +31,4 @@ protected function createSnapshot(EventContext $context): ?Snapshot return Snapshot::singleton()->createSnapshotFromAction($page, null, $message); } - - } diff --git a/src/Handler/GraphQL/MutationHandler.php b/src/Handler/GraphQL/Mutation/Handler.php similarity index 82% rename from src/Handler/GraphQL/MutationHandler.php rename to src/Handler/GraphQL/Mutation/Handler.php index d99bc06..d215acd 100644 --- a/src/Handler/GraphQL/MutationHandler.php +++ b/src/Handler/GraphQL/Mutation/Handler.php @@ -1,15 +1,14 @@ getAction(); + if ($type === null) { + return null; + } + $action = static::ACTION_PREFIX . $type; $message = $this->getMessage($action); $page = $this->getPageFromReferrer(); @@ -31,6 +34,4 @@ protected function createSnapshot(EventContext $context): ?Snapshot return Snapshot::singleton()->createSnapshotFromAction($page, null, $message); } - - } diff --git a/src/Handler/GridField/AlterationHandler.php b/src/Handler/GridField/Action/Handler.php similarity index 85% rename from src/Handler/GridField/AlterationHandler.php rename to src/Handler/GridField/Action/Handler.php index 7ec5ab2..6ca41f0 100644 --- a/src/Handler/GridField/AlterationHandler.php +++ b/src/Handler/GridField/Action/Handler.php @@ -1,7 +1,7 @@ getAction(); + if ($action === null) { + return null; + } + $message = $this->getMessage($action); /* @var Form $form */ $form = $context->get('gridField')->getForm(); @@ -41,6 +45,4 @@ protected function createSnapshot(EventContext $context): ?Snapshot return Snapshot::singleton()->createSnapshotFromAction($page, $record, $message); } - - } diff --git a/src/Handler/GridField/URLActionHandler.php b/src/Handler/GridField/Alteration/Handler.php similarity index 85% rename from src/Handler/GridField/URLActionHandler.php rename to src/Handler/GridField/Alteration/Handler.php index 062b342..b6d0028 100644 --- a/src/Handler/GridField/URLActionHandler.php +++ b/src/Handler/GridField/Alteration/Handler.php @@ -1,7 +1,7 @@ getAction(); + if ($action === null) { + return null; + } + $message = $this->getMessage($action); /* @var Form $form */ $form = $context->get('gridField')->getForm(); diff --git a/src/Handler/HandlerInterface.php b/src/Handler/HandlerInterface.php index b31512a..fbbd8ec 100644 --- a/src/Handler/HandlerInterface.php +++ b/src/Handler/HandlerInterface.php @@ -3,11 +3,9 @@ namespace SilverStripe\Snapshots\Handler; - use SilverStripe\Snapshots\Listener\EventContext; interface HandlerInterface { public function fire(EventContext $context): void; - } diff --git a/src/Listener/CMSMain/CMSMainActionListener.php b/src/Listener/CMSMain/Listener.php similarity index 93% rename from src/Listener/CMSMain/CMSMainActionListener.php rename to src/Listener/CMSMain/Listener.php index cacffac..f6a5eb5 100644 --- a/src/Listener/CMSMain/CMSMainActionListener.php +++ b/src/Listener/CMSMain/Listener.php @@ -15,7 +15,7 @@ * * @property CMSMain|$this $owner */ -class CMSMainActionListener extends Extension +class Listener extends Extension { /** * Extension point in @see CMSMain::handleAction @@ -24,7 +24,8 @@ class CMSMainActionListener extends Extension * @param $action * @param $result */ - public function afterCallActionHandler(HTTPRequest $request, $action, $result): void { + public function afterCallActionHandler(HTTPRequest $request, $action, $result): void + { Dispatcher::singleton()->trigger( 'cmsAction', new EventContext( diff --git a/src/Listener/EventContext.php b/src/Listener/EventContext.php index 6cce51a..e18383e 100644 --- a/src/Listener/EventContext.php +++ b/src/Listener/EventContext.php @@ -3,13 +3,10 @@ namespace SilverStripe\Snapshots\Listener; - -use phpDocumentor\Reflection\Types\Scalar; - class EventContext { /** - * @var string + * @var string|null */ private $action; @@ -20,19 +17,19 @@ class EventContext /** * EventContext constructor. - * @param string $action + * @param string|null $action * @param array $meta */ - public function __construct(string $action, array $meta = []) + public function __construct(?string $action = null, array $meta = []) { $this->action = $action; $this->meta = $meta; } /** - * @return string + * @return string|null */ - public function getAction(): string + public function getAction(): ?string { return $this->action; } @@ -54,5 +51,4 @@ public function __get($name) { return $this->get($name); } - } diff --git a/src/Listener/Form/FormSubmissionListener.php b/src/Listener/Form/Listener.php similarity index 87% rename from src/Listener/Form/FormSubmissionListener.php rename to src/Listener/Form/Listener.php index 37587f1..5ce2a97 100644 --- a/src/Listener/Form/FormSubmissionListener.php +++ b/src/Listener/Form/Listener.php @@ -16,7 +16,7 @@ * * @property FormRequestHandler|$this $owner */ -class FormSubmissionListener extends Extension +class Listener extends Extension { /** * Extension point in @see FormRequestHandler::httpSubmission @@ -27,7 +27,7 @@ class FormSubmissionListener extends Extension * @param $vars * @param Form $form */ - public function afterCallFormHandler (HTTPRequest $request, $funcName, $vars, $form): void + public function afterCallFormHandler(HTTPRequest $request, $funcName, $vars, $form): void { Dispatcher::singleton()->trigger( 'formSubmitted', diff --git a/src/Listener/GraphQL/GraphQLMiddlewareListener.php b/src/Listener/GraphQL/Middleware/Listener.php similarity index 95% rename from src/Listener/GraphQL/GraphQLMiddlewareListener.php rename to src/Listener/GraphQL/Middleware/Listener.php index b992349..55a77be 100644 --- a/src/Listener/GraphQL/GraphQLMiddlewareListener.php +++ b/src/Listener/GraphQL/Middleware/Listener.php @@ -1,6 +1,6 @@ trigger( 'gridFieldAction', new EventContext( diff --git a/src/Listener/GridField/GridFieldAlterationListener.php b/src/Listener/GridField/Alteration/Listener.php similarity index 95% rename from src/Listener/GridField/GridFieldAlterationListener.php rename to src/Listener/GridField/Alteration/Listener.php index c7cb79f..73e485a 100644 --- a/src/Listener/GridField/GridFieldAlterationListener.php +++ b/src/Listener/GridField/Alteration/Listener.php @@ -1,6 +1,6 @@ write(); } } - } diff --git a/src/SnapshotPublishable.php b/src/SnapshotPublishable.php index f964a7c..161be36 100644 --- a/src/SnapshotPublishable.php +++ b/src/SnapshotPublishable.php @@ -254,7 +254,7 @@ public function getActivity() 'SnapshotID' => $snapShotIDs, ]) ->where( - // Only get the items that were the subject of a user's action + // Only get the items that were the subject of a user's action "\"$snapshotTable\" . \"OriginHash\" = \"$itemTable\".\"ObjectHash\"" ) ->sort([ From f33858383bc4cca8bf963d0840907a80a9e838c3 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Tue, 14 Jan 2020 10:05:16 +1300 Subject: [PATCH 07/16] Use operation name when provided for graphql middleware --- src/Listener/GraphQL/Middleware/Listener.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Listener/GraphQL/Middleware/Listener.php b/src/Listener/GraphQL/Middleware/Listener.php index 55a77be..cf08887 100644 --- a/src/Listener/GraphQL/Middleware/Listener.php +++ b/src/Listener/GraphQL/Middleware/Listener.php @@ -64,6 +64,11 @@ private function getActionFromQuery(?string $query = null): string continue; } if (in_array($statement->operation, [Manager::MUTATION_ROOT, Manager::QUERY_ROOT])) { + // If the operation was given a name, use that + $name = $statement->name; + if ($name && $name->value) { + return $name->value; + } $selectionSet = $statement->selectionSet; if ($selectionSet) { $selections = $selectionSet->selections; From befa8870607055f482866fa686d0e762ad8e42ea Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Tue, 14 Jan 2020 10:12:46 +1300 Subject: [PATCH 08/16] Remove gridfield reorder from core --- _config/config.yml | 1 - lang/en.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/_config/config.yml b/_config/config.yml index 97a1880..eeea5d8 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -81,7 +81,6 @@ SilverStripe\Core\Injector\Injector: handler: %$SilverStripe\Snapshots\Handler\CMSMain\Handler gridFieldAction: on: - 'gridFieldAction.handleReorder': true 'gridFieldAlteration.deleterecord': true 'gridFieldAlteration.archiverecord': true handler: %$SilverStripe\Snapshots\Handler\GridField\Action\Handler diff --git a/lang/en.yml b/lang/en.yml index 59c538f..cb1e9f9 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -16,4 +16,3 @@ en: SilverStripe\Snapshots\Handler\GridField\URLActionHandler: HANDLER_deleterecord: 'Delete record' HANDLER_archiverecord: 'Archive record' - HANDLER_handleReorder: 'Reorder items' From 30a887fe521ca11cc4c5217877341087da77347e Mon Sep 17 00:00:00 2001 From: Mojmir Fendek Date: Tue, 14 Jan 2020 13:14:16 +1300 Subject: [PATCH 09/16] PR fixes: * message config * loader usage * form submission listener --- lang/en.yml | 8 ++--- src/Dispatch/Dispatcher.php | 4 +-- src/Listener/Form/Listener.php | 62 +++++++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/lang/en.yml b/lang/en.yml index cb1e9f9..1ea6ade 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -1,5 +1,5 @@ en: - SilverStripe\Snapshots\Handler\Form\FormSubmissionHandler: + SilverStripe\Snapshots\Handler\Form\Handler: HANDLER_unpublish: 'Unpublish page' HANDLER_doSave: 'Save item' HANDLER_doDelete: 'Delete item' @@ -7,12 +7,12 @@ en: HANDLER_save: 'Save page' SilverStripe\Snapshots\Handler\Form\PublishHandler: HANDLER_publish: 'Publish page' - SilverStripe\Snapshots\Handler\CMSMain\ActionHandler: + SilverStripe\Snapshots\Handler\CMSMain\Handler: HANDLER_savetreenode: 'Reordered site tree' - SilverStripe\Snapshots\Handler\GraphQL\MutationHandler: + SilverStripe\Snapshots\Handler\GraphQL\Mutation\Handler: HANDLER_graphql_crud_create: 'GraphQL create' HANDLER_graphql_crud_update: 'GraphQL update' HANDLER_graphql_crud_delete: 'GraphQL delete' - SilverStripe\Snapshots\Handler\GridField\URLActionHandler: + SilverStripe\Snapshots\Handler\GridField\Action\Handler: HANDLER_deleterecord: 'Delete record' HANDLER_archiverecord: 'Archive record' diff --git a/src/Dispatch/Dispatcher.php b/src/Dispatch/Dispatcher.php index d0fa192..bc4c5a4 100644 --- a/src/Dispatch/Dispatcher.php +++ b/src/Dispatch/Dispatcher.php @@ -109,7 +109,7 @@ public function addListener(string $event, HandlerInterface $handler): self public function removeListener(string $event, HandlerInterface $handler): self { $handlers = $this->handlers[$event] ?? []; - $this->handlers = array_filter($handlers, function ($existing) use ($handler) { + $this->handlers[$event] = array_filter($handlers, function ($existing) use ($handler) { return $existing !== $handler; }); @@ -124,7 +124,7 @@ public function removeListener(string $event, HandlerInterface $handler): self public function removeListenerByClassName(string $event, string $className): self { $handlers = $this->handlers[$event] ?? []; - $this->handlers = array_filter($handlers, function ($existing) use ($className) { + $this->handlers[$event] = array_filter($handlers, function ($existing) use ($className) { return get_class($existing) !== $className; }); diff --git a/src/Listener/Form/Listener.php b/src/Listener/Form/Listener.php index 5ce2a97..01eeee5 100644 --- a/src/Listener/Form/Listener.php +++ b/src/Listener/Form/Listener.php @@ -13,6 +13,13 @@ * Class Submission * * Snapshot action listener for form submissions + * This covers all extension points related to form submissions + * extension points are mutually exclusive (at most one of the extension points is used in one form submission) + * each extension point represents a different way how form submission is processed + * all of these extension points are necessary to cover all cases of form submissions + * we could use a more generic extension point to cover all cases with just one extension point + * however this would force us to duplicate functionality which extracts context information + * as such information is not provided in the more general extension point * * @property FormRequestHandler|$this $owner */ @@ -27,7 +34,60 @@ class Listener extends Extension * @param $vars * @param Form $form */ - public function afterCallFormHandler(HTTPRequest $request, $funcName, $vars, $form): void + public function afterCallFormHandlerController(HTTPRequest $request, $funcName, $vars, $form): void + { + $this->triggerAction($request, $funcName, $vars, $form); + } + + /** + * Extension point in @see FormRequestHandler::httpSubmission + * form handler action via form submission action + * + * @param HTTPRequest $request + * @param $funcName + * @param $vars + * @param Form $form + */ + public function afterCallFormHandlerMethod(HTTPRequest $request, $funcName, $vars, $form): void + { + $this->triggerAction($request, $funcName, $vars, $form); + } + + /** + * Extension point in @see FormRequestHandler::httpSubmission + * form method action via form submission action + * + * @param HTTPRequest $request + * @param $funcName + * @param $vars + * @param Form $form + */ + public function afterCallFormHandlerFormMethod(HTTPRequest $request, $funcName, $vars, $form): void + { + $this->triggerAction($request, $funcName, $vars, $form); + } + + /** + * Extension point in @see FormRequestHandler::httpSubmission + * form field method action via form submission action + * + * @param HTTPRequest $request + * @param $funcName + * @param $vars + * @param Form $form + */ + public function afterCallFormHandlerFieldMethod(HTTPRequest $request, $funcName, $vars, $form): void + { + $this->triggerAction($request, $funcName, $vars, $form); + } + + /** + * @param HTTPRequest $request + * @param $funcName + * @param $vars + * @param $form + */ + private function triggerAction(HTTPRequest $request, $funcName, $vars, $form): void { Dispatcher::singleton()->trigger( 'formSubmitted', From be1a0a664b043f1ebb304d8d8c0378bc2d05a43e Mon Sep 17 00:00:00 2001 From: Mojmir Fendek Date: Tue, 14 Jan 2020 14:38:28 +1300 Subject: [PATCH 10/16] Form listener changes reverted. --- src/Listener/Form/Listener.php | 65 +--------------------------------- 1 file changed, 1 insertion(+), 64 deletions(-) diff --git a/src/Listener/Form/Listener.php b/src/Listener/Form/Listener.php index 01eeee5..a82e1b3 100644 --- a/src/Listener/Form/Listener.php +++ b/src/Listener/Form/Listener.php @@ -12,82 +12,19 @@ /** * Class Submission * - * Snapshot action listener for form submissions - * This covers all extension points related to form submissions - * extension points are mutually exclusive (at most one of the extension points is used in one form submission) - * each extension point represents a different way how form submission is processed - * all of these extension points are necessary to cover all cases of form submissions - * we could use a more generic extension point to cover all cases with just one extension point - * however this would force us to duplicate functionality which extracts context information - * as such information is not provided in the more general extension point - * * @property FormRequestHandler|$this $owner */ class Listener extends Extension { /** * Extension point in @see FormRequestHandler::httpSubmission - * controller action via form submission action - * - * @param HTTPRequest $request - * @param $funcName - * @param $vars - * @param Form $form - */ - public function afterCallFormHandlerController(HTTPRequest $request, $funcName, $vars, $form): void - { - $this->triggerAction($request, $funcName, $vars, $form); - } - - /** - * Extension point in @see FormRequestHandler::httpSubmission - * form handler action via form submission action - * - * @param HTTPRequest $request - * @param $funcName - * @param $vars - * @param Form $form - */ - public function afterCallFormHandlerMethod(HTTPRequest $request, $funcName, $vars, $form): void - { - $this->triggerAction($request, $funcName, $vars, $form); - } - - /** - * Extension point in @see FormRequestHandler::httpSubmission - * form method action via form submission action * * @param HTTPRequest $request * @param $funcName * @param $vars * @param Form $form */ - public function afterCallFormHandlerFormMethod(HTTPRequest $request, $funcName, $vars, $form): void - { - $this->triggerAction($request, $funcName, $vars, $form); - } - - /** - * Extension point in @see FormRequestHandler::httpSubmission - * form field method action via form submission action - * - * @param HTTPRequest $request - * @param $funcName - * @param $vars - * @param Form $form - */ - public function afterCallFormHandlerFieldMethod(HTTPRequest $request, $funcName, $vars, $form): void - { - $this->triggerAction($request, $funcName, $vars, $form); - } - - /** - * @param HTTPRequest $request - * @param $funcName - * @param $vars - * @param $form - */ - private function triggerAction(HTTPRequest $request, $funcName, $vars, $form): void + public function afterCallFormHandler(HTTPRequest $request, $funcName, $vars, $form): void { Dispatcher::singleton()->trigger( 'formSubmitted', From c0aad86c9b9d60ac6ddff7581aca21af1b41de17 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Wed, 15 Jan 2020 14:10:13 +1300 Subject: [PATCH 11/16] Move event dispatcher --- _config/config.yml | 41 +---- composer.json | 3 +- src/Dispatch/Dispatcher.php | 171 ------------------ src/Dispatch/EventHandlerLoader.php | 9 - src/Handler/CMSMain/Handler.php | 6 +- src/{Listener => Handler}/CurrentPage.php | 2 +- src/Handler/Form/Handler.php | 10 +- src/Handler/Form/PublishHandler.php | 4 +- src/Handler/Form/SaveHandler.php | 6 +- src/Handler/GraphQL/Middleware/Handler.php | 6 +- src/Handler/GraphQL/Mutation/Handler.php | 6 +- src/Handler/GridField/Action/Handler.php | 6 +- src/Handler/GridField/Alteration/Handler.php | 6 +- src/Handler/HandlerAbstract.php | 12 +- src/Handler/HandlerInterface.php | 11 -- src/Listener/CMSMain/Listener.php | 41 ----- src/Listener/EventContext.php | 54 ------ src/Listener/Form/Listener.php | 41 ----- src/Listener/GraphQL/Middleware/Listener.php | 86 --------- src/Listener/GraphQL/Mutation/Listener.php | 73 -------- src/Listener/GridField/Action/Listener.php | 44 ----- .../GridField/Alteration/Listener.php | 93 ---------- 22 files changed, 41 insertions(+), 690 deletions(-) delete mode 100644 src/Dispatch/Dispatcher.php delete mode 100644 src/Dispatch/EventHandlerLoader.php rename src/{Listener => Handler}/CurrentPage.php (98%) delete mode 100644 src/Handler/HandlerInterface.php delete mode 100644 src/Listener/CMSMain/Listener.php delete mode 100644 src/Listener/EventContext.php delete mode 100644 src/Listener/Form/Listener.php delete mode 100644 src/Listener/GraphQL/Middleware/Listener.php delete mode 100644 src/Listener/GraphQL/Mutation/Listener.php delete mode 100644 src/Listener/GridField/Action/Listener.php delete mode 100644 src/Listener/GridField/Alteration/Listener.php diff --git a/_config/config.yml b/_config/config.yml index eeea5d8..e62a21c 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -21,44 +21,11 @@ Only: SilverStripe\CMS\Model\SiteTree: extensions: - SilverStripe\Snapshots\SnapshotSiteTree - ---- -Name: snapshot-listeners ---- -SilverStripe\CMS\Controllers\CMSMain: - extensions: - - SilverStripe\Snapshots\Listener\CMSMain\Listener - -SilverStripe\Forms\GridField\GridField: - extensions: - - SilverStripe\Snapshots\Listener\GridField\Alteration\Listener - - SilverStripe\Snapshots\Listener\GridField\Action\Listener - -SilverStripe\Forms\FormRequestHandler: - extensions: - - SilverStripe\Snapshots\Listener\Form\Listener - -SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Create: - extensions: - - SilverStripe\Snapshots\Listener\GraphQL\Mutation\Listener - -SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Delete: - extensions: - - SilverStripe\Snapshots\Listener\GraphQL\Mutation\Listener - -SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\Update: - extensions: - - SilverStripe\Snapshots\Listener\GraphQL\Mutation\Listener - -SilverStripe\GraphQL\Manager: - extensions: - - SilverStripe\Snapshots\Listener\GraphQL\Middleware\Listener - --- Name: snapshot-events --- SilverStripe\Core\Injector\Injector: - SilverStripe\Snapshots\Dispatch\Dispatcher: + SilverStripe\EventDispatcher\Dispatch\Dispatcher: properties: handlers: formGeneric: @@ -84,9 +51,15 @@ SilverStripe\Core\Injector\Injector: 'gridFieldAlteration.deleterecord': true 'gridFieldAlteration.archiverecord': true handler: %$SilverStripe\Snapshots\Handler\GridField\Action\Handler + gridFieldAlteration: + on: [] + handler: %$SilverStripe\Snapshots\Handler\GridField\Alteration\Handler graphqlMutation: on: 'graphqlMutation.create': true 'graphqlMutation.update': true 'graphqlMutation.delete': true handler: %$SilverStripe\Snapshots\Handler\GraphQL\Mutation\Handler + graphqlMutation: + on: [] + handler: %$SilverStripe\Snapshots\Handler\GraphQL\Middleware\Handler diff --git a/composer.json b/composer.json index 5edb583..39bd22e 100755 --- a/composer.json +++ b/composer.json @@ -22,7 +22,8 @@ "require": { "php": ">=7.1.0", "silverstripe/framework": "^4", - "silverstripe/vendor-plugin": "^1" + "silverstripe/vendor-plugin": "^1", + "silverstripe/event-dispatcher": "dev-master" }, "require-dev": { "phpunit/phpunit": "^5.7", diff --git a/src/Dispatch/Dispatcher.php b/src/Dispatch/Dispatcher.php deleted file mode 100644 index bc4c5a4..0000000 --- a/src/Dispatch/Dispatcher.php +++ /dev/null @@ -1,171 +0,0 @@ -loaders = $loaders; - - return $this; - } - - /** - * @param array $handlers - * @throws Exception - */ - public function setHandlers(array $handlers) - { - foreach ($handlers as $spec) { - if (!isset($spec['handler']) || !isset($spec['on'])) { - throw new InvalidArgumentException('Event handlers must have a "on" and "handler" nodes'); - } - $on = is_array($spec['on']) ? $spec['on'] : [$spec['on']]; - $handler = $spec['handler']; - - if (!$handler instanceof HandlerInterface) { - throw new InvalidArgumentException(sprintf( - 'Handler for %s is not an instance of %s', - implode(', ', $on), - HandlerInterface::class - )); - } - - foreach ($on as $eventName => $shouldInclude) { - if ($shouldInclude) { - $this->addListener($eventName, $handler); - } - } - } - } - - /** - * @param string $event - * @param HandlerInterface $handler - * @return $this - * @throws Exception - */ - public function addListener(string $event, HandlerInterface $handler): self - { - if (!isset($this->handlers[$event])) { - $this->handlers[$event] = []; - } - - foreach ($this->handlers[$event] as $existing) { - if ($existing === $handler) { - throw new Exception(sprintf( - 'Handler for %s has already been added', - $event - )); - } - } - $this->handlers[$event][] = $handler; - - return $this; - } - - /** - * @param string $event - * @param HandlerInterface $handler - * @return $this - */ - public function removeListener(string $event, HandlerInterface $handler): self - { - $handlers = $this->handlers[$event] ?? []; - $this->handlers[$event] = array_filter($handlers, function ($existing) use ($handler) { - return $existing !== $handler; - }); - - return $this; - } - - /** - * @param string $event - * @param string $className - * @return $this - */ - public function removeListenerByClassName(string $event, string $className): self - { - $handlers = $this->handlers[$event] ?? []; - $this->handlers[$event] = array_filter($handlers, function ($existing) use ($className) { - return get_class($existing) !== $className; - }); - - return $this; - } - - /** - * @param string $event - * @param EventContext $context - */ - public function trigger(string $event, EventContext $context): void - { - // TODO: This could be moved to procedural code in something like _config.php, - // or add a new class that bootstraps the dispatcher. - $this->initialise(); - - $action = $context->getAction(); - if ($action === null) { - return; - } - - // First fire listeners to , then just fire generic listeners - $eventsToFire = [ $event . '.' . $action, $event]; - foreach ($eventsToFire as $event) { - $handlers = $this->handlers[$event] ?? []; - /* @var HandlerInterface $handler */ - foreach ($handlers as $handler) { - $handler->fire($context); - } - } - } - - private function initialise(): void - { - if ($this->initialised) { - return; - } - - foreach ($this->loaders as $loader) { - $loader->addToDispatcher($this); - } - $this->initialised = true; - } -} diff --git a/src/Dispatch/EventHandlerLoader.php b/src/Dispatch/EventHandlerLoader.php deleted file mode 100644 index 4bf08b9..0000000 --- a/src/Dispatch/EventHandlerLoader.php +++ /dev/null @@ -1,9 +0,0 @@ -getAction(); if ($action === null) { diff --git a/src/Listener/CurrentPage.php b/src/Handler/CurrentPage.php similarity index 98% rename from src/Listener/CurrentPage.php rename to src/Handler/CurrentPage.php index f9386e2..2628c52 100644 --- a/src/Listener/CurrentPage.php +++ b/src/Handler/CurrentPage.php @@ -1,6 +1,6 @@ getAction(); if ($action === null) { @@ -40,10 +40,10 @@ protected function createSnapshot(EventContext $context): ?Snapshot } /** - * @param EventContext $context + * @param EventContextInterface $context * @return SiteTree|null */ - protected function getPage(EventContext $context): ?SiteTree + protected function getPage(EventContextInterface $context): ?SiteTree { /* @var HTTPRequest $request */ $request = $context->get('request'); diff --git a/src/Handler/Form/PublishHandler.php b/src/Handler/Form/PublishHandler.php index ef0c411..208fd99 100644 --- a/src/Handler/Form/PublishHandler.php +++ b/src/Handler/Form/PublishHandler.php @@ -3,13 +3,13 @@ namespace SilverStripe\Snapshots\Handler\Form; +use SilverStripe\EventDispatcher\Event\EventContextInterface; use SilverStripe\Forms\Form; -use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class PublishHandler extends Handler { - protected function createSnapshot(EventContext $context): ?Snapshot + protected function createSnapshot(EventContextInterface $context): ?Snapshot { $snapshot = parent::createSnapshot($context); if (!$snapshot) { diff --git a/src/Handler/Form/SaveHandler.php b/src/Handler/Form/SaveHandler.php index af35b01..a28ed65 100644 --- a/src/Handler/Form/SaveHandler.php +++ b/src/Handler/Form/SaveHandler.php @@ -3,18 +3,18 @@ namespace SilverStripe\Snapshots\Handler\Form; +use SilverStripe\EventDispatcher\Event\EventContextInterface; use SilverStripe\ORM\ValidationException; -use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class SaveHandler extends Handler { /** - * @param EventContext $context + * @param EventContextInterface $context * @return Snapshot|null * @throws ValidationException */ - protected function createSnapshot(EventContext $context): ?Snapshot + protected function createSnapshot(EventContextInterface $context): ?Snapshot { $page = $this->getPage($context); diff --git a/src/Handler/GraphQL/Middleware/Handler.php b/src/Handler/GraphQL/Middleware/Handler.php index 251f3e1..546f349 100644 --- a/src/Handler/GraphQL/Middleware/Handler.php +++ b/src/Handler/GraphQL/Middleware/Handler.php @@ -3,19 +3,19 @@ namespace SilverStripe\Snapshots\Handler\GraphQL\Middleware; +use SilverStripe\EventDispatcher\Event\EventContextInterface; use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; -use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class Handler extends HandlerAbstract { /** - * @param EventContext $context + * @param EventContextInterface $context * @return Snapshot|null * @throws ValidationException */ - protected function createSnapshot(EventContext $context): ?Snapshot + protected function createSnapshot(EventContextInterface $context): ?Snapshot { $action = $context->getAction(); if ($action === null) { diff --git a/src/Handler/GraphQL/Mutation/Handler.php b/src/Handler/GraphQL/Mutation/Handler.php index d215acd..c88dc02 100644 --- a/src/Handler/GraphQL/Mutation/Handler.php +++ b/src/Handler/GraphQL/Mutation/Handler.php @@ -3,9 +3,9 @@ namespace SilverStripe\Snapshots\Handler\GraphQL\Mutation; +use SilverStripe\EventDispatcher\Event\EventContextInterface; use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; -use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class Handler extends HandlerAbstract @@ -13,11 +13,11 @@ class Handler extends HandlerAbstract const ACTION_PREFIX = 'graphql_crud_'; /** - * @param EventContext $context + * @param EventContextInterface $context * @return Snapshot|null * @throws ValidationException */ - protected function createSnapshot(EventContext $context): ?Snapshot + protected function createSnapshot(EventContextInterface $context): ?Snapshot { $type = $context->getAction(); if ($type === null) { diff --git a/src/Handler/GridField/Action/Handler.php b/src/Handler/GridField/Action/Handler.php index 6ca41f0..3568b82 100644 --- a/src/Handler/GridField/Action/Handler.php +++ b/src/Handler/GridField/Action/Handler.php @@ -3,20 +3,20 @@ namespace SilverStripe\Snapshots\Handler\GridField\Action; +use SilverStripe\EventDispatcher\Event\EventContextInterface; use SilverStripe\Forms\Form; use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; -use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class Handler extends HandlerAbstract { /** - * @param EventContext $context + * @param EventContextInterface $context * @return Snapshot|null * @throws ValidationException */ - protected function createSnapshot(EventContext $context): ?Snapshot + protected function createSnapshot(EventContextInterface $context): ?Snapshot { $action = $context->getAction(); if ($action === null) { diff --git a/src/Handler/GridField/Alteration/Handler.php b/src/Handler/GridField/Alteration/Handler.php index b6d0028..2b30946 100644 --- a/src/Handler/GridField/Alteration/Handler.php +++ b/src/Handler/GridField/Alteration/Handler.php @@ -3,20 +3,20 @@ namespace SilverStripe\Snapshots\Handler\GridField\Alteration; +use SilverStripe\EventDispatcher\Event\EventContextInterface; use SilverStripe\Forms\Form; use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; -use SilverStripe\Snapshots\Listener\EventContext; use SilverStripe\Snapshots\Snapshot; class Handler extends HandlerAbstract { /** - * @param EventContext $context + * @param EventContextInterface $context * @return Snapshot|null * @throws ValidationException */ - protected function createSnapshot(EventContext $context): ?Snapshot + protected function createSnapshot(EventContextInterface $context): ?Snapshot { $action = $context->getAction(); if ($action === null) { diff --git a/src/Handler/HandlerAbstract.php b/src/Handler/HandlerAbstract.php index 0f33cc3..003e336 100644 --- a/src/Handler/HandlerAbstract.php +++ b/src/Handler/HandlerAbstract.php @@ -4,11 +4,11 @@ namespace SilverStripe\Snapshots\Handler; use SilverStripe\Core\Config\Configurable; -use SilverStripe\Snapshots\Listener\CurrentPage; -use SilverStripe\Snapshots\Listener\EventContext; +use SilverStripe\EventDispatcher\Event\EventContextInterface; +use SilverStripe\EventDispatcher\Event\EventHandlerInterface; use SilverStripe\Snapshots\Snapshot; -abstract class HandlerAbstract implements HandlerInterface +abstract class HandlerAbstract implements EventHandlerInterface { use CurrentPage; use Configurable; @@ -34,14 +34,14 @@ protected function getMessage(string $action): string return _t($key, $action); } - public function fire(EventContext $context): void + public function fire(EventContextInterface $context): void { $this->createSnapshot($context); } /** - * @param EventContext $context + * @param EventContextInterface $context * @return Snapshot|null */ - abstract protected function createSnapshot(EventContext $context): ?Snapshot; + abstract protected function createSnapshot(EventContextInterface $context): ?Snapshot; } diff --git a/src/Handler/HandlerInterface.php b/src/Handler/HandlerInterface.php deleted file mode 100644 index fbbd8ec..0000000 --- a/src/Handler/HandlerInterface.php +++ /dev/null @@ -1,11 +0,0 @@ -trigger( - 'cmsAction', - new EventContext( - $action, - [ - 'result' => $result, - 'treeClass' => $this->owner->config()->get('tree_class'), - 'id' => $request->requestVar('ID'), - ] - ) - ); - } -} diff --git a/src/Listener/EventContext.php b/src/Listener/EventContext.php deleted file mode 100644 index e18383e..0000000 --- a/src/Listener/EventContext.php +++ /dev/null @@ -1,54 +0,0 @@ -action = $action; - $this->meta = $meta; - } - - /** - * @return string|null - */ - public function getAction(): ?string - { - return $this->action; - } - - /** - * @param string $name - * @return string|int|bool|float|null - */ - public function get(string $name) - { - return $this->meta[$name] ?? null; - } - - /** - * @param $name - * @return string|int|bool|float|null - */ - public function __get($name) - { - return $this->get($name); - } -} diff --git a/src/Listener/Form/Listener.php b/src/Listener/Form/Listener.php deleted file mode 100644 index a82e1b3..0000000 --- a/src/Listener/Form/Listener.php +++ /dev/null @@ -1,41 +0,0 @@ -trigger( - 'formSubmitted', - new EventContext( - $funcName, - [ - 'form' => $form, - 'request' => $request, - 'vars' => $vars - ] - ) - ); - } -} diff --git a/src/Listener/GraphQL/Middleware/Listener.php b/src/Listener/GraphQL/Middleware/Listener.php deleted file mode 100644 index cf08887..0000000 --- a/src/Listener/GraphQL/Middleware/Listener.php +++ /dev/null @@ -1,86 +0,0 @@ -trigger( - 'graphqlOperation', - new EventContext( - $this->getActionFromQuery($query), - [ - 'schema' => $schema, - 'context' => $context, - 'params' => $params, - ] - ) - ); - } - - /** - * @param string|null $query - * @return string - * @throws SyntaxError - */ - private function getActionFromQuery(?string $query = null): string - { - $document = Parser::parse(new Source($query ?: 'GraphQL')); - $defs = $document->definitions; - foreach ($defs as $statement) { - $options = [ - NodeKind::OPERATION_DEFINITION, - NodeKind::OPERATION_TYPE_DEFINITION - ]; - if (!in_array($statement->kind, $options, true)) { - continue; - } - if (in_array($statement->operation, [Manager::MUTATION_ROOT, Manager::QUERY_ROOT])) { - // If the operation was given a name, use that - $name = $statement->name; - if ($name && $name->value) { - return $name->value; - } - $selectionSet = $statement->selectionSet; - if ($selectionSet) { - $selections = $selectionSet->selections; - if (!empty($selections)) { - $firstField = $selections[0]; - - return $firstField->name->value; - } - } - return $statement->operation; - } - } - return 'graphql'; - } -} diff --git a/src/Listener/GraphQL/Mutation/Listener.php b/src/Listener/GraphQL/Mutation/Listener.php deleted file mode 100644 index e785e59..0000000 --- a/src/Listener/GraphQL/Mutation/Listener.php +++ /dev/null @@ -1,73 +0,0 @@ -trigger( - 'graphqlMutation', - new EventContext( - $this->getActionFromScaffolder($this->owner), - [ - 'list' => $recordOrList instanceof SS_List ? $recordOrList : null, - 'record' => !$recordOrList instanceof SS_List ? $recordOrList : null, - 'args' => $args, - 'context' => $context, - 'info' => $info, - ] - ) - ); - } - - private function getActionFromScaffolder(OperationResolver $scaffolder): ?string - { - if ($scaffolder instanceof Create) { - return static::TYPE_CREATE; - } - - if ($scaffolder instanceof Delete) { - return static::TYPE_DELETE; - } - - if ($scaffolder instanceof Update) { - return static::TYPE_UPDATE; - } - - return null; - } -} diff --git a/src/Listener/GridField/Action/Listener.php b/src/Listener/GridField/Action/Listener.php deleted file mode 100644 index d2baa5e..0000000 --- a/src/Listener/GridField/Action/Listener.php +++ /dev/null @@ -1,44 +0,0 @@ -trigger( - 'gridFieldAction', - new EventContext( - $action, - [ - 'request' => $request, - 'result' => $result, - 'gridField' => $this->owner - ] - ) - ); - } -} diff --git a/src/Listener/GridField/Alteration/Listener.php b/src/Listener/GridField/Alteration/Listener.php deleted file mode 100644 index 73e485a..0000000 --- a/src/Listener/GridField/Alteration/Listener.php +++ /dev/null @@ -1,93 +0,0 @@ -getActionData($request->requestVars(), $this->owner); - if ($actionData) { - list ($actionName, $arguments) = $actionData; - } - if (!$actionName === null) { - return; - } - Dispatcher::singleton()->trigger( - 'gridFieldAlteration', - new EventContext( - $actionName, - [ - 'request' => $request, - 'result' => $result, - 'gridField' => $this->owner, - 'args' => $arguments, - ] - ) - ); - } - - /** - * @param array $data - * @param GridField $gridField - * @return array|null - */ - private function getActionData(array $data, GridField $gridField): ?array - { - // Fetch the store for the "state" of actions (not the GridField) - /** @var StateStore $store */ - $store = Injector::inst()->create(StateStore::class . '.' . $gridField->getName()); - - foreach ($data as $dataKey => $dataValue) { - if (!preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) { - continue; - } - - $stateChange = $store->load($matches[1]); - - $actionName = $stateChange['actionName']; - $arguments = array_key_exists('args', $stateChange) ? $stateChange['args'] : []; - $arguments = is_array($arguments) ? $arguments : []; - - if ($actionName) { - return [ - $actionName, - $arguments, - $data, - ]; - } - } - - return null; - } -} From 8a9d60ec0c767adc3477d7465a4ab1af93a71285 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Tue, 21 Jan 2020 15:58:10 +1300 Subject: [PATCH 12/16] Unit tests failing --- README.md | 78 +------- _config/config.yml | 24 +-- composer.json | 2 +- src/Handler/CurrentPage.php | 10 +- src/Handler/Form/Handler.php | 10 +- src/SnapshotHasher.php | 3 + tests/SnapshotTest.php | 361 +++++++++++++++++++++++++---------- 7 files changed, 289 insertions(+), 199 deletions(-) diff --git a/README.md b/README.md index 3221227..3e4adfa 100644 --- a/README.md +++ b/README.md @@ -83,81 +83,9 @@ owned modification state (WORK IN PROGRESS, POC ONLY) ## How it works -Snapshots are created in response to user events in the CMS. These events are driven by a simple -pub/sub API in the `SilverStripe\Snapshots\Dispatch\Dispatcher` API. - -### CMS Events and the Dispatcher - -By default, there are a handful -of broad-based CMS events that will trigger handlers that create snapshots. A suite of listeners are added via -extensions to key classes in the admin that then trigger these events through the -`Dispatcher` API, e.g. `Dispatcher::singleton()->trigger('formSubmitted')`. - -#### Event: formSubmitted -* **Description**: Any form submitted in the CMS -* **Example**: save, publish, unpublish, delete -* **Listener**: `SilverStripe\Snapshots\Listener\Form\Listener` -* **Handler**: `SilverStripe\Snapshots\Handler\Form\Handler` - -#### Event: cmsAction -* **Description**: A `CMSMain` controller action -* **Example**: `savetreenode` (reorder site tree) -* **Listener**: `SilverStripe\Snapshots\Listener\CMSMain\Listener` -* **Handler**: `SilverStripe\Snapshots\Handler\CMSMain\Handler` - -#### Event: gridFieldAction -* **Description**: A standard GridField action invoked via a URL (`GridField_URLHandler`) -* **Example**: `handleReorder` (reorder items) -* **Listener**: `SilverStripe\Snapshots\Listener\GridField\Action\Listener` -* **Handler**: `SilverStripe\Snapshots\Handler\GridField\Action\Handler` - -#### Event: gridFieldAlteration -* **Description**: A GridField action invoked via a URL (`GridField_ActionProvider`) -* **Example**: `deleterecord`, `archiverecord` -* **Listener**: `SilverStripe\Snapshots\Listener\GridField\Alteration\Listener` -* **Handler**: `SilverStripe\Snapshots\Handler\GridField\Alteration\Handler` - -#### Event: graphqlMutation -* **Description**: A scaffolded GraphQL mutation -* **Example**: `mutation createMyDataObject(Input: $Input)` -* **Listener**: `SilverStripe\Snapshots\Listener\GraphQL\Mutation\Listener` -* **Handler**: `SilverStripe\Snapshots\Handler\GraphQL\Mutation\Handler` - -#### Event: graphqlOperation -* **Description**: Any generic GraphQL operation -* **Example**: `mutation publishAllFiles`, `query allTheThings` -* **Listener**: `SilverStripe\Snapshots\Listener\GraphQL\Middleware\Listener` -* **Handler**: `SilverStripe\Snapshots\Handler\GraphQL\Middleware\Handler` - -### Action identifiers - -Each of these handlers is passed a context object that exposes an **action identifier**. This is a string that -provides specific information about what happened in the event that the handler can then use in its implementation. -For instance, if a form was submitted, and the function that handles the form is `doSave($data, $form)`, the action -identifier is `doSave`. Likewise, controller actions, GridField actions, and GraphQL operations are all action -identifiers. - -Events are always called with `eventName.`. For instance `formSubmitted.doSave`, allowing -the subscribers to only react to a specific subset of events. - -#### How to find your action identifier - -In the above example, we subscribe to the main event `formSubmitted`, but we've added more specificity with `myFormHandler`. -This is the name of the `action` provided in the context of the event. - -The easiest way to debug events is to put breakpoints or logging into the `Dispatcher::trigger()` function. This -will provide all the detail you need about what events are triggered when, and with what context. - -```php -public function trigger(string $event, EventContext $context): void -{ - error_log($event); - error_log($context->getAction()); - // ... -``` - -When the logging is in place you just go to the CMS and perform the action you are interested in. -This should narrow the list of identifier down to a much smaller subset. +Snapshots are created with handlers registered to user events in the CMS triggered by +the [`silverstripe/cms-events`](https://github.com/silverstripe/silverstripe-cms-events) +module. ### Customising the snapshot messages diff --git a/_config/config.yml b/_config/config.yml index e62a21c..54ed3e9 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -29,37 +29,23 @@ SilverStripe\Core\Injector\Injector: properties: handlers: formGeneric: - on: - 'formSubmitted.unpublish': true - 'formSubmitted.doSave': true - 'formSubmitted.doDelete': true + on: [ 'formSubmitted.unpublish', 'formSubmitted.doSave', 'formSubmitted.doDelete' ] handler: %$SilverStripe\Snapshots\Handler\Form\Handler save: - on: - 'formSubmitted.save': true + on: [ 'formSubmitted.save' ] handler: %$SilverStripe\Snapshots\Handler\Form\SaveHandler publish: - on: - 'formSubmitted.publish': true + on: [ 'formSubmitted.publish' ] handler: %$SilverStripe\Snapshots\Handler\Form\PublishHandler cmsMain: - on: - 'cmsAction.savetreenode': true + on: [ 'cmsAction.savetreenode' ] handler: %$SilverStripe\Snapshots\Handler\CMSMain\Handler gridFieldAction: - on: - 'gridFieldAlteration.deleterecord': true - 'gridFieldAlteration.archiverecord': true + on: [ 'gridFieldAlteration.deleterecord', 'gridFieldAlteration.archiverecord' ] handler: %$SilverStripe\Snapshots\Handler\GridField\Action\Handler gridFieldAlteration: on: [] handler: %$SilverStripe\Snapshots\Handler\GridField\Alteration\Handler - graphqlMutation: - on: - 'graphqlMutation.create': true - 'graphqlMutation.update': true - 'graphqlMutation.delete': true - handler: %$SilverStripe\Snapshots\Handler\GraphQL\Mutation\Handler graphqlMutation: on: [] handler: %$SilverStripe\Snapshots\Handler\GraphQL\Middleware\Handler diff --git a/composer.json b/composer.json index 39bd22e..0360fdd 100755 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "php": ">=7.1.0", "silverstripe/framework": "^4", "silverstripe/vendor-plugin": "^1", - "silverstripe/event-dispatcher": "dev-master" + "silverstripe/cms-events": "dev-master" }, "require-dev": { "phpunit/phpunit": "^5.7", diff --git a/src/Handler/CurrentPage.php b/src/Handler/CurrentPage.php index 2628c52..e0ee9e9 100644 --- a/src/Handler/CurrentPage.php +++ b/src/Handler/CurrentPage.php @@ -25,9 +25,9 @@ trait CurrentPage * note that this can be only used for actions that are aware of the current page * * @param mixed $controller - * @return Page|null + * @return SiteTree|null */ - protected function getCurrentPageFromController($controller): ?Page + protected function getCurrentPageFromController($controller): ?SiteTree { while ($controller && ($controller instanceof Form || $controller instanceof GridFieldDetailForm_ItemRequest)) { $controller = $controller->getController(); @@ -66,8 +66,10 @@ protected function getCurrentPageFromRequestUrl(?string $url): ?Page } $adminSegment = AdminRootController::get_admin_route(); - $controllerSegment = CMSPageEditController::config()->get('url_segment'); - $formSegment = 'EditForm'; + $controller = CMSPageEditController::singleton(); + $controllerSegment = $controller->config()->get('url_segment'); + $editForm = $controller->getEditForm(); + $formSegment = $editForm->getName(); $viewSegment = 'show'; foreach ([$adminSegment, $controllerSegment, $formSegment, $viewSegment] as $segment) { diff --git a/src/Handler/Form/Handler.php b/src/Handler/Form/Handler.php index 4774b48..aa92598 100644 --- a/src/Handler/Form/Handler.php +++ b/src/Handler/Form/Handler.php @@ -6,6 +6,7 @@ use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Control\HTTPRequest; use SilverStripe\EventDispatcher\Event\EventContextInterface; +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\ValidationException; use SilverStripe\Snapshots\Handler\HandlerAbstract; use SilverStripe\Snapshots\Snapshot; @@ -41,10 +42,15 @@ protected function createSnapshot(EventContextInterface $context): ?Snapshot /** * @param EventContextInterface $context - * @return SiteTree|null + * @return DataObject|null */ - protected function getPage(EventContextInterface $context): ?SiteTree + protected function getPage(EventContextInterface $context): ?DataObject { + $page = $context->get('page'); + if ($page) { + return $page; + } + /* @var HTTPRequest $request */ $request = $context->get('request'); $url = $request->getURL(); diff --git a/src/SnapshotHasher.php b/src/SnapshotHasher.php index e93ef6e..0c315b8 100644 --- a/src/SnapshotHasher.php +++ b/src/SnapshotHasher.php @@ -2,6 +2,7 @@ namespace SilverStripe\Snapshots; +use SilverStripe\Core\ClassInfo; use SilverStripe\ORM\DataObject; /** @@ -18,6 +19,8 @@ trait SnapshotHasher */ public static function hashForSnapshot($class, $id): string { + // test code. remove. + return ClassInfo::shortName($class) . '#' . $id; return base64_encode(hash('sha256', sprintf('%s:%s', $class, $id), true)); } diff --git a/tests/SnapshotTest.php b/tests/SnapshotTest.php index ce73278..a4765b1 100644 --- a/tests/SnapshotTest.php +++ b/tests/SnapshotTest.php @@ -4,8 +4,20 @@ namespace SilverStripe\Snapshots\Tests; use DateTime; +use SilverStripe\Admin\AdminRootController; +use SilverStripe\CMS\Controllers\CMSPageEditController; +use SilverStripe\CMS\Model\SiteTree; +use SilverStripe\CMSEvents\Listener\Form\Listener; +use SilverStripe\Control\HTTPRequest; +use SilverStripe\Control\Session; use SilverStripe\Core\Config\Config; +use SilverStripe\Core\Path; use SilverStripe\Dev\FunctionalTest; +use SilverStripe\EventDispatcher\Dispatch\Dispatcher; +use SilverStripe\EventDispatcher\Event\EventContextInterface; +use SilverStripe\EventDispatcher\Symfony\Event; +use SilverStripe\Forms\FieldList; +use SilverStripe\Forms\Form; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\FieldType\DBDatetime; @@ -38,6 +50,11 @@ class SnapshotTest extends FunctionalTest ChangeSetItem::class, ]; + /** + * @var SiteTree + */ + private $currentPage; + protected function setUp(): void { parent::setUp(); @@ -55,27 +72,35 @@ public function testFundamentals() /* @var DataObject|SnapshotPublishable $a1 */ $a1 = new BlockPage(['Title' => 'A1 Block Page']); - $a1->write(); - $a1->publishRecursive(); + $this->editingPage($a1); + $this->formSaveObject($a1); + $this->formPublishObject($a1); /* @var DataObject|SnapshotPublishable $a2 */ $a2 = new BlockPage(['Title' => 'A2 Block Page']); - $a2->write(); - $a2->publishRecursive(); + $this->editingPage($a2); + $this->formSaveObject($a2); + $this->formPublishObject($a2); + + // A1 Block page edits + $this->editingPage($a1); /* @var DataObject|SnapshotPublishable $a1Block1 */ $a1Block1 = new Block(['Title' => 'Block 1 on A1', 'ParentID' => $a1->ID]); - $a1Block1->write(); + + $this->formSaveObject($a1Block1); $a1Block2 = new Block(['Title' => 'Block 2 on A1', 'ParentID' => $a1->ID]); - $a1Block2->write(); + $this->formSaveObject($a1Block2); // A1 // block1 (draft, new) * // block2 (draft, new) * + $this->editingPage($a2); + /* @var DataObject|SnapshotPublishable $a2Block1 */ $a2Block1 = new Block(['Title' => 'Block 1 on A2', 'ParentID' => $a2->ID]); - $a2Block1->write(); + $this->formSaveObject($a2Block1); // A1 // block1 (draft, new) @@ -83,8 +108,10 @@ public function testFundamentals() // A2 // block1 (draft, new) * + $this->editingPage($a1); + $a1->Title = 'A1 Block Page -- changed'; - $a1->write(); + $this->formSaveObject($a1); // A1 (draft, modified) * // block1 (draft, new) @@ -93,7 +120,7 @@ public function testFundamentals() // block1 (draft, new) $a1Block1->Title = 'Block 1 on A1 -- changed'; - $a1Block1->write(); + $this->formSaveObject($a1Block1); // A1 (draft, modified) // block1 (draft, modified) * @@ -129,10 +156,12 @@ public function testFundamentals() ] ); + $this->editingPage($a1); + // Testing third level /* @var DataObject|SnapshotPublishable|Versioned $gallery1 */ $gallery1 = new Gallery(['Title' => 'Gallery 1 on Block 1 on A1', 'BlockID' => $a1Block1->ID]); - $gallery1->write(); + $this->formSaveObject($gallery1); // A1 (draft, modified) // block1 (draft, modified) @@ -173,7 +202,7 @@ public function testFundamentals() ); $gallery1->Title = 'Gallery 1 on Block 1 on A1 -- changed'; - $gallery1->write(); + $this->formSaveObject($gallery1); // A1 (draft, modified) // block1 (draft, modified) @@ -262,9 +291,11 @@ public function testFundamentals() ] ); + $this->editingPage($a2); + /* @var DataObject|SnapshotPublishable $gallery1a */ $gallery1a = new Gallery(['Title' => 'Gallery 1 on Block 1 on A2', 'BlockID' => $a2Block1->ID]); - $gallery1a->write(); + $this->formSaveObject($gallery1a); $gallery1a->Images()->add($galleryItem1); // A1 (draft, modified) @@ -302,7 +333,7 @@ public function testFundamentals() ); $galleryItem1->URL = '/changed/url'; - $galleryItem1->write(); + $this->formSaveObject($galleryItem1); // A1 (draft, modified) // block1 (draft, modified) @@ -369,8 +400,10 @@ public function testFundamentals() ] ); + $this->editingPage($a1); + // Publish, and clear the slate - $a1->publishRecursive(); + $this->formPublishObject($a1); // A1 (published) * // block1 (published) * @@ -386,7 +419,8 @@ public function testFundamentals() $this->assertFalse($a1->hasOwnedModifications()); $this->assertTrue($a2->hasOwnedModifications()); - $a2->publishRecursive(); + $this->editingPage($a2); + $this->formPublishObject($a2); // A1 (published) // block1 (published) @@ -415,8 +449,10 @@ public function testRevertChanges() /* @var DataObject|SnapshotVersioned|SnapshotPublishable $gallery1 */ list ($a1, $a2, $a1Block1, $a1Block2, $a2Block1, $gallery1, $gallery2) = $this->buildState(); + $this->editingPage($a1); + $gallery1->Title = 'Gallery 1 is changed'; - $gallery1->write(); + $this->formSaveObject($gallery1); // A1 (published) // block1 (published) @@ -454,8 +490,10 @@ public function testIntermediaryObjects() /* @var DataObject|SnapshotVersioned|SnapshotPublishable $gallery1 */ list ($a1, $a2, $a1Block1, $a1Block2, $a2Block1, $gallery1, $gallery2) = $this->buildState(); + $this->editingPage($a1); + $gallery1->Title = 'Gallery 1 changed'; - $gallery1->write(); + $this->formSaveObject($gallery1); // A1 (published) // block1 (published) @@ -468,7 +506,7 @@ public function testIntermediaryObjects() $this->assertTrue($a1Block1->hasOwnedModifications()); $a1->Title = 'A1 changed'; - $a1->write(); + $this->formSaveObject($a1); // A1 (draft, modified) * // block1 (published) @@ -480,7 +518,7 @@ public function testIntermediaryObjects() // Publish the intermediary block - $a1Block1->publishRecursive(); + $this->formPublishObject($a1Block1); // A1 (draft, modified) // block1 (published) @@ -497,7 +535,7 @@ public function testIntermediaryObjects() // Changed block page still does $this->assertTrue($a1->hasOwnedModifications()); - $a1->publishRecursive(); + $this->formPublishObject($a1); // A1 (published) * // block1 (published) @@ -510,7 +548,7 @@ public function testIntermediaryObjects() $this->assertFalse($a1->hasOwnedModifications()); $a1Block1->Title = "A1 is changed again"; - $a1Block1->write(); + $this->formSaveObject($a1Block1); // A1 (draft, modified) * // block1 (published) @@ -531,11 +569,13 @@ public function testChangeOwnershipStructure() /* @var DataObject|SnapshotVersioned|SnapshotPublishable $gallery2 */ list ($a1, $a2, $a1Block1, $a1Block2, $a2Block1, $gallery1, $gallery2) = $this->buildState(); + $this->editingPage($a1); + $this->assertFalse($a1->hasOwnedModifications()); $this->assertFalse($a2->hasOwnedModifications()); $a1Block1->Title = 'Block 1 changed'; - $a1Block1->write(); + $this->formSaveObject($a1Block1); // A1 (published) // block1 (draft, modified) * @@ -548,8 +588,10 @@ public function testChangeOwnershipStructure() $this->assertTrue($a1->hasOwnedModifications()); $this->assertFalse($a2->hasOwnedModifications()); + $this->editingPage($a2); + $a1Block1->ParentID = $a2->ID; - $a1Block1->write(); + $this->formSaveObject($a1Block1); $blockMoved = $a1Block1; // A1 (published) @@ -577,8 +619,10 @@ public function testChangeOwnershipStructure() [$blockMoved, ActivityEntry::MODIFIED], ]); - // This should do nothing. A1 has nothing publishable anymore. - $a1->publishRecursive(); + $this->editingPage($a1); + + // This should do nothing. A1 has nothing publishable anymore.= + $this->formPublishObject($a1); // A1 (published) // block2 (published) @@ -597,7 +641,9 @@ public function testChangeOwnershipStructure() [$blockMoved, ActivityEntry::MODIFIED], ]); - $a2->publishRecursive(); + $this->editingPage($a2); + + $this->formPublishObject($a2); // A1 (published) // block2 (published) @@ -610,14 +656,16 @@ public function testChangeOwnershipStructure() $this->assertEmpty($a2->getActivityFeed()); $this->assertFalse($a2->hasOwnedModifications()); + $this->editingPage($a2); + $blockMoved->Title = "The moved block is modified"; - $blockMoved->write(); + $this->formSaveObject($blockMoved); $gallery1->Title = "The gallery that belongs to the moved block is modified"; - $gallery1->write(); + $this->formSaveObject($gallery1); $item = new GalleryImage(['URL' => '/belongs/to/moved/block']); - $item->write(); + $this->formSaveObject($item); $gallery1->Images()->add($item); @@ -646,7 +694,9 @@ public function testChangeOwnershipStructure() // Refresh the block so that changed fields flushes $blockMoved = DataObject::get_by_id(Block::class, $blockMoved->ID, false); $blockMoved->ParentID = $a1->ID; - $blockMoved->write(); + + $this->editingPage($a1); + $this->formSaveObject($blockMoved); // A1 (published) // block1-moved-back-to-A1 (draft, modified) * @@ -674,8 +724,11 @@ public function testChangeOwnershipStructure() [$blockMoved, ActivityEntry::MODIFIED], ]); - $a2->publishRecursive(); - $a1->publishRecursive(); + $this->editingPage($a2); + $this->formPublishObject($a2); + + $this->editingPage($a1); + $this->formPublishObject($a1); // A1 (published) // block1-moved-back-to-A1 (published) * @@ -695,8 +748,10 @@ public function testChangeOwnershipStructure() $gallery1->Images()->remove($item); $gallery2->Images()->add($item); + $this->editingPage($a1); + $item->URL = '/new/url'; - $item->write(); + $this->formSaveObject($item); // A1 (published) // block1-moved-back-to-A1 (published) @@ -732,14 +787,15 @@ public function testPartialActivityMigration() // Test that we can transplant a node and relevant activity will be migrated // but unrelated activity will be preserved. + $this->editingPage($a1); $a1Block1->Title = 'Take one for the team'; - $a1Block1->write(); + $this->formSaveObject($a1Block1); $a1Block2->Title = 'You got this'; - $a1Block2->write(); + $this->formSaveObject($a1Block2); $gallery = new Gallery(['Title' => 'A new gallery for block 2', 'BlockID' => $a1Block2->ID]); - $gallery->write(); + $this->formSaveObject($gallery); // A1 (published) // block1 (draft, modified) * @@ -760,7 +816,8 @@ public function testPartialActivityMigration() // Move one modified block, but leave the other. $a1Block2->ParentID = $a2->ID; - $a1Block2->write(); + $this->editingPage($a2); + $this->formSaveObject($a1Block2); // A1 (published) // block1 (draft, modified) * @@ -798,10 +855,12 @@ public function testDeletions() /* @var DataObject|SnapshotVersioned|SnapshotPublishable $gallery2 */ list ($a1, $a2, $a1Block1, $a1Block2, $a2Block1, $gallery1, $gallery2) = $this->buildState(); + $this->editingPage($a1); + $this->assertFalse($a1->hasOwnedModifications()); $this->assertFalse($a2->hasOwnedModifications()); - $a1Block1->delete(); + $this->formDeleteObject($a1Block1); // A1 (published) // block1 (deleted) * @@ -817,11 +876,13 @@ public function testDeletions() [$a1Block1, ActivityEntry::DELETED], ]); + $this->editingPage($a2); + $a2Block1->Title = 'Change change change'; - $a2Block1->write(); + $this->formSaveObject($a2Block1); $gallery2->Title = 'Changey McChangerson'; - $gallery2->write(); + $this->formSaveObject($gallery2); // A1 (published) // block1 (deleted) @@ -838,7 +899,7 @@ public function testDeletions() [$gallery2, ActivityEntry::MODIFIED], ]); - $a2Block1->delete(); + $this->formDeleteObject($a2Block1); // A1 (published) // block1 (deleted) // gallery1 (published) @@ -868,6 +929,8 @@ public function testGetAtSnapshot() /* @var DataObject|SnapshotVersioned|SnapshotPublishable $gallery2 */ list ($a1, $a2, $a1Block1, $a1Block2, $a2Block1, $gallery1, $gallery2) = $this->buildState(); + $this->editingPage($a1); + $this->assertCount(2, $a1->Blocks()); $this->assertCount(1, $a2->Blocks()); @@ -877,40 +940,42 @@ public function testGetAtSnapshot() $stamp1 = $this->sleep(1); $a1Block1->Title = 'A1 Block 1 changed'; - $a1Block1->write(); + $this->formSaveObject($a1Block1); $stamp2 = $this->sleep(1); $a1Block2->Title = 'A1 Block 2 changed'; - $a1Block2->write(); + $this->formSaveObject($a1Block2); $stamp3 = $this->sleep(1); $a1Block1->Title = 'A1 Block 1 changed again'; - $a1Block1->write(); + $this->formSaveObject($a1Block1); $stamp4 = $this->sleep(1); $gallery1->Title = 'new-gallery title'; - $gallery1->write(); + $this->formSaveObject($gallery1); $stamp5 = $this->sleep(1); + $this->editingPage($a2); + $a2Block2 = new Block([ 'Title' => 'Block 2 on A2', 'ParentID' => $a2->ID, ]); - $a2Block2->write(); + $this->formSaveObject($a2Block2); $stamp6 = $this->sleep(1); $a2Block1->Title = 'A2 Block 1 changed'; - $a2Block1->write(); + $this->formSaveObject($a2Block1); $stamp7 = $this->sleep(1); $a2->Title = 'The new A2'; - $a2->write(); + $this->formSaveObject($a2); // Sanity check the activity $this->assertCount(4, $a1->getActivityFeed()); @@ -986,6 +1051,8 @@ public function testRollbackSnapshot() /* @var DataObject|SnapshotVersioned|SnapshotPublishable $gallery2 */ list ($a1, $a2, $a1Block1, $a1Block2, $a2Block1, $gallery1, $gallery2) = $this->buildState(); + $this->editingPage($a1); + $this->assertCount(2, $a1->Blocks()); $this->assertCount(1, $a2->Blocks()); @@ -995,40 +1062,42 @@ public function testRollbackSnapshot() $stamp1 = $this->sleep(1); $a1Block1->Title = 'A1 Block 1 changed'; - $a1Block1->write(); + $this->formSaveObject($a1Block1); $stamp2 = $this->sleep(1); $a1Block2->Title = 'A1 Block 2 changed'; - $a1Block2->write(); + $this->formSaveObject($a1Block2); $stamp3 = $this->sleep(1); $a1Block1->Title = 'A1 Block 1 changed again'; - $a1Block1->write(); + $this->formSaveObject($a1Block1); $stamp4 = $this->sleep(1); $gallery1->Title = 'new-gallery title'; - $gallery1->write(); + $this->formSaveObject($gallery1); $stamp5 = $this->sleep(1); + $this->editingPage($a2); + $a2Block2 = new Block([ 'Title' => 'Block 2 on A2', 'ParentID' => $a2->ID, ]); - $a2Block2->write(); + $this->formSaveObject($a2Block2); $stamp6 = $this->sleep(1); $a2Block1->Title = 'A2 Block 1 changed'; - $a2Block1->write(); + $this->formSaveObject($a2Block1); $stamp7 = $this->sleep(1); $a2->Title = 'The new A2'; - $a2->write(); + $this->formSaveObject($a2); $stamp8 = $this->sleep(1); @@ -1048,7 +1117,7 @@ public function testRollbackSnapshot() $a2 = $a2->getAtVersion(Versioned::LIVE); $this->assertCount(1, $a2->Blocks()); - $a2->publishRecursive(); + $this->formPublishObject($a2); // Still has only one block because the draft stage was a rolled back snapshot. $this->assertCount(1, $a2->Blocks()); @@ -1057,7 +1126,7 @@ public function testRollbackSnapshot() $this->assertEquals('The new A2', $a2->Title); $this->assertCount(2, $a2->Blocks()); - $a2->publishRecursive(); + $this->formPublishObject($a2); $a2 = $a2->getAtVersion(Versioned::LIVE); $this->assertCount(2, $a2->Blocks()); } @@ -1065,14 +1134,19 @@ public function testRollbackSnapshot() public function testWonkyOwner() { $page = new BlockPage(['Title' => 'The Page']); - $page->write(); - $page->publishRecursive(); + $this->editingPage($page); + $this->formSaveObject($page); + $this->formPublishObject($page); + + // This block is saved in isolation + $this->editingPage(null); $block = new Block(['Title' => 'The Block', 'ParentID' => 0]); - $block->write(); + $this->formSaveObject($block); $block->ParentID = $page->ID; - $block->write(); + $this->editingPage($page); + $this->formSaveObject($block); $activity = $page->getActivityFeed(); $this->assertCount(1, $activity); @@ -1087,13 +1161,18 @@ public function testWonkyOwner() public function testChangeToUnpublishedOwner() { $page = new BlockPage(['Title' => 'The Page']); - $page->write(); + $this->editingPage($page); + + $this->formSaveObject($page); + + $this->editingPage(null); $block = new Block(['Title' => 'The Block']); - $block->write(); + $this->formSaveObject($block); + $this->editingPage($page); $block->ParentID = $page->ID; - $block->write(); + $this->formSaveObject($block); $activity = $page->getActivityFeed(); @@ -1110,15 +1189,16 @@ public function testChangeToUnpublishedOwner() public function testMany() { $p = new BlockPage(['Title' => 'The Page']); - $p->write(); + $this->editingPage($p); + $this->formSaveObject($p); $b = new Block(['Title' => 'The Block on The Page', 'ParentID' => $p->ID]); - $b->write(); + $this->formSaveObject($b); $g = new Gallery(['Title' => 'The Gallery on The Block on The Page', 'BlockID' => $b->ID]); - $g->write(); + $this->formSaveObject($g); - $p->publishRecursive(); + $this->formPublishObject($p); $this->assertFalse($p->hasOwnedModifications()); $this->assertCount(0, $p->getActivityFeed()); @@ -1140,14 +1220,15 @@ public function testMany() public function testPlainActivityFeed() { $page = new BlockPage(); + $this->editingPage($page); $page->Title = 'The Page -- version 1'; - $page->write(); + $this->formSaveObject($page); $page->Title = 'The Page -- version 2'; - $page->write(); + $this->formSaveObject($page); $page->Title = 'The Page -- version 3'; - $page->write(); + $this->formSaveObject($page); $activity = $page->getActivityFeed(); $this->assertCount(3, $activity); @@ -1172,7 +1253,7 @@ public function testPlainActivityFeed() ] ); - $page->publishRecursive(); + $this->formPublishObject($page); $activity = $page->getActivityFeed(); $this->assertCount(0, $activity); @@ -1191,10 +1272,10 @@ public function testPlainActivityFeed() ); $page->Title = 'The Page -- version 5'; - $page->write(); + $this->formSaveObject($page); $page->Title = 'The Page -- version 6'; - $page->write(); + $this->formSaveObject($page); $activity = $page->getActivityFeed(); $this->assertCount(2, $activity); @@ -1232,7 +1313,7 @@ public function testPlainActivityFeed() ] ); - $page->publishRecursive(); + $this->formPublishObject($page); $versionedActivity = $page->getActivityFeed(3); $this->assertCount(5, $versionedActivity); @@ -1251,15 +1332,16 @@ public function testPlainActivityFeed() public function testNestedActivityFeed() { $p = new BlockPage(['Title' => 'Page -- v01']); - $p->write(); + $this->editingPage($p); + $this->formSaveObject($p); $b = new Block(['Title' => 'Block -- v01', 'ParentID' => $p->ID]); - $b->write(); + $this->formSaveObject($b); $g = new Gallery(['Title' => 'Gallery -- v01', 'BlockID' => $b->ID]); - $g->write(); + $this->formSaveObject($g); - $p->publishRecursive(); + $this->formPublishObject($p); $this->assertFalse($p->hasOwnedModifications()); $this->assertCount(0, $p->getActivityFeed()); @@ -1277,12 +1359,12 @@ public function testNestedActivityFeed() ); $b->Title = 'Block -- v02'; - $b->write(); + $this->formSaveObject($b); - $p->publishRecursive(); + $this->formPublishObject($p); $i->URL = '/gallery/image/2'; - $i->write(); + $this->formSaveObject($i); $activity = $p->getActivityFeed(); $this->assertActivityContains( @@ -1292,7 +1374,7 @@ public function testNestedActivityFeed() ] ); - $p->publishRecursive(); + $this->formPublishObject($p); $a = $p->getActivityFeed(2); $this->assertActivityContains($a, [ @@ -1325,7 +1407,7 @@ public function testNestedActivityFeed() * @param ArrayList $activity * @param array $objs */ - protected function assertActivityContains(ArrayList $activity, $objs = []) + private function assertActivityContains(ArrayList $activity, $objs = []) { $this->assertCount(sizeof($objs), $activity); foreach ($activity as $i => $entry) { @@ -1357,7 +1439,7 @@ protected function assertActivityContains(ArrayList $activity, $objs = []) * @param ArrayList $items * @param array $objs */ - protected function assertPublishableObjectsContains(ArrayList $items, $objs = []) + private function assertPublishableObjectsContains(ArrayList $items, $objs = []) { $this->assertCount(sizeof($objs), $items); foreach ($items as $i => $dataObject) { @@ -1378,7 +1460,7 @@ protected function assertPublishableObjectsContains(ArrayList $items, $objs = [] * @param int $minutes * @return string */ - protected function sleep($minutes) + private function sleep($minutes) { $now = DBDatetime::now(); $date = DateTime::createFromFormat('Y-m-d H:i:s', $now->getValue()); @@ -1390,43 +1472,43 @@ protected function sleep($minutes) } - protected function buildState($publish = true) + private function buildState($publish = true) { /* @var DataObject|SnapshotPublishable $a1 */ $a1 = new BlockPage(['Title' => 'A1 Block Page']); - $a1->write(); + $this->formSaveObject($a1); /* @var DataObject|SnapshotPublishable $a2 */ $a2 = new BlockPage(['Title' => 'A2 Block Page']); - $a2->write(); + $this->formSaveObject($a2); /* @var DataObject|SnapshotPublishable $a1Block1 */ $a1Block1 = new Block(['Title' => 'Block 1 on A1', 'ParentID' => $a1->ID]); - $a1Block1->write(); + $this->formSaveObject($a1Block1); $a1Block2 = new Block(['Title' => 'Block 2 on A1', 'ParentID' => $a1->ID]); - $a1Block2->write(); + $this->formSaveObject($a1Block2); /* @var DataObject|SnapshotPublishable $a2Block1 */ $a2Block1 = new Block(['Title' => 'Block 1 on A2', 'ParentID' => $a2->ID]); - $a2Block1->write(); + $this->formSaveObject($a2Block1); /* @var DataObject|SnapshotPublishable|Versioned $gallery1 */ $gallery1 = new Gallery(['Title' => 'Gallery 1 on Block 1 on A1', 'BlockID' => $a1Block1->ID]); - $gallery1->write(); + $this->formSaveObject($gallery1); /* @var DataObject|SnapshotPublishable|Versioned $gallery1 */ $gallery2 = new Gallery(['Title' => 'Gallery 2 on Block 1 on A2', 'BlockID' => $a2Block1->ID]); - $gallery2->write(); + $this->formSaveObject($gallery2); if ($publish) { - $a1->publishRecursive(); - $a2->publishRecursive(); + $this->formPublishObject($a1); + $this->formPublishObject($a2); } return [$a1, $a2, $a1Block1, $a1Block2, $a2Block1, $gallery1, $gallery2]; } - protected function debugActivity($activity) + private function debugActivity($activity) { $list = []; foreach ($activity as $entry) { @@ -1442,7 +1524,7 @@ protected function debugActivity($activity) return implode("\n", $list); } - protected function debugPublishable($items) + private function debugPublishable($items) { $list = []; foreach ($items as $item) { @@ -1456,4 +1538,87 @@ protected function debugPublishable($items) return implode("\n", $list); } + + private function formSaveObject(DataObject $object) + { + $object->write(); + $actionName = $object instanceof SiteTree ? 'save' : 'doSave'; + $event = $this->createEvent($object, $actionName); + $this->dispatch($event); + } + + private function formPublishObject(DataObject $object) + { + $object->write(); + $object->publishRecursive(); + $event = $this->createEvent($object, 'publish'); + $this->dispatch($event); + } + + private function formUnpublishObject(DataObject $object) + { + $object->doUnpublish(); + $event = $this->createEvent($object, 'unpublish'); + $this->dispatch($event); + } + + private function formDeleteObject(DataObject $object) + { + $object->delete(); + $event = $this->createEvent($object, 'doDelete'); + $this->dispatch($event); + } + + private function dispatch(EventContextInterface $event) + { + Dispatcher::singleton()->trigger(Listener::EVENT_NAME, $event); + } + + private function createEvent(DataObject $object, string $actionName): Event + { + if (!$this->currentPage) { + return Event::create($actionName); + } + $controller = CMSPageEditController::singleton(); + $segment = $controller->config()->get('url_segment'); + + $adminURL = Path::join( + AdminRootController::get_admin_route(), + $segment, + 'EditForm', + 'show', + $this->currentPage->ID + ); + + $request = new HTTPRequest( + 'POST', + $adminURL, + [] + ); + + $request->setSession(new Session([])); + + $form = Form::create( + $controller, + 'EditForm', + FieldList::create(), + FieldList::create() + ); + + $form->loadDataFrom($object); + + return Event::create( + $actionName, + [ + 'page' => $this->currentPage, + 'request' => $request, + 'form' => $form, + ] + ); + } + + private function editingPage(?DataObject $page = null) + { + $this->currentPage = $page; + } } From 8500a937ee8ff3091917e4533a7870490fc23165 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Fri, 24 Jan 2020 14:55:54 +1300 Subject: [PATCH 13/16] Unit tests passing --- _config/config.yml | 2 +- _config/tests.yml | 9 - lang/en.yml | 3 + src/ActivityEntry.php | 13 +- src/Dev/State/DisableSnapshots.php | 63 ----- src/Handler/Form/Handler.php | 118 +++++++++- src/Snapshot.php | 21 +- src/SnapshotAdmin.php | 51 ----- src/SnapshotItem.php | 17 +- src/SnapshotPublishable.php | 357 ++++++++--------------------- tests/SnapshotTest.php | 117 +++++----- 11 files changed, 283 insertions(+), 488 deletions(-) delete mode 100644 _config/tests.yml delete mode 100644 src/Dev/State/DisableSnapshots.php delete mode 100644 src/SnapshotAdmin.php diff --git a/_config/config.yml b/_config/config.yml index 54ed3e9..3accb48 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -29,7 +29,7 @@ SilverStripe\Core\Injector\Injector: properties: handlers: formGeneric: - on: [ 'formSubmitted.unpublish', 'formSubmitted.doSave', 'formSubmitted.doDelete' ] + on: [ 'formSubmitted.unpublish', 'formSubmitted.doSave', 'formSubmitted.doDelete', 'formSubmitted.archive' ] handler: %$SilverStripe\Snapshots\Handler\Form\Handler save: on: [ 'formSubmitted.save' ] diff --git a/_config/tests.yml b/_config/tests.yml deleted file mode 100644 index d675576..0000000 --- a/_config/tests.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -Name: snapshotssapphiretest -Before: '#sapphiretest' ---- -SilverStripe\Core\Injector\Injector: - SilverStripe\Dev\State\SapphireTestState: - properties: - States: - disablesnapshots: '%$SilverStripe\Snapshots\Dev\State\DisableSnapshots' diff --git a/lang/en.yml b/lang/en.yml index 1ea6ade..ee64841 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -16,3 +16,6 @@ en: SilverStripe\Snapshots\Handler\GridField\Action\Handler: HANDLER_deleterecord: 'Delete record' HANDLER_archiverecord: 'Archive record' + SilverStripe\Snapshots\Handler\Form: + MANY_MANY_ADD: 'Added {count} {name}' + MANY_MANY_REMOVE: 'Removed {count} {name}' diff --git a/src/ActivityEntry.php b/src/ActivityEntry.php index 684c0fb..801adb3 100644 --- a/src/ActivityEntry.php +++ b/src/ActivityEntry.php @@ -22,17 +22,10 @@ class ActivityEntry extends ArrayData public static function createFromSnapshotItem(SnapshotItem $item) { - if ($item->LinkedToObjectID > 0 && $item->LinkedToObject()->exists()) { - return new static([ - 'Subject' => $item->LinkedToObject(), - 'Action' => $item->WasDeleted ? self::REMOVED : self::ADDED, - 'Owner' => $item->LinkedFromObject(), - 'Date' => $item->obj('Created')->Nice(), - ]); - } - $flag = null; - if ($item->WasDeleted) { + if ($item->Parent()->exists()) { + $flag = $item->WasDeleted ? self::REMOVED : self::ADDED; + } elseif ($item->WasDeleted) { $flag = self::DELETED; } elseif ($item->WasPublished) { $flag = self::PUBLISHED; diff --git a/src/Dev/State/DisableSnapshots.php b/src/Dev/State/DisableSnapshots.php deleted file mode 100644 index 60a1310..0000000 --- a/src/Dev/State/DisableSnapshots.php +++ /dev/null @@ -1,63 +0,0 @@ -getMessage($action); - return Snapshot::singleton()->createSnapshotFromAction($page, $record, $message); + $intermediaryObjects = []; + $implicitObjects = []; + $previous = $this->getPreviousVersion($record); + /* @var SnapshotPublishable|Versioned|DataObject $record */ + if ($record->hasExtension(SnapshotPublishable::class)) { + // Get all the owners that aren't the page + $intermediaryObjects = $record->findOwners()->filterByCallback(function ($owner) use ($page) { + return !SnapshotHasher::hashSnapshotCompare($page, $owner); + })->toArray(); + $manyManyDiff = $record->diffManyMany(); + if (!empty($manyManyDiff)) { + $message = $this->getMessageForManyMany($manyManyDiff); + foreach ($manyManyDiff as $childClass => $details) { + // Any new many_many objects that were added should be tracked in the snapshot + foreach (['added', 'removed'] as $type) { + foreach ($details[$type] as $id) { + $obj = DataObject::get_by_id($childClass, $id); + if ($obj) { + $implicitObjects[] = ['type' => $type, 'record' => $obj]; + } + } + } + } + } + } + $extraObjects = []; + + foreach ($intermediaryObjects as $extra) { + $extraObjects[SnapshotHasher::hashObjectForSnapshot($extra)] = $extra; + } + foreach($implicitObjects as $spec) { + $extraObjects[SnapshotHasher::hashObjectForSnapshot($spec['record'])] = $spec['record']; + } + + $snapshot = Snapshot::singleton()->createSnapshotFromAction($page, $record, $message, $extraObjects); + if ($snapshot && !empty($implicitObjects)) { + $parentItem = $snapshot->Items()->filter( + 'ObjectHash', SnapshotHasher::hashObjectForSnapshot($record) + )->first(); + if ($parentItem) { + foreach ($implicitObjects as $spec) { + $obj = $spec['record']; + $type = $spec['type']; + $item = $snapshot->Items()->filter( + 'ObjectHash', SnapshotHasher::hashObjectForSnapshot($obj) + )->first(); + if ($item) { + $item->ParentID = $parentItem->ID; + $item->WasDeleted = $type === 'removed'; + $item->write(); + } + } + } + } + + $record->reconcileOwnershipChanges($previous); + + return $snapshot; } /** @@ -56,4 +118,58 @@ protected function getPage(EventContextInterface $context): ?DataObject $url = $request->getURL(); return $this->getCurrentPageFromRequestUrl($url); } + + private function getMessageForManyMany(array $manyManyDiff): string + { + $messages = []; + foreach ($manyManyDiff as $childClass => $details) { + $sng = Injector::inst()->get($childClass); + $ct = count($details['added']); + if ($ct) { + $messages[] = _t( + __CLASS__ . '.MANY_MANY_ADD', + 'Added {count} {name}', + [ + 'count' => $ct, + 'name' => $ct > 1 ? $sng->plural_name() : $sng->singular_name() + ] + ); + } + $ct = count($details['removed']); + if ($ct) { + $messages[] = _t( + __CLASS__ . '.MANY_MANY_REMOVE', + 'Removed {count} {name}', + [ + 'count' => $ct, + 'name' => $ct > 1 ? $sng->plural_name() : $sng->singular_name() + ] + ); + } + + } + + return implode("\n", $messages); + } + + /** + * @param DataObject $record + * @param null $version + * @return DataObject|null + */ + private function getPreviousVersion(DataObject $record, $version = null): ?DataObject + { + $previous = null; + if ($record->Version == 1) { + $previous = Injector::inst()->create(get_class($record)); + } else { + if ($version === null) { + $version = $record->Version - 1; + } + + $previous = $record->getAtVersion($version); + } + + return $previous; + } } diff --git a/src/Snapshot.php b/src/Snapshot.php index a1a6ff3..b0c3314 100644 --- a/src/Snapshot.php +++ b/src/Snapshot.php @@ -2,12 +2,6 @@ namespace SilverStripe\Snapshots; -use GraphQL\Type\Definition\ResolveInfo; -use GraphQL\Type\Schema; -use Page; -use SilverStripe\Control\HTTPRequest; -use SilverStripe\Forms\Form; -use SilverStripe\Forms\GridField\GridField; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\HasManyList; use SilverStripe\ORM\ValidationException; @@ -35,7 +29,6 @@ class Snapshot extends DataObject use SnapshotHasher; const TRIGGER_ACTION = 'action'; - const TRIGGER_MODEL = 'model'; /** * @var array @@ -214,7 +207,7 @@ public function onBeforeWrite() * @param DataObject $owner * @param DataObject|null $origin * @param string $message - * @param array $objects + * @param array $extraObjects * @return Snapshot|null * @throws ValidationException */ @@ -222,12 +215,12 @@ public function createSnapshotFromAction( DataObject $owner, ?DataObject $origin, string $message, - array $objects = [] + array $extraObjects = [] ): ?Snapshot { if (!$owner->isInDB()) { return null; } - + $objectsToAdd = []; if ($origin === null || !$origin->isInDB()) { // case 1: no origin provided or the origin got deleted // this means we can't point to a specific origin object @@ -248,7 +241,7 @@ public function createSnapshotFromAction( ); $origin = $event; - array_unshift($objects, $origin); + array_unshift($objectsToAdd, $origin); } else { // no message is available - fallback to the owner // no need to add the origin to the list of objects as it's already there @@ -260,11 +253,11 @@ public function createSnapshotFromAction( $origin = $owner; } else { // case 3: stadard origin - add it to the object list - array_unshift($objects, $origin); + $objectsToAdd[] = $origin; } // owner is added as the last item - array_push($objects, $owner); + $objectsToAdd[] = $owner; $currentUser = Security::getCurrentUser(); $snapshot = Snapshot::create(); @@ -277,7 +270,7 @@ public function createSnapshotFromAction( : 0; $snapshot->write(); - + $objects = array_merge($objectsToAdd, $extraObjects); // the rest of the objects are processed in the provided order foreach ($objects as $object) { if (!$object instanceof DataObject) { diff --git a/src/SnapshotAdmin.php b/src/SnapshotAdmin.php deleted file mode 100644 index 82c2eae..0000000 --- a/src/SnapshotAdmin.php +++ /dev/null @@ -1,51 +0,0 @@ - 'handleRollback' - ]; - - private static $allowed_actions = [ - 'handleRollback', - ]; - - public function handleRollback(HTTPRequest $request) - { - $class = $request->param('RecordClass'); - $id = $request->param('RecordID'); - $snapshot = urldecode($request->param('Snapshot')); - $class = str_replace('__', '\\', $class); - /* @var SnapshotPublishable|SnapshotVersioned $record */ - $record = DataObject::get_by_id($class, $id); - - if (!$record) { - $this->httpError(404); - return; - } - - $record->doRollbackToSnapshot($snapshot); - - return $this->redirectBack(); - } -} diff --git a/src/SnapshotItem.php b/src/SnapshotItem.php index 11dc981..558564c 100644 --- a/src/SnapshotItem.php +++ b/src/SnapshotItem.php @@ -8,6 +8,7 @@ use SilverStripe\Security\Security; use SilverStripe\Versioned\ChangeSet; use SilverStripe\Versioned\Versioned; +use Exception; /** * Class SnapshotItem @@ -20,14 +21,8 @@ * @property int $SnapshotID * @property int $ObjectID * @property string $ObjectClass - * @property int $LinkedFromObjectID - * @property string $LinkedFromObjectClass - * @property int $LinkedToObjectID - * @property string $LinkedToObjectClass * @method Snapshot Snapshot() * @method DataObject Object() - * @method DataObject LinkedFromObject() - * @method DataObject LinkedToObject() * @package SilverStripe\Snapshots */ class SnapshotItem extends DataObject @@ -53,8 +48,7 @@ class SnapshotItem extends DataObject private static $has_one = [ 'Snapshot' => Snapshot::class, 'Object' => DataObject::class, - 'LinkedFromObject' => DataObject::class, - 'LinkedToObject' => DataObject::class, + 'Parent' => SnapshotItem::class, ]; /** @@ -188,17 +182,14 @@ public function getItemTitle() } /** - * @param DataObject|Versioned $object + * @param DataObject|Versioned|SnapshotPublishable $object * @return SnapshotItem + * @throws Exception */ public function hydrateFromDataObject(DataObject $object) { $this->ObjectClass = $object->baseClass(); $this->ObjectID = (int) $object->ID; - $this->LinkedFromObjectClass = null; - $this->LinkedFromObjectID = 0; - $this->LinkedToObjectClass = null; - $this->LinkedToObjectID = 0; // Track versioning changes on the record if the owner is versioned if ($object->hasExtension(Versioned::class)) { diff --git a/src/SnapshotPublishable.php b/src/SnapshotPublishable.php index 161be36..7e4ebec 100644 --- a/src/SnapshotPublishable.php +++ b/src/SnapshotPublishable.php @@ -25,20 +25,6 @@ class SnapshotPublishable extends RecursivePublishable use SnapshotHasher; - /** - * Global state to tell all write hooks that a snapshot is in progress. - * Prevents recursion and duplicity. - * @var Snapshot - */ - protected $activeSnapshot = null; - - /** - * Indicates if snapshotting is currently active. Control this state with the ::pause and ::resume methods - * - * @var bool - */ - protected static $active = true; - /** * @param $class * @param $id @@ -170,7 +156,7 @@ public function getSnapshotsBetweenVersionsFilters($min, $max = null, $includeAl $hash = static::hashObjectForSnapshot($this->owner); $minShot = SQLSelect::create( - 'MIN("SnapshotID")', + "MIN(\"$itemTable\".\"SnapshotID\")", "\"$itemTable\"", [ '"ObjectHash" = ?' => $hash, @@ -180,7 +166,7 @@ public function getSnapshotsBetweenVersionsFilters($min, $max = null, $includeAl $minShotSQL = $minShot->sql($minParams); $maxShot = SQLSelect::create( - 'MAX("SnapshotID")', + "MAX(\"$itemTable\".\"SnapshotID\")", "\"$itemTable\"", [ '"ObjectHash" = ?' => $hash, @@ -190,8 +176,8 @@ public function getSnapshotsBetweenVersionsFilters($min, $max = null, $includeAl $maxShotSQL = $maxShot->sql($maxParams); $condition = $max === null - ? sprintf('"SnapshotID" >= (%s)', $minShotSQL) - : sprintf('"SnapshotID" BETWEEN (%s) and (%s)', $minShotSQL, $maxShotSQL); + ? sprintf("\"$itemTable\".\"SnapshotID\" >= (%s)", $minShotSQL) + : sprintf("\"$itemTable\".\"SnapshotID\" BETWEEN (%s) and (%s)", $minShotSQL, $maxShotSQL); $condtionStatement = [ $condition => $max === null ? $minParams : array_merge($minParams, $maxParams), @@ -203,14 +189,14 @@ public function getSnapshotsBetweenVersionsFilters($min, $max = null, $includeAl } $query = SQLSelect::create( - "\"SnapshotID\"", + "\"$itemTable\".\"SnapshotID\"", "\"$itemTable\"", $condtionStatement ); $sql = $query->sql($params); return [ - sprintf('"SnapshotID" IN (%s)', $sql) => $params + sprintf("\"$itemTable\".\"SnapshotID\" IN (%s)", $sql) => $params ]; } @@ -226,12 +212,21 @@ public function getActivityBetweenVersions($min, $max = null) $items = SnapshotItem::get() ->innerJoin($snapshotTable, "\"$snapshotTable\".\"ID\" = \"$itemTable\".\"SnapshotID\"") - ->where(array_merge([ + ->leftJoin($itemTable, "\"ChildItem\".\"ParentID\" = \"$itemTable\".\"ID\"", "ChildItem") + ->where([ + $this->getSnapshotsBetweenVersionsFilters($min, $max), // Only get the items that were the subject of a user's action - "\"$snapshotTable\" . \"OriginHash\" = \"$itemTable\".\"ObjectHash\"" - ], $this->getSnapshotsBetweenVersionsFilters($min, $max))) + "( + \"$snapshotTable\" . \"OriginHash\" = \"$itemTable\".\"ObjectHash\" AND + \"ChildItem\".\"ID\" IS NULL + ) OR ( + \"$snapshotTable\" . \"OriginHash\" != \"$itemTable\".\"ObjectHash\" AND + \"$itemTable\".\"ParentID\" != 0 + )" + ]) ->sort([ - "\"$itemTable\".\"SnapshotID\"" => "ASC" + "\"$itemTable\".\"SnapshotID\"" => "ASC", + "\"$itemTable\".\"ID\"" => "ASC", ]); return $items; @@ -251,12 +246,14 @@ public function getActivity() ->innerJoin($snapshotTable, "\"$snapshotTable\".\"ID\" = \"$itemTable\".\"SnapshotID\"") ->filter([ // Only relevant snapshots - 'SnapshotID' => $snapShotIDs, + "\"$itemTable\".\"SnapshotID\"" => $snapShotIDs, + ]) - ->where( + ->whereAny([ // Only get the items that were the subject of a user's action - "\"$snapshotTable\" . \"OriginHash\" = \"$itemTable\".\"ObjectHash\"" - ) + "\"$snapshotTable\" . \"OriginHash\" = \"$itemTable\".\"ObjectHash\"", + "\"$itemTable\".\"ParentID\" != 0", + ]) ->sort([ "\"$snapshotTable\".\"Created\"" => "ASC", "\"$snapshotTable\".\"ID\"" => "ASC" @@ -291,7 +288,6 @@ public function getActivityFeed($minVersion = null, $maxVersion = null) $items = $this->getActivityBetweenVersions($minVersion, $maxVersion); $list = ArrayList::create(); - foreach ($items as $item) { $list->push(ActivityEntry::createFromSnapshotItem($item)); } @@ -356,15 +352,7 @@ public function getPublishableObjects() $id = $row['ObjectID']; /* @var DataObject|SnapshotPublishable $obj */ $obj = DataObject::get_by_id($class, $id); - if ($obj->isManyManyLinkingObject()) { - $ownership = $obj->getManyManyOwnership(); - foreach ($ownership as $spec) { - list ($parentClass, $parentName, $parent, $child) = $spec; - $map[static::hashObjectForSnapshot($child)] = $child; - } - } else { - $map[static::hashObjectForSnapshot($obj)] = $obj; - } + $map[static::hashObjectForSnapshot($obj)] = $obj; } return ArrayList::create(array_values($map)); @@ -379,36 +367,6 @@ public function getAtSnapshot($snapshot) return static::get_at_snapshot($this->owner->baseClass(), $this->owner->ID, $snapshot); } - /** - * @return SnapshotItem - */ - public function createSnapshotItem() - { - /* @var DataObject|Versioned|SnapshotPublishable $owner */ - $owner = $this->owner; - - $record = [ - 'ObjectClass' => $owner->baseClass(), - 'ObjectID' => $owner->ID, - 'LinkedObjectClass' => null, - 'LinkedObjectID' => 0 - ]; - - // Track versioning changes on the record if the owner is versioned - if ($owner->hasExtension(Versioned::class)) { - $record += [ - 'WasDraft' => $owner->isModifiedOnDraft(), - 'WasDeleted' => $owner->isOnLiveOnly() || $owner->isArchived(), - 'Version' => $owner->Version, - ]; - } else { - // Track publish state for non-versioned owners, they're always in a published state. - $record['WasPublished'] = true; - } - - return SnapshotItem::create($record); - } - /** * Tidy up all the irrelevant snapshot records now that the changes have been reverted. */ @@ -423,15 +381,7 @@ public function onAfterRevertToLive() } /** - * @return bool - */ - public function isManyManyLinkingObject() - { - return !empty($this->owner->getManyManyLinking()); - } - - /** - * @return Generator + * @return array * @throws Exception */ public function getManyManyOwnership() @@ -489,47 +439,59 @@ public function getManyManyLinking() } /** - * @param $snapshot + * Get the IDs that have been added and removed for each many_many component since the given version + * @return array */ - public function rollbackOwned($snapshot) + public function diffManyMany(): array { $owner = $this->owner; - // Rollback recursively - foreach ($owner->findOwned(false) as $object) { - if ($object->hasExtension(SnapshotVersioned::class)) { - $object->doRollbackToSnapshot($snapshot); + $diff = []; + // Get the last time this record was in a snapshot. Doesn't matter where or why. We just need a + // timestamp prior to now, because the Version may be unchanged. + $lastSnapshot = SnapshotItem::get()->filter([ + 'ObjectHash' => static::hashObjectForSnapshot($owner), + ])->max('LastEdited'); + foreach ($owner->config()->get('many_many') as $component => $spec) { + $currentIDs = $owner->$component()->column('ID'); + if ($lastSnapshot) { + $previousIDs = Versioned::withVersionedMode(function () use ($lastSnapshot, $owner, $component) { + Versioned::reading_archived_date($lastSnapshot); + return $owner->$component()->column('ID'); + }); } else { - $object->rollbackOwned($snapshot); + $previousIDs = []; + } + + $added = array_diff($currentIDs, $previousIDs); + $removed = array_diff($previousIDs, $currentIDs); + $mmData = $owner->getSchema()->manyManyComponent(get_class($owner), $component); + + if (empty($added) && empty($removed)) { + continue; } + + $diff[$mmData['childClass']] = [ + 'added' => $added, + 'removed' => $removed, + ]; } - } - /** - * Pause snapshotting - disabling tracking version changes across a versioned owner relationship tree. This should - * only be done in cases where you are manually creating a snapshot (or you are _really_ sure that you don't want a - * change to be tracked own "owner"s of a record. - */ - public static function pause() - { - self::$active = false; + return $diff; } /** - * Resume snapshotting after previously calling `SnapshotPublishable::pause`. + * @param $snapshot */ - public static function resume() - { - self::$active = true; - } - - public static function withPausedSnapshots(callable $callback) + public function rollbackOwned($snapshot) { - try { - static::pause(); - - return $callback(); - } finally { - static::resume(); + $owner = $this->owner; + // Rollback recursively + foreach ($owner->findOwned(false) as $object) { + if ($object->hasExtension(SnapshotVersioned::class)) { + $object->doRollbackToSnapshot($snapshot); + } else { + $object->rollbackOwned($snapshot); + } } } @@ -541,7 +503,6 @@ protected function publishableItemsQuery($snapShotIDs) { $snapshotTable = DataObject::getSchema()->tableName(Snapshot::class); $itemTable = DataObject::getSchema()->tableName(SnapshotItem::class); - $hash = static::hashObjectForSnapshot($this->owner); $query = new SQLSelect( ['MaxID' => "MAX(\"$itemTable\".\"ID\")"], @@ -555,7 +516,7 @@ protected function publishableItemsQuery($snapShotIDs) ['"SnapshotID" IN (' . DB::placeholders($snapShotIDs) . ')' => $snapShotIDs], ['"WasPublished" = ?' => 0], ['"WasDeleted" = ?' => 0], - '"ObjectHash" = "OriginHash"', + '"ObjectHash" = "OriginHash" OR "ParentID" != 0', ]) ->setGroupBy(['"ObjectHash"', "\"$itemTable\".\"Created\", \"$itemTable\".\"ID\""]) ->setOrderBy("\"$itemTable\".\"Created\", \"$itemTable\".\"ID\""); @@ -564,172 +525,39 @@ protected function publishableItemsQuery($snapShotIDs) } /** - * @return bool - */ - protected function requiresSnapshot() - { - // Check if snapshotting is currently "paused" - if (!self::$active) { - return false; - } - - $owner = $this->owner; - - // Explicitly blacklist these two, since they get so many writes in this context, - // and calculating snapshot eligibility is expensive. - if ($owner instanceof Snapshot || $owner instanceof SnapshotItem) { - return false; - } - - if (!$owner->hasExtension(Versioned::class)) { - return false; - } - - return !$owner->getNextWriteWithoutVersion(); - } - - /** - * @return void - * @throws Exception - */ - protected function doSnapshot() - { - // Block nested snapshots. One user action = one snapshot - if ($this->activeSnapshot) { - return null; - } - $owner = $this->owner; - /* @var DataObject|SnapshotPublishable $owner */ - if ($this->isManyManyLinkingObject()) { - foreach ($owner->getManyManyOwnership() as $spec) { - /* @var DataObject|SnapshotPublishable $parent */ - list ($parentClass, $parentName, $parent, $child) = $spec; - $this->openSnapshot(); - $this->addToSnapshot($owner, $parent, $child); - foreach ($parent->findOwners() as $owner) { - $this->addToSnapshot($owner); - } - $this->closeSnapshot(); - } - - return null; - } - - $this->openSnapshot(); - $this->addToSnapshot($owner); - - foreach ($this->owner->findOwners() as $owner) { - $this->addToSnapshot($owner); - } - - $this->closeSnapshot(); - } - - /** - * @param $origin - * @return Snapshot - */ - protected function createSnapshot($origin = null) - { - $snapshot = Snapshot::create([ - 'OriginClass' => $origin->baseClass(), - 'OriginID' => $origin->ID, - 'AuthorID' => Security::getCurrentUser() - ? Security::getCurrentUser()->ID - : 0 - ]); - - return $snapshot; - } - - /** - * @param null $origin - * @return Snapshot - */ - protected function openSnapshot($origin = null) - { - if (!$origin) { - $origin = $this->owner; - } - $snapshot = $this->createSnapshot($origin); - $snapshot->write(); - $this->activeSnapshot = $snapshot; - - return $snapshot; - } - - /** - * @param DataObject $obj - * @param DataObject|null $linkedFromObj - * @param DataObject|null $linkedToObj - */ - protected function addToSnapshot(DataObject $obj, $linkedFromObj = null, $linkedToObj = null) - { - if (!$this->activeSnapshot) { - throw new BadMethodCallException('Cannot call addToSnapshot() before openSnapshot()'); - } - - if (!$obj->hasExtension(RecursivePublishable::class)) { - throw new BadMethodCallException(sprintf( - 'addToSnapshot() only accepts objects with the %s extension', - RecursivePublishable::class - )); - } - /* @var SnapshotPublishable|DataObject $obj */ - $item = $obj->createSnapshotItem(); - if ($linkedFromObj) { - $item->LinkedFromObjectClass = $linkedFromObj->baseClass(); - $item->LinkedFromObjectID = $linkedFromObj->ID; - } - if ($linkedToObj) { - $item->LinkedToObjectClass = $linkedToObj->baseClass(); - $item->LinkedToObjectID = $linkedToObj->ID; - } - - $this->activeSnapshot->Items()->add($item); - } - - protected function closeSnapshot() - { - $this->activeSnapshot = null; - } - - /** + * @param DataObject $previous * @return array */ - protected function getChangedOwnership() + private function getChangedOwnership(DataObject $previous): array { $owner = $this->owner; $hasOne = $owner->hasOne(); $fields = array_map(function ($field) { return $field . 'ID'; }, array_keys($hasOne)); - $changed = $owner->getChangedFields($fields); $map = array_combine($fields, array_values($hasOne)); $result = []; foreach ($fields as $field) { - if (isset($changed[$field])) { - $spec = $changed[$field]; - - if (is_null($spec['before']) || is_null($spec['after']) || $spec['before'] == $spec['after']) { - continue; - } + $previousValue = $previous->$field; + $currentValue = $owner->$field; + if ($previousValue === $currentValue) { + continue; + } - $class = $map[$field]; + $class = $map[$field]; - if (!$previous = DataObject::get_by_id($class, $spec['before'])) { - continue; - } - - if (!$current = DataObject::get_by_id($class, $spec['after'])) { - continue; - } + if (!$previousOwner = DataObject::get_by_id($class, $previousValue)) { + continue; + } - $result[] = [ - 'previous' => $previous, - 'current' => $current, - ]; + if (!$currentOwner = DataObject::get_by_id($class, $currentValue)) { + continue; } + + $result[] = [ + 'previous' => $previousOwner, + 'current' => $currentOwner, + ]; } return $result; @@ -741,10 +569,14 @@ protected function getChangedOwnership() * is nothing the user can do about it. Recursive publishing the old owner * will not affect this record, as it is no longer in its ownership graph. * - * @param array $changes */ - protected function reconcileOwnershipChanges($changes) + public function reconcileOwnershipChanges(?DataObject $previous = null): void { + if (!$previous) { + return; + } + + $changes = $this->getChangedOwnership($previous); foreach ($changes as $spec) { /* @var DataObject|SnapshotPublishable|Versioned $previousOwner */ $previousOwner = $spec['previous']; @@ -784,7 +616,8 @@ protected function reconcileOwnershipChanges($changes) // Replace them with the new owners /* @var DataObject|SnapshotPublishable $owner */ foreach ($currentOwners as $owner) { - $item = $owner->createSnapshotItem(); + $item = SnapshotItem::create(); + $item->hydrateFromDataObject($owner); $item->SnapshotID = $snapshot->ID; $item->write(); } diff --git a/tests/SnapshotTest.php b/tests/SnapshotTest.php index a4765b1..4cbf3b0 100644 --- a/tests/SnapshotTest.php +++ b/tests/SnapshotTest.php @@ -4,14 +4,9 @@ namespace SilverStripe\Snapshots\Tests; use DateTime; -use SilverStripe\Admin\AdminRootController; use SilverStripe\CMS\Controllers\CMSPageEditController; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMSEvents\Listener\Form\Listener; -use SilverStripe\Control\HTTPRequest; -use SilverStripe\Control\Session; -use SilverStripe\Core\Config\Config; -use SilverStripe\Core\Path; use SilverStripe\Dev\FunctionalTest; use SilverStripe\EventDispatcher\Dispatch\Dispatcher; use SilverStripe\EventDispatcher\Event\EventContextInterface; @@ -22,7 +17,6 @@ use SilverStripe\ORM\DataObject; use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\Snapshots\ActivityEntry; -use SilverStripe\Snapshots\Snapshot; use SilverStripe\Snapshots\SnapshotPublishable; use SilverStripe\Snapshots\SnapshotVersioned; use SilverStripe\Snapshots\Tests\SnapshotTest\BaseJoin; @@ -55,13 +49,6 @@ class SnapshotTest extends FunctionalTest */ private $currentPage; - protected function setUp(): void - { - parent::setUp(); - - Config::modify()->set(Snapshot::class, 'trigger', Snapshot::TRIGGER_MODEL); - } - public function testFundamentals() { // Model: @@ -250,8 +237,7 @@ public function testFundamentals() /* @var DataObject|SnapshotPublishable $galleryItem2 */ $galleryItem2 = new GalleryImage(['URL' => '/gallery/image/2']); - $gallery1->Images()->add($galleryItem1); - $gallery1->Images()->add($galleryItem2); + $this->formSaveRelations($gallery1, 'Images', [$galleryItem1, $galleryItem2]); // A1 (draft, modified) // block1 (draft, modified) @@ -274,8 +260,8 @@ public function testFundamentals() [$a1Block1, ActivityEntry::MODIFIED], [$gallery1, ActivityEntry::CREATED], [$gallery1, ActivityEntry::MODIFIED], - [$galleryItem1, ActivityEntry::ADDED, $gallery1], - [$galleryItem2, ActivityEntry::ADDED, $gallery1], + [$galleryItem1, ActivityEntry::ADDED], + [$galleryItem2, ActivityEntry::ADDED], ] ); @@ -296,7 +282,8 @@ public function testFundamentals() /* @var DataObject|SnapshotPublishable $gallery1a */ $gallery1a = new Gallery(['Title' => 'Gallery 1 on Block 1 on A2', 'BlockID' => $a2Block1->ID]); $this->formSaveObject($gallery1a); - $gallery1a->Images()->add($galleryItem1); + + $this->formSaveRelations($gallery1a, 'Images', [$galleryItem1]); // A1 (draft, modified) // block1 (draft, modified) @@ -603,7 +590,7 @@ public function testChangeOwnershipStructure() // gallery1 (published) // Change of ownership. A1 no longer has modifications, but A2 does. - $this->assertFalse($a1->hasOwnedModifications()); + $this->assertTrue($a2->hasOwnedModifications()); // A1 doesn't have activity anymore. @@ -665,9 +652,10 @@ public function testChangeOwnershipStructure() $this->formSaveObject($gallery1); $item = new GalleryImage(['URL' => '/belongs/to/moved/block']); - $this->formSaveObject($item); + $item->write(); + + $this->formSaveRelations($gallery1, 'Images', [$item]); - $gallery1->Images()->add($item); // A1 (published) // block2 (published) @@ -681,13 +669,12 @@ public function testChangeOwnershipStructure() $this->assertTrue($a2->hasOwnedModifications()); $this->assertFalse($a1->hasOwnedModifications()); - $activity = $a2->getActivityFeed(); $this->assertCount(3, $activity); $this->assertActivityContains($activity, [ [$blockMoved, ActivityEntry::MODIFIED], [$gallery1, ActivityEntry::MODIFIED], - [$item, ActivityEntry::ADDED, $gallery1], + [$item, ActivityEntry::ADDED], ]); // Move the block back to A1 @@ -720,7 +707,7 @@ public function testChangeOwnershipStructure() $this->assertActivityContains($activity, [ [$blockMoved, ActivityEntry::MODIFIED], [$gallery1, ActivityEntry::MODIFIED], - [$item, ActivityEntry::ADDED, $gallery1], + [$item, ActivityEntry::ADDED], [$blockMoved, ActivityEntry::MODIFIED], ]); @@ -745,10 +732,11 @@ public function testChangeOwnershipStructure() $this->assertEmpty($a2->getActivityFeed()); // Move a many_many - $gallery1->Images()->remove($item); - $gallery2->Images()->add($item); + $this->formSaveRelations($gallery1, 'Images', [$item], ActivityEntry::REMOVED); - $this->editingPage($a1); + $this->editingPage($a2); + + $this->formSaveRelations($gallery2, 'Images', [$item], ActivityEntry::ADDED); $item->URL = '/new/url'; $this->formSaveObject($item); @@ -766,13 +754,13 @@ public function testChangeOwnershipStructure() $activity = $a1->getActivityFeed(); $this->assertCount(1, $activity); $this->assertActivityContains($activity, [ - [$item, ActivityEntry::REMOVED, $gallery1], + [$item, ActivityEntry::REMOVED], ]); $activity = $a2->getActivityFeed(); $this->assertCount(2, $activity); $this->assertActivityContains($activity, [ - [$item, ActivityEntry::ADDED, $gallery2], + [$item, ActivityEntry::ADDED], [$item, ActivityEntry::MODIFIED], ]); } @@ -1142,7 +1130,7 @@ public function testWonkyOwner() $this->editingPage(null); $block = new Block(['Title' => 'The Block', 'ParentID' => 0]); - $this->formSaveObject($block); + $block->write(); $block->ParentID = $page->ID; $this->editingPage($page); @@ -1168,7 +1156,7 @@ public function testChangeToUnpublishedOwner() $this->editingPage(null); $block = new Block(['Title' => 'The Block']); - $this->formSaveObject($block); + $block->write(); $this->editingPage($page); $block->ParentID = $page->ID; @@ -1205,7 +1193,7 @@ public function testMany() $this->assertCount(0, $p->getPublishableObjects()); $i = new GalleryImage(['URL' => '/gallery/image/1']); - $g->Images()->add($i); + $this->formSaveRelations($g, 'Images', [$i]); $activity = $p->getActivityFeed(); $this->assertActivityContains( @@ -1348,7 +1336,7 @@ public function testNestedActivityFeed() $this->assertCount(0, $p->getPublishableObjects()); $i = new GalleryImage(['URL' => '/gallery/image/1']); - $g->Images()->add($i); + $this->formSaveRelations($g, 'Images', [$i]); $activity = $p->getActivityFeed(); $this->assertActivityContains( @@ -1424,14 +1412,6 @@ private function assertActivityContains(ArrayList $activity, $objs = []) SnapshotPublishable::hashObjectForSnapshot($entry->Subject) ); $this->assertEquals($action, $entry->Action); - if ($owner) { - $this->assertNotNull($entry->Owner); - - $this->assertEquals( - SnapshotPublishable::hashObjectForSnapshot($owner), - SnapshotPublishable::hashObjectForSnapshot($entry->Owner) - ); - } } } @@ -1476,26 +1456,32 @@ private function buildState($publish = true) { /* @var DataObject|SnapshotPublishable $a1 */ $a1 = new BlockPage(['Title' => 'A1 Block Page']); + $this->editingPage($a1); $this->formSaveObject($a1); /* @var DataObject|SnapshotPublishable $a2 */ $a2 = new BlockPage(['Title' => 'A2 Block Page']); + $this->editingPage($a2); $this->formSaveObject($a2); + $this->editingPage($a1); /* @var DataObject|SnapshotPublishable $a1Block1 */ $a1Block1 = new Block(['Title' => 'Block 1 on A1', 'ParentID' => $a1->ID]); $this->formSaveObject($a1Block1); $a1Block2 = new Block(['Title' => 'Block 2 on A1', 'ParentID' => $a1->ID]); $this->formSaveObject($a1Block2); + $this->editingPage($a2); /* @var DataObject|SnapshotPublishable $a2Block1 */ $a2Block1 = new Block(['Title' => 'Block 1 on A2', 'ParentID' => $a2->ID]); $this->formSaveObject($a2Block1); + $this->editingPage($a1); /* @var DataObject|SnapshotPublishable|Versioned $gallery1 */ $gallery1 = new Gallery(['Title' => 'Gallery 1 on Block 1 on A1', 'BlockID' => $a1Block1->ID]); $this->formSaveObject($gallery1); + $this->editingPage($a2); /* @var DataObject|SnapshotPublishable|Versioned $gallery1 */ $gallery2 = new Gallery(['Title' => 'Gallery 2 on Block 1 on A2', 'BlockID' => $a2Block1->ID]); $this->formSaveObject($gallery2); @@ -1564,11 +1550,31 @@ private function formUnpublishObject(DataObject $object) private function formDeleteObject(DataObject $object) { - $object->delete(); + $object->doArchive(); $event = $this->createEvent($object, 'doDelete'); $this->dispatch($event); } + /** + * Relation saves need to be wrapped in NOW() increments because they rely on + * timestamp driven history + * @param DataObject $object + * @param string $component + * @param DataObject[] $items + * @param string $type + */ + private function formSaveRelations(DataObject $object, $component, array $items, $type = ActivityEntry::ADDED) + { + $this->sleep(2); + $method = $type === ActivityEntry::ADDED ? 'add' : 'remove'; + foreach ($items as $item) { + $object->$component()->$method($item); + } + $event = $this->createEvent($object, 'doSave'); + $this->dispatch($event); + $this->sleep(2); + } + private function dispatch(EventContextInterface $event) { Dispatcher::singleton()->trigger(Listener::EVENT_NAME, $event); @@ -1579,39 +1585,22 @@ private function createEvent(DataObject $object, string $actionName): Event if (!$this->currentPage) { return Event::create($actionName); } - $controller = CMSPageEditController::singleton(); - $segment = $controller->config()->get('url_segment'); - - $adminURL = Path::join( - AdminRootController::get_admin_route(), - $segment, - 'EditForm', - 'show', - $this->currentPage->ID - ); - - $request = new HTTPRequest( - 'POST', - $adminURL, - [] - ); - - $request->setSession(new Session([])); - $form = Form::create( - $controller, + CMSPageEditController::singleton(), 'EditForm', FieldList::create(), FieldList::create() ); $form->loadDataFrom($object); + $page = $this->currentPage->isInDB() + ? DataObject::get_by_id(get_class($this->currentPage), $this->currentPage->ID) + : $this->currentPage; return Event::create( $actionName, [ - 'page' => $this->currentPage, - 'request' => $request, + 'page' => $page, 'form' => $form, ] ); From ad3e519d5aeb1933a52c6166dd1a499a41b643d3 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Fri, 24 Jan 2020 14:56:07 +1300 Subject: [PATCH 14/16] Linting --- src/Handler/Form/Handler.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Handler/Form/Handler.php b/src/Handler/Form/Handler.php index 6ec4a28..53e10cd 100644 --- a/src/Handler/Form/Handler.php +++ b/src/Handler/Form/Handler.php @@ -72,21 +72,23 @@ protected function createSnapshot(EventContextInterface $context): ?Snapshot foreach ($intermediaryObjects as $extra) { $extraObjects[SnapshotHasher::hashObjectForSnapshot($extra)] = $extra; } - foreach($implicitObjects as $spec) { + foreach ($implicitObjects as $spec) { $extraObjects[SnapshotHasher::hashObjectForSnapshot($spec['record'])] = $spec['record']; } $snapshot = Snapshot::singleton()->createSnapshotFromAction($page, $record, $message, $extraObjects); if ($snapshot && !empty($implicitObjects)) { $parentItem = $snapshot->Items()->filter( - 'ObjectHash', SnapshotHasher::hashObjectForSnapshot($record) + 'ObjectHash', + SnapshotHasher::hashObjectForSnapshot($record) )->first(); if ($parentItem) { foreach ($implicitObjects as $spec) { $obj = $spec['record']; $type = $spec['type']; $item = $snapshot->Items()->filter( - 'ObjectHash', SnapshotHasher::hashObjectForSnapshot($obj) + 'ObjectHash', + SnapshotHasher::hashObjectForSnapshot($obj) )->first(); if ($item) { $item->ParentID = $parentItem->ID; @@ -146,7 +148,6 @@ private function getMessageForManyMany(array $manyManyDiff): string ] ); } - } return implode("\n", $messages); From 929e3ead2eb7040aac3f30a209190fb7e1469eee Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Tue, 28 Jan 2020 11:43:59 +1300 Subject: [PATCH 15/16] Remove PHP 7.1 --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 28595ea..bef203a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,9 +9,6 @@ env: matrix: fast_finish: true include: - - php: &min-version '7.1' - env: DB=MYSQL PHPUNIT_TEST=1 - - php: '7.2' env: DB=MYSQL PHPUNIT_TEST=1 From d0049ff100346a2c0b08e1fa55cbee573006acb4 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Tue, 28 Jan 2020 12:01:04 +1300 Subject: [PATCH 16/16] New framework constraint --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0360fdd..162bcd4 100755 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ ], "require": { "php": ">=7.1.0", - "silverstripe/framework": "^4", + "silverstripe/framework": "^4.5.x-dev", "silverstripe/vendor-plugin": "^1", "silverstripe/cms-events": "dev-master" },