diff --git a/src/applications/drydock/logtype/DrydockLogType.php b/src/applications/drydock/logtype/DrydockLogType.php
index aa1f4fc4e0..7faab42dfe 100644
--- a/src/applications/drydock/logtype/DrydockLogType.php
+++ b/src/applications/drydock/logtype/DrydockLogType.php
@@ -1,69 +1,41 @@
 <?php
 
 abstract class DrydockLogType extends Phobject {
 
   private $viewer;
   private $log;
 
   abstract public function getLogTypeName();
   abstract public function getLogTypeIcon(array $data);
   abstract public function renderLog(array $data);
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->viewer;
   }
 
   final public function setLog(DrydockLog $log) {
     $this->log = $log;
     return $this;
   }
 
   final public function getLog() {
     return $this->log;
   }
 
   final public function getLogTypeConstant() {
-    $class = new ReflectionClass($this);
-
-    $const = $class->getConstant('LOGCONST');
-    if ($const === false) {
-      throw new Exception(
-        pht(
-          '"%s" class "%s" must define a "%s" property.',
-          __CLASS__,
-          get_class($this),
-          'LOGCONST'));
-    }
-
-    $limit = self::getLogTypeConstantByteLimit();
-    if (!is_string($const) || (strlen($const) > $limit)) {
-      throw new Exception(
-        pht(
-          '"%s" class "%s" has an invalid "%s" property. Field constants '.
-          'must be strings and no more than %s bytes in length.',
-          __CLASS__,
-          get_class($this),
-          'LOGCONST',
-          new PhutilNumber($limit)));
-    }
-
-    return $const;
-  }
-
-  final private static function getLogTypeConstantByteLimit() {
-    return 64;
+    return $this->getPhobjectClassConstant('LOGCONST', 64);
   }
 
   final public static function getAllLogTypes() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setUniqueMethod('getLogTypeConstant')
       ->execute();
   }
 
 }
diff --git a/src/applications/harbormaster/artifact/HarbormasterArtifact.php b/src/applications/harbormaster/artifact/HarbormasterArtifact.php
index a0d3a86101..8d3d8dd169 100644
--- a/src/applications/harbormaster/artifact/HarbormasterArtifact.php
+++ b/src/applications/harbormaster/artifact/HarbormasterArtifact.php
@@ -1,88 +1,60 @@
 <?php
 
 
 abstract class HarbormasterArtifact extends Phobject {
 
   private $buildArtifact;
 
   abstract public function getArtifactTypeName();
 
   public function getArtifactTypeSummary() {
     return $this->getArtifactTypeDescription();
   }
 
   abstract public function getArtifactTypeDescription();
   abstract public function getArtifactParameterSpecification();
   abstract public function getArtifactParameterDescriptions();
   abstract public function willCreateArtifact(PhabricatorUser $actor);
 
   public function validateArtifactData(array $artifact_data) {
     $artifact_spec = $this->getArtifactParameterSpecification();
     PhutilTypeSpec::checkMap($artifact_data, $artifact_spec);
   }
 
   public function renderArtifactSummary(PhabricatorUser $viewer) {
     return null;
   }
 
   public function releaseArtifact(PhabricatorUser $actor) {
     return;
   }
 
   public function getArtifactDataExample() {
     return null;
   }
 
   public function setBuildArtifact(HarbormasterBuildArtifact $build_artifact) {
     $this->buildArtifact = $build_artifact;
     return $this;
   }
 
   public function getBuildArtifact() {
     return $this->buildArtifact;
   }
 
   final public function getArtifactConstant() {
-    $class = new ReflectionClass($this);
-
-    $const = $class->getConstant('ARTIFACTCONST');
-    if ($const === false) {
-      throw new Exception(
-        pht(
-          '"%s" class "%s" must define a "%s" property.',
-          __CLASS__,
-          get_class($this),
-          'ARTIFACTCONST'));
-    }
-
-    $limit = self::getArtifactConstantByteLimit();
-    if (!is_string($const) || (strlen($const) > $limit)) {
-      throw new Exception(
-        pht(
-          '"%s" class "%s" has an invalid "%s" property. Action constants '.
-          'must be strings and no more than %s bytes in length.',
-          __CLASS__,
-          get_class($this),
-          'ARTIFACTCONST',
-          new PhutilNumber($limit)));
-    }
-
-    return $const;
-  }
-
-  final public static function getArtifactConstantByteLimit() {
-    return 32;
+    return $this->getPhobjectClassConstant('ARTIFACTCONST', 32);
   }
 
   final public static function getAllArtifactTypes() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setUniqueMethod('getArtifactConstant')
       ->execute();
   }
 
   final public static function getArtifactType($type) {
     return idx(self::getAllArtifactTypes(), $type);
   }
 
 }
