Skip to content

Commit

Permalink
Recursive stage check:
Browse files Browse the repository at this point in the history
- Move methods from extension into core
- Modify tests to remove App namespacing
- Add test objects
  • Loading branch information
danaenz committed Feb 26, 2021
1 parent 3c006fc commit 2d45baf
Show file tree
Hide file tree
Showing 8 changed files with 541 additions and 0 deletions.
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

0 comments on commit 2d45baf

Please sign in to comment.