@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1@title Understanding Application Transaction Editors
2@group developer
3
4An incomplete guide to implementing and using Application Transaction Editors.
5
6= Overview
7
8Transaction editors, subclasses of
9@{class:PhabricatorApplicationTransactionEditor}, provide a common abstraction
10to applying mutations to an object in an extensible way. Each application is
11responsible for providing a transaction editor for object types. By implementing
12your object mutation logic as a transaction editor, you gain benefits like being
13able to use standard CRUD (Create, Read, Update, Delete) components like
14@{class:PhabricatorEditEngine} which gives you standard edit and create forms
15for your object types, as well as the transaction history for each object.
16
17At a high level, an editor takes an object and a list of actions to apply, and
18then in a rather large set of phases: Validates each action, applies the
19mutations, performs various ancillary work (such as queuing Herald actions), and
20inserts logs of the mutations into a transaction table which is used principally
21to render timelines in the UI, but are general enough that you //could// do
22more. As an example, they, like feed, can be used for incremental
23synchronization with external or even internal sources.
24
25It's important to understand that because the base transaction editor class is
26attempting to consolidate a large amount of ad-hoc, legacy, and custom object
27mutation code, it's //very// large and complex.
28
29= Concepts
30
31== Getting an Editor
32
33The best way to get a transaction editor for an object type is to instantiate or
34get an object of that type, which must implement
35@{interface:PhabricatorApplicationTransactionInterface}, and call
36@{method:PhabricatorApplicationTransactionInterface::getApplicationTransactionEditor}.
37
38Editors operate in one of two modes: real or live, and "preview". Of course the
39"live" mode actually applies mutations and triggers email, etc. The preview mode
40is used when a form (such as in Phriction) wants to render a preview of the
41changes to be made. In the case of Phriction, that means showing the new
42rendered content. **The preview path is //not// expanded upon in this guide.**
43
44== Transactions and Transaction Types
45
46Transactions refer to the actual storage objects for an object type's
47transaction table. These are typically referred to as `xactions` and are
48subclasses of @{class:PhabricatorModularTransaction}.
49
50Transaction //types// refer to the implementation logic for a particular kind of
51mutation. These are typically referred to as `xtypes`, but very occassionally
52they are also called `xactions` in the base editor code. There are two kinds of
53transactions types: legacy, and modern or modular. Legacy transaction types will
54not be discussed as no new legacy transaction types should be added. Modular
55transaction types inherit from @{class:PhabricatorModularTransactionType}.
56Certain core transaction types apply to almost all object types, and those can
57be found in @{class:PhabricatorTransactions}.
58
59Providing a list of mutations to an editor involves constructing transaction
60objects for the object type and setting the transaction object's type to a
61constant. Example code is worth at least 500 words, so here's an example to
62clarify this relationship:
63
64```lang=php
65$xactions = array();
66// ManiphestTransaction inherits from PhabricatorModularTransaction.
67$xactions[] = (new ManiphestTransaction())
68 // You set the transaction type to a constant, and then the editor intantiates
69 // the appropriate transaction type class to perform the mutation.
70 ->setTransactionType(ManiphestTaskTitleTransaction::TRANSACTIONTYPE)
71 // The value to set on the object. See below for a discussion of new/old
72 // values.
73 ->setNewValue('A hot task title meant to inspire action');
74
75$xactions[] = (new ManiphestTransaction())
76 // This is one of the core transaction types. It's applicable to anything that
77 // implements PhabricatorSubscribableInterface.
78 ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
79 // This sets the subscribers to $some_phid, discarding any others.
80 ->setNewValue(array(
81 '=' => array($some_phid => $some_phid)
82 ));
83// ...
84(new ManiphestTransactionEditor())
85 // common builder methods not applicable here, see below for more details
86 ->applyTransactions($a_task_object, $xactions);
87```
88
89= Edit Phases
90
91The most daunting aspect of transaction editors is just how complicated the edit
92process //is.// There's thirty primary phases with a large number of hooks for
93applications to customize the process to varying degrees.
94
95| # | Summary | Clones xtype | Can hook |
96| --- | ------------------------------- | ------------ | ------------ |
97| 1 | Open txn and lock object | no | |
98| 2 | Edit params validated | no | |
99| 3 | MFA requirements | **yes** | xtype |
100| 4 | Object+viewer txn attached | no | |
101| 5 | Expand transactions | no | editor |
102| 6 | Implicit+support txns added | no | editor |
103| 7 | Merge transactions | no | editor+xtype |
104| 8 | Common attributes | no | editor |
105| 9 | Transaction type validators | no | editor+xtype |
106| 10 | Editor xaction validation | no | editor |
107| 11 | Extension xaction validation | no | |
108| 12 | Any validation errors thrown | no | |
109| 13 | new/old values generated | no | xtype |
110| 14 | Capability checks | no | xtype |
111| 15 | No-op transactions are filtered | no | editor+xtype |
112| 16 | MFA requirement execution | no | |
113| 17 | //Initial// effects applied | no | editor |
114| 18 | Fixup isCreate flag on xactions | no | |
115| 19 | Transactions are sorted | no | editor |
116| 20 | //Internal// effects applied | **yes** | xtype |
117| 21 | Object committed | no | |
118| 22 | handle duplicate key errs | no | editor |
119| 23 | xactions commit | no | |
120| 24 | //External// effects applied | **yes** | xtype |
121| 25 | //Final// effects applied | no | editor |
122| 26 | "did commit" callback | **yes** | xtype |
123| 27 | Cache engine updates | no | extensions |
124| 28 | Herald rules | no | editor |
125| 29 | "did commit" part 2 | no | editor |
126| 30 | Email+feed processing hooks | no | editor |
127
1281. **Open transaction and lock object**
129
130If it's an existing object and this isn't a preview edit, then it's reloaded
131from the database, a db transaction is opened and the object is loaded with
132`SELECT .. FOR UPDATE` to prevent concurrent modification.
133
1342. **High level parameters of the edit are validated.**
135
136E.g., all the actions to perform are instances of the base Transaction DAO,
137that it's not a transaction that's already been applied.
138
1393. **Checks for MFA authentication requirements**
140
141If any xaction has such a requirement, a MFA xaction at the front of the
142transaction list. The presence of such a transaction configures edit forms to
143require MFA re-authentication to submit the form. An object or transaction type
144that requires MFA to edit/apply cannot be edited outside the web UI, unless the
145omnipotent viewer is used.
146
1474. **The object-under-edit and current viewer are attached to the xactions.**
148
149This is not helpful for implementing new types because it attaches them to the
150transaction objects for internal purposes, not the transaction //types.//
151Transaction types can always access the actor the editor is using
152@{method:PhabricatorModularTransactionType::getActor}.
153
1545. **Transactions are "expanded".**
155
156Which means that a transaction like "resign from diff" also means "remove
157myself as a reviewer." Hooks are provided but do not instantiate transaction
158types. Transaction expansion runs in the context of the editor.
159
1606. **Some implicit/automatic support transactions are added to the process**
161
162for things like where your transaction has some reMarkup changes, or the
163object has subscribers and those subscribers have changed... within some
164reMarkup.
165
1667. **Transactions are combined**
167
168To coalesce two updates of one field into one update. Has hook on transaction
169type objects, but only works if you have two of the same type in an edit.
170
1718. **Common attributes are added to the transactions.**
172
173**(NO HOOKS)** This is stuff like the author/actor, content source (e.g.,
174web), edit policy.
175
1769. **Transaction type validation logic is run.**
177
178The transactions are grouped by their type and then all of the xactions of
179that type are passed to the transaction type //once// for validation. Any state
180you set on the input transactions to the editor (expect builtin state like
181newObject) //will not be present.//
182
18310. **The editor gets the chance to validate every transaction.**
184
185This is presumably for domain specific editing logic.
186
18711. **Transaction editor //extensions// get to validate the transactions.**
188
189NOTE: Currently undocumented.
190
19112. **Missing field errors are checked for and processed.**
192
193These errors may not be raised if the editor is configured to not care.
194
19513. **New/old values generated + some legacy file attachment handling.**
196
197This is where new and old values are generated from the xtype as well as some
198custom logic for fixing up the values for file type transactions.
199
20014. **Capability checks are performed.**
201
202Transaction types are allowed to declare additional capabilities a user needs in
203order to perform the action.
204
20515. **Transactions are filtered for effect and special effects.**
206
207Transactions are allowed to define what "has an effect" means. This means that
208they can conditionally filter themselves out based on arbitrary logic. There is
209also a number of built-in filtering for comment and MFA transactions.
210
21116. **MFA requirement tested and if needed executed.**
212
213MFA requirements only work if the call is from conduit or web. Anything else
214simply can't use MFA and transaction editors.
215
21617. **Initial effects are executed.**
217
218These allow the editor to prepare state to handle subsequent phases, as well as
219other mysterious purposes. It's really important to note that
220`shouldApplyInitialEffect` will get called **TWICE** because of some weirdness
221around previewing.
222
22318. **Marks all the xactions as create if needed.**
224
225When an object is being created a special key in the transaction metadata is
226set to indicate that the transaction group was the creation txn.
227
22819. **Transactions are sorted for display purposes.**
229
230An opportunity is given to editors to reorder how the transactions will be
231committed to the database. There is also default behavior for comments.
232
23320. **Internal effects are executed.**
234
235Internal effects (defined on the transaction type) are where most
236transactions apply the new state to the object being worked on and other
237ancillary but closely related objects.
238
23921. **//The object is saved//.**
240
241All the internal effects have run successfully to build new object(s) state.
242The object is inserted/updated in the database.
243
24422. **The editor is given a chance to react to duplicate key errors.**
245
246This is nominally to allow the editor to process the exception and throw
247something else.
248
24923. **The xactions themselves are saved to the database.**
250
251This involves setting some final metadata such as the object PHID and
252transaction group id. There's some special case logic around a new EDGE type
253transaction format.
254
25524. **External effects are executed.**
256
257These effects (defined on the transaction types) are used to perform side
258effects on other objects, enqueue daemon jobs, or potentially talk to
259external services.
260
26125. **Final effects are executed.**
262
263This allows the editor to perform side final side effects before the overall
264database transaction is committed. Immediately after this is transaction
265commit, call it phase 25a.
266
26726. **A "did commit" callback is executed on the xactions.**
268
269Each transaction type is able to react to the fact that the overall database
270transaction has been applied successfully. This is typically used for
271notifying related applications of a change they need to respond to.
272
27327. **Cache engines are notified of the object change.**
274
275Someone ought to write some prose for this.
276
27728. **Herald rules are run.**
278
279This is kinda interesting. The editor can decide if there are herald rules
280that need running based on all the transactions applied. If there are any,
281then the editor must provide a @{class:HeraldAdapter} by some means. The
282adapter then runs it's rules and afterwards the editor can generate further
283transactions for the object for things like rules that automatically assign
284tasks with titles starting with "[LOL]" to the team's intern.
285Finally, the herald editor is run to commit those transactions.
286
28729. **Editors can handle the completion of the primary edit portion.**
288
289This doesn't include the major side effects of enqueueing the jobs to send
290email and publish feed stories.
291
29230. **Various hooks for email processing are called on the editor.**
293
294The hooks are for things like deciding if mail should be sent, whom they
295should be sent to, what mail content to create, queue final transactions to
296be run after all is said and done. This is a wild scenario because a copy of
297the editor will be created and then will be called all over again for the
298transactions it just generated.
299
300= Implementing an Editor
301
302The process for creating an editor is rather straightforward. The overwhelming
303majority of the logic is in the base class, and can't be overridden. In short
304you must:
305
3061. Create a subclass of @{class:PhabricatorApplicationTransactionEditor}
3072. Implement @{interface:PhabricatorApplicationTransactionInterface} on the
308 object types of your application. I.e., your storage objects that descend
309 from @{class:LiskDAO}.
3103. Implement zero or more transaction types by creating a subclass of
311 @{class:PhabricatorModularTransactionType} for each storage object type in
312 your application.
3134. Use the editor!
314
315If you need to exit an edit early, the only way out is to record an error in
316`xtype` validation logic, or throw an exception in one of the editor hooks.
317
318== Implementing Transaction Types
319
320For simple object types, the majority of the logic will go into the transaction
321types. There are a few methods that are largely mandatory to implement to have
322any kind of reasonable logic.
323
324The most important is
325@{method:PhabricatorModularTransactionType::validateTransactions}. This is where
326you'll ensure that the changes are well formed. Logic like ensuring a maximum
327length for a value, or that it's a PHID should go here. This method will be
328called with //all// of the transactions of this type that will be applied to the
329object, so this is also where you could ensure that only one "Title" transaction
330is applied.
331
332Next is @{method:PhabricatorModularTransactionType::generateOldValue}. Typically
333the implementation of this will just return the value already on the object, but
334can also always return `null` if that's challenging or not meaningful to do.
335
336There are two methods you can implement to actually perform mutations. The
337first, and most common is
338@{method:PhabricatorModularTransactionType::applyInternalEffects}. This method
339should be used to mutate the actual object being edited. The second is
340@{method:PhabricatorModularTransactionType::applyExternalEffects} which is where
341you should place mutations that affect other objects such as caches or internal
342state.
343
344NOTE: It's important that your transaction types are **stateless**! Because of
345how the types are cloned inside the base editor, it's very challenging or
346impossible to have stateful transaction types.
347
348= Next Steps
349
350Try reading a few transaction editors and their transaction types.
351@{class:PhrictionTransactionEditor} and @{class:PonderEditor} are both simple
352editors that are not too difficult to understand. A much more complex one is
353@{class:ManiphestTransactionEditor}.