diff --git a/src/symbols/PhutilClassMapQuery.php b/src/symbols/PhutilClassMapQuery.php index 78ab2d2..2973631 100644 --- a/src/symbols/PhutilClassMapQuery.php +++ b/src/symbols/PhutilClassMapQuery.php @@ -1,275 +1,284 @@ 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 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) { + 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; } /* -( 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]; } /** * 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)) { throw new Exception( pht( 'Trying to execute a class map query for descendants of class '. '"%s", but no such class exists.', $ancestor)); } $expand = $this->expandMethod; $unique = $this->uniqueMethod; $sort = $this->sortMethod; if (strlen($expand)) { if (!strlen($unique)) { 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) ->loadObjects(); // Apply the "expand" mechanism, if it is configured. if (strlen($expand)) { $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 (strlen($unique)) { $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 "sort" mechanism, if it is configured. if (strlen($sort)) { $map = msort($map, $sort); } return $map; } /* -( Managing the Map Cache )--------------------------------------------- */ /** * Return a cache key for this query. * * @return string Cache key. * @task cache */ private function getCacheKey() { $parts = array( $this->ancestorClass, $this->uniqueMethod, $this->expandMethod, $this->sortMethod, ); return implode(':', $parts); } }