Skip to content

Commit

Permalink
Top page feature
Browse files Browse the repository at this point in the history
  • Loading branch information
mfendeksilverstripe committed Feb 24, 2020
1 parent 7acea6c commit e87728d
Show file tree
Hide file tree
Showing 16 changed files with 1,154 additions and 6 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ env:

matrix:
include:
- php: 5.6
- php: 7.1
env: DB=MYSQL RECIPE_VERSION=4.4.x-dev PHPCS_TEST=1 PHPUNIT_TEST=1
- php: 7.0
- php: 7.1
env: DB=PGSQL RECIPE_VERSION=4.4.x-dev PHPUNIT_TEST=1
- php: 7.1
env: DB=MYSQL RECIPE_VERSION=4.4.x-dev PHPUNIT_COVERAGE_TEST=1
Expand Down
16 changes: 16 additions & 0 deletions _config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,19 @@ Symbiote\GridFieldExtensions\GridFieldAddNewMultiClassHandler:
SilverStripe\Core\Injector\Injector:
SilverStripe\CMS\Controllers\CMSSiteTreeFilter_Search:
class: DNADesign\Elemental\Controllers\ElementSiteTreeFilterSearch
SilverStripe\Dev\State\SapphireTestState:
properties:
States:
topPageTestState: '%$DNADesign\Elemental\TopPage\TestState'

DNADesign\Elemental\Models\BaseElement:
extensions:
topPageDataExtension: DNADesign\Elemental\TopPage\DataExtension

DNADesign\Elemental\Models\ElementalArea:
extensions:
topPageDataExtension: DNADesign\Elemental\TopPage\DataExtension