diff --git a/src/applications/harbormaster/stepgroup/HarbormasterBuildStepGroup.php b/src/applications/harbormaster/stepgroup/HarbormasterBuildStepGroup.php
index bb439b75b4..5806861836 100644
--- a/src/applications/harbormaster/stepgroup/HarbormasterBuildStepGroup.php
+++ b/src/applications/harbormaster/stepgroup/HarbormasterBuildStepGroup.php
@@ -1,52 +1,40 @@
 <?php
 
 abstract class HarbormasterBuildStepGroup extends Phobject {
 
   abstract public function getGroupName();
   abstract public function getGroupOrder();
 
   public function isEnabled() {
     return true;
   }
 
   public function shouldShowIfEmpty() {
     return true;
   }
 
   final public function getGroupKey() {
-    $class = new ReflectionClass($this);
-
-    $const = $class->getConstant('GROUPKEY');
-    if ($const === false) {
-      throw new Exception(
-        pht(
-          '"%s" class "%s" must define a "%s" property.',
-          __CLASS__,
-          get_class($this),
-          'GROUPKEY'));
-    }
-
-    return $const;
+    return $this->getPhobjectClassConstant('GROUPKEY');
   }
 
   final public static function getAllGroups() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setUniqueMethod('getGroupKey')
       ->setSortMethod('getGroupOrder')
       ->execute();
   }
 
   final public static function getAllEnabledGroups() {
     $groups = self::getAllGroups();
 
     foreach ($groups as $key => $group) {
       if (!$group->isEnabled()) {
         unset($groups[$key]);
       }
     }
 
     return $groups;
   }
 
 }
