ancestorClass = $class; return $this; } /** * Provide a method to select a unique key for each instance. * * If you provide a method here, the map will be keyed with these values, * instead of with class names. Exceptions will be raised if entries are * not unique. * * You must provide a method here to use @{method:setExpandMethod}. * * @param string Name of the unique key method. * @param bool If true, then classes which return `null` will be filtered * from the results. * @return this * @task config */ public function setUniqueMethod($unique_method, $filter_null = false) { $this->uniqueMethod = $unique_method; $this->filterNull = $filter_null; return $this; } /** * Provide a method to expand each concrete subclass into available instances. * * With some class maps, each class is allowed to provide multiple entries * in the map by returning alternatives from some method with a default * implementation like this: * * public function generateVariants() { * return array($this); * } * * For example, a "color" class may really generate and configure several * instances in the final class map: * * public function generateVariants() { * return array( * self::newColor('red'), * self::newColor('green'), * self::newColor('blue'), * ); * } * * This allows multiple entires in the final map to share an entire * implementation, rather than requiring that they each have their own unique * subclass. * * This pattern is most useful if several variants are nearly identical (so * the stub subclasses would be essentially empty) or the available variants * are driven by configuration. * * If a class map uses this pattern, it must also provide a unique key for * each instance with @{method:setUniqueMethod}. * * @param string Name of the expansion method. * @return this * @task config */ public function setExpandMethod($expand_method) { $this->expandMethod = $expand_method; return $this; } /** * Provide a method to sort the final map. * * The map will be sorted using @{function:msort} and passing this method * name. * * @param string Name of the sorting method. * @return this * @task config */ public function setSortMethod($sort_method) { $this->sortMethod = $sort_method; return $this; } /** * Provide a method to filter the map. * * @param string Name of the filtering method. * @return this * @task config */ public function setFilterMethod($filter_method) { $this->filterMethod = $filter_method; return $this; } public function setContinueOnFailure($continue) { $this->continueOnFailure = $continue; return $this; } /* -( Executing the Query )------------------------------------------------ */ /** * Execute the query as configured. * * @return map Realized class map. * @task exec */ public function execute() { $cache_key = $this->getCacheKey(); if (!isset(self::$cache[$cache_key])) { self::$cache[$cache_key] = $this->loadMap(); } return self::$cache[$cache_key]; } /** * Delete all class map caches. * * @return void * @task exec */ public static function deleteCaches() { self::$cache = array(); } /** * Generate the core query results. * * This method is used to fill the cache. * * @return map Realized class map. * @task exec */ private function loadMap() { $ancestor = $this->ancestorClass; if (!strlen($ancestor)) { throw new PhutilInvalidStateException('setAncestorClass'); } if (!class_exists($ancestor) && !interface_exists($ancestor)) { throw new Exception( pht( 'Trying to execute a class map query for descendants of class '. '"%s", but no such class or interface exists.', $ancestor)); } $expand = $this->expandMethod; $filter = $this->filterMethod; $unique = $this->uniqueMethod; $sort = $this->sortMethod; if ($expand !== null) { if ($unique === null) { throw new Exception( pht( 'Trying to execute a class map query for descendants of class '. '"%s", but the query specifies an "expand method" ("%s") without '. 'specifying a "unique method". Class maps which support expansion '. 'must have unique keys.', $ancestor, $expand)); } } $objects = id(new PhutilSymbolLoader()) ->setAncestorClass($ancestor) ->setContinueOnFailure($this->continueOnFailure) ->loadObjects(); // Apply the "expand" mechanism, if it is configured. if ($expand !== null) { $list = array(); foreach ($objects as $object) { foreach (call_user_func(array($object, $expand)) as $instance) { $list[] = $instance; } } } else { $list = $objects; } // Apply the "unique" mechanism, if it is configured. if ($unique !== null) { $map = array(); foreach ($list as $object) { $key = call_user_func(array($object, $unique)); if ($key === null && $this->filterNull) { continue; } if (empty($map[$key])) { $map[$key] = $object; continue; } throw new Exception( pht( 'Two objects (of classes "%s" and "%s", descendants of ancestor '. 'class "%s") returned the same key from "%s" ("%s"), but each '. 'object in this class map must be identified by a unique key.', get_class($object), get_class($map[$key]), $ancestor, $unique.'()', $key)); } } else { $map = $list; } // Apply the "filter" mechanism, if it is configured. if ($filter !== null) { $map = mfilter($map, $filter); } // Apply the "sort" mechanism, if it is configured. if ($sort !== null) { if ($map) { // The "sort" method may return scalars (which we want to sort with // "msort()"), or may return PhutilSortVector objects (which we want // to sort with "msortv()"). $item = call_user_func(array(head($map), $sort)); // Since we may be early in the stack, use a string to avoid triggering // autoload in old versions of PHP. $vector_class = 'PhutilSortVector'; if ($item instanceof $vector_class) { $map = msortv($map, $sort); } else { $map = msort($map, $sort); } } } return $map; } /* -( Managing the Map Cache )--------------------------------------------- */ /** * Return a cache key for this query. * * @return string Cache key. * @task cache */ public function getCacheKey() { $parts = array( $this->ancestorClass, $this->uniqueMethod, $this->filterNull, $this->expandMethod, $this->filterMethod, $this->sortMethod, ); return implode(':', $parts); } }