Page:
extensions:
topPageSiteTreeExtension: DNADesign\Elemental\TopPage\SiteTreeExtension
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": "^7.1",
"silverstripe/cms": "^4.4@dev",
"silverstripe/admin": "^1.4@dev",
"silverstripe/versioned-admin": "^1.2@dev",
Expand Down
42 changes: 42 additions & 0 deletions docs/en/advanced_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,45 @@ FluentState::singleton()->withState(function (FluentState $state) {

This is very important as global state is reverted back after the callback is executed so it's safe to be used.
Unit tests benefit mostly from this as this makes sure that there are no dependencies between unit tests as the global state is always changed only locally in one test.

## Top page reference feature

In some cases your project setup may have deeply nested blocks, for example:

```
Page
ElementalArea
RowBlock (represents grid row on frontend)
ElementalArea
AccordionBlock (block which can contain other content blocks)
ElementalArea
ContentBlock
```

It's quite common to use top page lookups from block context, i.e. a block is querying data from the page that the block belongs to.

Most common cases are:

* `CMS fields` - block level conditional logic depends on page data
* `templates` - block level render logic depends on page data

This module uses some in-memory caching but this isn't good enough for such deeply nested data structures by default.

In such cases it is recommended to use this feature which stores the top page reference on individual blocks and elemental areas.
This speeds up data lookup significantly.

If your project makes use of the Fluent module, it is recommended to use the following extensions to replace the ones used by default:

```
DNADesign\Elemental\Models\BaseElement:
extensions:
topPageDataExtension: DNADesign\Elemental\TopPage\FluentExtension
DNADesign\Elemental\Models\ElementalArea:
extensions:
topPageDataExtension: DNADesign\Elemental\TopPage\FluentExtension
```

This will store the locale of the top page on blocks which simplifies top page lookup in case the locale is unknown at the time of page lookup from block context.

The page reference data on the blocks can also be used for maintenance dev tasks as it's easy to identify which blocks belong to which pages in which locale.
6 changes: 2 additions & 4 deletions src/Extensions/ElementalPageExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@

namespace DNADesign\Elemental\Extensions;

use Exception;
use DNADesign\Elemental\Models\ElementalArea;
use SilverStripe\Control\Controller;
use SilverStripe\View\Parsers\HTML4Value;
use SilverStripe\Core\Config\Config;
use SilverStripe\View\SSViewer;

/**
* @method ElementalArea ElementalArea
* @property ElementalArea ElementalArea
* @method ElementalArea ElementalArea()
* @property int ElementalAreaID
*/
class ElementalPageExtension extends ElementalAreasExtension
{
Expand Down
1 change: 1 addition & 0 deletions src/Models/BaseElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
* @property int $Sort
* @property string $ExtraClass
* @property string $Style
* @property int $ParentID
*
* @method ElementalArea Parent()
*/
Expand Down
241 changes: 241 additions & 0 deletions src/TopPage/DataExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
<?php

namespace DNADesign\Elemental\TopPage;

use DNADesign\Elemental\Models\BaseElement;
use DNADesign\Elemental\Models\ElementalArea;
use Page;
use SilverStripe\ORM\DataExtension as BaseDataExtension;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ValidationException;
use SilverStripe\Versioned\Versioned;

/**
* Class DataExtension
*
* Provides a db-cached reference to the top-level page for improved read performance on projects
* with deeply nested block structures. Apply to @see BaseElement and @see ElementalArea.
*
* @property int $TopPageID
* @method Page TopPage()
* @property BaseElement|ElementalArea|$this $owner
* @package DNADesign\Elemental\TopPage
*/
class DataExtension extends BaseDataExtension
{
/**
* @config
* @var array
*/
private static $has_one = [
'TopPage' => Page::class,
];

/**
* @config
* @var array
*/
private static $indexes = [
'TopPageID' => true,
];

/**
* @var bool
*/
private $topPageUpdate = true;

/**
* Extension point in @see DataObject::onAfterWrite()
*/
public function onAfterWrite(): void
{
$this->setTopPage();
}

/**
* Extension point in @see DataObject::duplicate()
*/
public function onBeforeDuplicate(): void
{
$this->clearTopPage();
}

/**
* Extension point in @see DataObject::duplicate()
*/
public function onAfterDuplicate(): void
{
$this->updateTopPage();
}

/**
* Finds the top-level Page object for a Block / ElementalArea, using the cached TopPageID
* reference when possible.
*
* @return Page|null
* @throws ValidationException
*/
public function getTopPage(): ?Page
{
$list = [$this->owner];

while (count($list) > 0) {
/** @var DataObject|DataExtension $item */
$item = array_shift($list);

if ($item instanceof Page) {
// trivial case
return $item;
}

if ($item->hasExtension(DataExtension::class) && $item->TopPageID > 0) {
// top page is stored inside data object - just fetch it via cached call
$page = Page::get_by_id($item->TopPageID);

if ($page !== null && $page->exists()) {
return $page;
}
}

if ($item instanceof BaseElement) {
// parent lookup via block
$parent = $item->Parent();

if ($parent !== null && $parent->exists()) {
array_push($list, $parent);
}

continue;
}

if ($item instanceof ElementalArea) {
// parent lookup via elemental area
$parent = $item->getOwnerPage();

if ($parent !== null && $parent->exists()) {
array_push($list, $parent);
}

continue;
}
}

return null;
}

/**
* @param Page|null $page
* @throws ValidationException
*/
public function setTopPage(?Page $page = null): void
{
if (!$this->topPageUpdate) {
return;
}

/** @var BaseElement|ElementalArea|Versioned|DataExtension $owner */
$owner = $this->owner;

if (!$owner->hasExtension(DataExtension::class)) {
return;
}

if ($owner->TopPageID > 0) {
return;
}

$page = $page ?? $owner->getTopPage();

if ($page === null) {
return;
}

// set the page to properties in case this object is re-used later
$this->assignTopPage($page);

if ($owner->hasExtension(Versioned::class)) {
$owner->writeWithoutVersion();

return;
}

$owner->write();
}

public function getTopPageUpdate(): bool
{
return $this->topPageUpdate;
}

/**
* Enable top page update
* useful for unit tests
*/
public function enableTopPageUpdate(): void
{
$this->topPageUpdate = true;
}

/**
* Disable top page update
* useful for unit tests
*/
public function disableTopPageUpdate(): void
{
$this->topPageUpdate = false;
}

/**
* Use this to wrap any code which is supposed to run with desired top page update setting
* useful for unit tests
*
* @param bool $update
* @param callable $callback
* @return mixed
*/
public function withTopPageUpdate(bool $update, callable $callback)
{
$original = $this->topPageUpdate;
$this->topPageUpdate = $update;

try {
return $callback();
} finally {
$this->topPageUpdate = $original;
}
}

/**
* Registers the object for a TopPage update. Ensures that this operation is deferred to a point
* when all required relations have been written.
*/
protected function updateTopPage(): void
{
if (!$this->topPageUpdate) {
return;
}

/** @var SiteTreeExtension $extension */
$extension = singleton(SiteTreeExtension::class);
$extension->addDuplicatedObject($this->owner);
}

/**
* Assigns top page relation
*
* @param Page $page
*/
protected function assignTopPage(Page $page): void
{
$this->owner->TopPageID = (int) $page->ID;
}

/**
* Clears top page relation, this is useful when duplicating object as the new object doesn't necessarily
* belong to the original page
*/
protected function clearTopPage(): void
{
$this->owner->TopPageID = 0;
}
}
Loading

0 comments on commit e87728d

Please sign in to comment.