XML_val(aggregate_function, array(field)) - For templates * $aggregate->aggregate_function(field) - For PHP * * Aggregate functions are uppercased by this class, but are otherwise assumed to be valid SQL functions. Some * examples: Min, Max, Avg * * Aggregates are often used as portions of a cacheblock key. They are therefore cached themselves, in the 'aggregate' * cache, although the invalidation logic prefers speed over keeping valid data. * The aggregate cache is cleared through {@link DataObject::flushCache()}, which in turn is called on * {@link DataObject->write()} and other write operations. * This means most write operations to the database will invalidate the cache correctly. * Use {@link Aggregate::flushCache()} to manually clear. * * NOTE: The cache logic uses tags, and so a backend that supports tags is required. Currently only the File * backend (and the two-level backend with the File backend as the slow store) meets this requirement * * @author hfried * @package framework * @subpackage core */ class Aggregate extends ViewableData { static $cache = null; /** Build & cache the cache object */ protected static function cache() { return self::$cache ? self::$cache : (self::$cache = SS_Cache::factory('aggregate')); } /** Clear the aggregate cache for a given type, or pass nothing to clear all aggregate caches */ public static function flushCache($class=null) { $cache = self::cache(); if (!$class || $class == 'DataObject') { $cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('aggregate')); } else { $tags = ClassInfo::ancestry($class); foreach($tags as &$tag) { $tag = preg_replace('/[^a-zA-Z0-9_]/', '_', $tag); } $cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, $tags); } } /** * Constructor * * @param string $type The DataObject type we are building an aggregate for * @param string $filter (optional) An SQL filter to apply to the selected rows before calculating the aggregate */ public function __construct($type, $filter = '') { $this->type = $type; $this->filter = $filter; parent::__construct(); } /** * Build the SQLQuery to calculate the aggregate * This is a seperate function so that subtypes of Aggregate can change just this bit * @param string $attr - the SQL field statement for selection (i.e. "MAX(LastUpdated)") * @return SQLQuery */ protected function query($attr) { $singleton = singleton($this->type); $query = $singleton->buildSQL($this->filter); $query->setSelect($attr); $query->setOrderBy(array()); $singleton->extend('augmentSQL', $query); return $query; } /** * Entry point for being called from a template. * * This gets the aggregate function * */ public function XML_val($name, $args = null, $cache = false) { $func = strtoupper( strpos($name, 'get') === 0 ? substr($name, 3) : $name ); $attribute = $args ? $args[0] : 'ID'; $table = null; foreach (ClassInfo::ancestry($this->type, true) as $class) { $fields = DataObject::database_fields($class); if (array_key_exists($attribute, $fields)) { $table = $class; break; } } if (!$table) user_error("Couldn't find table for field $attribute in type {$this->type}", E_USER_ERROR); $query = $this->query("$func(\"$table\".\"$attribute\")"); // Cache results of this specific SQL query until flushCache() is triggered. $cachekey = sha1($query->sql()); $cache = self::cache(); if (!($result = $cache->load($cachekey))) { $result = (string)$query->execute()->value(); if (!$result) $result = '0'; $cache->save($result, null, array('aggregate', preg_replace('/[^a-zA-Z0-9_]/', '_', $this->type))); } return $result; } /** * Entry point for being called from PHP. */ public function __call($method, $arguments) { return $this->XML_val($method, $arguments); } } /** * A subclass of Aggregate that calculates aggregates for the result of a has_many query. * * @author hfried * @package framework * @subpackage core */ class Aggregate_Relationship extends Aggregate { /** * Constructor * * @param DataObject $object The object that has_many somethings that we're calculating the aggregate for * @param string $relationship The name of the relationship * @param string $filter (optional) An SQL filter to apply to the relationship rows before calculating the * aggregate */ public function __construct($object, $relationship, $filter = '') { $this->object = $object; $this->relationship = $relationship; $this->has_many = $object->has_many($relationship); $this->many_many = $object->many_many($relationship); 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); } parent::__construct($this->has_many ? $this->has_many : $this->many_many[1], $filter); } protected function query($attr) { if ($this->has_many) { $query = $this->object->getComponentsQuery($this->relationship, $this->filter); } else { $query = $this->object->getManyManyComponentsQuery($this->relationship, $this->filter); } $query->setSelect($attr); $query->setGroupBy(array()); $singleton = singleton($this->type); $singleton->extend('augmentSQL', $query); return $query; } }