Unique key for DataObject (#9400)

NEW Unique key for DataObject
This commit is contained in:
Mojmir Fendek 2020-05-04 09:10:51 +12:00 committed by GitHub
parent 6bd0697a71
commit 7dc6b36c16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 274 additions and 5 deletions

6
_config/unique-id.yml Normal file
View File

@ -0,0 +1,6 @@
---
Name: unique-id
---
SilverStripe\Core\Injector\Injector:
SilverStripe\ORM\UniqueKey\UniqueKeyInterface:
class: SilverStripe\ORM\UniqueKey\UniqueKeyService

View File

@ -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`.

View File

@ -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');
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace SilverStripe\ORM\UniqueKey;
use SilverStripe\ORM\DataObject;
/**
* Interface UniqueKeyInterface
*
* Useful when you want to implement your own custom service and use it instead of the default one (@see UniqueKeyService)
* your custom service needs to implement this interface
*/
interface UniqueKeyInterface
{
/**
* Generate a unique key for data object
*
* @param DataObject $object
* @param array $keyComponents
* @return string
*/
public function generateKey(DataObject $object, array $keyComponents = []): string;
}

View File

@ -0,0 +1,40 @@
<?php
namespace SilverStripe\ORM\UniqueKey;
use Exception;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\ORM\DataObject;
/**
* Class UniqueKeyService
*
* Generate a unique key for data object
*
* 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)
*/
class UniqueKeyService implements UniqueKeyInterface
{
use Injectable;
/**
* @param DataObject $object
* @param array $keyComponents key components are expected to be strings (or at least scalar values)
* @return string
* @throws Exception
*/
public function generateKey(DataObject $object, array $keyComponents = []): string
{
$id = $object->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);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace SilverStripe\Tests\ORM\UniqueKey;
use SilverStripe\Core\Extension;
use SilverStripe\Dev\TestOnly;
class ExtraKeysExtension extends Extension implements TestOnly
{
public function cacheKeyComponent(): string
{
return 'extra-key';
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace SilverStripe\Tests\ORM\UniqueKey;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class Mountain extends DataObject implements TestOnly
{
/**
* @var string
*/
private static $table_name = 'UniqueKeyTest_Mountain';
/**
* @var array
*/
private static $db = [
'Title' => 'Varchar',
];
}

View File

@ -0,0 +1,21 @@
<?php
namespace SilverStripe\Tests\ORM\UniqueKey;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class River extends DataObject implements TestOnly
{
/**
* @var string
*/
private static $table_name = 'UniqueKeyTest_River';
/**
* @var array
*/
private static $db = [
'Title' => 'Varchar',
];
}

View File

@ -0,0 +1,56 @@
<?php
namespace SilverStripe\Tests\ORM\UniqueKey;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataObject;
class ServiceTest extends SapphireTest
{
/**
* @var array
*/
protected static $extra_dataobjects = [
River::class,
Mountain::class,
];
/**
* @param int $id
* @param string $class
* @param bool $extraKeys
* @param string $expected
* @dataProvider uniqueKeysProvider
*/
public function testUniqueKey(int $id, string $class, bool $extraKeys, string $expected): void
{
if ($extraKeys) {
$class::add_extension(ExtraKeysExtension::class);
}
/** @var DataObject $object */
$object = Injector::inst()->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'],
];
}
}