Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/customfield/field/PhabricatorCustomField.php
12242 views
1
<?php
2
3
/**
4
* @task apps Building Applications with Custom Fields
5
* @task core Core Properties and Field Identity
6
* @task proxy Field Proxies
7
* @task context Contextual Data
8
* @task render Rendering Utilities
9
* @task storage Field Storage
10
* @task edit Integration with Edit Views
11
* @task view Integration with Property Views
12
* @task list Integration with List views
13
* @task appsearch Integration with ApplicationSearch
14
* @task appxaction Integration with ApplicationTransactions
15
* @task xactionmail Integration with Transaction Mail
16
* @task globalsearch Integration with Global Search
17
* @task herald Integration with Herald
18
*/
19
abstract class PhabricatorCustomField extends Phobject {
20
21
private $viewer;
22
private $object;
23
private $proxy;
24
25
const ROLE_APPLICATIONTRANSACTIONS = 'ApplicationTransactions';
26
const ROLE_TRANSACTIONMAIL = 'ApplicationTransactions.mail';
27
const ROLE_APPLICATIONSEARCH = 'ApplicationSearch';
28
const ROLE_STORAGE = 'storage';
29
const ROLE_DEFAULT = 'default';
30
const ROLE_EDIT = 'edit';
31
const ROLE_VIEW = 'view';
32
const ROLE_LIST = 'list';
33
const ROLE_GLOBALSEARCH = 'GlobalSearch';
34
const ROLE_CONDUIT = 'conduit';
35
const ROLE_HERALD = 'herald';
36
const ROLE_EDITENGINE = 'EditEngine';
37
const ROLE_HERALDACTION = 'herald.action';
38
const ROLE_EXPORT = 'export';
39
40
41
/* -( Building Applications with Custom Fields )--------------------------- */
42
43
44
/**
45
* @task apps
46
*/
47
public static function getObjectFields(
48
PhabricatorCustomFieldInterface $object,
49
$role) {
50
51
try {
52
$attachment = $object->getCustomFields();
53
} catch (PhabricatorDataNotAttachedException $ex) {
54
$attachment = new PhabricatorCustomFieldAttachment();
55
$object->attachCustomFields($attachment);
56
}
57
58
try {
59
$field_list = $attachment->getCustomFieldList($role);
60
} catch (PhabricatorCustomFieldNotAttachedException $ex) {
61
$base_class = $object->getCustomFieldBaseClass();
62
63
$spec = $object->getCustomFieldSpecificationForRole($role);
64
if (!is_array($spec)) {
65
throw new Exception(
66
pht(
67
"Expected an array from %s for object of class '%s'.",
68
'getCustomFieldSpecificationForRole()',
69
get_class($object)));
70
}
71
72
$fields = self::buildFieldList(
73
$base_class,
74
$spec,
75
$object);
76
77
$fields = self::adjustCustomFieldsForObjectSubtype(
78
$object,
79
$role,
80
$fields);
81
82
foreach ($fields as $key => $field) {
83
// NOTE: We perform this filtering in "buildFieldList()", but may need
84
// to filter again after subtype adjustment.
85
if (!$field->isFieldEnabled()) {
86
unset($fields[$key]);
87
continue;
88
}
89
90
if (!$field->shouldEnableForRole($role)) {
91
unset($fields[$key]);
92
continue;
93
}
94
}
95
96
foreach ($fields as $field) {
97
$field->setObject($object);
98
}
99
100
$field_list = new PhabricatorCustomFieldList($fields);
101
$attachment->addCustomFieldList($role, $field_list);
102
}
103
104
return $field_list;
105
}
106
107
108
/**
109
* @task apps
110
*/
111
public static function getObjectField(
112
PhabricatorCustomFieldInterface $object,
113
$role,
114
$field_key) {
115
116
$fields = self::getObjectFields($object, $role)->getFields();
117
118
return idx($fields, $field_key);
119
}
120
121
122
/**
123
* @task apps
124
*/
125
public static function buildFieldList(
126
$base_class,
127
array $spec,
128
$object,
129
array $options = array()) {
130
131
$field_objects = id(new PhutilClassMapQuery())
132
->setAncestorClass($base_class)
133
->execute();
134
135
$fields = array();
136
foreach ($field_objects as $field_object) {
137
$field_object = clone $field_object;
138
foreach ($field_object->createFields($object) as $field) {
139
$key = $field->getFieldKey();
140
if (isset($fields[$key])) {
141
throw new Exception(
142
pht(
143
"Both '%s' and '%s' define a custom field with ".
144
"field key '%s'. Field keys must be unique.",
145
get_class($fields[$key]),
146
get_class($field),
147
$key));
148
}
149
$fields[$key] = $field;
150
}
151
}
152
153
foreach ($fields as $key => $field) {
154
if (!$field->isFieldEnabled()) {
155
unset($fields[$key]);
156
}
157
}
158
159
$fields = array_select_keys($fields, array_keys($spec)) + $fields;
160
161
if (empty($options['withDisabled'])) {
162
foreach ($fields as $key => $field) {
163
if (isset($spec[$key]['disabled'])) {
164
$is_disabled = $spec[$key]['disabled'];
165
} else {
166
$is_disabled = $field->shouldDisableByDefault();
167
}
168
169
if ($is_disabled) {
170
if ($field->canDisableField()) {
171
unset($fields[$key]);
172
}
173
}
174
}
175
}
176
177
return $fields;
178
}
179
180
181
/* -( Core Properties and Field Identity )--------------------------------- */
182
183
184
/**
185
* Return a key which uniquely identifies this field, like
186
* "mycompany:dinosaur:count". Normally you should provide some level of
187
* namespacing to prevent collisions.
188
*
189
* @return string String which uniquely identifies this field.
190
* @task core
191
*/
192
public function getFieldKey() {
193
if ($this->proxy) {
194
return $this->proxy->getFieldKey();
195
}
196
throw new PhabricatorCustomFieldImplementationIncompleteException(
197
$this,
198
$field_key_is_incomplete = true);
199
}
200
201
public function getModernFieldKey() {
202
if ($this->proxy) {
203
return $this->proxy->getModernFieldKey();
204
}
205
return $this->getFieldKey();
206
}
207
208
209
/**
210
* Return a human-readable field name.
211
*
212
* @return string Human readable field name.
213
* @task core
214
*/
215
public function getFieldName() {
216
if ($this->proxy) {
217
return $this->proxy->getFieldName();
218
}
219
return $this->getModernFieldKey();
220
}
221
222
223
/**
224
* Return a short, human-readable description of the field's behavior. This
225
* provides more context to administrators when they are customizing fields.
226
*
227
* @return string|null Optional human-readable description.
228
* @task core
229
*/
230
public function getFieldDescription() {
231
if ($this->proxy) {
232
return $this->proxy->getFieldDescription();
233
}
234
return null;
235
}
236
237
238
/**
239
* Most field implementations are unique, in that one class corresponds to
240
* one field. However, some field implementations are general and a single
241
* implementation may drive several fields.
242
*
243
* For general implementations, the general field implementation can return
244
* multiple field instances here.
245
*
246
* @param object The object to create fields for.
247
* @return list<PhabricatorCustomField> List of fields.
248
* @task core
249
*/
250
public function createFields($object) {
251
return array($this);
252
}
253
254
255
/**
256
* You can return `false` here if the field should not be enabled for any
257
* role. For example, it might depend on something (like an application or
258
* library) which isn't installed, or might have some global configuration
259
* which allows it to be disabled.
260
*
261
* @return bool False to completely disable this field for all roles.
262
* @task core
263
*/
264
public function isFieldEnabled() {
265
if ($this->proxy) {
266
return $this->proxy->isFieldEnabled();
267
}
268
return true;
269
}
270
271
272
/**
273
* Low level selector for field availability. Fields can appear in different
274
* roles (like an edit view, a list view, etc.), but not every field needs
275
* to appear everywhere. Fields that are disabled in a role won't appear in
276
* that context within applications.
277
*
278
* Normally, you do not need to override this method. Instead, override the
279
* methods specific to roles you want to enable. For example, implement
280
* @{method:shouldUseStorage()} to activate the `'storage'` role.
281
*
282
* @return bool True to enable the field for the given role.
283
* @task core
284
*/
285
public function shouldEnableForRole($role) {
286
287
// NOTE: All of these calls proxy individually, so we don't need to
288
// proxy this call as a whole.
289
290
switch ($role) {
291
case self::ROLE_APPLICATIONTRANSACTIONS:
292
return $this->shouldAppearInApplicationTransactions();
293
case self::ROLE_APPLICATIONSEARCH:
294
return $this->shouldAppearInApplicationSearch();
295
case self::ROLE_STORAGE:
296
return $this->shouldUseStorage();
297
case self::ROLE_EDIT:
298
return $this->shouldAppearInEditView();
299
case self::ROLE_VIEW:
300
return $this->shouldAppearInPropertyView();
301
case self::ROLE_LIST:
302
return $this->shouldAppearInListView();
303
case self::ROLE_GLOBALSEARCH:
304
return $this->shouldAppearInGlobalSearch();
305
case self::ROLE_CONDUIT:
306
return $this->shouldAppearInConduitDictionary();
307
case self::ROLE_TRANSACTIONMAIL:
308
return $this->shouldAppearInTransactionMail();
309
case self::ROLE_HERALD:
310
return $this->shouldAppearInHerald();
311
case self::ROLE_HERALDACTION:
312
return $this->shouldAppearInHeraldActions();
313
case self::ROLE_EDITENGINE:
314
return $this->shouldAppearInEditView() ||
315
$this->shouldAppearInEditEngine();
316
case self::ROLE_EXPORT:
317
return $this->shouldAppearInDataExport();
318
case self::ROLE_DEFAULT:
319
return true;
320
default:
321
throw new Exception(pht("Unknown field role '%s'!", $role));
322
}
323
}
324
325
326
/**
327
* Allow administrators to disable this field. Most fields should allow this,
328
* but some are fundamental to the behavior of the application and can be
329
* locked down to avoid chaos, disorder, and the decline of civilization.
330
*
331
* @return bool False to prevent this field from being disabled through
332
* configuration.
333
* @task core
334
*/
335
public function canDisableField() {
336
return true;
337
}
338
339
public function shouldDisableByDefault() {
340
return false;
341
}
342
343
344
/**
345
* Return an index string which uniquely identifies this field.
346
*
347
* @return string Index string which uniquely identifies this field.
348
* @task core
349
*/
350
final public function getFieldIndex() {
351
return PhabricatorHash::digestForIndex($this->getFieldKey());
352
}
353
354
355
/* -( Field Proxies )------------------------------------------------------ */
356
357
358
/**
359
* Proxies allow a field to use some other field's implementation for most
360
* of their behavior while still subclassing an application field. When a
361
* proxy is set for a field with @{method:setProxy}, all of its methods will
362
* call through to the proxy by default.
363
*
364
* This is most commonly used to implement configuration-driven custom fields
365
* using @{class:PhabricatorStandardCustomField}.
366
*
367
* This method must be overridden to return `true` before a field can accept
368
* proxies.
369
*
370
* @return bool True if you can @{method:setProxy} this field.
371
* @task proxy
372
*/
373
public function canSetProxy() {
374
if ($this instanceof PhabricatorStandardCustomFieldInterface) {
375
return true;
376
}
377
return false;
378
}
379
380
381
/**
382
* Set the proxy implementation for this field. See @{method:canSetProxy} for
383
* discussion of field proxies.
384
*
385
* @param PhabricatorCustomField Field implementation.
386
* @return this
387
*/
388
final public function setProxy(PhabricatorCustomField $proxy) {
389
if (!$this->canSetProxy()) {
390
throw new PhabricatorCustomFieldNotProxyException($this);
391
}
392
393
$this->proxy = $proxy;
394
return $this;
395
}
396
397
398
/**
399
* Get the field's proxy implementation, if any. For discussion, see
400
* @{method:canSetProxy}.
401
*
402
* @return PhabricatorCustomField|null Proxy field, if one is set.
403
*/
404
final public function getProxy() {
405
return $this->proxy;
406
}
407
408
409
/* -( Contextual Data )---------------------------------------------------- */
410
411
412
/**
413
* Sets the object this field belongs to.
414
*
415
* @param PhabricatorCustomFieldInterface The object this field belongs to.
416
* @return this
417
* @task context
418
*/
419
final public function setObject(PhabricatorCustomFieldInterface $object) {
420
if ($this->proxy) {
421
$this->proxy->setObject($object);
422
return $this;
423
}
424
425
$this->object = $object;
426
$this->didSetObject($object);
427
return $this;
428
}
429
430
431
/**
432
* Read object data into local field storage, if applicable.
433
*
434
* @param PhabricatorCustomFieldInterface The object this field belongs to.
435
* @return this
436
* @task context
437
*/
438
public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
439
if ($this->proxy) {
440
$this->proxy->readValueFromObject($object);
441
}
442
return $this;
443
}
444
445
446
/**
447
* Get the object this field belongs to.
448
*
449
* @return PhabricatorCustomFieldInterface The object this field belongs to.
450
* @task context
451
*/
452
final public function getObject() {
453
if ($this->proxy) {
454
return $this->proxy->getObject();
455
}
456
457
return $this->object;
458
}
459
460
461
/**
462
* This is a hook, primarily for subclasses to load object data.
463
*
464
* @return PhabricatorCustomFieldInterface The object this field belongs to.
465
* @return void
466
*/
467
protected function didSetObject(PhabricatorCustomFieldInterface $object) {
468
return;
469
}
470
471
472
/**
473
* @task context
474
*/
475
final public function setViewer(PhabricatorUser $viewer) {
476
if ($this->proxy) {
477
$this->proxy->setViewer($viewer);
478
return $this;
479
}
480
481
$this->viewer = $viewer;
482
return $this;
483
}
484
485
486
/**
487
* @task context
488
*/
489
final public function getViewer() {
490
if ($this->proxy) {
491
return $this->proxy->getViewer();
492
}
493
494
return $this->viewer;
495
}
496
497
498
/**
499
* @task context
500
*/
501
final protected function requireViewer() {
502
if ($this->proxy) {
503
return $this->proxy->requireViewer();
504
}
505
506
if (!$this->viewer) {
507
throw new PhabricatorCustomFieldDataNotAvailableException($this);
508
}
509
return $this->viewer;
510
}
511
512
513
/* -( Rendering Utilities )------------------------------------------------ */
514
515
516
/**
517
* @task render
518
*/
519
protected function renderHandleList(array $handles) {
520
if (!$handles) {
521
return null;
522
}
523
524
$out = array();
525
foreach ($handles as $handle) {
526
$out[] = $handle->renderHovercardLink();
527
}
528
529
return phutil_implode_html(phutil_tag('br'), $out);
530
}
531
532
533
/* -( Storage )------------------------------------------------------------ */
534
535
536
/**
537
* Return true to use field storage.
538
*
539
* Fields which can be edited by the user will most commonly use storage,
540
* while some other types of fields (for instance, those which just display
541
* information in some stylized way) may not. Many builtin fields do not use
542
* storage because their data is available on the object itself.
543
*
544
* If you implement this, you must also implement @{method:getValueForStorage}
545
* and @{method:setValueFromStorage}.
546
*
547
* @return bool True to use storage.
548
* @task storage
549
*/
550
public function shouldUseStorage() {
551
if ($this->proxy) {
552
return $this->proxy->shouldUseStorage();
553
}
554
return false;
555
}
556
557
558
/**
559
* Return a new, empty storage object. This should be a subclass of
560
* @{class:PhabricatorCustomFieldStorage} which is bound to the application's
561
* database.
562
*
563
* @return PhabricatorCustomFieldStorage New empty storage object.
564
* @task storage
565
*/
566
public function newStorageObject() {
567
// NOTE: This intentionally isn't proxied, to avoid call cycles.
568
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
569
}
570
571
572
/**
573
* Return a serialized representation of the field value, appropriate for
574
* storing in auxiliary field storage. You must implement this method if
575
* you implement @{method:shouldUseStorage}.
576
*
577
* If the field value is a scalar, it can be returned unmodiifed. If not,
578
* it should be serialized (for example, using JSON).
579
*
580
* @return string Serialized field value.
581
* @task storage
582
*/
583
public function getValueForStorage() {
584
if ($this->proxy) {
585
return $this->proxy->getValueForStorage();
586
}
587
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
588
}
589
590
591
/**
592
* Set the field's value given a serialized storage value. This is called
593
* when the field is loaded; if no data is available, the value will be
594
* null. You must implement this method if you implement
595
* @{method:shouldUseStorage}.
596
*
597
* Usually, the value can be loaded directly. If it isn't a scalar, you'll
598
* need to undo whatever serialization you applied in
599
* @{method:getValueForStorage}.
600
*
601
* @param string|null Serialized field representation (from
602
* @{method:getValueForStorage}) or null if no value has
603
* ever been stored.
604
* @return this
605
* @task storage
606
*/
607
public function setValueFromStorage($value) {
608
if ($this->proxy) {
609
return $this->proxy->setValueFromStorage($value);
610
}
611
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
612
}
613
614
public function didSetValueFromStorage() {
615
if ($this->proxy) {
616
return $this->proxy->didSetValueFromStorage();
617
}
618
return $this;
619
}
620
621
622
/* -( ApplicationSearch )-------------------------------------------------- */
623
624
625
/**
626
* Appearing in ApplicationSearch allows a field to be indexed and searched
627
* for.
628
*
629
* @return bool True to appear in ApplicationSearch.
630
* @task appsearch
631
*/
632
public function shouldAppearInApplicationSearch() {
633
if ($this->proxy) {
634
return $this->proxy->shouldAppearInApplicationSearch();
635
}
636
return false;
637
}
638
639
640
/**
641
* Return one or more indexes which this field can meaningfully query against
642
* to implement ApplicationSearch.
643
*
644
* Normally, you should build these using @{method:newStringIndex} and
645
* @{method:newNumericIndex}. For example, if a field holds a numeric value
646
* it might return a single numeric index:
647
*
648
* return array($this->newNumericIndex($this->getValue()));
649
*
650
* If a field holds a more complex value (like a list of users), it might
651
* return several string indexes:
652
*
653
* $indexes = array();
654
* foreach ($this->getValue() as $phid) {
655
* $indexes[] = $this->newStringIndex($phid);
656
* }
657
* return $indexes;
658
*
659
* @return list<PhabricatorCustomFieldIndexStorage> List of indexes.
660
* @task appsearch
661
*/
662
public function buildFieldIndexes() {
663
if ($this->proxy) {
664
return $this->proxy->buildFieldIndexes();
665
}
666
return array();
667
}
668
669
670
/**
671
* Return an index against which this field can be meaningfully ordered
672
* against to implement ApplicationSearch.
673
*
674
* This should be a single index, normally built using
675
* @{method:newStringIndex} and @{method:newNumericIndex}.
676
*
677
* The value of the index is not used.
678
*
679
* Return null from this method if the field can not be ordered.
680
*
681
* @return PhabricatorCustomFieldIndexStorage A single index to order by.
682
* @task appsearch
683
*/
684
public function buildOrderIndex() {
685
if ($this->proxy) {
686
return $this->proxy->buildOrderIndex();
687
}
688
return null;
689
}
690
691
692
/**
693
* Build a new empty storage object for storing string indexes. Normally,
694
* this should be a concrete subclass of
695
* @{class:PhabricatorCustomFieldStringIndexStorage}.
696
*
697
* @return PhabricatorCustomFieldStringIndexStorage Storage object.
698
* @task appsearch
699
*/
700
protected function newStringIndexStorage() {
701
// NOTE: This intentionally isn't proxied, to avoid call cycles.
702
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
703
}
704
705
706
/**
707
* Build a new empty storage object for storing string indexes. Normally,
708
* this should be a concrete subclass of
709
* @{class:PhabricatorCustomFieldStringIndexStorage}.
710
*
711
* @return PhabricatorCustomFieldStringIndexStorage Storage object.
712
* @task appsearch
713
*/
714
protected function newNumericIndexStorage() {
715
// NOTE: This intentionally isn't proxied, to avoid call cycles.
716
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
717
}
718
719
720
/**
721
* Build and populate storage for a string index.
722
*
723
* @param string String to index.
724
* @return PhabricatorCustomFieldStringIndexStorage Populated storage.
725
* @task appsearch
726
*/
727
protected function newStringIndex($value) {
728
if ($this->proxy) {
729
return $this->proxy->newStringIndex();
730
}
731
732
$key = $this->getFieldIndex();
733
return $this->newStringIndexStorage()
734
->setIndexKey($key)
735
->setIndexValue($value);
736
}
737
738
739
/**
740
* Build and populate storage for a numeric index.
741
*
742
* @param string Numeric value to index.
743
* @return PhabricatorCustomFieldNumericIndexStorage Populated storage.
744
* @task appsearch
745
*/
746
protected function newNumericIndex($value) {
747
if ($this->proxy) {
748
return $this->proxy->newNumericIndex();
749
}
750
$key = $this->getFieldIndex();
751
return $this->newNumericIndexStorage()
752
->setIndexKey($key)
753
->setIndexValue($value);
754
}
755
756
757
/**
758
* Read a query value from a request, for storage in a saved query. Normally,
759
* this method should, e.g., read a string out of the request.
760
*
761
* @param PhabricatorApplicationSearchEngine Engine building the query.
762
* @param AphrontRequest Request to read from.
763
* @return wild
764
* @task appsearch
765
*/
766
public function readApplicationSearchValueFromRequest(
767
PhabricatorApplicationSearchEngine $engine,
768
AphrontRequest $request) {
769
if ($this->proxy) {
770
return $this->proxy->readApplicationSearchValueFromRequest(
771
$engine,
772
$request);
773
}
774
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
775
}
776
777
778
/**
779
* Constrain a query, given a field value. Generally, this method should
780
* use `with...()` methods to apply filters or other constraints to the
781
* query.
782
*
783
* @param PhabricatorApplicationSearchEngine Engine executing the query.
784
* @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
785
* @param wild Constraint provided by the user.
786
* @return void
787
* @task appsearch
788
*/
789
public function applyApplicationSearchConstraintToQuery(
790
PhabricatorApplicationSearchEngine $engine,
791
PhabricatorCursorPagedPolicyAwareQuery $query,
792
$value) {
793
if ($this->proxy) {
794
return $this->proxy->applyApplicationSearchConstraintToQuery(
795
$engine,
796
$query,
797
$value);
798
}
799
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
800
}
801
802
803
/**
804
* Append search controls to the interface.
805
*
806
* @param PhabricatorApplicationSearchEngine Engine constructing the form.
807
* @param AphrontFormView The form to update.
808
* @param wild Value from the saved query.
809
* @return void
810
* @task appsearch
811
*/
812
public function appendToApplicationSearchForm(
813
PhabricatorApplicationSearchEngine $engine,
814
AphrontFormView $form,
815
$value) {
816
if ($this->proxy) {
817
return $this->proxy->appendToApplicationSearchForm(
818
$engine,
819
$form,
820
$value);
821
}
822
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
823
}
824
825
826
/* -( ApplicationTransactions )-------------------------------------------- */
827
828
829
/**
830
* Appearing in ApplicationTrasactions allows a field to be edited using
831
* standard workflows.
832
*
833
* @return bool True to appear in ApplicationTransactions.
834
* @task appxaction
835
*/
836
public function shouldAppearInApplicationTransactions() {
837
if ($this->proxy) {
838
return $this->proxy->shouldAppearInApplicationTransactions();
839
}
840
return false;
841
}
842
843
844
/**
845
* @task appxaction
846
*/
847
public function getApplicationTransactionType() {
848
if ($this->proxy) {
849
return $this->proxy->getApplicationTransactionType();
850
}
851
return PhabricatorTransactions::TYPE_CUSTOMFIELD;
852
}
853
854
855
/**
856
* @task appxaction
857
*/
858
public function getApplicationTransactionMetadata() {
859
if ($this->proxy) {
860
return $this->proxy->getApplicationTransactionMetadata();
861
}
862
return array();
863
}
864
865
866
/**
867
* @task appxaction
868
*/
869
public function getOldValueForApplicationTransactions() {
870
if ($this->proxy) {
871
return $this->proxy->getOldValueForApplicationTransactions();
872
}
873
return $this->getValueForStorage();
874
}
875
876
877
/**
878
* @task appxaction
879
*/
880
public function getNewValueForApplicationTransactions() {
881
if ($this->proxy) {
882
return $this->proxy->getNewValueForApplicationTransactions();
883
}
884
return $this->getValueForStorage();
885
}
886
887
888
/**
889
* @task appxaction
890
*/
891
public function setValueFromApplicationTransactions($value) {
892
if ($this->proxy) {
893
return $this->proxy->setValueFromApplicationTransactions($value);
894
}
895
return $this->setValueFromStorage($value);
896
}
897
898
899
/**
900
* @task appxaction
901
*/
902
public function getNewValueFromApplicationTransactions(
903
PhabricatorApplicationTransaction $xaction) {
904
if ($this->proxy) {
905
return $this->proxy->getNewValueFromApplicationTransactions($xaction);
906
}
907
return $xaction->getNewValue();
908
}
909
910
911
/**
912
* @task appxaction
913
*/
914
public function getApplicationTransactionHasEffect(
915
PhabricatorApplicationTransaction $xaction) {
916
if ($this->proxy) {
917
return $this->proxy->getApplicationTransactionHasEffect($xaction);
918
}
919
return ($xaction->getOldValue() !== $xaction->getNewValue());
920
}
921
922
923
/**
924
* @task appxaction
925
*/
926
public function applyApplicationTransactionInternalEffects(
927
PhabricatorApplicationTransaction $xaction) {
928
if ($this->proxy) {
929
return $this->proxy->applyApplicationTransactionInternalEffects($xaction);
930
}
931
return;
932
}
933
934
935
/**
936
* @task appxaction
937
*/
938
public function getApplicationTransactionRemarkupBlocks(
939
PhabricatorApplicationTransaction $xaction) {
940
if ($this->proxy) {
941
return $this->proxy->getApplicationTransactionRemarkupBlocks($xaction);
942
}
943
return array();
944
}
945
946
947
/**
948
* @task appxaction
949
*/
950
public function applyApplicationTransactionExternalEffects(
951
PhabricatorApplicationTransaction $xaction) {
952
if ($this->proxy) {
953
return $this->proxy->applyApplicationTransactionExternalEffects($xaction);
954
}
955
956
if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) {
957
return;
958
}
959
960
$this->setValueFromApplicationTransactions($xaction->getNewValue());
961
$value = $this->getValueForStorage();
962
963
$table = $this->newStorageObject();
964
$conn_w = $table->establishConnection('w');
965
966
if ($value === null) {
967
queryfx(
968
$conn_w,
969
'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s',
970
$table->getTableName(),
971
$this->getObject()->getPHID(),
972
$this->getFieldIndex());
973
} else {
974
queryfx(
975
$conn_w,
976
'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)
977
VALUES (%s, %s, %s)
978
ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)',
979
$table->getTableName(),
980
$this->getObject()->getPHID(),
981
$this->getFieldIndex(),
982
$value);
983
}
984
985
return;
986
}
987
988
989
/**
990
* Validate transactions for an object. This allows you to raise an error
991
* when a transaction would set a field to an invalid value, or when a field
992
* is required but no transactions provide value.
993
*
994
* @param PhabricatorLiskDAO Editor applying the transactions.
995
* @param string Transaction type. This type is always
996
* `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for
997
* convenience when constructing exceptions.
998
* @param list<PhabricatorApplicationTransaction> Transactions being applied,
999
* which may be empty if this field is not being edited.
1000
* @return list<PhabricatorApplicationTransactionValidationError> Validation
1001
* errors.
1002
*
1003
* @task appxaction
1004
*/
1005
public function validateApplicationTransactions(
1006
PhabricatorApplicationTransactionEditor $editor,
1007
$type,
1008
array $xactions) {
1009
if ($this->proxy) {
1010
return $this->proxy->validateApplicationTransactions(
1011
$editor,
1012
$type,
1013
$xactions);
1014
}
1015
return array();
1016
}
1017
1018
public function getApplicationTransactionTitle(
1019
PhabricatorApplicationTransaction $xaction) {
1020
if ($this->proxy) {
1021
return $this->proxy->getApplicationTransactionTitle(
1022
$xaction);
1023
}
1024
1025
$author_phid = $xaction->getAuthorPHID();
1026
return pht(
1027
'%s updated this object.',
1028
$xaction->renderHandleLink($author_phid));
1029
}
1030
1031
public function getApplicationTransactionTitleForFeed(
1032
PhabricatorApplicationTransaction $xaction) {
1033
if ($this->proxy) {
1034
return $this->proxy->getApplicationTransactionTitleForFeed(
1035
$xaction);
1036
}
1037
1038
$author_phid = $xaction->getAuthorPHID();
1039
$object_phid = $xaction->getObjectPHID();
1040
return pht(
1041
'%s updated %s.',
1042
$xaction->renderHandleLink($author_phid),
1043
$xaction->renderHandleLink($object_phid));
1044
}
1045
1046
1047
public function getApplicationTransactionHasChangeDetails(
1048
PhabricatorApplicationTransaction $xaction) {
1049
if ($this->proxy) {
1050
return $this->proxy->getApplicationTransactionHasChangeDetails(
1051
$xaction);
1052
}
1053
return false;
1054
}
1055
1056
public function getApplicationTransactionChangeDetails(
1057
PhabricatorApplicationTransaction $xaction,
1058
PhabricatorUser $viewer) {
1059
if ($this->proxy) {
1060
return $this->proxy->getApplicationTransactionChangeDetails(
1061
$xaction,
1062
$viewer);
1063
}
1064
return null;
1065
}
1066
1067
public function getApplicationTransactionRequiredHandlePHIDs(
1068
PhabricatorApplicationTransaction $xaction) {
1069
if ($this->proxy) {
1070
return $this->proxy->getApplicationTransactionRequiredHandlePHIDs(
1071
$xaction);
1072
}
1073
return array();
1074
}
1075
1076
public function shouldHideInApplicationTransactions(
1077
PhabricatorApplicationTransaction $xaction) {
1078
if ($this->proxy) {
1079
return $this->proxy->shouldHideInApplicationTransactions($xaction);
1080
}
1081
return false;
1082
}
1083
1084
1085
/* -( Transaction Mail )--------------------------------------------------- */
1086
1087
1088
/**
1089
* @task xactionmail
1090
*/
1091
public function shouldAppearInTransactionMail() {
1092
if ($this->proxy) {
1093
return $this->proxy->shouldAppearInTransactionMail();
1094
}
1095
return false;
1096
}
1097
1098
1099
/**
1100
* @task xactionmail
1101
*/
1102
public function updateTransactionMailBody(
1103
PhabricatorMetaMTAMailBody $body,
1104
PhabricatorApplicationTransactionEditor $editor,
1105
array $xactions) {
1106
if ($this->proxy) {
1107
return $this->proxy->updateTransactionMailBody($body, $editor, $xactions);
1108
}
1109
return;
1110
}
1111
1112
1113
/* -( Edit View )---------------------------------------------------------- */
1114
1115
1116
public function getEditEngineFields(PhabricatorEditEngine $engine) {
1117
$field = $this->newStandardEditField();
1118
1119
return array(
1120
$field,
1121
);
1122
}
1123
1124
protected function newEditField() {
1125
$field = id(new PhabricatorCustomFieldEditField())
1126
->setCustomField($this);
1127
1128
$http_type = $this->getHTTPParameterType();
1129
if ($http_type) {
1130
$field->setCustomFieldHTTPParameterType($http_type);
1131
}
1132
1133
$conduit_type = $this->getConduitEditParameterType();
1134
if ($conduit_type) {
1135
$field->setCustomFieldConduitParameterType($conduit_type);
1136
}
1137
1138
$bulk_type = $this->getBulkParameterType();
1139
if ($bulk_type) {
1140
$field->setCustomFieldBulkParameterType($bulk_type);
1141
}
1142
1143
$comment_action = $this->getCommentAction();
1144
if ($comment_action) {
1145
$field
1146
->setCustomFieldCommentAction($comment_action)
1147
->setCommentActionLabel(
1148
pht(
1149
'Change %s',
1150
$this->getFieldName()));
1151
}
1152
1153
return $field;
1154
}
1155
1156
protected function newStandardEditField() {
1157
if ($this->proxy) {
1158
return $this->proxy->newStandardEditField();
1159
}
1160
1161
if ($this->shouldAppearInEditView()) {
1162
$form_field = true;
1163
} else {
1164
$form_field = false;
1165
}
1166
1167
$bulk_label = $this->getBulkEditLabel();
1168
1169
return $this->newEditField()
1170
->setKey($this->getFieldKey())
1171
->setEditTypeKey($this->getModernFieldKey())
1172
->setLabel($this->getFieldName())
1173
->setBulkEditLabel($bulk_label)
1174
->setDescription($this->getFieldDescription())
1175
->setTransactionType($this->getApplicationTransactionType())
1176
->setIsFormField($form_field)
1177
->setValue($this->getNewValueForApplicationTransactions());
1178
}
1179
1180
protected function getBulkEditLabel() {
1181
if ($this->proxy) {
1182
return $this->proxy->getBulkEditLabel();
1183
}
1184
1185
return pht('Set "%s" to', $this->getFieldName());
1186
}
1187
1188
public function getBulkParameterType() {
1189
return $this->newBulkParameterType();
1190
}
1191
1192
protected function newBulkParameterType() {
1193
if ($this->proxy) {
1194
return $this->proxy->newBulkParameterType();
1195
}
1196
return null;
1197
}
1198
1199
protected function getHTTPParameterType() {
1200
if ($this->proxy) {
1201
return $this->proxy->getHTTPParameterType();
1202
}
1203
return null;
1204
}
1205
1206
/**
1207
* @task edit
1208
*/
1209
public function shouldAppearInEditView() {
1210
if ($this->proxy) {
1211
return $this->proxy->shouldAppearInEditView();
1212
}
1213
return false;
1214
}
1215
1216
/**
1217
* @task edit
1218
*/
1219
public function shouldAppearInEditEngine() {
1220
if ($this->proxy) {
1221
return $this->proxy->shouldAppearInEditEngine();
1222
}
1223
return false;
1224
}
1225
1226
1227
/**
1228
* @task edit
1229
*/
1230
public function readValueFromRequest(AphrontRequest $request) {
1231
if ($this->proxy) {
1232
return $this->proxy->readValueFromRequest($request);
1233
}
1234
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1235
}
1236
1237
1238
/**
1239
* @task edit
1240
*/
1241
public function getRequiredHandlePHIDsForEdit() {
1242
if ($this->proxy) {
1243
return $this->proxy->getRequiredHandlePHIDsForEdit();
1244
}
1245
return array();
1246
}
1247
1248
1249
/**
1250
* @task edit
1251
*/
1252
public function getInstructionsForEdit() {
1253
if ($this->proxy) {
1254
return $this->proxy->getInstructionsForEdit();
1255
}
1256
return null;
1257
}
1258
1259
1260
/**
1261
* @task edit
1262
*/
1263
public function renderEditControl(array $handles) {
1264
if ($this->proxy) {
1265
return $this->proxy->renderEditControl($handles);
1266
}
1267
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1268
}
1269
1270
1271
/* -( Property View )------------------------------------------------------ */
1272
1273
1274
/**
1275
* @task view
1276
*/
1277
public function shouldAppearInPropertyView() {
1278
if ($this->proxy) {
1279
return $this->proxy->shouldAppearInPropertyView();
1280
}
1281
return false;
1282
}
1283
1284
1285
/**
1286
* @task view
1287
*/
1288
public function renderPropertyViewLabel() {
1289
if ($this->proxy) {
1290
return $this->proxy->renderPropertyViewLabel();
1291
}
1292
return $this->getFieldName();
1293
}
1294
1295
1296
/**
1297
* @task view
1298
*/
1299
public function renderPropertyViewValue(array $handles) {
1300
if ($this->proxy) {
1301
return $this->proxy->renderPropertyViewValue($handles);
1302
}
1303
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1304
}
1305
1306
1307
/**
1308
* @task view
1309
*/
1310
public function getStyleForPropertyView() {
1311
if ($this->proxy) {
1312
return $this->proxy->getStyleForPropertyView();
1313
}
1314
return 'property';
1315
}
1316
1317
1318
/**
1319
* @task view
1320
*/
1321
public function getIconForPropertyView() {
1322
if ($this->proxy) {
1323
return $this->proxy->getIconForPropertyView();
1324
}
1325
return null;
1326
}
1327
1328
1329
/**
1330
* @task view
1331
*/
1332
public function getRequiredHandlePHIDsForPropertyView() {
1333
if ($this->proxy) {
1334
return $this->proxy->getRequiredHandlePHIDsForPropertyView();
1335
}
1336
return array();
1337
}
1338
1339
1340
/* -( List View )---------------------------------------------------------- */
1341
1342
1343
/**
1344
* @task list
1345
*/
1346
public function shouldAppearInListView() {
1347
if ($this->proxy) {
1348
return $this->proxy->shouldAppearInListView();
1349
}
1350
return false;
1351
}
1352
1353
1354
/**
1355
* @task list
1356
*/
1357
public function renderOnListItem(PHUIObjectItemView $view) {
1358
if ($this->proxy) {
1359
return $this->proxy->renderOnListItem($view);
1360
}
1361
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1362
}
1363
1364
1365
/* -( Global Search )------------------------------------------------------ */
1366
1367
1368
/**
1369
* @task globalsearch
1370
*/
1371
public function shouldAppearInGlobalSearch() {
1372
if ($this->proxy) {
1373
return $this->proxy->shouldAppearInGlobalSearch();
1374
}
1375
return false;
1376
}
1377
1378
1379
/**
1380
* @task globalsearch
1381
*/
1382
public function updateAbstractDocument(
1383
PhabricatorSearchAbstractDocument $document) {
1384
if ($this->proxy) {
1385
return $this->proxy->updateAbstractDocument($document);
1386
}
1387
return $document;
1388
}
1389
1390
1391
/* -( Data Export )-------------------------------------------------------- */
1392
1393
1394
public function shouldAppearInDataExport() {
1395
if ($this->proxy) {
1396
return $this->proxy->shouldAppearInDataExport();
1397
}
1398
1399
try {
1400
$this->newExportFieldType();
1401
return true;
1402
} catch (PhabricatorCustomFieldImplementationIncompleteException $ex) {
1403
return false;
1404
}
1405
}
1406
1407
public function newExportField() {
1408
if ($this->proxy) {
1409
return $this->proxy->newExportField();
1410
}
1411
1412
return $this->newExportFieldType()
1413
->setLabel($this->getFieldName());
1414
}
1415
1416
public function newExportData() {
1417
if ($this->proxy) {
1418
return $this->proxy->newExportData();
1419
}
1420
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1421
}
1422
1423
protected function newExportFieldType() {
1424
if ($this->proxy) {
1425
return $this->proxy->newExportFieldType();
1426
}
1427
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1428
}
1429
1430
1431
/* -( Conduit )------------------------------------------------------------ */
1432
1433
1434
/**
1435
* @task conduit
1436
*/
1437
public function shouldAppearInConduitDictionary() {
1438
if ($this->proxy) {
1439
return $this->proxy->shouldAppearInConduitDictionary();
1440
}
1441
return false;
1442
}
1443
1444
1445
/**
1446
* @task conduit
1447
*/
1448
public function getConduitDictionaryValue() {
1449
if ($this->proxy) {
1450
return $this->proxy->getConduitDictionaryValue();
1451
}
1452
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1453
}
1454
1455
1456
public function shouldAppearInConduitTransactions() {
1457
if ($this->proxy) {
1458
return $this->proxy->shouldAppearInConduitDictionary();
1459
}
1460
return false;
1461
}
1462
1463
public function getConduitSearchParameterType() {
1464
return $this->newConduitSearchParameterType();
1465
}
1466
1467
protected function newConduitSearchParameterType() {
1468
if ($this->proxy) {
1469
return $this->proxy->newConduitSearchParameterType();
1470
}
1471
return null;
1472
}
1473
1474
public function getConduitEditParameterType() {
1475
return $this->newConduitEditParameterType();
1476
}
1477
1478
protected function newConduitEditParameterType() {
1479
if ($this->proxy) {
1480
return $this->proxy->newConduitEditParameterType();
1481
}
1482
return null;
1483
}
1484
1485
public function getCommentAction() {
1486
return $this->newCommentAction();
1487
}
1488
1489
protected function newCommentAction() {
1490
if ($this->proxy) {
1491
return $this->proxy->newCommentAction();
1492
}
1493
return null;
1494
}
1495
1496
1497
/* -( Herald )------------------------------------------------------------- */
1498
1499
1500
/**
1501
* Return `true` to make this field available in Herald.
1502
*
1503
* @return bool True to expose the field in Herald.
1504
* @task herald
1505
*/
1506
public function shouldAppearInHerald() {
1507
if ($this->proxy) {
1508
return $this->proxy->shouldAppearInHerald();
1509
}
1510
return false;
1511
}
1512
1513
1514
/**
1515
* Get the name of the field in Herald. By default, this uses the
1516
* normal field name.
1517
*
1518
* @return string Herald field name.
1519
* @task herald
1520
*/
1521
public function getHeraldFieldName() {
1522
if ($this->proxy) {
1523
return $this->proxy->getHeraldFieldName();
1524
}
1525
return $this->getFieldName();
1526
}
1527
1528
1529
/**
1530
* Get the field value for evaluation by Herald.
1531
*
1532
* @return wild Field value.
1533
* @task herald
1534
*/
1535
public function getHeraldFieldValue() {
1536
if ($this->proxy) {
1537
return $this->proxy->getHeraldFieldValue();
1538
}
1539
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1540
}
1541
1542
1543
/**
1544
* Get the available conditions for this field in Herald.
1545
*
1546
* @return list<const> List of Herald condition constants.
1547
* @task herald
1548
*/
1549
public function getHeraldFieldConditions() {
1550
if ($this->proxy) {
1551
return $this->proxy->getHeraldFieldConditions();
1552
}
1553
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1554
}
1555
1556
1557
/**
1558
* Get the Herald value type for the given condition.
1559
*
1560
* @param const Herald condition constant.
1561
* @return const|null Herald value type, or null to use the default.
1562
* @task herald
1563
*/
1564
public function getHeraldFieldValueType($condition) {
1565
if ($this->proxy) {
1566
return $this->proxy->getHeraldFieldValueType($condition);
1567
}
1568
return null;
1569
}
1570
1571
public function getHeraldFieldStandardType() {
1572
if ($this->proxy) {
1573
return $this->proxy->getHeraldFieldStandardType();
1574
}
1575
return null;
1576
}
1577
1578
public function getHeraldDatasource() {
1579
if ($this->proxy) {
1580
return $this->proxy->getHeraldDatasource();
1581
}
1582
return null;
1583
}
1584
1585
1586
public function shouldAppearInHeraldActions() {
1587
if ($this->proxy) {
1588
return $this->proxy->shouldAppearInHeraldActions();
1589
}
1590
return false;
1591
}
1592
1593
1594
public function getHeraldActionName() {
1595
if ($this->proxy) {
1596
return $this->proxy->getHeraldActionName();
1597
}
1598
1599
return null;
1600
}
1601
1602
1603
public function getHeraldActionStandardType() {
1604
if ($this->proxy) {
1605
return $this->proxy->getHeraldActionStandardType();
1606
}
1607
1608
return null;
1609
}
1610
1611
1612
public function getHeraldActionDescription($value) {
1613
if ($this->proxy) {
1614
return $this->proxy->getHeraldActionDescription($value);
1615
}
1616
1617
return null;
1618
}
1619
1620
1621
public function getHeraldActionEffectDescription($value) {
1622
if ($this->proxy) {
1623
return $this->proxy->getHeraldActionEffectDescription($value);
1624
}
1625
1626
return null;
1627
}
1628
1629
1630
public function getHeraldActionDatasource() {
1631
if ($this->proxy) {
1632
return $this->proxy->getHeraldActionDatasource();
1633
}
1634
1635
return null;
1636
}
1637
1638
private static function adjustCustomFieldsForObjectSubtype(
1639
PhabricatorCustomFieldInterface $object,
1640
$role,
1641
array $fields) {
1642
assert_instances_of($fields, __CLASS__);
1643
1644
// We only apply subtype adjustment for some roles. For example, when
1645
// writing Herald rules or building a Search interface, we always want to
1646
// show all the fields in their default state, so we do not apply any
1647
// adjustments.
1648
$subtype_roles = array(
1649
self::ROLE_EDITENGINE,
1650
self::ROLE_VIEW,
1651
self::ROLE_EDIT,
1652
);
1653
1654
$subtype_roles = array_fuse($subtype_roles);
1655
if (!isset($subtype_roles[$role])) {
1656
return $fields;
1657
}
1658
1659
// If the object doesn't support subtypes, we can't possibly make
1660
// any adjustments based on subtype.
1661
if (!($object instanceof PhabricatorEditEngineSubtypeInterface)) {
1662
return $fields;
1663
}
1664
1665
$subtype_map = $object->newEditEngineSubtypeMap();
1666
$subtype_key = $object->getEditEngineSubtype();
1667
$subtype_object = $subtype_map->getSubtype($subtype_key);
1668
1669
$map = array();
1670
foreach ($fields as $field) {
1671
$modern_key = $field->getModernFieldKey();
1672
if (!strlen($modern_key)) {
1673
continue;
1674
}
1675
1676
$map[$modern_key] = $field;
1677
}
1678
1679
foreach ($map as $field_key => $field) {
1680
// For now, only support overriding standard custom fields. In the
1681
// future there's no technical or product reason we couldn't let you
1682
// override (some properites of) other fields like "Title", but they
1683
// don't usually support appropriate "setX()" methods today.
1684
if (!($field instanceof PhabricatorStandardCustomField)) {
1685
// For fields that are proxies on top of StandardCustomField, which
1686
// is how most application custom fields work today, we can reconfigure
1687
// the proxied field instead.
1688
$field = $field->getProxy();
1689
if (!$field || !($field instanceof PhabricatorStandardCustomField)) {
1690
continue;
1691
}
1692
}
1693
1694
$subtype_config = $subtype_object->getSubtypeFieldConfiguration(
1695
$field_key);
1696
1697
if (!$subtype_config) {
1698
continue;
1699
}
1700
1701
if (isset($subtype_config['disabled'])) {
1702
$field->setIsEnabled(!$subtype_config['disabled']);
1703
}
1704
1705
if (isset($subtype_config['name'])) {
1706
$field->setFieldName($subtype_config['name']);
1707
}
1708
}
1709
1710
return $fields;
1711
}
1712
1713
}
1714
1715