-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9b9595b
commit 3c781a2
Showing
14 changed files
with
512 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# For more information about the properties used in this file, | ||
# please see the EditorConfig documentation: | ||
# http://editorconfig.org | ||
|
||
[*] | ||
charset = utf-8 | ||
end_of_line = lf | ||
indent_size = 4 | ||
indent_style = tab | ||
insert_final_newline = true | ||
trim_trailing_whitespace = true | ||
|
||
[{*.yml,package.json}] | ||
indent_size = 2 | ||
|
||
# The indent size used in the package.json file cannot be changed: | ||
# https://github.com/npm/npm/pull/3180#issuecomment-16336516 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<FilesMatch "\.(php|php3|php4|php5|phtml|inc)$"> | ||
Deny from all | ||
</FilesMatch> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,34 @@ | ||
# silverstripe-persistent-dataobject | ||
Persistent, immutable and optionally versioned DataObjects for SilverStripe 4 | ||
# Persistent DataObjects - Experimental / Work in Progress | ||
|
||
Persistent and optionally immutable & versioned DataObjects for SilverStripe | ||
|
||
The two major features of this module are: | ||
|
||
1. **A DataObject subclass that can not be deleted** | ||
Calling `->delete()` will mark an object as deleted, but not actually delete it | ||
*(Where necessary, objects can still be deleted by calling `->purge()`)* | ||
1. **A DataObjectExtension that adds versioning** | ||
In contrast to the "silverstripe-versioned" module, versioning is achived by | ||
making DataObjects immutable and overloading `->write()` to create a duplicate | ||
of the current record instead of saving the existing. | ||
This means the `ID` becomes the unique version number, while an additional | ||
`VersionGroupID` and `VersionGroupLatest` keep track of the relation of records. | ||
The benefit of this approach is being able to easily reference a version of an | ||
entry rather than always the latest version. Thus making it possible to have | ||
persistent storage of information that is easily integrable in other parts of | ||
SilverStripe (eg an invoice can safely reference a product price and does not | ||
need to create a snapshot). | ||
|
||
## TODOs / Planed features | ||
|
||
- [ ] Tests | ||
- [ ] Revisit decision to put VersionGroup_ID on DataObject rather than subclasses | ||
- [ ] Extend GridField integration | ||
- [ ] Hide/Show deleted records button | ||
- [ ] History view to access older version from within a DataObject | ||
- [ ] Implement non GridField form fields (eg Relation Dropdown that let's the user pick a entry and a version) | ||
- [ ] Implement Database Field/Relation that references VersionGroupID instead of ID? | ||
- [ ] Documentation | ||
- [ ] Explain the usecase in more detail | ||
- [ ] Examples | ||
- [ ] SilverStripe 4 Support |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<?php | ||
|
||
namespace zauberfisch\PersistentDataObject\Admin; | ||
|
||
use zauberfisch\PersistentDataObject\Form\GridField\Config_RecordEditor; | ||
use zauberfisch\PersistentDataObject\Form\GridField\DetailForm; | ||
|
||
/** | ||
* @author zauberfisch | ||
*/ | ||
class ModelAdmin extends \ModelAdmin { | ||
public function getEditForm($id = null, $fields = null) { | ||
/** @var \Form $return */ | ||
$return = parent::getEditForm($id, $fields); | ||
/** @var \GridField $grid */ | ||
$grid = $return->Fields()->fieldByName($this->sanitiseClassName($this->modelClass)); | ||
$config = new Config_RecordEditor(); | ||
$config->removeComponentsByType(\GridFieldFilterHeader::class); | ||
// Validation | ||
if (singleton($this->modelClass)->hasMethod('getCMSValidator')) { | ||
$detailValidator = singleton($this->modelClass)->getCMSValidator(); | ||
/** @var DetailForm $detailForm */ | ||
$detailForm = $config->getComponentByType(DetailForm::class); | ||
$detailForm->setValidator($detailValidator); | ||
} | ||
// Import / Export | ||
$config->addComponent((new \GridFieldExportButton('buttons-before-left'))->setExportColumns($this->getExportFields())); | ||
//if ($this->showImportForm) { | ||
// $config->addComponent( | ||
// GridFieldImportButton::create('buttons-before-left') | ||
// ->setImportForm($this->ImportForm()) | ||
// ->setModalTitle(_t('ModelAdmin.IMPORT', 'Import from CSV')) | ||
// ); | ||
//} | ||
$grid->setConfig($config); | ||
return $return; | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?php | ||
|
||
namespace zauberfisch\PersistentDataObject\Form\GridField\Action; | ||
|
||
use zauberfisch\PersistentDataObject\Model\VersionedDataObjectExtension; | ||
|
||
class EditButton extends \GridFieldEditButton { | ||
public function getColumnContent($gridField, $record, $columnName) { | ||
if ($record->hasExtension(VersionedDataObjectExtension::class)) { | ||
/** @var \PersistentDataObject_Model_DataObject|VersionedDataObjectExtension $record */ | ||
$data = new \ArrayData([ | ||
'Link' => \Controller::join_links( | ||
$gridField->Link('version-group'), | ||
$record->VersionGroupID, | ||
'item', | ||
$record->isLatestVersion(true) ? 'latest' : $record->ID, | ||
'edit' | ||
), | ||
]); | ||
$template = \SSViewer::get_templates_by_class(\GridFieldEditButton::class, '', \GridFieldEditButton::class); | ||
return $data->renderWith($template); | ||
} | ||
return parent::getColumnContent($gridField, $record, $columnName); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<?php | ||
|
||
namespace zauberfisch\PersistentDataObject\Form\GridField; | ||
|
||
use zauberfisch\PersistentDataObject\Form\GridField\Action\EditButton; | ||
use zauberfisch\PersistentDataObject\Form\GridField\Filter\HideDeletedFilter; | ||
use zauberfisch\PersistentDataObject\Form\GridField\Filter\LatestVersionFilter; | ||
|
||
class Config_RecordEditor extends \GridFieldConfig_RecordEditor { | ||
public function __construct($itemsPerPage = null) { | ||
parent::__construct($itemsPerPage); | ||
$this->removeComponentsByType(\GridFieldDetailForm::class); | ||
$this->addComponent(new DetailForm()); | ||
$this->removeComponentsByType(\GridFieldEditButton::class); | ||
$this->addComponent(new EditButton()); | ||
$this->addComponent(new LatestVersionFilter()); | ||
$this->addComponent(new HideDeletedFilter()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
<?php | ||
|
||
namespace zauberfisch\PersistentDataObject\Form\GridField; | ||
|
||
use zauberfisch\PersistentDataObject\Model\VersionedDataObjectExtension; | ||
|
||
class DetailForm extends \GridFieldDetailForm { | ||
public function getURLHandlers($gridField) { | ||
return array_merge([ | ||
'version-group/$VersionGroupID/item/$ID' => 'handleVersionedItem', | ||
], parent::getURLHandlers($gridField)); | ||
} | ||
|
||
/** | ||
* @param \GridField $gridField | ||
* @param \SS_HTTPRequest $request | ||
* @return \GridFieldDetailForm_ItemRequest|\RequestHandler | ||
*/ | ||
public function handleVersionedItem($gridField, $request) { | ||
// Our getController could either give us a true Controller, if this is the top-level GridField. | ||
// It could also give us a RequestHandler in the form of GridFieldDetailForm_ItemRequest if this is a | ||
// nested GridField. | ||
$requestHandler = $gridField->getForm()->getController(); | ||
|
||
$versionedID = $request->param('VersionGroupID'); | ||
$id = $request->param('ID'); | ||
|
||
if ($versionedID && is_numeric($versionedID)) { | ||
/** @var \DataList $list */ | ||
$list = $gridField->getList(); | ||
$list = $list->filter('VersionGroupID', $versionedID); | ||
if ($id && is_numeric($id)) { | ||
/** @var \DataObject|\PersistentDataObject_Model_DataObject|VersionedDataObjectExtension $record */ | ||
$record = $list->byID($id); | ||
} else { | ||
$record = $list->filter('VersionGroupLatest', true)->first(); | ||
} | ||
} else { | ||
$record = \Object::create($gridField->getModelClass()); | ||
} | ||
$class = $this->getItemRequestClass(); | ||
/** @var DetailForm_ItemRequest $handler */ | ||
$handler = \Object::create($class, $gridField, $this, $record, $requestHandler, $this->name); | ||
$handler->setTemplate($this->template); | ||
|
||
// if no validator has been set on the GridField and the record has a CMS validator, use that. | ||
if (!$this->getValidator() | ||
&& ( | ||
method_exists($record, 'getCMSValidator') | ||
|| $record instanceof \Object && $record->hasMethod('getCMSValidator') | ||
) | ||
) { | ||
/** @noinspection PhpUndefinedMethodInspection */ | ||
$this->setValidator($record->getCMSValidator()); | ||
} | ||
return $handler->handleRequest($request, \DataModel::inst()); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<?php | ||
|
||
namespace zauberfisch\PersistentDataObject\Form\GridField; | ||
|
||
use zauberfisch\PersistentDataObject\Model\VersionedDataObjectExtension; | ||
|
||
class DetailForm_ItemRequest extends \GridFieldDetailForm_ItemRequest { | ||
private static $allowed_actions = [ | ||
'edit', | ||
'view', | ||
'ItemEditForm', | ||
]; | ||
|
||
public function Link($action = null) { | ||
$r = $this->record; | ||
if ($r && $r->hasExtension(VersionedDataObjectExtension::class)) { | ||
/** @var \DataObject|VersionedDataObjectExtension $r */ | ||
return \Controller::join_links( | ||
$this->gridField->Link('version-group'), | ||
$r->VersionGroupID ?: 'new', | ||
'item', | ||
$r->isLatestVersion(true) ? 'latest' : $r->ID, | ||
$action | ||
); | ||
} | ||
return parent::Link($action); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<?php | ||
|
||
namespace zauberfisch\PersistentDataObject\Form\GridField\Filter; | ||
|
||
class HideDeletedFilter implements \GridField_DataManipulator { | ||
/** | ||
* @param \GridField $gridField | ||
* @param \SS_List|\DataList|\ArrayList $dataList | ||
* @return mixed | ||
*/ | ||
public function getManipulatedData(\GridField $gridField, \SS_List $dataList) { | ||
$latest = $dataList->filter('VersionGroupLatest', true); | ||
$groupIDs = array_merge( | ||
[0], | ||
$latest->exclude('Deleted', 1)->column('VersionGroupID') | ||
); | ||
return $dataList->filter('VersionGroupID', $groupIDs); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php | ||
|
||
namespace zauberfisch\PersistentDataObject\Form\GridField\Filter; | ||
|
||
class LatestVersionFilter implements \GridField_DataManipulator { | ||
/** | ||
* @param \GridField $gridField | ||
* @param \SS_List|\DataList|\ArrayList $dataList | ||
* @return mixed | ||
*/ | ||
public function getManipulatedData(\GridField $gridField, \SS_List $dataList) { | ||
return $dataList->filter('VersionGroupLatest', true); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
<?php | ||
|
||
//namespace zauberfisch\PersistentDataObject\Model; | ||
|
||
/** | ||
* DataObject classes can not be namespaced in SilverStripe 3.x | ||
* @author zauberfisch | ||
* @property boolean $Deleted | ||
* @property int $VersionGroupID | ||
* @property boolean $VersionGroupLatest | ||
*/ | ||
class PersistentDataObject_Model_DataObject extends \DataObject { | ||
private static $db = [ | ||
'Deleted' => 'Boolean', | ||
'VersionGroupID' => 'Int', | ||
'VersionGroupLatest' => 'Boolean', | ||
]; | ||
private static $defaults = [ | ||
'VersionGroupLatest' => true, | ||
]; | ||
|
||
public function requireTable() { | ||
parent::requireTable(); | ||
} | ||
|
||
|
||
public function getCMSFields() { | ||
$return = new FieldList([ | ||
new TabSet('Root'), | ||
]); | ||
$return->addFieldsToTab('Root', [ | ||
new Tab('Main', _t(self::class . '.MainTab', 'Main')), | ||
]); | ||
$s = new FormScaffolder($this); | ||
$return->addFieldsToTab('Root.Main', $s->getFieldList()->toArray()); | ||
$return->removeByName('Deleted'); | ||
$return->removeByName('VersionGroupID'); | ||
$return->removeByName('VersionGroupLatest'); | ||
return $return; | ||
} | ||
|
||
public function delete() { | ||
$this->extend('onBeforeMarkDeleted'); | ||
$this->Deleted = true; | ||
$this->write(); | ||
$this->flushCache(); | ||
$this->extend('onAfterMarkDeleted'); | ||
} | ||
|
||
public function purge() { | ||
$this->extend('onBeforePurge'); | ||
parent::delete(); | ||
$this->extend('onAfterPurge'); | ||
} | ||
|
||
public function canDelete($member = null) { | ||
return $this->isDeleted() ? false : parent::canDelete($member); | ||
} | ||
|
||
public function canPurge($member = null) { | ||
// only allow purging a record if it has been marked as deleted, making deletion a 2 step process | ||
return $this->isDeleted() ? parent::canDelete($member) : false; | ||
} | ||
|
||
public function canEdit($member = null) { | ||
return $this->isDeleted() ? false : parent::canEdit($member); | ||
} | ||
|
||
public function isDeleted() { | ||
$result = (bool)$this->Deleted; | ||
$results = $this->extend('isDeleted'); | ||
if ($results && is_array($results)) { | ||
// Remove NULLs | ||
$results = array_filter($results, function ($v) { | ||
return !is_null($v); | ||
}); | ||
// If there are any non-NULL responses, then return the lowest one of them. | ||
// If any explicitly deny the permission, then we don't get access | ||
if ($results) { | ||
$result = (bool)min($results); | ||
} | ||
} | ||
return $result; | ||
} | ||
|
||
protected function writeBaseRecord($baseTable, $now) { | ||
$this->extend('onBeforeWriteBaseRecord', $baseTable, $now); | ||
parent::writeBaseRecord($baseTable, $now); | ||
$this->onAfterWriteBaseRecord($baseTable, $now); | ||
$this->extend('onAfterWriteBaseRecord', $baseTable, $now); | ||
} | ||
|
||
public function onAfterWriteBaseRecord($baseTable, $now) { | ||
if (!$this->owner->getRawFieldData('VersionGroupID')) { | ||
$id = $this->owner->getRawFieldData('ID'); | ||
$this->owner->setRawFieldData('VersionGroupID', $id); | ||
(new \SQLUpdate('"' . $baseTable . '"')) | ||
->assign('"VersionGroupID"', $id) | ||
//->assign('"VersionGroupLatest"', 1) | ||
->addWhere(['"ID"' => $id]) | ||
->execute(); | ||
} | ||
} | ||
|
||
protected function onBeforeWrite() { | ||
parent::onBeforeWrite(); | ||
$this->owner->setRawFieldData('VersionGroupLatest', 1); | ||
} | ||
|
||
|
||
public function setRawFieldData($name, $value) { | ||
$this->record[$name] = $value; | ||
return $this; | ||
} | ||
|
||
public function getRawFieldData($name) { | ||
return isset($this->record[$name]) ? $this->record[$name] : null; | ||
} | ||
} |
Oops, something went wrong.