diff --git a/src/applications/herald/action/HeraldAction.php b/src/applications/herald/action/HeraldAction.php
index 02a9dfa60a..f4217cd4db 100644
--- a/src/applications/herald/action/HeraldAction.php
+++ b/src/applications/herald/action/HeraldAction.php
@@ -1,407 +1,379 @@
 <?php
 
 abstract class HeraldAction extends Phobject {
 
   private $adapter;
   private $viewer;
   private $applyLog = array();
 
   const STANDARD_NONE = 'standard.none';
   const STANDARD_PHID_LIST = 'standard.phid.list';
   const STANDARD_TEXT = 'standard.text';
 
   const DO_STANDARD_EMPTY = 'do.standard.empty';
   const DO_STANDARD_NO_EFFECT = 'do.standard.no-effect';
   const DO_STANDARD_INVALID = 'do.standard.invalid';
   const DO_STANDARD_UNLOADABLE = 'do.standard.unloadable';
   const DO_STANDARD_PERMISSION = 'do.standard.permission';
   const DO_STANDARD_INVALID_ACTION = 'do.standard.invalid-action';
   const DO_STANDARD_WRONG_RULE_TYPE = 'do.standard.wrong-rule-type';
 
   abstract public function getHeraldActionName();
   abstract public function supportsObject($object);
   abstract public function supportsRuleType($rule_type);
   abstract public function applyEffect($object, HeraldEffect $effect);
 
   abstract public function renderActionDescription($value);
 
   protected function renderActionEffectDescription($type, $data) {
     return null;
   }
 
   public function getActionGroupKey() {
     return null;
   }
 
   public function getActionsForObject($object) {
     return array($this->getActionConstant() => $this);
   }
 
   protected function getDatasource() {
     throw new PhutilMethodNotImplementedException();
   }
 
   protected function getDatasourceValueMap() {
     return null;
   }
 
   public function getHeraldActionStandardType() {
     throw new PhutilMethodNotImplementedException();
   }
 
   public function getHeraldActionValueType() {
     switch ($this->getHeraldActionStandardType()) {
       case self::STANDARD_NONE:
         return new HeraldEmptyFieldValue();
       case self::STANDARD_TEXT:
         return new HeraldTextFieldValue();
       case self::STANDARD_PHID_LIST:
         $tokenizer = id(new HeraldTokenizerFieldValue())
           ->setKey($this->getHeraldActionName())
           ->setDatasource($this->getDatasource());
 
         $value_map = $this->getDatasourceValueMap();
         if ($value_map !== null) {
           $tokenizer->setValueMap($value_map);
         }
 
         return $tokenizer;
     }
 
     throw new PhutilMethodNotImplementedException();
   }
 
   public function willSaveActionValue($value) {
     try {
       $type = $this->getHeraldActionStandardType();
     } catch (PhutilMethodNotImplementedException $ex) {
       return $value;
     }
 
     switch ($type) {
       case self::STANDARD_PHID_LIST:
         return array_keys($value);
     }
 
     return $value;
   }
 
   public function getEditorValue(PhabricatorUser $viewer, $target) {
     try {
       $type = $this->getHeraldActionStandardType();
     } catch (PhutilMethodNotImplementedException $ex) {
       return $target;
     }
 
     switch ($type) {
       case self::STANDARD_PHID_LIST:
         $handles = $viewer->loadHandles($target);
         $handles = iterator_to_array($handles);
         return mpull($handles, 'getName', 'getPHID');
     }
 
     return $target;
   }
 
   final public function setAdapter(HeraldAdapter $adapter) {
     $this->adapter = $adapter;
     return $this;
   }
 
   final public function getAdapter() {
     return $this->adapter;
   }
 
   final public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   final public function getViewer() {
     return $this->viewer;
   }
 
   final public function getActionConstant() {
-    $class = new ReflectionClass($this);
-
-    $const = $class->getConstant('ACTIONCONST');
-    if ($const === false) {
-      throw new Exception(
-        pht(
-          '"%s" class "%s" must define a "%s" property.',
-          __CLASS__,
-          get_class($this),
-          'ACTIONCONST'));
-    }
-
-    $limit = self::getActionConstantByteLimit();
-    if (!is_string($const) || (strlen($const) > $limit)) {
-      throw new Exception(
-        pht(
-          '"%s" class "%s" has an invalid "%s" property. Action constants '.
-          'must be strings and no more than %s bytes in length.',
-          __CLASS__,
-          get_class($this),
-          'ACTIONCONST',
-          new PhutilNumber($limit)));
-    }
-
-    return $const;
-  }
-
-  final public static function getActionConstantByteLimit() {
-    return 64;
+    return $this->getPhobjectClassConstant('ACTIONCONST', 64);
   }
 
   final public static function getAllActions() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setUniqueMethod('getActionConstant')
       ->execute();
   }
 
   protected function logEffect($type, $data = null) {
     if (!is_string($type)) {
       throw new Exception(
         pht(
           'Effect type passed to "%s" must be a scalar string.',
           'logEffect()'));
     }
 
     $this->applyLog[] = array(
       'type' => $type,
       'data' => $data,
     );
 
     return $this;
   }
 
   final public function getApplyTranscript(HeraldEffect $effect) {
     $context = $this->applyLog;
     $this->applyLog = array();
     return new HeraldApplyTranscript($effect, true, $context);
   }
 
   protected function getActionEffectMap() {
     throw new PhutilMethodNotImplementedException();
   }
 
   private function getActionEffectSpec($type) {
     $map = $this->getActionEffectMap() + $this->getStandardEffectMap();
     return idx($map, $type, array());
   }
 
   final public function renderActionEffectIcon($type, $data) {
     $map = $this->getActionEffectSpec($type);
     return idx($map, 'icon');
   }
 
   final public function renderActionEffectColor($type, $data) {
     $map = $this->getActionEffectSpec($type);
     return idx($map, 'color');
   }
 
   final public function renderActionEffectName($type, $data) {
     $map = $this->getActionEffectSpec($type);
     return idx($map, 'name');
   }
 
   protected function renderHandleList($phids) {
     if (!is_array($phids)) {
       return pht('(Invalid List)');
     }
 
     return $this->getViewer()
       ->renderHandleList($phids)
       ->setAsInline(true)
       ->render();
   }
 
   protected function loadStandardTargets(
     array $phids,
     array $allowed_types,
     array $current_value) {
 
     $phids = array_fuse($phids);
     if (!$phids) {
       $this->logEffect(self::DO_STANDARD_EMPTY);
     }
 
     $current_value = array_fuse($current_value);
     $no_effect = array();
     foreach ($phids as $phid) {
       if (isset($current_value[$phid])) {
         $no_effect[] = $phid;
         unset($phids[$phid]);
       }
     }
 
     if ($no_effect) {
       $this->logEffect(self::DO_STANDARD_NO_EFFECT, $no_effect);
     }
 
     if (!$phids) {
       return;
     }
 
     $allowed_types = array_fuse($allowed_types);
     $invalid = array();
     foreach ($phids as $phid) {
       $type = phid_get_type($phid);
       if ($type == PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
         $invalid[] = $phid;
         unset($phids[$phid]);
         continue;
       }
 
       if ($allowed_types && empty($allowed_types[$type])) {
         $invalid[] = $phid;
         unset($phids[$phid]);
         continue;
       }
     }
 
     if ($invalid) {
       $this->logEffect(self::DO_STANDARD_INVALID, $invalid);
     }
 
     if (!$phids) {
       return;
     }
 
     $targets = id(new PhabricatorObjectQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withPHIDs($phids)
       ->execute();
     $targets = mpull($targets, null, 'getPHID');
 
     $unloadable = array();
     foreach ($phids as $phid) {
       if (empty($targets[$phid])) {
         $unloadable[] = $phid;
         unset($phids[$phid]);
       }
     }
 
     if ($unloadable) {
       $this->logEffect(self::DO_STANDARD_UNLOADABLE, $unloadable);
     }
 
     if (!$phids) {
       return;
     }
 
     $adapter = $this->getAdapter();
     $object = $adapter->getObject();
 
     if ($object instanceof PhabricatorPolicyInterface) {
       $no_permission = array();
       foreach ($targets as $phid => $target) {
         if (!($target instanceof PhabricatorUser)) {
           continue;
         }
 
         $can_view = PhabricatorPolicyFilter::hasCapability(
           $target,
           $object,
           PhabricatorPolicyCapability::CAN_VIEW);
         if ($can_view) {
           continue;
         }
 
         $no_permission[] = $phid;
         unset($targets[$phid]);
       }
     }
 
     if ($no_permission) {
       $this->logEffect(self::DO_STANDARD_PERMISSION, $no_permission);
     }
 
     return $targets;
   }
 
   protected function getStandardEffectMap() {
     return array(
       self::DO_STANDARD_EMPTY => array(
         'icon' => 'fa-ban',
         'color' => 'grey',
         'name' => pht('No Targets'),
       ),
       self::DO_STANDARD_NO_EFFECT => array(
         'icon' => 'fa-circle-o',
         'color' => 'grey',
         'name' => pht('No Effect'),
       ),
       self::DO_STANDARD_INVALID => array(
         'icon' => 'fa-ban',
         'color' => 'red',
         'name' => pht('Invalid Targets'),
       ),
       self::DO_STANDARD_UNLOADABLE => array(
         'icon' => 'fa-ban',
         'color' => 'red',
         'name' => pht('Unloadable Targets'),
       ),
       self::DO_STANDARD_PERMISSION => array(
         'icon' => 'fa-lock',
         'color' => 'red',
         'name' => pht('No Permission'),
       ),
       self::DO_STANDARD_INVALID_ACTION => array(
         'icon' => 'fa-ban',
         'color' => 'red',
         'name' => pht('Invalid Action'),
       ),
       self::DO_STANDARD_WRONG_RULE_TYPE => array(
         'icon' => 'fa-ban',
         'color' => 'red',
         'name' => pht('Wrong Rule Type'),
       ),
     );
   }
 
   final public function renderEffectDescription($type, $data) {
     $result = $this->renderActionEffectDescription($type, $data);
     if ($result !== null) {
       return $result;
     }
 
     switch ($type) {
       case self::DO_STANDARD_EMPTY:
         return pht(
           'This action specifies no targets.');
       case self::DO_STANDARD_NO_EFFECT:
         return pht(
           'This action has no effect on %s target(s): %s.',
           new PhutilNumber(count($data)),
           $this->renderHandleList($data));
       case self::DO_STANDARD_INVALID:
         return pht(
           '%s target(s) are invalid or of the wrong type: %s.',
           new PhutilNumber(count($data)),
           $this->renderHandleList($data));
       case self::DO_STANDARD_UNLOADABLE:
         return pht(
           '%s target(s) could not be loaded: %s.',
           new PhutilNumber(count($data)),
           $this->renderHandleList($data));
       case self::DO_STANDARD_PERMISSION:
         return pht(
           '%s target(s) do not have permission to see this object: %s.',
           new PhutilNumber(count($data)),
           $this->renderHandleList($data));
       case self::DO_STANDARD_INVALID_ACTION:
         return pht(
           'No implementation is available for rule "%s".',
           $data);
       case self::DO_STANDARD_WRONG_RULE_TYPE:
         return pht(
           'This action does not support rules of type "%s".',
           $data);
     }
 
     return null;
   }
 
 }
diff --git a/src/applications/herald/action/HeraldActionGroup.php b/src/applications/herald/action/HeraldActionGroup.php
index a087909609..ad4fecd6b6 100644
--- a/src/applications/herald/action/HeraldActionGroup.php
+++ b/src/applications/herald/action/HeraldActionGroup.php
@@ -1,28 +1,16 @@
 <?php
 
 abstract class HeraldActionGroup extends HeraldGroup {
 
   final public function getGroupKey() {
-    $class = new ReflectionClass($this);
-
-    $const = $class->getConstant('ACTIONGROUPKEY');
-    if ($const === false) {
-      throw new Exception(
-        pht(
-          '"%s" class "%s" must define a "%s" property.',
-          __CLASS__,
-          get_class($this),
-          'ACTIONGROUPKEY'));
-    }
-
-    return $const;
+    return $this->getPhobjectClassConstant('ACTIONGROUPKEY');
   }
 
   final public static function getAllActionGroups() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setUniqueMethod('getGroupKey')
       ->setSortMethod('getSortKey')
       ->execute();
   }
 }
diff --git a/src/applications/herald/field/HeraldField.php b/src/applications/herald/field/HeraldField.php
index a6d2e580c2..2aba443077 100644
--- a/src/applications/herald/field/HeraldField.php
+++ b/src/applications/herald/field/HeraldField.php
@@ -1,210 +1,188 @@
 <?php
 
 abstract class HeraldField extends Phobject {
 
   private $adapter;
 
   const STANDARD_BOOL = 'standard.bool';
   const STANDARD_TEXT = 'standard.text';
   const STANDARD_TEXT_LIST = 'standard.text.list';
   const STANDARD_TEXT_MAP = 'standard.text.map';
   const STANDARD_PHID = 'standard.phid';
   const STANDARD_PHID_LIST = 'standard.phid.list';
   const STANDARD_PHID_BOOL = 'standard.phid.bool';
   const STANDARD_PHID_NULLABLE = 'standard.phid.nullable';
 
   abstract public function getHeraldFieldName();
   abstract public function getHeraldFieldValue($object);
 
   public function getFieldGroupKey() {
     return null;
   }
 
   protected function getHeraldFieldStandardType() {
     throw new PhutilMethodNotImplementedException();
   }
 
   protected function getDatasource() {
     throw new PhutilMethodNotImplementedException();
   }
 
   protected function getDatasourceValueMap() {
     return null;
   }
 
   public function getHeraldFieldConditions() {
     $standard_type = $this->getHeraldFieldStandardType();
     switch ($standard_type) {
       case self::STANDARD_BOOL:
         return array(
           HeraldAdapter::CONDITION_IS_TRUE,
           HeraldAdapter::CONDITION_IS_FALSE,
         );
       case self::STANDARD_TEXT:
         return array(
           HeraldAdapter::CONDITION_CONTAINS,
           HeraldAdapter::CONDITION_NOT_CONTAINS,
           HeraldAdapter::CONDITION_IS,
           HeraldAdapter::CONDITION_IS_NOT,
           HeraldAdapter::CONDITION_REGEXP,
         );
       case self::STANDARD_PHID:
         return array(
           HeraldAdapter::CONDITION_IS_ANY,
           HeraldAdapter::CONDITION_IS_NOT_ANY,
         );
       case self::STANDARD_PHID_LIST:
         return array(
           HeraldAdapter::CONDITION_INCLUDE_ALL,
           HeraldAdapter::CONDITION_INCLUDE_ANY,
           HeraldAdapter::CONDITION_INCLUDE_NONE,
           HeraldAdapter::CONDITION_EXISTS,
           HeraldAdapter::CONDITION_NOT_EXISTS,
         );
       case self::STANDARD_PHID_BOOL:
         return array(
           HeraldAdapter::CONDITION_EXISTS,
           HeraldAdapter::CONDITION_NOT_EXISTS,
         );
       case self::STANDARD_PHID_NULLABLE:
         return array(
           HeraldAdapter::CONDITION_IS_ANY,
           HeraldAdapter::CONDITION_IS_NOT_ANY,
           HeraldAdapter::CONDITION_EXISTS,
           HeraldAdapter::CONDITION_NOT_EXISTS,
         );
       case self::STANDARD_TEXT_LIST:
         return array(
           HeraldAdapter::CONDITION_CONTAINS,
           HeraldAdapter::CONDITION_REGEXP,
         );
       case self::STANDARD_TEXT_MAP:
         return array(
           HeraldAdapter::CONDITION_CONTAINS,
           HeraldAdapter::CONDITION_REGEXP,
           HeraldAdapter::CONDITION_REGEXP_PAIR,
         );
     }
 
     throw new Exception(
       pht(
         'Herald field "%s" has unknown standard type "%s".',
         get_class($this),
         $standard_type));
   }
 
   public function getHeraldFieldValueType($condition) {
     $standard_type = $this->getHeraldFieldStandardType();
     switch ($standard_type) {
       case self::STANDARD_BOOL:
       case self::STANDARD_PHID_BOOL:
         return new HeraldEmptyFieldValue();
       case self::STANDARD_TEXT:
       case self::STANDARD_TEXT_LIST:
       case self::STANDARD_TEXT_MAP:
         return new HeraldTextFieldValue();
       case self::STANDARD_PHID:
       case self::STANDARD_PHID_NULLABLE:
       case self::STANDARD_PHID_LIST:
         switch ($condition) {
           case HeraldAdapter::CONDITION_EXISTS:
           case HeraldAdapter::CONDITION_NOT_EXISTS:
             return new HeraldEmptyFieldValue();
           default:
             $tokenizer = id(new HeraldTokenizerFieldValue())
               ->setKey($this->getHeraldFieldName())
               ->setDatasource($this->getDatasource());
 
             $value_map = $this->getDatasourceValueMap();
             if ($value_map !== null) {
               $tokenizer->setValueMap($value_map);
             }
 
             return $tokenizer;
         }
         break;
 
     }
 
     throw new Exception(
       pht(
         'Herald field "%s" has unknown standard type "%s".',
         get_class($this),
         $standard_type));
   }
 
   abstract public function supportsObject($object);
 
   public function getFieldsForObject($object) {
     return array($this->getFieldConstant() => $this);
   }
 
   public function renderConditionValue(
     PhabricatorUser $viewer,
     $condition,
     $value) {
 
     $value_type = $this->getHeraldFieldValueType($condition);
     $value_type->setViewer($viewer);
     return $value_type->renderFieldValue($value);
   }
 
   public function getEditorValue(
     PhabricatorUser $viewer,
     $condition,
     $value) {
 
     $value_type = $this->getHeraldFieldValueType($condition);
     $value_type->setViewer($viewer);
     return $value_type->renderEditorValue($value);
   }
 
   final public function setAdapter(HeraldAdapter $adapter) {
     $this->adapter = $adapter;
     return $this;
   }
 
   final public function getAdapter() {
     return $this->adapter;
   }
 
   final public function getFieldConstant() {
-    $class = new ReflectionClass($this);
-
-    $const = $class->getConstant('FIELDCONST');
-    if ($const === false) {
-      throw new Exception(
-        pht(
-          '"%s" class "%s" must define a "%s" property.',
-          __CLASS__,
-          get_class($this),
-          'FIELDCONST'));
-    }
-
-    $limit = self::getFieldConstantByteLimit();
-    if (!is_string($const) || (strlen($const) > $limit)) {
-      throw new Exception(
-        pht(
-          '"%s" class "%s" has an invalid "%s" property. Field constants '.
-          'must be strings and no more than %s bytes in length.',
-          __CLASS__,
-          get_class($this),
-          'FIELDCONST',
-          new PhutilNumber($limit)));
-    }
-
-    return $const;
+    return $this->getPhobjectClassConstant(
+      'FIELDCONST',
+      self::getFieldConstantByteLimit());
   }
 
   final public static function getFieldConstantByteLimit() {
     return 64;
   }
 
   final public static function getAllFields() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setUniqueMethod('getFieldConstant')
       ->execute();
   }
 
 }
diff --git a/src/applications/herald/field/HeraldFieldGroup.php b/src/applications/herald/field/HeraldFieldGroup.php
index adb7fbe372..9e13f17cb7 100644
--- a/src/applications/herald/field/HeraldFieldGroup.php
+++ b/src/applications/herald/field/HeraldFieldGroup.php
@@ -1,28 +1,16 @@
 <?php
 
 abstract class HeraldFieldGroup extends HeraldGroup {
 
   final public function getGroupKey() {
-    $class = new ReflectionClass($this);
-
-    $const = $class->getConstant('FIELDGROUPKEY');
-    if ($const === false) {
-      throw new Exception(
-        pht(
-          '"%s" class "%s" must define a "%s" property.',
-          __CLASS__,
-          get_class($this),
-          'FIELDGROUPKEY'));
-    }
-
-    return $const;
+    return $this->getPhobjectClassConstant('FIELDGROUPKEY');
   }
 
   final public static function getAllFieldGroups() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setUniqueMethod('getGroupKey')
       ->setSortMethod('getSortKey')
       ->execute();
   }
 }
diff --git a/src/applications/phid/type/PhabricatorPHIDType.php b/src/applications/phid/type/PhabricatorPHIDType.php
index c69075bdad..a8502b12f7 100644
--- a/src/applications/phid/type/PhabricatorPHIDType.php
+++ b/src/applications/phid/type/PhabricatorPHIDType.php
@@ -1,212 +1,202 @@
 <?php
 
 abstract class PhabricatorPHIDType extends Phobject {
 
   final public function getTypeConstant() {
-    $class = new ReflectionClass($this);
-
-    $const = $class->getConstant('TYPECONST');
-    if ($const === false) {
-      throw new Exception(
-        pht(
-          '%s class "%s" must define a %s property.',
-          __CLASS__,
-          get_class($this),
-          'TYPECONST'));
-    }
+    $const = $this->getPhobjectClassConstant('TYPECONST');
 
     if (!is_string($const) || !preg_match('/^[A-Z]{4}$/', $const)) {
       throw new Exception(
         pht(
           '%s class "%s" has an invalid %s property. PHID '.
           'constants must be a four character uppercase string.',
           __CLASS__,
           get_class($this),
           'TYPECONST'));
     }
 
     return $const;
   }
 
   abstract public function getTypeName();
 
   public function newObject() {
     return null;
   }
 
   public function getTypeIcon() {
     // Default to the application icon if the type doesn't specify one.
     $application_class = $this->getPHIDTypeApplicationClass();
     if ($application_class) {
       $application = newv($application_class, array());
       return $application->getFontIcon();
     }
 
     return null;
   }
 
 
   /**
    * Get the class name for the application this type belongs to.
    *
    * @return string|null Class name of the corresponding application, or null
    *   if the type is not bound to an application.
    */
   public function getPHIDTypeApplicationClass() {
     // TODO: Some day this should probably be abstract, but for now it only
     // affects global search and there's no real burning need to go classify
     // every PHID type.
     return null;
   }
 
   /**
    * Build a @{class:PhabricatorPolicyAwareQuery} to load objects of this type
    * by PHID.
    *
    * If you can not build a single query which satisfies this requirement, you
    * can provide a dummy implementation for this method and overload
    * @{method:loadObjects} instead.
    *
    * @param PhabricatorObjectQuery Query being executed.
    * @param list<phid> PHIDs to load.
    * @return PhabricatorPolicyAwareQuery Query object which loads the
    *   specified PHIDs when executed.
    */
   abstract protected function buildQueryForObjects(
     PhabricatorObjectQuery $query,
     array $phids);
 
 
   /**
    * Load objects of this type, by PHID. For most PHID types, it is only
    * necessary to implement @{method:buildQueryForObjects} to get object
    * loading to work.
    *
    * @param PhabricatorObjectQuery Query being executed.
    * @param list<phid> PHIDs to load.
    * @return list<wild> Corresponding objects.
    */
   public function loadObjects(
     PhabricatorObjectQuery $query,
     array $phids) {
 
     $object_query = $this->buildQueryForObjects($query, $phids)
       ->setViewer($query->getViewer())
       ->setParentQuery($query);
 
     // If the user doesn't have permission to use the application at all,
     // just mark all the PHIDs as filtered. This primarily makes these
     // objects show up as "Restricted" instead of "Unknown" when loaded as
     // handles, which is technically true.
     if (!$object_query->canViewerUseQueryApplication()) {
       $object_query->addPolicyFilteredPHIDs(array_fuse($phids));
       return array();
     }
 
     return $object_query->execute();
   }
 
 
   /**
    * Populate provided handles with application-specific data, like titles and
    * URIs.
    *
    * NOTE: The `$handles` and `$objects` lists are guaranteed to be nonempty
    * and have the same keys: subclasses are expected to load information only
    * for handles with visible objects.
    *
    * Because of this guarantee, a safe implementation will typically look like*
    *
    *   foreach ($handles as $phid => $handle) {
    *     $object = $objects[$phid];
    *
    *     $handle->setStuff($object->getStuff());
    *     // ...
    *   }
    *
    * In general, an implementation should call `setName()` and `setURI()` on
    * each handle at a minimum. See @{class:PhabricatorObjectHandle} for other
    * handle properties.
    *
    * @param PhabricatorHandleQuery          Issuing query object.
    * @param list<PhabricatorObjectHandle>   Handles to populate with data.
    * @param list<Object>                    Objects for these PHIDs loaded by
    *                                        @{method:buildQueryForObjects()}.
    * @return void
    */
   abstract public function loadHandles(
     PhabricatorHandleQuery $query,
     array $handles,
     array $objects);
 
   public function canLoadNamedObject($name) {
     return false;
   }
 
   public function loadNamedObjects(
     PhabricatorObjectQuery $query,
     array $names) {
     throw new PhutilMethodNotImplementedException();
   }
 
 
   /**
    * Get all known PHID types.
    *
    * To get PHID types a given user has access to, see
    * @{method:getAllInstalledTypes}.
    *
    * @return dict<string, PhabricatorPHIDType> Map of type constants to types.
    */
   final public static function getAllTypes() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setUniqueMethod('getTypeConstant')
       ->execute();
   }
 
 
   /**
    * Get all PHID types of applications installed for a given viewer.
    *
    * @param PhabricatorUser Viewing user.
    * @return dict<string, PhabricatorPHIDType> Map of constants to installed
    *  types.
    */
   public static function getAllInstalledTypes(PhabricatorUser $viewer) {
     $all_types = self::getAllTypes();
 
     $installed_types = array();
 
     $app_classes = array();
     foreach ($all_types as $key => $type) {
       $app_class = $type->getPHIDTypeApplicationClass();
 
       if ($app_class === null) {
         // If the PHID type isn't bound to an application, include it as
         // installed.
         $installed_types[$key] = $type;
         continue;
       }
 
       // Otherwise, we need to check if this application is installed before
       // including the PHID type.
       $app_classes[$app_class][$key] = $type;
     }
 
     if ($app_classes) {
       $apps = id(new PhabricatorApplicationQuery())
         ->setViewer($viewer)
         ->withInstalled(true)
         ->withClasses(array_keys($app_classes))
         ->execute();
 
       foreach ($apps as $app_class => $app) {
         $installed_types += $app_classes[$app_class];
       }
     }
 
     return $installed_types;
   }
 
 }
