Version: current

      1 <?php
      2
      3 /**
      4  * Calculate an Aggregate on a particular field of a particular DataObject type (possibly with
      5  * an additional filter before the aggregate)
      6  *
      7  * Implemented as a class to provide a semi-DSL method of calculating Aggregates. DataObject has a function
      8  * that will create & return an instance of this class with the DataObject type and filter set,
      9  * but at that point we don't yet know the aggregate function or field
     10  *
     11  * This class captures any XML_val or unknown call, and uses that to get the field & aggregate function &
     12  * then return the result
     13  *
     14  * Two ways of calling
     15  *
     16  * $aggregate->XML_val(aggregate_function, array(field))     - For templates
     17  * $aggregate->aggregate_function(field)                     - For PHP
     18  *
     19  * Aggregate functions are uppercased by this class, but are otherwise assumed to be valid SQL functions. Some
     20  * examples: Min, Max, Avg
     21  *
     22  * Aggregates are often used as portions of a cacheblock key. They are therefore cached themselves, in the 'aggregate'
     23  * cache, although the invalidation logic prefers speed over keeping valid data.
     24  *
     25  * NOTE: The cache logic uses tags, and so a backend that supports tags is required. Currently only the File
     26  * backend (and the two-level backend with the File backend as the slow store) meets this requirement
     27  *
     28  * @author hfried
     29  * @package sapphire
     30  * @subpackage core
     31  */
     32 class Aggregate extends ViewableData {
     33
     34     static $cache = null;
     35
     36     /** Build & cache the cache object */
     37     protected static function cache() {
     38         return self::$cache ? self::$cache : (self::$cache = SS_Cache::factory('aggregate'));
     39     }
     40
     41     /** Clear the aggregate cache for a given type, or pass nothing to clear all aggregate caches */
     42     public static function flushCache($class=null) {
     43         $cache = self::cache();
     44
     45         if (!$class || $class == 'DataObject') {
     46             $cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('aggregate'));
     47         }
     48         else {
     49             $cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, ClassInfo::ancestry($class));
     50         }
     51     }
     52
     53     /**
     54      * Constructor
     55      *
     56      * @param string $type The DataObject type we are building an aggregate for
     57      * @param string $filter (optional) An SQL filter to apply to the selected rows before calculating the aggregate
     58      */
     59     public function __construct($type, $filter = '') {
     60         $this->type = $type;
     61         $this->filter = $filter;
     62         parent::__construct();
     63     }
     64
     65     /**
     66      * Build the SQLQuery to calculate the aggregate
     67      * This is a seperate function so that subtypes of Aggregate can change just this bit
     68      * @param string $attr - the SQL field statement for selection (i.e. "MAX(LastUpdated)")
     69      * @return SQLQuery
     70      */
     71     protected function query($attr) {
     72         $singleton = singleton($this->type);
     73         $query = $singleton->buildSQL($this->filter);
     74         $query->select = array($attr);
     75         $query->orderby = null;
     76         $singleton->extend('augmentSQL', $query);
     77         return $query;
     78     }
     79
     80     /**
     81      * Entry point for being called from a template.
     82      *
     83      * This gets the aggregate function
     84      *
     85      */
     86     public function XML_val($name, $args) {
     87         $func = strtoupper( strpos($name, 'get') === 0 ? substr($name, 3) : $name );
     88         $attribute = $args ? $args[0] : 'ID';
     89
     90         $table = null;
     91
     92         foreach (ClassInfo::ancestry($this->type, true) as $class) {
     93             $fields = DataObject::database_fields($class);
     94             if (array_key_exists($attribute, $fields)) { $table = $class; break; }
     95         }
     96
     97         if (!$table) user_error("Couldn't find table for field $attribute in type {$this->type}", E_USER_ERROR);
     98
     99         $query = $this->query("$func(\"$table\".\"$attribute\")");
    100
    101         $cachekey = sha1($query->sql());
    102         $cache = self::cache();
    103
    104         if (!($result = $cache->load($cachekey))) {
    105             $result = (string)$query->execute()->value(); if (!$result) $result = '0';
    106             $cache->save($result, null, array('aggregate', $this->type));
    107         }
    108
    109         return $result;
    110     }
    111
    112     /**
    113      * Entry point for being called from PHP.
    114      */
    115     public function __call($method, $arguments) {
    116         return $this->XML_val($method, $arguments);
    117     }
    118 }
    119
    120 /**
    121  * A subclass of Aggregate that calculates aggregates for the result of a has_many query.
    122  *
    123  * @author hfried
    124  * @package sapphire
    125  * @subpackage core
    126  */
    127 class Aggregate_Relationship extends Aggregate {
    128
    129     /**
    130      * Constructor
    131      *
    132      * @param DataObject $object The object that has_many somethings that we're calculating the aggregate for
    133      * @param string $relationship The name of the relationship
    134      * @param string $filter (optional) An SQL filter to apply to the relationship rows before calculating the aggregate
    135      */
    136     public function __construct($object, $relationship, $filter = '') {
    137         $this->object = $object;
    138         $this->relationship = $relationship;
    139
    140         $this->has_many = $object->has_many($relationship);
    141         $this->many_many = $object->many_many($relationship);
    142
    143         if (!$this->has_many && !$this->many_many) user_error("Could not find relationship $relationship on object class {$object->class} in Aggregate Relationship", E_USER_ERROR);
    144
    145         parent::__construct($this->has_many ? $this->has_many : $this->many_many[1], $filter);
    146     }
    147
    148     protected function query($attr) {
    149         if ($this->has_many) {
    150             $query = $this->object->getComponentsQuery($this->relationship, $this->filter);
    151         }
    152         else {
    153             $query = $this->object->getManyManyComponentsQuery($this->relationship, $this->filter);
    154         }
    155
    156         $query->select = array($attr);
    157         $query->groupby = array();
    158
    159         $singleton = singleton($this->type);
    160         $singleton->extend('augmentSQL', $query);
    161
    162         return $query;
    163     }
    164 }
    165

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -