/src/applications/differential/field/specification/base/DifferentialFieldSpecification.php
PHP | 764 lines | 223 code | 85 blank | 456 comment | 15 complexity | ce9d70b51f1ba3c2cdf776bab08ef8d2 MD5 | raw file
Possible License(s): JSON, MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause, LGPL-2.0, MIT, LGPL-2.1, LGPL-3.0
- <?php
- /*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * Describes and implements the behavior for a custom field on Differential
- * revisions. Along with other configuration, you can extend this class to add
- * custom fields to Differential revisions and commit messages.
- *
- * Generally, you should implement all methods from the storage task and then
- * the methods from one or more interface tasks.
- *
- * @task storage Field Storage
- * @task edit Extending the Revision Edit Interface
- * @task view Extending the Revision View Interface
- * @task conduit Extending the Conduit View Interface
- * @task commit Extending Commit Messages
- * @task load Loading Additional Data
- * @task context Contextual Data
- */
- abstract class DifferentialFieldSpecification {
- private $revision;
- private $diff;
- private $handles;
- private $diffProperties;
- private $user;
- /* -( Storage )------------------------------------------------------------ */
- /**
- * Return a unique string used to key storage of this field's value, like
- * "mycompany.fieldname" or similar. You can return null (the default) to
- * indicate that this field does not use any storage. This is appropriate for
- * display fields, like @{class:DifferentialLinesFieldSpecification}. If you
- * implement this, you must also implement @{method:getValueForStorage} and
- * @{method:setValueFromStorage}.
- *
- * @return string|null Unique key which identifies this field in auxiliary
- * field storage. Maximum length is 32. Alternatively,
- * null (default) to indicate that this field does not
- * use auxiliary field storage.
- * @task storage
- */
- public function getStorageKey() {
- return null;
- }
- /**
- * Return a serialized representation of the field value, appropriate for
- * storing in auxiliary field storage. You must implement this method if
- * you implement @{method:getStorageKey}.
- *
- * @return string Serialized field value.
- * @task storage
- */
- public function getValueForStorage() {
- throw new DifferentialFieldSpecificationIncompleteException($this);
- }
- /**
- * Set the field's value given a serialized storage value. This is called
- * when the field is loaded; if no data is available, the value will be
- * null. You must implement this method if you implement
- * @{method:getStorageKey}.
- *
- * @param string|null Serialized field representation (from
- * @{method:getValueForStorage}) or null if no value has
- * ever been stored.
- * @return this
- * @task storage
- */
- public function setValueFromStorage($value) {
- throw new DifferentialFieldSpecificationIncompleteException($this);
- }
- /* -( Extending the Revision Edit Interface )------------------------------ */
- /**
- * Determine if this field should appear on the "Edit Revision" interface. If
- * you return true from this method, you must implement
- * @{method:setValueFromRequest}, @{method:renderEditControl} and
- * @{method:validateField}.
- *
- * For a concrete example of a field which implements an edit interface, see
- * @{class:DifferentialRevertPlanFieldSpecification}.
- *
- * @return bool True to indicate that this field implements an edit interface.
- * @task edit
- */
- public function shouldAppearOnEdit() {
- return false;
- }
- /**
- * Set the field's value from an HTTP request. Generally, you should read
- * the value of some field name you emitted in @{method:renderEditControl}
- * and save it into the object, e.g.:
- *
- * $this->value = $request->getStr('my-custom-field');
- *
- * If you have some particularly complicated field, you may need to read
- * more data; this is why you have access to the entire request.
- *
- * You must implement this if you implement @{method:shouldAppearOnEdit}.
- *
- * You should not perform field validation here; instead, you should implement
- * @{method:validateField}.
- *
- * @param AphrontRequest HTTP request representing a user submitting a form
- * with this field in it.
- * @return this
- * @task edit
- */
- public function setValueFromRequest(AphrontRequest $request) {
- throw new DifferentialFieldSpecificationIncompleteException($this);
- }
- /**
- * Build a renderable object (generally, some @{class:AphrontFormControl})
- * which can be appended to a @{class:AphrontFormView} and represents the
- * interface the user sees on the "Edit Revision" screen when interacting
- * with this field.
- *
- * For example:
- *
- * return id(new AphrontFormTextControl())
- * ->setLabel('Custom Field')
- * ->setName('my-custom-key')
- * ->setValue($this->value);
- *
- * You must implement this if you implement @{method:shouldAppearOnEdit}.
- *
- * @return AphrontView|string Something renderable.
- * @task edit
- */
- public function renderEditControl() {
- throw new DifferentialFieldSpecificationIncompleteException($this);
- }
- /**
- * This method will be called after @{method:setValueFromRequest} but before
- * the field is saved. It gives you an opportunity to inspect the field value
- * and throw a @{class:DifferentialFieldValidationException} if there is a
- * problem with the value the user has provided (for example, the value the
- * user entered is not correctly formatted). This method is also called after
- * @{method:setValueFromParsedCommitMessage} before the revision is saved.
- *
- * By default, fields are not validated.
- *
- * @return void
- * @task edit
- */
- public function validateField() {
- return;
- }
- /**
- * Hook for applying revision changes via the editor. Normally, you should
- * not implement this, but a number of builtin fields use the revision object
- * itself as storage. If you need to do something similar for whatever reason,
- * this method gives you an opportunity to interact with the editor or
- * revision before changes are saved (for example, you can write the field's
- * value into some property of the revision).
- *
- * @param DifferentialRevisionEditor Active editor which is applying changes
- * to the revision.
- * @return void
- * @task edit
- */
- public function willWriteRevision(DifferentialRevisionEditor $editor) {
- return;
- }
- /**
- * Hook after an edit operation has completed. This allows you to update
- * link tables or do other write operations which should happen after the
- * revision is saved. Normally you don't need to implement this.
- *
- *
- * @param DifferentialRevisionEditor Active editor which has just applied
- * changes to the revision.
- * @return void
- * @task edit
- */
- public function didWriteRevision(DifferentialRevisionEditor $editor) {
- return;
- }
- /* -( Extending the Revision View Interface )------------------------------ */
- /**
- * Determine if this field should appear on the revision detail view
- * interface. One use of this interface is to add purely informational
- * fields to the revision view, without any sort of backing storage.
- *
- * If you return true from this method, you must implement the methods
- * @{method:renderLabelForRevisionView} and
- * @{method:renderValueForRevisionView}.
- *
- * @return bool True if this field should appear when viewing a revision.
- * @task view
- */
- public function shouldAppearOnRevisionView() {
- return false;
- }
- /**
- * Return a string field label which will appear in the revision detail
- * table.
- *
- * You must implement this method if you return true from
- * @{method:shouldAppearOnRevisionView}.
- *
- * @return string Label for field in revision detail view.
- * @task view
- */
- public function renderLabelForRevisionView() {
- throw new DifferentialFieldSpecificationIncompleteException($this);
- }
- /**
- * Return a markup block representing the field for the revision detail
- * view. Note that you can return null to suppress display (for instance,
- * if the field shows related objects of some type and the revision doesn't
- * have any related objects).
- *
- * You must implement this method if you return true from
- * @{method:shouldAppearOnRevisionView}.
- *
- * @return string|null Display markup for field value, or null to suppress
- * field rendering.
- * @task view
- */
- public function renderValueForRevisionView() {
- throw new DifferentialFieldSpecificationIncompleteException($this);
- }
- /* -( Extending the Conduit Interface )------------------------------------ */
- /**
- * @task conduit
- */
- public function shouldAppearOnConduitView() {
- return false;
- }
- /**
- * @task conduit
- */
- public function getValueForConduit() {
- throw new DifferentialFieldSpecificationIncompleteException($this);
- }
- /**
- * @task conduit
- */
- public function getKeyForConduit() {
- $key = $this->getStorageKey();
- if ($key === null) {
- throw new DifferentialFieldSpecificationIncompleteException($this);
- }
- return $key;
- }
- /* -( Extending Commit Messages )------------------------------------------ */
- /**
- * Determine if this field should appear in commit messages. You should return
- * true if this field participates in any part of the commit message workflow,
- * even if it is not rendered by default.
- *
- * If you implement this method, you must implement
- * @{method:getCommitMessageKey} and
- * @{method:setValueFromParsedCommitMessage}.
- *
- * @return bool True if this field appears in commit messages in any capacity.
- * @task commit
- */
- public function shouldAppearOnCommitMessage() {
- return false;
- }
- /**
- * Key which identifies this field in parsed commit messages. Commit messages
- * exist in two forms: raw textual commit messages and parsed dictionaries of
- * fields. This method must return a unique string which identifies this field
- * in dictionaries. Principally, this dictionary is shipped to and from arc
- * over Conduit. Keys should be appropriate property names, like "testPlan"
- * (not "Test Plan") and must be globally unique.
- *
- * You must implement this method if you return true from
- * @{method:shouldAppearOnCommitMessage}.
- *
- * @return string Key which identifies the field in dictionaries.
- * @task commit
- */
- public function getCommitMessageKey() {
- throw new DifferentialFieldSpecificationIncompleteException($this);
- }
- /**
- * Set this field's value from a value in a parsed commit message dictionary.
- * Afterward, this field will go through the normal write workflows and the
- * change will be permanently stored via either the storage mechanisms (if
- * your field implements them), revision write hooks (if your field implements
- * them) or discarded (if your field implements neither, e.g. is just a
- * display field).
- *
- * The value you receive will either be null or something you originally
- * returned from @{method:parseValueFromCommitMessage}.
- *
- * You must implement this method if you return true from
- * @{method:shouldAppearOnCommitMessage}.
- *
- * @param mixed Field value from a parsed commit message dictionary.
- * @return this
- * @task commit
- */
- public function setValueFromParsedCommitMessage($value) {
- throw new DifferentialFieldSpecificationIncompleteException($this);
- }
- /**
- * In revision control systems which read revision information from the
- * working copy, the user may edit the commit message outside of invoking
- * "arc diff --edit". When they do this, only some fields (those fields which
- * can not be edited by other users) are safe to overwrite. For instance, it
- * is fine to overwrite "Summary" because no one else can edit it, but not
- * to overwrite "Reviewers" because reviewers may have been added or removed
- * via the web interface.
- *
- * If a field is safe to overwrite when edited in a working copy commit
- * message, return true. If the authoritative value should always be used,
- * return false. By default, fields can not be overwritten.
- *
- * @return bool True to indicate the field is save to overwrite.
- * @task commit
- */
- public function shouldOverwriteWhenCommitMessageIsEdited() {
- return false;
- }
- /**
- * Return true if this field should be suggested to the user during
- * "arc diff --edit". Basicially, return true if the field is something the
- * user might want to fill out (like "Summary"), and false if it's a
- * system/display/readonly field (like "Differential Revision"). If this
- * method returns true, the field will be rendered even if it has no value
- * during edit and update operations.
- *
- * @return bool True to indicate the field should appear in the edit template.
- * @task commit
- */
- public function shouldAppearOnCommitMessageTemplate() {
- return true;
- }
- /**
- * Render a human-readable label for this field, like "Summary" or
- * "Test Plan". This is distinct from the commit message key, but generally
- * they should be similar.
- *
- * @return string Human-readable field label for commit messages.
- * @task commit
- */
- public function renderLabelForCommitMessage() {
- throw new DifferentialFieldSpecificationIncompleteException($this);
- }
- /**
- * Render a human-readable value for this field when it appears in commit
- * messages (for instance, lists of users should be rendered as user names).
- *
- * The ##$is_edit## parameter allows you to distinguish between commit
- * messages being rendered for editing and those being rendered for amending
- * or commit. Some fields may decline to render a value in one mode (for
- * example, "Reviewed By" appears only when doing commit/amend, not while
- * editing).
- *
- * @param bool True if the message is being edited.
- * @return string Human-readable field value.
- * @task commit
- */
- public function renderValueForCommitMessage($is_edit) {
- throw new DifferentialFieldSpecificationIncompleteException($this);
- }
- /**
- * Return one or more labels which this field parses in commit messages. For
- * example, you might parse all of "Task", "Tasks" and "Task Numbers" or
- * similar. This is just to make it easier to get commit messages to parse
- * when users are typing in the fields manually as opposed to using a
- * template, by accepting alternate spellings / pluralizations / etc. By
- * default, only the label returned from @{method:renderLabelForCommitMessage}
- * is parsed.
- *
- * @return list List of supported labels that this field can parse from commit
- * messages.
- * @task commit
- */
- public function getSupportedCommitMessageLabels() {
- return array($this->renderLabelForCommitMessage());
- }
- /**
- * Parse a raw text block from a commit message into a canonical
- * representation of the field value. For example, the "CC" field accepts a
- * comma-delimited list of usernames and emails and parses them into valid
- * PHIDs, emitting a PHID list.
- *
- * If you encounter errors (like a nonexistent username) while parsing,
- * you should throw a @{class:DifferentialFieldParseException}.
- *
- * Generally, this method should accept whatever you return from
- * @{method:renderValueForCommitMessage} and parse it back into a sensible
- * representation.
- *
- * You must implement this method if you return true from
- * @{method:shouldAppearOnCommitMessage}.
- *
- * @param string
- * @return mixed The canonical representation of the field value. For example,
- * you should lookup usernames and object references.
- * @task commit
- */
- public function parseValueFromCommitMessage($value) {
- throw new DifferentialFieldSpecificationIncompleteException($this);
- }
- /* -( Loading Additional Data )-------------------------------------------- */
- /**
- * Specify which @{class:PhabricatorObjectHandles} need to be loaded for your
- * field to render correctly.
- *
- * This is a convenience method which makes the handles available on all
- * interfaces where the field appears. If your field needs handles on only
- * some interfaces (or needs different handles on different interfaces) you
- * can overload the more specific methods to customize which interfaces you
- * retrieve handles for. Requesting only the handles you need will improve
- * the performance of your field.
- *
- * You can later retrieve these handles by calling @{method:getHandle}.
- *
- * @return list List of PHIDs to load handles for.
- * @task load
- */
- protected function getRequiredHandlePHIDs() {
- return array();
- }
- /**
- * Specify which @{class:PhabricatorObjectHandles} need to be loaded for your
- * field to render correctly on the view interface.
- *
- * This is a more specific version of @{method:getRequiredHandlePHIDs} which
- * can be overridden to improve field performance by loading only data you
- * need.
- *
- * @return list List of PHIDs to load handles for.
- * @task load
- */
- public function getRequiredHandlePHIDsForRevisionView() {
- return $this->getRequiredHandlePHIDs();
- }
- /**
- * Specify which @{class:PhabricatorObjectHandles} need to be loaded for your
- * field to render correctly on the edit interface.
- *
- * This is a more specific version of @{method:getRequiredHandlePHIDs} which
- * can be overridden to improve field performance by loading only data you
- * need.
- *
- * @return list List of PHIDs to load handles for.
- * @task load
- */
- public function getRequiredHandlePHIDsForRevisionEdit() {
- return $this->getRequiredHandlePHIDs();
- }
- /**
- * Specify which @{class:PhabricatorObjectHandles} need to be loaded for your
- * field to render correctly on the commit message interface.
- *
- * This is a more specific version of @{method:getRequiredHandlePHIDs} which
- * can be overridden to improve field performance by loading only data you
- * need.
- *
- * @return list List of PHIDs to load handles for.
- * @task load
- */
- public function getRequiredHandlePHIDsForCommitMessage() {
- return $this->getRequiredHandlePHIDs();
- }
- /**
- * Specify which diff properties this field needs to load.
- *
- * @return list List of diff property keys this field requires.
- * @task load
- */
- public function getRequiredDiffProperties() {
- return array();
- }
- /**
- * Parse a list of users into a canonical PHID list.
- *
- * @param string Raw list of comma-separated user names.
- * @return list List of corresponding PHIDs.
- * @task load
- */
- protected function parseCommitMessageUserList($value) {
- return $this->parseCommitMessageObjectList($value, $mailables = false);
- }
- /**
- * Parse a list of mailable objects into a canonical PHID list.
- *
- * @param string Raw list of comma-separated mailable names.
- * @return list List of corresponding PHIDs.
- * @task load
- */
- protected function parseCommitMessageMailableList($value) {
- return $this->parseCommitMessageObjectList($value, $mailables = true);
- }
- /**
- * Parse and lookup a list of object names, converting them to PHIDs.
- *
- * @param string Raw list of comma-separated object names.
- * @return list List of corresponding PHIDs.
- * @task load
- */
- private function parseCommitMessageObjectList($value, $include_mailables) {
- $value = array_unique(array_filter(preg_split('/[\s,]+/', $value)));
- if (!$value) {
- return array();
- }
- $object_map = array();
- $users = id(new PhabricatorUser())->loadAllWhere(
- '((username IN (%Ls)) OR (email IN (%Ls)))
- AND isDisabled = 0
- AND isSystemAgent = 0',
- $value,
- $value);
- $user_map = mpull($users, 'getPHID', 'getUsername');
- foreach ($user_map as $username => $phid) {
- // Usernames may have uppercase letters in them. Put both names in the
- // map so we can try the original case first, so that username *always*
- // works in weird edge cases where some other mailable object collides.
- $object_map[$username] = $phid;
- $object_map[strtolower($username)] = $phid;
- }
- $object_map += mpull($users, 'getPHID', 'getEmail');
- if ($include_mailables) {
- $mailables = id(new PhabricatorMetaMTAMailingList())->loadAllWhere(
- '(email IN (%Ls)) OR (name IN (%Ls))',
- $value,
- $value);
- $object_map += mpull($mailables, 'getPHID', 'getName');
- $object_map += mpull($mailables, 'getPHID', 'getEmail');
- }
- $invalid = array();
- $results = array();
- foreach ($value as $name) {
- if (empty($object_map[$name])) {
- if (empty($object_map[strtolower($name)])) {
- $invalid[] = $name;
- } else {
- $results[] = $object_map[strtolower($name)];
- }
- } else {
- $results[] = $object_map[$name];
- }
- }
- if ($invalid) {
- $invalid = implode(', ', $invalid);
- $what = $include_mailables
- ? "users and mailing lists"
- : "users";
- throw new DifferentialFieldParseException(
- "Commit message references disabled or nonexistent {$what}: ".
- "{$invalid}.");
- }
- return array_unique($results);
- }
- /* -( Contextual Data )---------------------------------------------------- */
- /**
- * @task context
- */
- final public function setRevision(DifferentialRevision $revision) {
- $this->revision = $revision;
- $this->didSetRevision();
- return $this;
- }
- /**
- * @task context
- */
- protected function didSetRevision() {
- return;
- }
- /**
- * @task context
- */
- final public function setDiff(DifferentialDiff $diff) {
- $this->diff = $diff;
- return $this;
- }
- /**
- * @task context
- */
- final public function setHandles(array $handles) {
- $this->handles = $handles;
- return $this;
- }
- /**
- * @task context
- */
- final public function setDiffProperties(array $diff_properties) {
- $this->diffProperties = $diff_properties;
- return $this;
- }
- /**
- * @task context
- */
- final public function setUser(PhabricatorUser $user) {
- $this->user = $user;
- return $this;
- }
- /**
- * @task context
- */
- final protected function getRevision() {
- if (empty($this->revision)) {
- throw new DifferentialFieldDataNotAvailableException($this);
- }
- return $this->revision;
- }
- /**
- * @task context
- */
- final protected function getDiff() {
- if (empty($this->diff)) {
- throw new DifferentialFieldDataNotAvailableException($this);
- }
- return $this->diff;
- }
- /**
- * @task context
- */
- final protected function getUser() {
- if (empty($this->user)) {
- throw new DifferentialFieldDataNotAvailableException($this);
- }
- return $this->user;
- }
- /**
- * Get the handle for an object PHID. You must overload
- * @{method:getRequiredHandlePHIDs} (or a more specific version thereof)
- * and include the PHID you want in the list for it to be available here.
- *
- * @return PhabricatorObjectHandle Handle to the object.
- * @task context
- */
- final protected function getHandle($phid) {
- if ($this->handles === null) {
- throw new DifferentialFieldDataNotAvailableException($this);
- }
- if (empty($this->handles[$phid])) {
- $class = get_class($this);
- throw new Exception(
- "A differential field (of class '{$class}') is attempting to retrieve ".
- "a handle ('{$phid}') which it did not request. Return all handle ".
- "PHIDs you need from getRequiredHandlePHIDs().");
- }
- return $this->handles[$phid];
- }
- /**
- * Get a diff property which this field previously requested by returning
- * the key from @{method:getRequiredDiffProperties}.
- *
- * @param string Diff property key.
- * @return string|null Diff property, or null if the property does not have
- * a value.
- * @task context
- */
- final public function getDiffProperty($key) {
- if ($this->diffProperties === null) {
- // This will be set to some (possibly empty) array if we've loaded
- // properties, so null means diff properties aren't available in this
- // context.
- throw new DifferentialFieldDataNotAvailableException($this);
- }
- if (!array_key_exists($key, $this->diffProperties)) {
- $class = get_class($this);
- throw new Exception(
- "A differential field (of class '{$class}') is attempting to retrieve ".
- "a diff property ('{$key}') which it did not request. Return all ".
- "diff property keys you need from getRequiredDiffProperties().");
- }
- return $this->diffProperties[$key];
- }
- }