diff --git a/src/applications/policy/capability/PhabricatorPolicyCapability.php b/src/applications/policy/capability/PhabricatorPolicyCapability.php
index b7ff6a060b..36b8ea87c0 100644
--- a/src/applications/policy/capability/PhabricatorPolicyCapability.php
+++ b/src/applications/policy/capability/PhabricatorPolicyCapability.php
@@ -1,88 +1,66 @@
 <?php
 
 abstract class PhabricatorPolicyCapability extends Phobject {
 
   const CAN_VIEW        = 'view';
   const CAN_EDIT        = 'edit';
   const CAN_JOIN        = 'join';
 
   /**
    * Get the unique key identifying this capability. This key must be globally
    * unique. Application capabilities should be namespaced. For example:
    *
    *   application.create
    *
    * @return string Globally unique capability key.
    */
   final public function getCapabilityKey() {
-    $class = new ReflectionClass($this);
-
-    $const = $class->getConstant('CAPABILITY');
-    if ($const === false) {
-      throw new Exception(
-        pht(
-          '%s class "%s" must define a %s property.',
-          __CLASS__,
-          get_class($this),
-          'CAPABILITY'));
-    }
-
-    if (!is_string($const)) {
-      throw new Exception(
-        pht(
-          '%s class "%s" has an invalid %s property. '.
-          'Capability constants must be a string.',
-          __CLASS__,
-          get_class($this),
-          'CAPABILITY'));
-    }
-
-    return $const;
+    return $this->getPhobjectClassConstant('CAPABILITY');
   }
 
 
   /**
    * Return a human-readable descriptive name for this capability, like
    * "Can View".
    *
    * @return string Human-readable name describing the capability.
    */
   abstract public function getCapabilityName();
 
 
   /**
    * Return a human-readable string describing what not having this capability
    * prevents the user from doing. For example:
    *
    *   - You do not have permission to edit this object.
    *   - You do not have permission to create new tasks.
    *
    * @return string Human-readable name describing what failing a check for this
    *   capability prevents the user from doing.
    */
   public function describeCapabilityRejection() {
     return null;
   }
 
   /**
    * Can this capability be set to "public"? Broadly, this is only appropriate
    * for view and view-related policies.
    *
    * @return bool True to allow the "public" policy. Returns false by default.
    */
   public function shouldAllowPublicPolicySetting() {
     return false;
   }
 
   final public static function getCapabilityByKey($key) {
     return idx(self::getCapabilityMap(), $key);
   }
 
   final public static function getCapabilityMap() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setUniqueMethod('getCapabilityKey')
       ->execute();
   }
 
 }
