Skip to content

Commit

Permalink
Unique key for DataObject
Browse files Browse the repository at this point in the history
  • Loading branch information
mfendeksilverstripe committed Feb 26, 2020
1 parent 73990ac commit 98ed9b4
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 5 deletions.
34 changes: 29 additions & 5 deletions src/ORM/DataObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,17 @@
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\Security\Member;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
use SilverStripe\UniqueKey;
use SilverStripe\View\SSViewer;
use SilverStripe\View\ViewableData;
use stdClass;
Expand Down Expand Up @@ -3234,9 +3233,10 @@ public static function get(
*/
public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
{
$SNG = singleton($callerClass);
/** @var DataObject $singleton */
$singleton = singleton($callerClass);

$cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
$cacheComponents = [$filter, $orderby, $singleton->getCacheKeyComponent()];
$cacheKey = md5(serialize($cacheComponents));

$item = null;
Expand Down Expand Up @@ -4186,6 +4186,25 @@ public function mergeRelatedObjects($list, $items)
return $added;
}

/**
* Generate a unique key for data object
* the unique key uses the @see DataObject::getCacheKeyComponent() 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
*/
public function getUniqueKey(): string
{
$cacheKeys = $this->getCacheKeyComponent();

return UniqueKey\Service::singleton()->generateKey($this, $cacheKeys);
}

/**
* Merge single object into a list, but ensures that existing objects are not
* re-added.
Expand All @@ -4211,4 +4230,9 @@ protected function mergeRelatedObject($list, $added, $item)
$this->mergeRelatedObject($list, $added, $joined);
}
}

private function getCacheKeyComponent(): array
{
return $this->extend('cacheKeyComponent');
}
}
45 changes: 45 additions & 0 deletions src/UniqueKey/Service.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace SilverStripe\UniqueKey;

use SilverStripe\Core\Injector\Injectable;
use SilverStripe\ORM\DataObject;

class Service
{
use Injectable;

/**
* 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)
*
* @param DataObject $object
* @param array $extraKeys
* @return string
*/
public function generateKey(DataObject $object, array $extraKeys = []): string
{
if (!$object->isInDB()) {
return '';
}

// extract class name (remove namespaces)
$classSegments = explode('\\', $object->ClassName);

if (count($classSegments) === 0) {
return '';
}

$class = array_pop($classSegments);
$extraKeys = json_encode($extraKeys);

$hash = md5(sprintf('%s-%s-%d', $extraKeys, $object->ClassName, $object->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('ss-%s-%d-%s', $class, $object->ID, $hash);
}
}
14 changes: 14 additions & 0 deletions tests/php/UniqueKey/ExtraKeysExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace SilverStripe\Tests\UniqueKey;

use SilverStripe\Core\Extension;
use SilverStripe\Dev\TestOnly;

class ExtraKeysExtension extends Extension implements TestOnly
{
public function cacheKeyComponent(): string
{
return 'extra-key';
}
}
21 changes: 21 additions & 0 deletions tests/php/UniqueKey/Mountain.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace SilverStripe\Tests\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',
];
}
21 changes: 21 additions & 0 deletions tests/php/UniqueKey/River.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace SilverStripe\Tests\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',
];
}
56 changes: 56 additions & 0 deletions tests/php/UniqueKey/ServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace SilverStripe\Tests\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, 'ss-River-1-7eab00006ab6d090635b03f9fa1187d7'],
[1, River::class, true, 'ss-River-1-65474ab87fd42ca8cbfc32f87d5840e7'],
[2, River::class, false, 'ss-River-2-9c63d549d3a7a2f9679f7ce0dbb6a177'],
[2, River::class, true, 'ss-River-2-a028c9b5ecd2dd68edc6f20192e29c63'],
[1, Mountain::class, false, 'ss-Mountain-1-013d8ba56604ceeb2bda4b09d04c7e29'],
[1, Mountain::class, true, 'ss-Mountain-1-3dba35f13a9d3ad648be466946297444'],
[2, Mountain::class, false, 'ss-Mountain-2-a628f2db748065729d6a832a094cea3f'],
[2, Mountain::class, true, 'ss-Mountain-2-e6236799ab5a00d36ee704fa87d46021'],
];
}
}

0 comments on commit 98ed9b4

Please sign in to comment.