Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEW: Stages differ recursive #328

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions _config/versionedownership.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ OnlyIf:
SilverStripe\Admin\LeftAndMain:
extensions:
RecursivePublishableHandler: SilverStripe\Versioned\RecursivePublishableHandler
---
Name: versionedrecursivestages
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Versioned\RecursiveStagesInterface:
class: SilverStripe\Versioned\RecursiveStagesService
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
24 changes: 24 additions & 0 deletions src/RecursiveStagesInterface.php
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace SilverStripe\Versioned;

use SilverStripe\ORM\DataObject;

/**
* Interface RecursiveStagesInterface
*
* Interface for @see RecursiveStagesService to provide the capability to for "smart" durty model state
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
* which can cover nested models
*/
interface RecursiveStagesInterface
{

/**
* Determine if content differs on stages including nested objects
*
* @param DataObject $object
* @param string $mode
* @return bool
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
*/
public function stagesDifferRecursive(DataObject $object, string $mode): bool;
}
166 changes: 166 additions & 0 deletions src/RecursiveStagesService.php
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

namespace SilverStripe\Versioned;

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

/**
* Functionality for detecting the need of publishing nested objects owned by common parent / ancestor object
*/
class RecursiveStagesService implements RecursiveStagesInterface
{
use Injectable;

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

/**
* Weak ownership uses 'cascade_duplicates' configuration to determine relationships
*/
public const OWNERSHIP_WEAK = 'weak';
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved

/**
* Determine if content differs on stages including nested objects
*
* @param DataObject $object
* @param string $mode
* @return bool
*/
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
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
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
*
* @param DataObject $object
* @param string $mode
* @param string $stage
* @return array
*/
protected function findOwnedIdentifiers(DataObject $object, string $mode, string $stage): array
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
{
$ids = Versioned::withVersionedMode(function () use ($object, $mode, $stage): array {
Versioned::set_stage($stage);

$object = DataObject::get_by_id($object->ClassName, $object->ID);
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved

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

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

while ($object = array_shift($items)) {
$ids[] = implode('_', [$object->baseClass(), $object->ID]);
$relatedObjects = $this->findOwnedObjects($object, $mode);
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
$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
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
{
$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;
}
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
}
18 changes: 18 additions & 0 deletions src/Versioned.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Extension;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Resettable;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\ArrayList;
Expand Down Expand Up @@ -1997,6 +1998,23 @@ 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
{
/** @var RecursiveStagesInterface $service */
$service = Injector::inst()->get(RecursiveStagesInterface::class);

return $service->stagesDifferRecursive($this->owner, $mode);
}
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved

/**
* @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
Loading