diff --git a/src/infrastructure/edges/type/PhabricatorEdgeType.php b/src/infrastructure/edges/type/PhabricatorEdgeType.php
index c86af5c027..1729256050 100644
--- a/src/infrastructure/edges/type/PhabricatorEdgeType.php
+++ b/src/infrastructure/edges/type/PhabricatorEdgeType.php
@@ -1,219 +1,209 @@
 <?php
 
 /**
  * Defines an edge type.
  *
  * Edges are typed, directed connections between two objects. They are used to
  * represent most simple relationships, like when a user is subscribed to an
  * object or an object is a member of a project.
  *
  * @task load   Loading Types
  */
 abstract class PhabricatorEdgeType extends Phobject {
 
   final public function getEdgeConstant() {
-    $class = new ReflectionClass($this);
-
-    $const = $class->getConstant('EDGECONST');
-    if ($const === false) {
-      throw new Exception(
-        pht(
-          '%s class "%s" must define an %s property.',
-          __CLASS__,
-          get_class($this),
-          'EDGECONST'));
-    }
+    $const = $this->getPhobjectClassConstant('EDGECONST');
 
     if (!is_int($const) || ($const <= 0)) {
       throw new Exception(
         pht(
           '%s class "%s" has an invalid %s property. '.
           'Edge constants must be positive integers.',
           __CLASS__,
           get_class($this),
           'EDGECONST'));
     }
 
     return $const;
   }
 
   public function getInverseEdgeConstant() {
     return null;
   }
 
   public function shouldPreventCycles() {
     return false;
   }
 
   public function shouldWriteInverseTransactions() {
     return false;
   }
 
   public function getTransactionPreviewString($actor) {
     return pht(
       '%s edited edge metadata.',
       $actor);
   }
 
   public function getTransactionAddString(
     $actor,
     $add_count,
     $add_edges) {
 
     return pht(
       '%s added %s edge(s): %s.',
       $actor,
       $add_count,
       $add_edges);
   }
 
   public function getTransactionRemoveString(
     $actor,
     $rem_count,
     $rem_edges) {
 
     return pht(
       '%s removed %s edge(s): %s.',
       $actor,
       $rem_count,
       $rem_edges);
   }
 
   public function getTransactionEditString(
     $actor,
     $total_count,
     $add_count,
     $add_edges,
     $rem_count,
     $rem_edges) {
 
     return pht(
       '%s edited %s edge(s), added %s: %s; removed %s: %s.',
       $actor,
       $total_count,
       $add_count,
       $add_edges,
       $rem_count,
       $rem_edges);
   }
 
   public function getFeedAddString(
     $actor,
     $object,
     $add_count,
     $add_edges) {
 
     return pht(
       '%s added %s edge(s) to %s: %s.',
       $actor,
       $add_count,
       $object,
       $add_edges);
   }
 
   public function getFeedRemoveString(
     $actor,
     $object,
     $rem_count,
     $rem_edges) {
 
     return pht(
       '%s removed %s edge(s) from %s: %s.',
       $actor,
       $rem_count,
       $object,
       $rem_edges);
   }
 
   public function getFeedEditString(
     $actor,
     $object,
     $total_count,
     $add_count,
     $add_edges,
     $rem_count,
     $rem_edges) {
 
     return pht(
       '%s edited %s edge(s) for %s, added %s: %s; removed %s: %s.',
       $actor,
       $total_count,
       $object,
       $add_count,
       $add_edges,
       $rem_count,
       $rem_edges);
   }
 
 
 /* -(  Loading Types  )------------------------------------------------------ */
 
 
   /**
    * @task load
    */
   public static function getAllTypes() {
     static $type_map;
 
     if ($type_map === null) {
       $types = id(new PhutilClassMapQuery())
         ->setAncestorClass(__CLASS__)
         ->setUniqueMethod('getEdgeConstant')
         ->execute();
 
       // Check that all the inverse edge definitions actually make sense. If
       // edge type A says B is its inverse, B must exist and say that A is its
       // inverse.
 
       foreach ($types as $const => $type) {
         $inverse = $type->getInverseEdgeConstant();
         if ($inverse === null) {
           continue;
         }
 
         if (empty($types[$inverse])) {
           throw new Exception(
             pht(
               'Edge type "%s" ("%d") defines an inverse type ("%d") which '.
               'does not exist.',
               get_class($type),
               $const,
               $inverse));
         }
 
         $inverse_inverse = $types[$inverse]->getInverseEdgeConstant();
         if ($inverse_inverse !== $const) {
           throw new Exception(
             pht(
               'Edge type "%s" ("%d") defines an inverse type ("%d"), but that '.
               'inverse type defines a different type ("%d") as its '.
               'inverse.',
               get_class($type),
               $const,
               $inverse,
               $inverse_inverse));
         }
       }
 
       $type_map = $types;
     }
 
     return $type_map;
   }
 
 
   /**
    * @task load
    */
   public static function getByConstant($const) {
     $type = idx(self::getAllTypes(), $const);
 
     if (!$type) {
       throw new Exception(
         pht('Unknown edge constant "%s"!', $const));
     }
 
     return $type;
   }
 
 }