Skip to content

Commit

Permalink
NEW: Stages differ recursive.
Browse files Browse the repository at this point in the history
  • Loading branch information
danaenz authored and mfendeksilverstripe committed Mar 11, 2021
1 parent abf0a9d commit d314357
Show file tree
Hide file tree
Showing 8 changed files with 525 additions and 0 deletions.
171 changes: 171 additions & 0 deletions src/RecursiveStagesService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php

namespace SilverStripe\Versioned;

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

/**
* Class RecursiveStagesService
*
* Functionality for detecting the need of publishing nested objects owned by common parent / ancestor object
*
* @package SilverStripe\Versioned
*/
class RecursiveStagesService
{
use Injectable;

/**
* Strong ownership uses 'owns' configuration to determine relationships
*/
public const OWNERSHIP_STRONG = 'strong';

/**
* Strong ownership uses 'cascade_duplicates' configuration to determine relationships
*/
public const OWNERSHIP_WEAK = 'weak';

/**
* Determine if content differs on stages including nested objects
*
* @param DataObject $object
* @param string $mode
* @return bool
*/
public function stagesDifferRecursive(DataObject $object, string $mode): bool
{
if (!$object->exists()) {
return false;
}

$items = [$object];

// compare existing content
while ($item = array_shift($items)) {
if ($this->checkNeedPublishingItem($item)) {
return true;
}

$relatedObjects = $this->findOwnedObjects($item, $mode);
$items = array_merge($items, $relatedObjects);
}

// compare deleted content
$draftIdentifiers = $this->findOwnedIdentifiers($object, $mode, Versioned::DRAFT);
$liveIdentifiers = $this->findOwnedIdentifiers($object, $mode, Versioned::LIVE);

return $draftIdentifiers !== $liveIdentifiers;
}

/**
* Find all identifiers for owned objects
*
* @param DataObject $object
* @param string $mode
* @param string $stage
* @return array
*/
protected function findOwnedIdentifiers(DataObject $object, string $mode, string $stage): array
{
$ids = Versioned::withVersionedMode(function () use ($object, $mode, $stage): array {
Versioned::set_stage($stage);

$object = DataObject::get_by_id($object->ClassName, $object->ID);

if ($object === null) {
return [];
}

$items = [$object];
$ids = [];

while ($object = array_shift($items)) {
$ids[] = implode('_', [$object->baseClass(), $object->ID]);
$relatedObjects = $this->findOwnedObjects($object, $mode);
$items = array_merge($items, $relatedObjects);
}

return $ids;
});

sort($ids, SORT_STRING);

return array_values($ids);
}

/**
* This lookup will attempt to find "Strongly owned" objects
* such objects are unable to exist without the current object
* We will use "cascade_duplicates" setting for this purpose as we can assume that if an object needs to be
* duplicated along with the owner object, it uses the strong ownership relation
*
* "Weakly owned" objects could be looked up via "owns" setting
* Such objects can exist even without the owner objects as they are often used as shared objects
* managed independently of their owners
*
* @param DataObject $object
* @param string $mode
* @return array
*/
protected function findOwnedObjects(DataObject $object, string $mode): array
{
$ownershipType = $mode === self::OWNERSHIP_WEAK
? 'owns'
: 'cascade_duplicates';

$relations = (array) $object->config()->get($ownershipType);
$relations = array_unique($relations);
$result = [];

foreach ($relations as $relation) {
$relation = (string) $relation;

if (!$relation) {
continue;
}

$relationData = $object->{$relation}();

if ($relationData instanceof DataObject) {
if (!$relationData->exists()) {
continue;
}

$result[] = $relationData;

continue;
}

if (!$relationData instanceof SS_List) {
continue;
}

foreach ($relationData as $relatedObject) {
if (!$relatedObject instanceof DataObject || !$relatedObject->exists()) {
continue;
}

$result[] = $relatedObject;
}
}

return $result;
}

/**
* @param DataObject|Versioned $item
* @return bool
*/
protected function checkNeedPublishingItem(DataObject $item): bool
{
if ($item->hasExtension(Versioned::class)) {
/** @var $item Versioned */
return !$item->isPublished() || $item->stagesDiffer();
}

return false;
}
}
14 changes: 14 additions & 0 deletions src/Versioned.php
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,20 @@ public function stagesDiffer()
return (bool) $stagesDiffer;
}

/**
* Determine if content differs on stages including nested objects
* $mode determines which relation will be used to traverse the ownership tree
* "strong" will use "cascade_duplicates"
* "weak" will use "owns"
*
* @param string $mode "strong" or "weak"
* @return bool
*/
public function stagesDifferRecursive(string $mode = RecursiveStagesService::OWNERSHIP_STRONG): bool
{
return RecursiveStagesService::singleton()->stagesDifferRecursive($this->owner, $mode);
}

