PageRenderTime 44ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/src/applications/differential/field/specification/base/DifferentialFieldSpecification.php

http://github.com/facebook/phabricator
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
  1. <?php
  2. /*
  3. * Copyright 2012 Facebook, Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /**
  18. * Describes and implements the behavior for a custom field on Differential
  19. * revisions. Along with other configuration, you can extend this class to add
  20. * custom fields to Differential revisions and commit messages.
  21. *
  22. * Generally, you should implement all methods from the storage task and then
  23. * the methods from one or more interface tasks.
  24. *
  25. * @task storage Field Storage
  26. * @task edit Extending the Revision Edit Interface
  27. * @task view Extending the Revision View Interface
  28. * @task conduit Extending the Conduit View Interface
  29. * @task commit Extending Commit Messages
  30. * @task load Loading Additional Data
  31. * @task context Contextual Data
  32. */
  33. abstract class DifferentialFieldSpecification {
  34. private $revision;
  35. private $diff;
  36. private $handles;
  37. private $diffProperties;
  38. private $user;
  39. /* -( Storage )------------------------------------------------------------ */
  40. /**
  41. * Return a unique string used to key storage of this field's value, like
  42. * "mycompany.fieldname" or similar. You can return null (the default) to
  43. * indicate that this field does not use any storage. This is appropriate for
  44. * display fields, like @{class:DifferentialLinesFieldSpecification}. If you
  45. * implement this, you must also implement @{method:getValueForStorage} and
  46. * @{method:setValueFromStorage}.
  47. *
  48. * @return string|null Unique key which identifies this field in auxiliary
  49. * field storage. Maximum length is 32. Alternatively,
  50. * null (default) to indicate that this field does not
  51. * use auxiliary field storage.
  52. * @task storage
  53. */
  54. public function getStorageKey() {
  55. return null;
  56. }
  57. /**
  58. * Return a serialized representation of the field value, appropriate for
  59. * storing in auxiliary field storage. You must implement this method if
  60. * you implement @{method:getStorageKey}.
  61. *
  62. * @return string Serialized field value.
  63. * @task storage
  64. */
  65. public function getValueForStorage() {
  66. throw new DifferentialFieldSpecificationIncompleteException($this);
  67. }
  68. /**
  69. * Set the field's value given a serialized storage value. This is called
  70. * when the field is loaded; if no data is available, the value will be
  71. * null. You must implement this method if you implement
  72. * @{method:getStorageKey}.
  73. *
  74. * @param string|null Serialized field representation (from
  75. * @{method:getValueForStorage}) or null if no value has
  76. * ever been stored.
  77. * @return this
  78. * @task storage
  79. */
  80. public function setValueFromStorage($value) {
  81. throw new DifferentialFieldSpecificationIncompleteException($this);
  82. }
  83. /* -( Extending the Revision Edit Interface )------------------------------ */
  84. /**
  85. * Determine if this field should appear on the "Edit Revision" interface. If
  86. * you return true from this method, you must implement
  87. * @{method:setValueFromRequest}, @{method:renderEditControl} and
  88. * @{method:validateField}.
  89. *
  90. * For a concrete example of a field which implements an edit interface, see
  91. * @{class:DifferentialRevertPlanFieldSpecification}.
  92. *
  93. * @return bool True to indicate that this field implements an edit interface.
  94. * @task edit
  95. */
  96. public function shouldAppearOnEdit() {
  97. return false;
  98. }
  99. /**
  100. * Set the field's value from an HTTP request. Generally, you should read
  101. * the value of some field name you emitted in @{method:renderEditControl}
  102. * and save it into the object, e.g.:
  103. *
  104. * $this->value = $request->getStr('my-custom-field');
  105. *
  106. * If you have some particularly complicated field, you may need to read
  107. * more data; this is why you have access to the entire request.
  108. *
  109. * You must implement this if you implement @{method:shouldAppearOnEdit}.
  110. *
  111. * You should not perform field validation here; instead, you should implement
  112. * @{method:validateField}.
  113. *
  114. * @param AphrontRequest HTTP request representing a user submitting a form
  115. * with this field in it.
  116. * @return this
  117. * @task edit
  118. */
  119. public function setValueFromRequest(AphrontRequest $request) {
  120. throw new DifferentialFieldSpecificationIncompleteException($this);
  121. }
  122. /**
  123. * Build a renderable object (generally, some @{class:AphrontFormControl})
  124. * which can be appended to a @{class:AphrontFormView} and represents the
  125. * interface the user sees on the "Edit Revision" screen when interacting
  126. * with this field.
  127. *
  128. * For example:
  129. *
  130. * return id(new AphrontFormTextControl())
  131. * ->setLabel('Custom Field')
  132. * ->setName('my-custom-key')
  133. * ->setValue($this->value);
  134. *
  135. * You must implement this if you implement @{method:shouldAppearOnEdit}.
  136. *
  137. * @return AphrontView|string Something renderable.
  138. * @task edit
  139. */
  140. public function renderEditControl() {
  141. throw new DifferentialFieldSpecificationIncompleteException($this);
  142. }
  143. /**
  144. * This method will be called after @{method:setValueFromRequest} but before
  145. * the field is saved. It gives you an opportunity to inspect the field value
  146. * and throw a @{class:DifferentialFieldValidationException} if there is a
  147. * problem with the value the user has provided (for example, the value the
  148. * user entered is not correctly formatted). This method is also called after
  149. * @{method:setValueFromParsedCommitMessage} before the revision is saved.
  150. *
  151. * By default, fields are not validated.
  152. *
  153. * @return void
  154. * @task edit
  155. */
  156. public function validateField() {
  157. return;
  158. }
  159. /**
  160. * Hook for applying revision changes via the editor. Normally, you should
  161. * not implement this, but a number of builtin fields use the revision object
  162. * itself as storage. If you need to do something similar for whatever reason,
  163. * this method gives you an opportunity to interact with the editor or
  164. * revision before changes are saved (for example, you can write the field's
  165. * value into some property of the revision).
  166. *
  167. * @param DifferentialRevisionEditor Active editor which is applying changes
  168. * to the revision.
  169. * @return void
  170. * @task edit
  171. */
  172. public function willWriteRevision(DifferentialRevisionEditor $editor) {
  173. return;
  174. }
  175. /**
  176. * Hook after an edit operation has completed. This allows you to update
  177. * link tables or do other write operations which should happen after the
  178. * revision is saved. Normally you don't need to implement this.
  179. *
  180. *
  181. * @param DifferentialRevisionEditor Active editor which has just applied
  182. * changes to the revision.
  183. * @return void
  184. * @task edit
  185. */
  186. public function didWriteRevision(DifferentialRevisionEditor $editor) {
  187. return;
  188. }
  189. /* -( Extending the Revision View Interface )------------------------------ */
  190. /**
  191. * Determine if this field should appear on the revision detail view
  192. * interface. One use of this interface is to add purely informational
  193. * fields to the revision view, without any sort of backing storage.
  194. *
  195. * If you return true from this method, you must implement the methods
  196. * @{method:renderLabelForRevisionView} and
  197. * @{method:renderValueForRevisionView}.
  198. *
  199. * @return bool True if this field should appear when viewing a revision.
  200. * @task view
  201. */
  202. public function shouldAppearOnRevisionView() {
  203. return false;
  204. }
  205. /**
  206. * Return a string field label which will appear in the revision detail
  207. * table.
  208. *
  209. * You must implement this method if you return true from
  210. * @{method:shouldAppearOnRevisionView}.
  211. *
  212. * @return string Label for field in revision detail view.
  213. * @task view
  214. */
  215. public function renderLabelForRevisionView() {
  216. throw new DifferentialFieldSpecificationIncompleteException($this);
  217. }
  218. /**
  219. * Return a markup block representing the field for the revision detail
  220. * view. Note that you can return null to suppress display (for instance,
  221. * if the field shows related objects of some type and the revision doesn't
  222. * have any related objects).
  223. *
  224. * You must implement this method if you return true from
  225. * @{method:shouldAppearOnRevisionView}.
  226. *
  227. * @return string|null Display markup for field value, or null to suppress
  228. * field rendering.
  229. * @task view
  230. */
  231. public function renderValueForRevisionView() {
  232. throw new DifferentialFieldSpecificationIncompleteException($this);
  233. }
  234. /* -( Extending the Conduit Interface )------------------------------------ */
  235. /**
  236. * @task conduit
  237. */
  238. public function shouldAppearOnConduitView() {
  239. return false;
  240. }
  241. /**
  242. * @task conduit
  243. */
  244. public function getValueForConduit() {
  245. throw new DifferentialFieldSpecificationIncompleteException($this);
  246. }
  247. /**
  248. * @task conduit
  249. */
  250. public function getKeyForConduit() {
  251. $key = $this->getStorageKey();
  252. if ($key === null) {
  253. throw new DifferentialFieldSpecificationIncompleteException($this);
  254. }
  255. return $key;
  256. }
  257. /* -( Extending Commit Messages )------------------------------------------ */
  258. /**
  259. * Determine if this field should appear in commit messages. You should return
  260. * true if this field participates in any part of the commit message workflow,
  261. * even if it is not rendered by default.
  262. *
  263. * If you implement this method, you must implement
  264. * @{method:getCommitMessageKey} and
  265. * @{method:setValueFromParsedCommitMessage}.
  266. *
  267. * @return bool True if this field appears in commit messages in any capacity.
  268. * @task commit
  269. */
  270. public function shouldAppearOnCommitMessage() {
  271. return false;
  272. }
  273. /**
  274. * Key which identifies this field in parsed commit messages. Commit messages
  275. * exist in two forms: raw textual commit messages and parsed dictionaries of
  276. * fields. This method must return a unique string which identifies this field
  277. * in dictionaries. Principally, this dictionary is shipped to and from arc
  278. * over Conduit. Keys should be appropriate property names, like "testPlan"
  279. * (not "Test Plan") and must be globally unique.
  280. *
  281. * You must implement this method if you return true from
  282. * @{method:shouldAppearOnCommitMessage}.
  283. *
  284. * @return string Key which identifies the field in dictionaries.
  285. * @task commit
  286. */
  287. public function getCommitMessageKey() {
  288. throw new DifferentialFieldSpecificationIncompleteException($this);
  289. }
  290. /**
  291. * Set this field's value from a value in a parsed commit message dictionary.
  292. * Afterward, this field will go through the normal write workflows and the
  293. * change will be permanently stored via either the storage mechanisms (if
  294. * your field implements them), revision write hooks (if your field implements
  295. * them) or discarded (if your field implements neither, e.g. is just a
  296. * display field).
  297. *
  298. * The value you receive will either be null or something you originally
  299. * returned from @{method:parseValueFromCommitMessage}.
  300. *
  301. * You must implement this method if you return true from
  302. * @{method:shouldAppearOnCommitMessage}.
  303. *
  304. * @param mixed Field value from a parsed commit message dictionary.
  305. * @return this
  306. * @task commit
  307. */
  308. public function setValueFromParsedCommitMessage($value) {
  309. throw new DifferentialFieldSpecificationIncompleteException($this);
  310. }
  311. /**
  312. * In revision control systems which read revision information from the
  313. * working copy, the user may edit the commit message outside of invoking
  314. * "arc diff --edit". When they do this, only some fields (those fields which
  315. * can not be edited by other users) are safe to overwrite. For instance, it
  316. * is fine to overwrite "Summary" because no one else can edit it, but not
  317. * to overwrite "Reviewers" because reviewers may have been added or removed
  318. * via the web interface.
  319. *
  320. * If a field is safe to overwrite when edited in a working copy commit
  321. * message, return true. If the authoritative value should always be used,
  322. * return false. By default, fields can not be overwritten.
  323. *
  324. * @return bool True to indicate the field is save to overwrite.
  325. * @task commit
  326. */
  327. public function shouldOverwriteWhenCommitMessageIsEdited() {
  328. return false;
  329. }
  330. /**
  331. * Return true if this field should be suggested to the user during
  332. * "arc diff --edit". Basicially, return true if the field is something the
  333. * user might want to fill out (like "Summary"), and false if it's a
  334. * system/display/readonly field (like "Differential Revision"). If this
  335. * method returns true, the field will be rendered even if it has no value
  336. * during edit and update operations.
  337. *
  338. * @return bool True to indicate the field should appear in the edit template.
  339. * @task commit
  340. */
  341. public function shouldAppearOnCommitMessageTemplate() {
  342. return true;
  343. }
  344. /**
  345. * Render a human-readable label for this field, like "Summary" or
  346. * "Test Plan". This is distinct from the commit message key, but generally
  347. * they should be similar.
  348. *
  349. * @return string Human-readable field label for commit messages.
  350. * @task commit
  351. */
  352. public function renderLabelForCommitMessage() {
  353. throw new DifferentialFieldSpecificationIncompleteException($this);
  354. }
  355. /**
  356. * Render a human-readable value for this field when it appears in commit
  357. * messages (for instance, lists of users should be rendered as user names).
  358. *
  359. * The ##$is_edit## parameter allows you to distinguish between commit
  360. * messages being rendered for editing and those being rendered for amending
  361. * or commit. Some fields may decline to render a value in one mode (for
  362. * example, "Reviewed By" appears only when doing commit/amend, not while
  363. * editing).
  364. *
  365. * @param bool True if the message is being edited.
  366. * @return string Human-readable field value.
  367. * @task commit
  368. */
  369. public function renderValueForCommitMessage($is_edit) {
  370. throw new DifferentialFieldSpecificationIncompleteException($this);
  371. }
  372. /**
  373. * Return one or more labels which this field parses in commit messages. For
  374. * example, you might parse all of "Task", "Tasks" and "Task Numbers" or
  375. * similar. This is just to make it easier to get commit messages to parse
  376. * when users are typing in the fields manually as opposed to using a
  377. * template, by accepting alternate spellings / pluralizations / etc. By
  378. * default, only the label returned from @{method:renderLabelForCommitMessage}
  379. * is parsed.
  380. *
  381. * @return list List of supported labels that this field can parse from commit
  382. * messages.
  383. * @task commit
  384. */
  385. public function getSupportedCommitMessageLabels() {
  386. return array($this->renderLabelForCommitMessage());
  387. }
  388. /**
  389. * Parse a raw text block from a commit message into a canonical
  390. * representation of the field value. For example, the "CC" field accepts a
  391. * comma-delimited list of usernames and emails and parses them into valid
  392. * PHIDs, emitting a PHID list.
  393. *
  394. * If you encounter errors (like a nonexistent username) while parsing,
  395. * you should throw a @{class:DifferentialFieldParseException}.
  396. *
  397. * Generally, this method should accept whatever you return from
  398. * @{method:renderValueForCommitMessage} and parse it back into a sensible
  399. * representation.
  400. *
  401. * You must implement this method if you return true from
  402. * @{method:shouldAppearOnCommitMessage}.
  403. *
  404. * @param string
  405. * @return mixed The canonical representation of the field value. For example,
  406. * you should lookup usernames and object references.
  407. * @task commit
  408. */
  409. public function parseValueFromCommitMessage($value) {
  410. throw new DifferentialFieldSpecificationIncompleteException($this);
  411. }
  412. /* -( Loading Additional Data )-------------------------------------------- */
  413. /**
  414. * Specify which @{class:PhabricatorObjectHandles} need to be loaded for your
  415. * field to render correctly.
  416. *
  417. * This is a convenience method which makes the handles available on all
  418. * interfaces where the field appears. If your field needs handles on only
  419. * some interfaces (or needs different handles on different interfaces) you
  420. * can overload the more specific methods to customize which interfaces you
  421. * retrieve handles for. Requesting only the handles you need will improve
  422. * the performance of your field.
  423. *
  424. * You can later retrieve these handles by calling @{method:getHandle}.
  425. *
  426. * @return list List of PHIDs to load handles for.
  427. * @task load
  428. */
  429. protected function getRequiredHandlePHIDs() {
  430. return array();
  431. }
  432. /**
  433. * Specify which @{class:PhabricatorObjectHandles} need to be loaded for your
  434. * field to render correctly on the view interface.
  435. *
  436. * This is a more specific version of @{method:getRequiredHandlePHIDs} which
  437. * can be overridden to improve field performance by loading only data you
  438. * need.
  439. *
  440. * @return list List of PHIDs to load handles for.
  441. * @task load
  442. */
  443. public function getRequiredHandlePHIDsForRevisionView() {
  444. return $this->getRequiredHandlePHIDs();
  445. }
  446. /**
  447. * Specify which @{class:PhabricatorObjectHandles} need to be loaded for your
  448. * field to render correctly on the edit interface.
  449. *
  450. * This is a more specific version of @{method:getRequiredHandlePHIDs} which
  451. * can be overridden to improve field performance by loading only data you
  452. * need.
  453. *
  454. * @return list List of PHIDs to load handles for.
  455. * @task load
  456. */
  457. public function getRequiredHandlePHIDsForRevisionEdit() {
  458. return $this->getRequiredHandlePHIDs();
  459. }
  460. /**
  461. * Specify which @{class:PhabricatorObjectHandles} need to be loaded for your
  462. * field to render correctly on the commit message interface.
  463. *
  464. * This is a more specific version of @{method:getRequiredHandlePHIDs} which
  465. * can be overridden to improve field performance by loading only data you
  466. * need.
  467. *
  468. * @return list List of PHIDs to load handles for.
  469. * @task load
  470. */
  471. public function getRequiredHandlePHIDsForCommitMessage() {
  472. return $this->getRequiredHandlePHIDs();
  473. }
  474. /**
  475. * Specify which diff properties this field needs to load.
  476. *
  477. * @return list List of diff property keys this field requires.
  478. * @task load
  479. */
  480. public function getRequiredDiffProperties() {
  481. return array();
  482. }
  483. /**
  484. * Parse a list of users into a canonical PHID list.
  485. *
  486. * @param string Raw list of comma-separated user names.
  487. * @return list List of corresponding PHIDs.
  488. * @task load
  489. */
  490. protected function parseCommitMessageUserList($value) {
  491. return $this->parseCommitMessageObjectList($value, $mailables = false);
  492. }
  493. /**
  494. * Parse a list of mailable objects into a canonical PHID list.
  495. *
  496. * @param string Raw list of comma-separated mailable names.
  497. * @return list List of corresponding PHIDs.
  498. * @task load
  499. */
  500. protected function parseCommitMessageMailableList($value) {
  501. return $this->parseCommitMessageObjectList($value, $mailables = true);
  502. }
  503. /**
  504. * Parse and lookup a list of object names, converting them to PHIDs.
  505. *
  506. * @param string Raw list of comma-separated object names.
  507. * @return list List of corresponding PHIDs.
  508. * @task load
  509. */
  510. private function parseCommitMessageObjectList($value, $include_mailables) {
  511. $value = array_unique(array_filter(preg_split('/[\s,]+/', $value)));
  512. if (!$value) {
  513. return array();
  514. }
  515. $object_map = array();
  516. $users = id(new PhabricatorUser())->loadAllWhere(
  517. '((username IN (%Ls)) OR (email IN (%Ls)))
  518. AND isDisabled = 0
  519. AND isSystemAgent = 0',
  520. $value,
  521. $value);
  522. $user_map = mpull($users, 'getPHID', 'getUsername');
  523. foreach ($user_map as $username => $phid) {
  524. // Usernames may have uppercase letters in them. Put both names in the
  525. // map so we can try the original case first, so that username *always*
  526. // works in weird edge cases where some other mailable object collides.
  527. $object_map[$username] = $phid;
  528. $object_map[strtolower($username)] = $phid;
  529. }
  530. $object_map += mpull($users, 'getPHID', 'getEmail');
  531. if ($include_mailables) {
  532. $mailables = id(new PhabricatorMetaMTAMailingList())->loadAllWhere(
  533. '(email IN (%Ls)) OR (name IN (%Ls))',
  534. $value,
  535. $value);
  536. $object_map += mpull($mailables, 'getPHID', 'getName');
  537. $object_map += mpull($mailables, 'getPHID', 'getEmail');
  538. }
  539. $invalid = array();
  540. $results = array();
  541. foreach ($value as $name) {
  542. if (empty($object_map[$name])) {
  543. if (empty($object_map[strtolower($name)])) {
  544. $invalid[] = $name;
  545. } else {
  546. $results[] = $object_map[strtolower($name)];
  547. }
  548. } else {
  549. $results[] = $object_map[$name];
  550. }
  551. }
  552. if ($invalid) {
  553. $invalid = implode(', ', $invalid);
  554. $what = $include_mailables
  555. ? "users and mailing lists"
  556. : "users";
  557. throw new DifferentialFieldParseException(
  558. "Commit message references disabled or nonexistent {$what}: ".
  559. "{$invalid}.");
  560. }
  561. return array_unique($results);
  562. }
  563. /* -( Contextual Data )---------------------------------------------------- */
  564. /**
  565. * @task context
  566. */
  567. final public function setRevision(DifferentialRevision $revision) {
  568. $this->revision = $revision;
  569. $this->didSetRevision();
  570. return $this;
  571. }
  572. /**
  573. * @task context
  574. */
  575. protected function didSetRevision() {
  576. return;
  577. }
  578. /**
  579. * @task context
  580. */
  581. final public function setDiff(DifferentialDiff $diff) {
  582. $this->diff = $diff;
  583. return $this;
  584. }
  585. /**
  586. * @task context
  587. */
  588. final public function setHandles(array $handles) {
  589. $this->handles = $handles;
  590. return $this;
  591. }
  592. /**
  593. * @task context
  594. */
  595. final public function setDiffProperties(array $diff_properties) {
  596. $this->diffProperties = $diff_properties;
  597. return $this;
  598. }
  599. /**
  600. * @task context
  601. */
  602. final public function setUser(PhabricatorUser $user) {
  603. $this->user = $user;
  604. return $this;
  605. }
  606. /**
  607. * @task context
  608. */
  609. final protected function getRevision() {
  610. if (empty($this->revision)) {
  611. throw new DifferentialFieldDataNotAvailableException($this);
  612. }
  613. return $this->revision;
  614. }
  615. /**
  616. * @task context
  617. */
  618. final protected function getDiff() {
  619. if (empty($this->diff)) {
  620. throw new DifferentialFieldDataNotAvailableException($this);
  621. }
  622. return $this->diff;
  623. }
  624. /**
  625. * @task context
  626. */
  627. final protected function getUser() {
  628. if (empty($this->user)) {
  629. throw new DifferentialFieldDataNotAvailableException($this);
  630. }
  631. return $this->user;
  632. }
  633. /**
  634. * Get the handle for an object PHID. You must overload
  635. * @{method:getRequiredHandlePHIDs} (or a more specific version thereof)
  636. * and include the PHID you want in the list for it to be available here.
  637. *
  638. * @return PhabricatorObjectHandle Handle to the object.
  639. * @task context
  640. */
  641. final protected function getHandle($phid) {
  642. if ($this->handles === null) {
  643. throw new DifferentialFieldDataNotAvailableException($this);
  644. }
  645. if (empty($this->handles[$phid])) {
  646. $class = get_class($this);
  647. throw new Exception(
  648. "A differential field (of class '{$class}') is attempting to retrieve ".
  649. "a handle ('{$phid}') which it did not request. Return all handle ".
  650. "PHIDs you need from getRequiredHandlePHIDs().");
  651. }
  652. return $this->handles[$phid];
  653. }
  654. /**
  655. * Get a diff property which this field previously requested by returning
  656. * the key from @{method:getRequiredDiffProperties}.
  657. *
  658. * @param string Diff property key.
  659. * @return string|null Diff property, or null if the property does not have
  660. * a value.
  661. * @task context
  662. */
  663. final public function getDiffProperty($key) {
  664. if ($this->diffProperties === null) {
  665. // This will be set to some (possibly empty) array if we've loaded
  666. // properties, so null means diff properties aren't available in this
  667. // context.
  668. throw new DifferentialFieldDataNotAvailableException($this);
  669. }
  670. if (!array_key_exists($key, $this->diffProperties)) {
  671. $class = get_class($this);
  672. throw new Exception(
  673. "A differential field (of class '{$class}') is attempting to retrieve ".
  674. "a diff property ('{$key}') which it did not request. Return all ".
  675. "diff property keys you need from getRequiredDiffProperties().");
  676. }
  677. return $this->diffProperties[$key];
  678. }
  679. }