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

ENHANCEMENT: Recursive stage check #327

Open
wants to merge 1 commit into
base: 1.7
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
84 changes: 84 additions & 0 deletions src/PublishStateHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace SilverStripe\Versioned;

use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\SS_List;
use SilverStripe\Versioned\Versioned;

/**
* Class PublishStateHelper
*
* functionality which is related to detecting the need of publishing nested objects within a block page
*
* @package App\Helpers
*/
class PublishStateHelper
{
/**
* @param DataObject|Versioned|null $item
* @return bool
*/
public static function checkNeedPublishingItem(?DataObject $item): bool
{
if ($item === null || !$item->exists()) {
return false;
}

if ($item->hasExtension(Versioned::class)) {
/** @var $item Versioned */
return !$item->isPublished() || $item->stagesDiffer();
}

return false;
}

/**
* @param SS_List $list
* @return bool
*/
public static function checkNeedPublishingList(SS_List $list): bool
{
/** @var $item Versioned */
foreach ($list as $item) {
if (static::checkNeedPublishingItem($item)) {
return true;
}
}

return false;
}

/**
* @param DataList $items
* @param int $parentId
* @return bool
*/
public static function checkNeedPublishVersionedItems(DataList $items, int $parentId): bool
{
// check for differences in models
foreach ($items as $item) {
if (PublishStateHelper::checkNeedPublishingItem($item)) {
return true;
}
}

// check for deletion of a model
$draftCount = $items->count();

// we need to fetch live records and compare amount because if a record was deleted from stage
// the above draft items loop will not cover the missing item
$liveCount = Versioned::get_by_stage(
$items->dataClass(),
Versioned::LIVE,
['ParentID' => $parentId]
)->count();

if ($draftCount != $liveCount) {
return true;
}

return false;
}
}
146 changes: 146 additions & 0 deletions src/Versioned.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataQuery;
use SilverStripe\ORM\DB;
Expand Down Expand Up @@ -75,6 +76,16 @@ class Versioned extends DataExtension implements TemplateGlobalProvider, Resetta
*/
const DRAFT = 'Stage';

/**
* 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';

/**
* A cache used by get_versionnumber_by_stage().
* Clear through {@link flushCache()}.
Expand Down Expand Up @@ -2060,6 +2071,141 @@ public function stagesDiffer()
return (bool) $stagesDiffer;
}

/**
* Determine if content differs on stages including nested objects
*
* @param string $mode
* @return bool
*/
public function stagesDifferRecursive(string $mode = self::OWNERSHIP_STRONG): bool
{
$owner = $this->owner;

if ($owner === null || !$owner->exists()) {
return false;
}

$records = [$owner];

// compare existing content
while ($record = array_shift($records)) {
if (PublishStateHelper::checkNeedPublishingItem($record)) {
return true;
}

$relatedRecords = $this->findOwnedObjects($record, $mode);

foreach ($relatedRecords as $relatedRecord) {
$records[] = $relatedRecord;
}
}

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

return $draftIdentifiers !== $liveIdentifiers;
}


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

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

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

$records = [$record];
$ids = [];

while ($record = array_shift($records)) {
$ids[] = implode('_', [$record->baseClass(), $record->ID]);
$relatedRecords = $this->findOwnedObjects($record, $mode);

foreach ($relatedRecords as $relatedRecord) {
$records[] = $relatedRecord;
}
}

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 $record
* @param string $mode
* @return array
*/
protected function findOwnedObjects(DataObject $record, string $mode): array
{
$ownershipType = $mode === self::OWNERSHIP_WEAK
? 'owns'
: 'cascade_duplicates';

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

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

if (!$relation) {
continue;
}

$relationData = $record->$relation();

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

$result[] = $relationData;

continue;
}

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

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

$result[] = $relatedRecord;
}
}

return $result;
}

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

namespace SilverStripe\Versioned\Tests;

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

class VersionedNestedTest extends SapphireTest
{
/**
* @var string
*/
protected static $fixture_file = 'VersionedNestedTest.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 $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 objectsProvider(): array
{
return [
[PrimaryObject::class, 'primary-object-1', false],
[ColumnObject::class, 'column-1', false],
[GroupObject::class, 'group-1', false],
[ChildObject::class, 'child-object-1', false],
[ColumnObject::class, 'column-1', true],
[GroupObject::class, 'group-1', true],
[ChildObject::class, 'child-object-1', true],
];
}
}
24 changes: 24 additions & 0 deletions tests/php/VersionedNestedTest.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\VersionedNestedTest\PrimaryObject:
primary-object-1:
Title: PrimaryObject1

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

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

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

namespace SilverStripe\Versioned\Tests\VersionedNestedTest;

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

class ChildObject extends DataObject implements TestOnly
{

/**
* @var string
*/
private static $table_name = 'VersionedNestedTest_ChildObject';

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

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