Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14399848
D13391.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
9 KB
Referenced Files
None
Subscribers
None
D13391.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D13391: Lift class map construction into a new class map query
Attached
Detach File
Event Timeline
Log In to Comment