diff --git a/docs/en/changelogs/3.0.0.md b/docs/en/changelogs/3.0.0.md index c93040700..85b602c6c 100644 --- a/docs/en/changelogs/3.0.0.md +++ b/docs/en/changelogs/3.0.0.md @@ -193,6 +193,17 @@ The abstract `RelationList` class and its implementations `ManyManyList` and `Ha are replacing the `ComponentSet` API, which is only relevant if you have instanciated these manually. Relations are retrieved through the same way (e.g. `$myMember->Groups()`). +### Aggregate changes for partial caching in templates ### + +`DataObject::Aggregate()` and `DataObject::RelationshipAggregate()` are now deprecated. To replace your deprecated aggregate calls +in PHP code, you should query with something like `Member::get()->max('LastEdited')`, that is, calling the aggregate on the `DataList` directly. +The same concept applies for replacing `RelationshipAggregate()`, just call the aggregate method on the relationship instead, +so something like `Member::get()->Groups()->max('LastEdited')`. + +For partial caching in templates, the syntax `<% cached Aggregate(Page).Max(LastEdited) %>` has been deprecated. The new syntax is similar, +except you use `List()` instead of `Aggregate()`, and the aggregate call `Max()` is now lowercase, as in `max()`. +An example of the new syntax is `<% cached List(Page).max(LastEdited) %>`. Check `DataList` class for more aggregate methods to use. + ### `SQLQuery` changes ### `SQLQuery` has been changed so direct access to internal properties `$from`, `$select`, `$orderby` is diff --git a/docs/en/reference/partial-caching.md b/docs/en/reference/partial-caching.md index 7f0d16dd4..8a7245eb1 100644 --- a/docs/en/reference/partial-caching.md +++ b/docs/en/reference/partial-caching.md @@ -61,21 +61,21 @@ For example, if we have a menu, we want that menu to update whenever _any_ page otherwise. By using aggregates, that's easy :::ss - <% cached 'navigation', Aggregate(Page).Max(LastEdited) %> + <% cached 'navigation', List(Page).max(LastEdited) %> If we have a block that shows a list of categories, we can make sure the cache updates every time a category is added or edited :::ss - <% cached 'categorylist', Aggregate(Category).Max(LastEdited) %> + <% cached 'categorylist', List(Category).max(LastEdited) %> We can also calculate aggregates on relationships. A block that shows the current member's favourites needs to update whenever the relationship Member::$has_many = array('Favourites' => Favourite') changes. :::ss - <% cached 'favourites', CurrentMember.ID, CurrentMember.RelationshipAggregate(Favourites).Max(LastEdited) %> + <% cached 'favourites', CurrentMember.ID, CurrentMember.Favourites.max(LastEdited) %> ## Cache key calculated in controller @@ -89,7 +89,7 @@ logic into the controller return implode('_', array( 'favourites', $member->ID, - $member->RelationshipAggregate('Favourites')->Max('LastEdited') + $member->Favourites()->max('LastEdited') )); } @@ -230,10 +230,10 @@ Can be re-written as: :::ss <% cached LastEdited %> - <% cached RelationshipAggregate(Children).Max(LastEdited) %> + <% cached Children.max(LastEdited) %> <% control Children %> $Name <% end_control %> <% end_cached %> - <% end_cached %> \ No newline at end of file + <% end_cached %> diff --git a/model/DataObject.php b/model/DataObject.php index d234540fa..d0939a485 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -2659,16 +2659,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @deprecated 3.0 Use DataList::create and DataList to do your querying */ public function Aggregate($class = null) { - Deprecation::notice('3.0', 'Use DataList::create and DataList to do your querying instead.'); + Deprecation::notice('3.0', 'Call aggregate methods on a DataList directly instead. In templates ' . + 'an example of the new syntax is <% cached List(Member).max(LastEdited) %> instead (check partial-caching.md documentation ' . + 'for more details.)'); - if($class) { + if($class) { $list = new DataList($class); $list->setDataModel(DataModel::inst()); } else if(isset($this)) { $list = new DataList(get_class($this)); $list->setDataModel($this->model); } - else throw new InvalidArgumentException("DataObject::aggregate() must be called as an instance method or passed a classname"); + else throw new InvalidArgumentException("DataObject::aggregate() must be called as an instance method or passed a classname"); return $list; } @@ -2676,9 +2678,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @deprecated 3.0 Use DataList::create and DataList to do your querying */ public function RelationshipAggregate($relationship) { - Deprecation::notice('3.0', 'Use DataList::create and DataList to do your querying instead.'); + Deprecation::notice('3.0', 'Call aggregate methods on a relationship directly instead.'); - return $this->$relationship(); + return $this->$relationship(); } /** diff --git a/model/DataQuery.php b/model/DataQuery.php index 986bd60cb..58974e758 100644 --- a/model/DataQuery.php +++ b/model/DataQuery.php @@ -315,7 +315,7 @@ class DataQuery { * Note that this will issue a separate SELECT COUNT() query. */ function count() { - $baseClass = ClassInfo::baseDataClass($this->dataClass); + $baseClass = ClassInfo::baseDataClass($this->dataClass); return $this->getFinalisedQuery()->count("DISTINCT \"$baseClass\".\"ID\""); } @@ -324,8 +324,8 @@ class DataQuery { * * @param String $field Unquoted database column name (will be escaped automatically) */ - function Max($field) { - return $this->getFinalisedQuery()->aggregate(sprintf('MAX("%s")', Convert::raw2sql($field)))->execute()->value(); + function max($field) { + return $this->getFinalisedQuery()->aggregate(sprintf('MAX("%s")', Convert::raw2sql($field)))->execute()->value(); } /** @@ -333,8 +333,8 @@ class DataQuery { * * @param String $field Unquoted database column name (will be escaped automatically) */ - function Min($field) { - return $this->getFinalisedQuery()->aggregate(sprintf('MIN("%s")', Convert::raw2sql($field)))->execute()->value(); + function min($field) { + return $this->getFinalisedQuery()->aggregate(sprintf('MIN("%s")', Convert::raw2sql($field)))->execute()->value(); } /** @@ -342,8 +342,8 @@ class DataQuery { * * @param String $field Unquoted database column name (will be escaped automatically) */ - function Avg($field) { - return $this->getFinalisedQuery()->aggregate(sprintf('AVG("%s")', Convert::raw2sql($field)))->execute()->value(); + function avg($field) { + return $this->getFinalisedQuery()->aggregate(sprintf('AVG("%s")', Convert::raw2sql($field)))->execute()->value(); } /** @@ -351,8 +351,8 @@ class DataQuery { * * @param String $field Unquoted database column name (will be escaped automatically) */ - function Sum($field) { - return $this->getFinalisedQuery()->aggregate(sprintf('SUM("%s")', Convert::raw2sql($field)))->execute()->value(); + function sum($field) { + return $this->getFinalisedQuery()->aggregate(sprintf('SUM("%s")', Convert::raw2sql($field)))->execute()->value(); } /** diff --git a/tests/model/AggregateTest.php b/tests/model/AggregateTest.php index 2c1dc15bc..ba477501a 100644 --- a/tests/model/AggregateTest.php +++ b/tests/model/AggregateTest.php @@ -1,7 +1,13 @@ assertFalse((bool)trim($template), "Should be no content in this return."); } - + function testComments() { $output = $this->render(<<This is some content<%-- this is another comment --%>This is the final content diff --git a/view/GenericTemplateGlobalProvider.php b/view/GenericTemplateGlobalProvider.php index bc4c1281f..47fed7e55 100644 --- a/view/GenericTemplateGlobalProvider.php +++ b/view/GenericTemplateGlobalProvider.php @@ -3,7 +3,8 @@ class GenericTemplateGlobalProvider implements TemplateGlobalProvider { public static function get_template_global_variables() { return array( - 'ModulePath' + 'ModulePath', + 'List' => 'getDataList' ); } @@ -30,5 +31,24 @@ class GenericTemplateGlobalProvider implements TemplateGlobalProvider { } } + /** + * This allows templates to create a new `DataList` from a known + * DataObject class name, and call methods such as aggregates. + * + * The common use case is for partial caching: + * + * <% cached List(Member).max(LastEdited) %> + * loop members here + * <% end_cached %> + * + * + * @return DataList + */ + public static function getDataList($className) { + $list = new DataList($className); + $list->setDataModel(DataModel::inst()); + return $list; + } + } diff --git a/view/SSViewer.php b/view/SSViewer.php index d03dd9336..23b1f4eaf 100644 --- a/view/SSViewer.php +++ b/view/SSViewer.php @@ -51,14 +51,13 @@ class SSViewer_Scope { list($this->item, $this->itemIterator, $this->itemIteratorTotal, $this->popIndex, $this->upIndex, $this->currentIndex) = $this->itemStack[$this->localIndex]; array_splice($this->itemStack, $this->localIndex+1); } - + function getObj($name, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) { $on = $this->itemIterator ? $this->itemIterator->current() : $this->item; return $on->obj($name, $arguments, $forceReturnedObject, $cache, $cacheName); } - - function obj($name, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null){ - + + function obj($name, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) { switch ($name) { case 'Up': if ($this->upIndex === null) user_error('Up called when we\'re already at the top of the scope', E_USER_ERROR); @@ -72,13 +71,12 @@ class SSViewer_Scope { default: $this->item = $this->getObj($name, $arguments, $forceReturnedObject, $cache, $cacheName); - $this->itemIterator = null; $this->upIndex = $this->currentIndex ? $this->currentIndex : count($this->itemStack)-1; $this->currentIndex = count($this->itemStack); break; } - + $this->itemStack[] = array($this->item, $this->itemIterator, $this->itemIteratorTotal, null, $this->upIndex, $this->currentIndex); return $this; } @@ -380,7 +378,6 @@ class SSViewer_DataPresenter extends SSViewer_Scope { // Then for iterator-specific overrides else if (array_key_exists($property, self::$iteratorProperties)) { $source = self::$iteratorProperties[$property]; - if ($this->itemIterator) { // Set the current iterator position and total (the object instance is the first item in the callable array) $source['implementer']->iteratorProperties($this->itemIterator->key(), $this->itemIteratorTotal); @@ -423,6 +420,7 @@ class SSViewer_DataPresenter extends SSViewer_Scope { return $res; } + } function getObj($name, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {