From 7dc6b36c1664de2e5b3dfeb991589ff3c4022740 Mon Sep 17 00:00:00 2001 From: Mojmir Fendek Date: Mon, 4 May 2020 09:10:51 +1200 Subject: [PATCH] Unique key for DataObject (#9400) NEW Unique key for DataObject --- _config/unique-id.yml | 6 ++ .../01_Templates/10_Unique_Keys.md | 48 ++++++++++++++++ src/ORM/DataObject.php | 50 +++++++++++++++-- src/ORM/UniqueKey/UniqueKeyInterface.php | 23 ++++++++ src/ORM/UniqueKey/UniqueKeyService.php | 40 +++++++++++++ .../php/ORM/UniqueKey/ExtraKeysExtension.php | 14 +++++ tests/php/ORM/UniqueKey/Mountain.php | 21 +++++++ tests/php/ORM/UniqueKey/River.php | 21 +++++++ tests/php/ORM/UniqueKey/ServiceTest.php | 56 +++++++++++++++++++ 9 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 _config/unique-id.yml create mode 100644 docs/en/02_Developer_Guides/01_Templates/10_Unique_Keys.md create mode 100644 src/ORM/UniqueKey/UniqueKeyInterface.php create mode 100644 src/ORM/UniqueKey/UniqueKeyService.php create mode 100644 tests/php/ORM/UniqueKey/ExtraKeysExtension.php create mode 100644 tests/php/ORM/UniqueKey/Mountain.php create mode 100644 tests/php/ORM/UniqueKey/River.php create mode 100644 tests/php/ORM/UniqueKey/ServiceTest.php diff --git a/_config/unique-id.yml b/_config/unique-id.yml new file mode 100644 index 000000000..71c1320ec --- /dev/null +++ b/_config/unique-id.yml @@ -0,0 +1,6 @@ +--- +Name: unique-id +--- +SilverStripe\Core\Injector\Injector: + SilverStripe\ORM\UniqueKey\UniqueKeyInterface: + class: SilverStripe\ORM\UniqueKey\UniqueKeyService diff --git a/docs/en/02_Developer_Guides/01_Templates/10_Unique_Keys.md b/docs/en/02_Developer_Guides/01_Templates/10_Unique_Keys.md new file mode 100644 index 000000000..ee2f40d39 --- /dev/null +++ b/docs/en/02_Developer_Guides/01_Templates/10_Unique_Keys.md @@ -0,0 +1,48 @@ +--- +title: Generating Unique Keys +summary: Outputting unique keys in templates. +icon: code +--- + +# Unique Keys + +There are several cases where you may want to generate a unique key. For example: + +* populate `ID` attribute in your HTML output +* key for partial cache + +This can be done simply by including following code in your template: + +```ss +$DataObject.UniqueKey +``` + +`getUniqueKey` method is available on `DataObject` so you can use it on many object types like pages and blocks. + +## Customisation + +The unique key generation can be altered in two ways: + +* you can provide extra data to be used when generating a key via an extension +* you can inject over the key generation service and write your own custom code + +### Extension point + +`cacheKeyComponent` extension point is located in `DataObject::getUniqueKeyComponents`. +Use standard extension flow to define the `cacheKeyComponent` method on your extension which is expected to return a `string`. +This value will be used when unique key is generated. Common cases are: + +* versions - object in different version stages needs to have different unique keys +* locales - object in different locales needs to have different unique keys + +### Custom service + +`UniqueKeyService` is used by default but you can use injector to override it with your custom service. For example: + +```yaml +SilverStripe\Core\Injector\Injector: + SilverStripe\ORM\UniqueKey\UniqueKeyService: + class: App\Service\MyCustomService +``` + +Your custom service has to implement `UniqueKeyInterface`. diff --git a/src/ORM/DataObject.php b/src/ORM/DataObject.php index b101d2b84..b34cb9a86 100644 --- a/src/ORM/DataObject.php +++ b/src/ORM/DataObject.php @@ -18,15 +18,15 @@ use SilverStripe\Forms\FormScaffolder; use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18nEntityProvider; use SilverStripe\ORM\Connect\MySQLSchemaManager; -use SilverStripe\ORM\FieldType\DBClassName; -use SilverStripe\ORM\FieldType\DBEnum; use SilverStripe\ORM\FieldType\DBComposite; use SilverStripe\ORM\FieldType\DBDatetime; +use SilverStripe\ORM\FieldType\DBEnum; use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\Filters\SearchFilter; use SilverStripe\ORM\Queries\SQLDelete; -use SilverStripe\ORM\Queries\SQLInsert; use SilverStripe\ORM\Search\SearchContext; +use SilverStripe\ORM\UniqueKey\UniqueKeyInterface; +use SilverStripe\ORM\UniqueKey\UniqueKeyService; use SilverStripe\Security\Member; use SilverStripe\Security\Permission; use SilverStripe\Security\Security; @@ -3234,9 +3234,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "") { - $SNG = singleton($callerClass); + /** @var DataObject $singleton */ + $singleton = singleton($callerClass); - $cacheComponents = [$filter, $orderby, $SNG->extend('cacheKeyComponent')]; + $cacheComponents = [$filter, $orderby, $singleton->getUniqueKeyComponents()]; $cacheKey = md5(serialize($cacheComponents)); $item = null; @@ -4186,6 +4187,28 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity return $added; } + /** + * Generate a unique key for data object + * the unique key uses the @see DataObject::getUniqueKeyComponents() extension point so unique key modifiers + * such as versioned or fluent are covered + * i.e. same data object in different stages or different locales will produce different unique key + * + * recommended use: + * - when you need unique key for caching purposes + * - when you need unique id on the front end (for example JavaScript needs to target specific element) + * + * @return string + * @throws Exception + */ + public function getUniqueKey(): string + { + /** @var UniqueKeyInterface $service */ + $service = Injector::inst()->get(UniqueKeyInterface::class); + $keyComponents = $this->getUniqueKeyComponents(); + + return $service->generateKey($this, $keyComponents); + } + /** * Merge single object into a list, but ensures that existing objects are not * re-added. @@ -4211,4 +4234,21 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $this->mergeRelatedObject($list, $added, $joined); } } + + /** + * Extension point to add more cache key components. + * The framework extend method will return combined values from DataExtension method(s) as an array + * The method on your DataExtension class should return a single scalar value. For example: + * + * public function cacheKeyComponent() + * { + * return (string) $this->owner->MyColumn; + * } + * + * @return array + */ + private function getUniqueKeyComponents(): array + { + return $this->extend('cacheKeyComponent'); + } } diff --git a/src/ORM/UniqueKey/UniqueKeyInterface.php b/src/ORM/UniqueKey/UniqueKeyInterface.php new file mode 100644 index 000000000..e4a26a64c --- /dev/null +++ b/src/ORM/UniqueKey/UniqueKeyInterface.php @@ -0,0 +1,23 @@ +isInDB() ? (string) $object->ID : bin2hex(random_bytes(16)); + $class = ClassInfo::shortName($object); + $keyComponents = json_encode($keyComponents); + $hash = md5($keyComponents . $object->ClassName . $id); + + // note: class name and id are added just for readability as the hash already contains all parts + // needed to create a unique key + return sprintf('%s-%s-%s', $class, $id, $hash); + } +} diff --git a/tests/php/ORM/UniqueKey/ExtraKeysExtension.php b/tests/php/ORM/UniqueKey/ExtraKeysExtension.php new file mode 100644 index 000000000..44916e45a --- /dev/null +++ b/tests/php/ORM/UniqueKey/ExtraKeysExtension.php @@ -0,0 +1,14 @@ + 'Varchar', + ]; +} diff --git a/tests/php/ORM/UniqueKey/River.php b/tests/php/ORM/UniqueKey/River.php new file mode 100644 index 000000000..97aaee405 --- /dev/null +++ b/tests/php/ORM/UniqueKey/River.php @@ -0,0 +1,21 @@ + 'Varchar', + ]; +} diff --git a/tests/php/ORM/UniqueKey/ServiceTest.php b/tests/php/ORM/UniqueKey/ServiceTest.php new file mode 100644 index 000000000..d0e78a969 --- /dev/null +++ b/tests/php/ORM/UniqueKey/ServiceTest.php @@ -0,0 +1,56 @@ +create($class); + $object->ID = $id; + + $this->assertEquals($expected, $object->getUniqueKey()); + + if ($extraKeys) { + $class::remove_extension(ExtraKeysExtension::class); + } + } + + public function uniqueKeysProvider(): array + { + return [ + [1, River::class, false, 'River-1-8d3310e232f75a01f5a0c9344655263d'], + [1, River::class, true, 'River-1-ff2ea6e873a9e28538dd4af278f35e08'], + [2, River::class, false, 'River-2-c562c31e5c2caaabb124b46e274097c1'], + [2, River::class, true, 'River-2-410c1eb12697a26742bbe4b059625ab2'], + [1, Mountain::class, false, 'Mountain-1-93164c0f65fa28778fb75163c1e3e2f0'], + [1, Mountain::class, true, 'Mountain-1-2daf208e0b89252e5d239fbc0464a517'], + [2, Mountain::class, false, 'Mountain-2-62366f2b970a64de6f2a8e8654f179d5'], + [2, Mountain::class, true, 'Mountain-2-a724046b14d331a1486841eaa591d109'], + ]; + } +}