Skip to content

Commit

Permalink
Merge 768daa0 into 0bf069c
Browse files Browse the repository at this point in the history
  • Loading branch information
mfendeksilverstripe committed Feb 6, 2020
2 parents 0bf069c + 768daa0 commit c3f5a98
Show file tree
Hide file tree
Showing 12 changed files with 1,068 additions and 4 deletions.
72 changes: 72 additions & 0 deletions README.md
Expand Up @@ -323,6 +323,78 @@ globally in your command line.
**Note:** If adding or modifying colours, spacing, font sizes etc. please try
and use an appropriate variable from the silverstripe/admin module if available.

## Top page reference feature (optional performance improvement)

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
ContetnBlock
```

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 turn on this feature which stores the top page reference on individual blocks and elemental areas.
This speeds up data lookup significantly.

Please note that this feature only works with project setups which don't allow `block sharing`, i.e. `one block can only belong to a single page`.

To turn the feature on simply apply following extensions, like this:

```
DNADesign\Elemental\Models\BaseElement:
extensions:
topPage: DNADesign\Elemental\TopPage\DataExtension
DNADesign\Elemental\Models\ElementalArea:
extensions:
topPage: DNADesign\Elemental\TopPage\DataExtension
Page:
extensions:
topPage: DNADesign\Elemental\TopPage\SiteTreeExtension
```

If your project setup uses Fluent module it is recommended to use following configuration instead:

```
DNADesign\Elemental\Models\BaseElement:
extensions:
topPage: DNADesign\Elemental\TopPage\FluentExtension
DNADesign\Elemental\Models\ElementalArea:
extensions:
topPage: DNADesign\Elemental\TopPage\FluentExtension
Page:
extensions:
topPage: DNADesign\Elemental\TopPage\SiteTreeExtension
```

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.

### Top page reference data during object duplication

Duplicating data object will not duplicate the top page reference data.
Instead, the newly created data objects will have a new top page data assigned based on the context.

For example, duplicating a page with all of its blocks will create a new page and new blocks.
All the new blocks will have the new page stored as their top page reference. This works even if the deplication tree contains other pages as tree nodes.

## Integration with other modules

* [Multiple languages with tractorcow/silverstripe-fluent](docs/en/advanced_setup.md)
Expand Down
6 changes: 2 additions & 4 deletions src/Extensions/ElementalPageExtension.php
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
Expand Up @@ -39,6 +39,7 @@
* @property int $Sort
* @property string $ExtraClass
* @property string $Style
* @property int $ParentID
*
* @method ElementalArea Parent()
*/
Expand Down
216 changes: 216 additions & 0 deletions src/TopPage/DataExtension.php
@@ -0,0 +1,216 @@
<?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
*
* Top page data cache for improved performance
* intended owners of this extension are @see BaseElement and @see ElementalArea
* applying this extension to just one of these owners will not hinder top page functionality
* but the performance gain will be smaller
* it is recommended to apply this extension to BaseElement and for setups with deeper block nesting
* it is recommended to cover ElementalArea as well
*
* @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 $skipTopPageUpdate = false;

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

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

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

/**
* Find top level page of a block or elemental area
* this is very useful in case blocks are deeply nested
*
* for example:
* page -> elemental area -> block -> elemental area -> block
*
* this lookup is very performant as is safe to use in a template as well
*
* @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->skipTopPageUpdate) {
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();
}

/**
* Use this to wrap any code which is supposed to run without doing any top page updates
*
* @param callable $callback
* @return mixed
*/
public function withoutTopPageUpdate(callable $callback)
{
$this->skipTopPageUpdate = true;

try {
return $callback();
} finally {
$this->skipTopPageUpdate = false;
}
}

/**
* Register the object for top page update
* this is a little bit roundabout way to do it, but it's necessary because when cloned object is written
* the relations are not yet written so it's impossible to do a parent lookup at that time
*/
protected function updateTopPage(): void
{
/** @var SiteTreeExtension $extension */
$extension = singleton(SiteTreeExtension::class);
$extension->addDuplicatedObject($this->owner);
}

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;
}
}
43 changes: 43 additions & 0 deletions src/TopPage/FluentExtension.php
@@ -0,0 +1,43 @@
<?php

namespace DNADesign\Elemental\TopPage;

use DNADesign\Elemental\Models\BaseElement;
use DNADesign\Elemental\Models\ElementalArea;
use Page;
use TractorCow\Fluent\State\FluentState;

/**
* Class FluentExtension
*
* Use this extension in case you use the Fluent module (https://github.com/tractorcow-farm/silverstripe-fluent)
* for page localisation
* this will keep track of the locale the nested data object is stored in
*
* @property string $TopPageLocale
* @property BaseElement|ElementalArea|$this $owner
* @package DNADesign\Elemental\TopPage
*/
class FluentExtension extends DataExtension
{
/**
* @var array
*/
private static $db = [
'TopPageLocale' => 'Varchar',
];

protected function assignTopPage(Page $page): void
{
parent::assignTopPage($page);

$this->owner->TopPageLocale = FluentState::singleton()->getLocale();
}

protected function clearTopPage(): void
{
parent::clearTopPage();

$this->owner->TopPageLocale = null;
}
}

0 comments on commit c3f5a98

Please sign in to comment.