@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

Add "Understanding Application Transaction Editors" article to Diviner

Summary:
In writing a number of Phorge extensions, I found it quite challenging to
understand how to use and implement Application Transaction Editors. As these
are quite core to most Phorge applications, it seems that we should probably
have some documentation. Introduce a rough draft of a guide to using and
implementing such things.

Test Plan:
Check that the prose reads okay, and confirm that what's described is in line
with what PhabricatorApplicationTransactionEditor does. Additionally, run `./bin/diviner generate` and observe that the document "Understanding Application Transaction Editors" appears in the appropriate location.

Reviewers: O1 Blessed Committers, valerio.bozzolan, aklapper

Reviewed By: O1 Blessed Committers, aklapper

Subscribers: aklapper, tobiaswiese, Matthew, Cigaryno

Differential Revision: https://we.phorge.it/D25883

+353
+353
src/docs/contributor/transaction_editors.diviner
··· 1 + @title Understanding Application Transaction Editors 2 + @group developer 3 + 4 + An incomplete guide to implementing and using Application Transaction Editors. 5 + 6 + = Overview 7 + 8 + Transaction editors, subclasses of 9 + @{class:PhabricatorApplicationTransactionEditor}, provide a common abstraction 10 + to applying mutations to an object in an extensible way. Each application is 11 + responsible for providing a transaction editor for object types. By implementing 12 + your object mutation logic as a transaction editor, you gain benefits like being 13 + able to use standard CRUD (Create, Read, Update, Delete) components like 14 + @{class:PhabricatorEditEngine} which gives you standard edit and create forms 15 + for your object types, as well as the transaction history for each object. 16 + 17 + At a high level, an editor takes an object and a list of actions to apply, and 18 + then in a rather large set of phases: Validates each action, applies the 19 + mutations, performs various ancillary work (such as queuing Herald actions), and 20 + inserts logs of the mutations into a transaction table which is used principally 21 + to render timelines in the UI, but are general enough that you //could// do 22 + more. As an example, they, like feed, can be used for incremental 23 + synchronization with external or even internal sources. 24 + 25 + It's important to understand that because the base transaction editor class is 26 + attempting to consolidate a large amount of ad-hoc, legacy, and custom object 27 + mutation code, it's //very// large and complex. 28 + 29 + = Concepts 30 + 31 + == Getting an Editor 32 + 33 + The best way to get a transaction editor for an object type is to instantiate or 34 + get an object of that type, which must implement 35 + @{interface:PhabricatorApplicationTransactionInterface}, and call 36 + @{method:PhabricatorApplicationTransactionInterface::getApplicationTransactionEditor}. 37 + 38 + Editors 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 40 + is used when a form (such as in Phriction) wants to render a preview of the 41 + changes to be made. In the case of Phriction, that means showing the new 42 + rendered content. **The preview path is //not// expanded upon in this guide.** 43 + 44 + == Transactions and Transaction Types 45 + 46 + Transactions refer to the actual storage objects for an object type's 47 + transaction table. These are typically referred to as `xactions` and are 48 + subclasses of @{class:PhabricatorModularTransaction}. 49 + 50 + Transaction //types// refer to the implementation logic for a particular kind of 51 + mutation. These are typically referred to as `xtypes`, but very occassionally 52 + they are also called `xactions` in the base editor code. There are two kinds of 53 + transactions types: legacy, and modern or modular. Legacy transaction types will 54 + not be discussed as no new legacy transaction types should be added. Modular 55 + transaction types inherit from @{class:PhabricatorModularTransactionType}. 56 + Certain core transaction types apply to almost all object types, and those can 57 + be found in @{class:PhabricatorTransactions}. 58 + 59 + Providing a list of mutations to an editor involves constructing transaction 60 + objects for the object type and setting the transaction object's type to a 61 + constant. Example code is worth at least 500 words, so here's an example to 62 + clarify 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 + 91 + The most daunting aspect of transaction editors is just how complicated the edit 92 + process //is.// There's thirty primary phases with a large number of hooks for 93 + applications 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 + 128 + 1. **Open transaction and lock object** 129 + 130 + If it's an existing object and this isn't a preview edit, then it's reloaded 131 + from the database, a db transaction is opened and the object is loaded with 132 + `SELECT .. FOR UPDATE` to prevent concurrent modification. 133 + 134 + 2. **High level parameters of the edit are validated.** 135 + 136 + E.g., all the actions to perform are instances of the base Transaction DAO, 137 + that it's not a transaction that's already been applied. 138 + 139 + 3. **Checks for MFA authentication requirements** 140 + 141 + If any xaction has such a requirement, a MFA xaction at the front of the 142 + transaction list. The presence of such a transaction configures edit forms to 143 + require MFA re-authentication to submit the form. An object or transaction type 144 + that requires MFA to edit/apply cannot be edited outside the web UI, unless the 145 + omnipotent viewer is used. 146 + 147 + 4. **The object-under-edit and current viewer are attached to the xactions.** 148 + 149 + This is not helpful for implementing new types because it attaches them to the 150 + transaction objects for internal purposes, not the transaction //types.// 151 + Transaction types can always access the actor the editor is using 152 + @{method:PhabricatorModularTransactionType::getActor}. 153 + 154 + 5. **Transactions are "expanded".** 155 + 156 + Which means that a transaction like "resign from diff" also means "remove 157 + myself as a reviewer." Hooks are provided but do not instantiate transaction 158 + types. Transaction expansion runs in the context of the editor. 159 + 160 + 6. **Some implicit/automatic support transactions are added to the process** 161 + 162 + for things like where your transaction has some reMarkup changes, or the 163 + object has subscribers and those subscribers have changed... within some 164 + reMarkup. 165 + 166 + 7. **Transactions are combined** 167 + 168 + To coalesce two updates of one field into one update. Has hook on transaction 169 + type objects, but only works if you have two of the same type in an edit. 170 + 171 + 8. **Common attributes are added to the transactions.** 172 + 173 + **(NO HOOKS)** This is stuff like the author/actor, content source (e.g., 174 + web), edit policy. 175 + 176 + 9. **Transaction type validation logic is run.** 177 + 178 + The transactions are grouped by their type and then all of the xactions of 179 + that type are passed to the transaction type //once// for validation. Any state 180 + you set on the input transactions to the editor (expect builtin state like 181 + newObject) //will not be present.// 182 + 183 + 10. **The editor gets the chance to validate every transaction.** 184 + 185 + This is presumably for domain specific editing logic. 186 + 187 + 11. **Transaction editor //extensions// get to validate the transactions.** 188 + 189 + NOTE: Currently undocumented. 190 + 191 + 12. **Missing field errors are checked for and processed.** 192 + 193 + These errors may not be raised if the editor is configured to not care. 194 + 195 + 13. **New/old values generated + some legacy file attachment handling.** 196 + 197 + This is where new and old values are generated from the xtype as well as some 198 + custom logic for fixing up the values for file type transactions. 199 + 200 + 14. **Capability checks are performed.** 201 + 202 + Transaction types are allowed to declare additional capabilities a user needs in 203 + order to perform the action. 204 + 205 + 15. **Transactions are filtered for effect and special effects.** 206 + 207 + Transactions are allowed to define what "has an effect" means. This means that 208 + they can conditionally filter themselves out based on arbitrary logic. There is 209 + also a number of built-in filtering for comment and MFA transactions. 210 + 211 + 16. **MFA requirement tested and if needed executed.** 212 + 213 + MFA requirements only work if the call is from conduit or web. Anything else 214 + simply can't use MFA and transaction editors. 215 + 216 + 17. **Initial effects are executed.** 217 + 218 + These allow the editor to prepare state to handle subsequent phases, as well as 219 + other mysterious purposes. It's really important to note that 220 + `shouldApplyInitialEffect` will get called **TWICE** because of some weirdness 221 + around previewing. 222 + 223 + 18. **Marks all the xactions as create if needed.** 224 + 225 + When an object is being created a special key in the transaction metadata is 226 + set to indicate that the transaction group was the creation txn. 227 + 228 + 19. **Transactions are sorted for display purposes.** 229 + 230 + An opportunity is given to editors to reorder how the transactions will be 231 + committed to the database. There is also default behavior for comments. 232 + 233 + 20. **Internal effects are executed.** 234 + 235 + Internal effects (defined on the transaction type) are where most 236 + transactions apply the new state to the object being worked on and other 237 + ancillary but closely related objects. 238 + 239 + 21. **//The object is saved//.** 240 + 241 + All the internal effects have run successfully to build new object(s) state. 242 + The object is inserted/updated in the database. 243 + 244 + 22. **The editor is given a chance to react to duplicate key errors.** 245 + 246 + This is nominally to allow the editor to process the exception and throw 247 + something else. 248 + 249 + 23. **The xactions themselves are saved to the database.** 250 + 251 + This involves setting some final metadata such as the object PHID and 252 + transaction group id. There's some special case logic around a new EDGE type 253 + transaction format. 254 + 255 + 24. **External effects are executed.** 256 + 257 + These effects (defined on the transaction types) are used to perform side 258 + effects on other objects, enqueue daemon jobs, or potentially talk to 259 + external services. 260 + 261 + 25. **Final effects are executed.** 262 + 263 + This allows the editor to perform side final side effects before the overall 264 + database transaction is committed. Immediately after this is transaction 265 + commit, call it phase 25a. 266 + 267 + 26. **A "did commit" callback is executed on the xactions.** 268 + 269 + Each transaction type is able to react to the fact that the overall database 270 + transaction has been applied successfully. This is typically used for 271 + notifying related applications of a change they need to respond to. 272 + 273 + 27. **Cache engines are notified of the object change.** 274 + 275 + Someone ought to write some prose for this. 276 + 277 + 28. **Herald rules are run.** 278 + 279 + This is kinda interesting. The editor can decide if there are herald rules 280 + that need running based on all the transactions applied. If there are any, 281 + then the editor must provide a @{class:HeraldAdapter} by some means. The 282 + adapter then runs it's rules and afterwards the editor can generate further 283 + transactions for the object for things like rules that automatically assign 284 + tasks with titles starting with "[LOL]" to the team's intern. 285 + Finally, the herald editor is run to commit those transactions. 286 + 287 + 29. **Editors can handle the completion of the primary edit portion.** 288 + 289 + This doesn't include the major side effects of enqueueing the jobs to send 290 + email and publish feed stories. 291 + 292 + 30. **Various hooks for email processing are called on the editor.** 293 + 294 + The hooks are for things like deciding if mail should be sent, whom they 295 + should be sent to, what mail content to create, queue final transactions to 296 + be run after all is said and done. This is a wild scenario because a copy of 297 + the editor will be created and then will be called all over again for the 298 + transactions it just generated. 299 + 300 + = Implementing an Editor 301 + 302 + The process for creating an editor is rather straightforward. The overwhelming 303 + majority of the logic is in the base class, and can't be overridden. In short 304 + you must: 305 + 306 + 1. Create a subclass of @{class:PhabricatorApplicationTransactionEditor} 307 + 2. Implement @{interface:PhabricatorApplicationTransactionInterface} on the 308 + object types of your application. I.e., your storage objects that descend 309 + from @{class:LiskDAO}. 310 + 3. Implement zero or more transaction types by creating a subclass of 311 + @{class:PhabricatorModularTransactionType} for each storage object type in 312 + your application. 313 + 4. Use the editor! 314 + 315 + If 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 + 320 + For simple object types, the majority of the logic will go into the transaction 321 + types. There are a few methods that are largely mandatory to implement to have 322 + any kind of reasonable logic. 323 + 324 + The most important is 325 + @{method:PhabricatorModularTransactionType::validateTransactions}. This is where 326 + you'll ensure that the changes are well formed. Logic like ensuring a maximum 327 + length for a value, or that it's a PHID should go here. This method will be 328 + called with //all// of the transactions of this type that will be applied to the 329 + object, so this is also where you could ensure that only one "Title" transaction 330 + is applied. 331 + 332 + Next is @{method:PhabricatorModularTransactionType::generateOldValue}. Typically 333 + the implementation of this will just return the value already on the object, but 334 + can also always return `null` if that's challenging or not meaningful to do. 335 + 336 + There are two methods you can implement to actually perform mutations. The 337 + first, and most common is 338 + @{method:PhabricatorModularTransactionType::applyInternalEffects}. This method 339 + should be used to mutate the actual object being edited. The second is 340 + @{method:PhabricatorModularTransactionType::applyExternalEffects} which is where 341 + you should place mutations that affect other objects such as caches or internal 342 + state. 343 + 344 + NOTE: It's important that your transaction types are **stateless**! Because of 345 + how the types are cloned inside the base editor, it's very challenging or 346 + impossible to have stateful transaction types. 347 + 348 + = Next Steps 349 + 350 + Try reading a few transaction editors and their transaction types. 351 + @{class:PhrictionTransactionEditor} and @{class:PonderEditor} are both simple 352 + editors that are not too difficult to understand. A much more complex one is 353 + @{class:ManiphestTransactionEditor}.