/**
* @param string $filter
* @param string $sort
Expand Down
92 changes: 92 additions & 0 deletions tests/php/RecursiveStagesServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

namespace SilverStripe\Versioned\Tests;

use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\ValidationException;
use SilverStripe\Versioned\Tests\RecursiveStagesServiceTest\ChildObject;
use SilverStripe\Versioned\Tests\RecursiveStagesServiceTest\ColumnObject;
use SilverStripe\Versioned\Tests\RecursiveStagesServiceTest\GroupObject;
use SilverStripe\Versioned\Tests\RecursiveStagesServiceTest\PrimaryObject;
use SilverStripe\Versioned\Versioned;

class RecursiveStagesServiceTest extends SapphireTest
{
/**
* @var string
*/
protected static $fixture_file = 'RecursiveStagesServiceTest.yml';

/**
* @var string[]
*/
protected static $extra_dataobjects = [
PrimaryObject::class,
ColumnObject::class,
GroupObject::class,
ChildObject::class,
];

protected static $required_extensions = [
PrimaryObject::class => [
Versioned::class,
],
ColumnObject::class => [
Versioned::class,
],
GroupObject::class => [
Versioned::class,
],
ChildObject::class => [
Versioned::class,
],
];

/**
* @param string $class
* @param string $identifier
* @param bool $delete
* @throws ValidationException
* @dataProvider objectsProvider
*/
public function testStageDiffersRecursive(string $class, string $identifier, bool $delete): void
{
/** @var PrimaryObject|Versioned $primaryItem */
$primaryItem = $this->objFromFixture(PrimaryObject::class, 'primary-object-1');
$primaryItem->publishRecursive();

$this->assertFalse($primaryItem->stagesDifferRecursive());

$record = $this->objFromFixture($class, $identifier);

if ($delete) {
$record->delete();
} else {
$record->Title = 'New Title';
$record->write();
}

$this->assertTrue($primaryItem->stagesDifferRecursive());
}

public function testStageDiffersRecursiveWithInvalidObject(): void
{
/** @var PrimaryObject|Versioned $primaryItem */
$primaryItem = PrimaryObject::create();

$this->assertFalse($primaryItem->stagesDifferRecursive());
}

public function objectsProvider(): array
{
return [
[PrimaryObject::class, 'primary-object-1', false],
[ColumnObject::class, 'column-1', false],
[ColumnObject::class, 'column-1', true],
[GroupObject::class, 'group-1', false],
[GroupObject::class, 'group-1', true],
[ChildObject::class, 'child-object-1', false],
[ChildObject::class, 'child-object-1', true],
];
}
}
24 changes: 24 additions & 0 deletions tests/php/RecursiveStagesServiceTest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# site-config-1
# -> primary-object-1 (top level publish object)
# --> column-1
# ---> group-1
# ----> child-object-1

SilverStripe\Versioned\Tests\RecursiveStagesServiceTest\PrimaryObject:
primary-object-1:
Title: PrimaryObject1

SilverStripe\Versioned\Tests\RecursiveStagesServiceTest\ColumnObject:
column-1:
Title: Column1
PrimaryObject: =>SilverStripe\Versioned\Tests\RecursiveStagesServiceTest\PrimaryObject.primary-object-1

SilverStripe\Versioned\Tests\RecursiveStagesServiceTest\GroupObject:
group-1:
Title: Group1
Column: =>SilverStripe\Versioned\Tests\RecursiveStagesServiceTest\ColumnObject.column-1

SilverStripe\Versioned\Tests\RecursiveStagesServiceTest\ChildObject:
child-object-1:
Title: Item1
Group: =>SilverStripe\Versioned\Tests\RecursiveStagesServiceTest\GroupObject.group-1
36 changes: 36 additions & 0 deletions tests/php/RecursiveStagesServiceTest/ChildObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace SilverStripe\Versioned\Tests\RecursiveStagesServiceTest;

use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;

/**
* Class ChildObject
*
* @property string $Title
* @property int $GroupID
* @method GroupObject Group()
* @package SilverStripe\Versioned\Tests\RecursiveStagesServiceTest
*/
class ChildObject extends DataObject implements TestOnly
{
/**
* @var string
*/
private static $table_name = 'RecursiveStagesServiceTest_ChildObject';

/**
* @var array
*/
private static $db = [
'Title' => 'Varchar(255)',
];

/**
* @var array
*/
private static $has_one = [
'Group' => GroupObject::class,
];
}
Loading

0 comments on commit d314357

Please sign in to comment.