Page MenuHomePhabricator

D13391.diff
No OneTemporary

D13391.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -120,6 +120,7 @@
'PhutilChannelTestCase' => 'channel/__tests__/PhutilChannelTestCase.php',
'PhutilChunkedIterator' => 'utils/PhutilChunkedIterator.php',
'PhutilChunkedIteratorTestCase' => 'utils/__tests__/PhutilChunkedIteratorTestCase.php',
+ 'PhutilClassMapQuery' => 'symbols/PhutilClassMapQuery.php',
'PhutilCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCodeSnippetContextFreeGrammar.php',
'PhutilCommandString' => 'xsprintf/PhutilCommandString.php',
'PhutilConsole' => 'console/PhutilConsole.php',
@@ -621,6 +622,7 @@
'Iterator',
),
'PhutilChunkedIteratorTestCase' => 'PhutilTestCase',
+ 'PhutilClassMapQuery' => 'Phobject',
'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhutilCommandString' => 'Phobject',
'PhutilConsole' => 'Phobject',
diff --git a/src/symbols/PhutilClassMapQuery.php b/src/symbols/PhutilClassMapQuery.php
new file mode 100644
--- /dev/null
+++ b/src/symbols/PhutilClassMapQuery.php
@@ -0,0 +1,275 @@
+<?php
+
+/**
+ * Load a map of concrete subclasses of some abstract parent class.
+ *
+ * libphutil is extensively modular through runtime introspection of class
+ * maps. This method makes querying class maps easier.
+ *
+ * There are several common patterns used with modular class maps:
+ *
+ * - A `getUniqueKey()` method which returns a unique scalar key identifying
+ * the class.
+ * - An `expandVariants()` method which potentially returns multiple
+ * instances of the class with different configurations.
+ * - A `getSortName()` method which sorts results.
+ * - Caching of the map.
+ *
+ * This class provides support for these mechanisms.
+ *
+ * Using the unique key mechanism with @{method:setUniqueMethod} allows you
+ * to use a more human-readable, storage-friendly key to identify objects,
+ * allows classes to be freely renamed, and enables variant expansion.
+ *
+ * Using the expansion mechanism with @{method:setExpandMethod} allows you to
+ * have multiple similar modular instances, or configuration-driven instances.
+ *
+ * Even if they have no immediate need for either mechanism, class maps should
+ * generally provide unique keys in their initial design so they are more
+ * flexible later on. Adding unique keys later can require database migrations,
+ * whle adding an expansion mechanim is trivial if a class map already has
+ * unique keys.
+ *
+ * This class automatically caches class maps and does not need to be wrapped
+ * in caching logic.
+ *
+ * @task config Configuring the Query
+ * @task exec Executing the Query
+ * @task cache Managing the Map Cache
+ */
+final class PhutilClassMapQuery extends Phobject {
+
+ private $ancestorClass;
+ private $expandMethod;
+ private $uniqueMethod;
+ private $sortMethod;
+
+ // NOTE: If you add more configurable properties here, make sure that
+ // cache key construction in getCacheKey() is updated properly.
+
+ private static $cache = array();
+
+
+/* -( Configuring the Query )---------------------------------------------- */
+
+
+ /**
+ * Set the ancestor class name to load the concrete descendants of.
+ *
+ * @param string Ancestor class name.
+ * @return this
+ * @task config
+ */
+ public function setAncestorClass($class) {
+ $this->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.
+ * @return this
+ * @task config
+ */
+ public function setUniqueMethod($unique_method) {
+ $this->uniqueMethod = $unique_method;
+ 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<string, object> 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<string, object> 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 (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);
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Mon, Dec 23, 5:58 PM (18 h, 15 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6921739
Default Alt Text
D13391.diff (9 KB)

Event Timeline