Page MenuHomePhorge

D25883.1739940629.diff
No OneTemporary

D25883.1739940629.diff

diff --git a/src/docs/contributor/transaction_editors.diviner b/src/docs/contributor/transaction_editors.diviner
new file mode 100644
--- /dev/null
+++ b/src/docs/contributor/transaction_editors.diviner
@@ -0,0 +1,353 @@
+@title Understanding Application Transaction Editors
+@group developer
+
+An incomplete guide to implementing and using Application Transaction Editors.
+
+= Overview
+
+Transaction editors, subclasses of
+@{class:PhabricatorApplicationTransactionEditor}, provide a common abstraction
+to applying mutations to an object in an extensible way. Each application is
+responsible for providing a transaction editor for object types. By implementing
+your object mutation logic as a transaction editor, you gain benefits like being
+able to use standard CRUD (Create, Read, Update, Delete) components like
+@{class:PhabricatorEditEngine} which gives you standard edit and create forms
+for your object types, as well as the transaction history for each object.
+
+At a high level, an editor takes an object and a list of actions to apply, and
+then in a rather large set of phases: Validates each action, applies the
+mutations, performs various ancillary work (such as queuing Herald actions), and
+inserts logs of the mutations into a transaction table which is used principally
+to render timelines in the UI, but are general enough that you //could// do
+more. As an example, they, like feed, can be used for incremental
+synchronization with external or even internal sources.
+
+It's important to understand that because the base transaction editor class is
+attempting to consolidate a large amount of ad-hoc, legacy, and custom object
+mutation code, it's //very// large and complex.
+
+= Concepts
+
+== Getting an Editor
+
+The best way to get a transaction editor for an object type is to instantiate or
+get an object of that type, which must implement
+@{interface:PhabricatorApplicationTransactionInterface}, and call
+@{method:PhabricatorApplicationTransactionInterface::getApplicationTransactionEditor}.
+
+Editors operate in one of two modes: real or live, and "preview". Of course the
+"live" mode actually applies mutations and triggers email, etc. The preview mode
+is used when a form (such as in Phriction) wants to render a preview of the
+changes to be made. In the case of Phriction, that means showing the new
+rendered content. **The preview path is //not// expanded upon in this guide.**
+
+== Transactions and Transaction Types
+
+Transactions refer to the actual storage objects for an object type's
+transaction table. These are typically referred to as `xactions` and are
+subclasses of @{class:PhabricatorModularTransaction}.
+
+Transaction //types// refer to the implementation logic for a particular kind of
+mutation. These are typically referred to as `xtypes`, but very occassionally
+they are also called `xactions` in the base editor code. There are two kinds of
+transactions types: legacy, and modern or modular. Legacy transaction types will
+not be discussed as no new legacy transaction types should be added. Modular
+transaction types inherit from @{class:PhabricatorModularTransactionType}.
+Certain core transaction types apply to almost all object types, and those can
+be found in @{class:PhabricatorTransactions}.
+
+Providing a list of mutations to an editor involves constructing transaction
+objects for the object type and setting the transaction object's type to a
+constant. Example code is worth at least 500 words, so here's an example to
+clarify this relationship:
+
+```lang=php
+$xactions = array();
+// ManiphestTransaction inherits from PhabricatorModularTransaction.
+$xactions[] = (new ManiphestTransaction())
+ // You set the transaction type to a constant, and then the editor intantiates
+ // the appropriate transaction type class to perform the mutation.
+ ->setTransactionType(ManiphestTaskTitleTransaction::TRANSACTIONTYPE)
+ // The value to set on the object. See below for a discussion of new/old
+ // values.
+ ->setNewValue('A hot task title meant to inspire action');
+
+$xactions[] = (new ManiphestTransaction())
+ // This is one of the core transaction types. It's applicable to anything that
+ // implements PhabricatorSubscribableInterface.
+ ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
+ // This sets the subscribers to $some_phid, discarding any others.
+ ->setNewValue(array(
+ '=' => array($some_phid => $some_phid)
+ ));
+// ...
+(new ManiphestTransactionEditor())
+ // common builder methods not applicable here, see below for more details
+ ->applyTransactions($a_task_object, $xactions);
+```
+
+= Edit Phases
+
+The most daunting aspect of transaction editors is just how complicated the edit
+process //is.// There's thirty primary phases with a large number of hooks for
+applications to customize the process to varying degrees.
+
+| # | Summary | Clones xtype | Can hook |
+| --- | ------------------------------- | ------------ | ------------ |
+| 1 | Open txn and lock object | no | |
+| 2 | Edit params validated | no | |
+| 3 | MFA requirements | **yes** | xtype |
+| 4 | Object+viewer txn attached | no | |
+| 5 | Expand transactions | no | editor |
+| 6 | Implicit+support txns added | no | editor |
+| 7 | Merge transactions | no | editor+xtype |
+| 8 | Common attributes | no | editor |
+| 9 | Transaction type validators | no | editor+xtype |
+| 10 | Editor xaction validation | no | editor |
+| 11 | Extension xaction validation | no | |
+| 12 | Any validation errors thrown | no | |
+| 13 | new/old values generated | no | xtype |
+| 14 | Capability checks | no | xtype |
+| 15 | No-op transactions are filtered | no | editor+xtype |
+| 16 | MFA requirement execution | no | |
+| 17 | //Initial// effects applied | no | editor |
+| 18 | Fixup isCreate flag on xactions | no | |
+| 19 | Transactions are sorted | no | editor |
+| 20 | //Internal// effects applied | **yes** | xtype |
+| 21 | Object committed | no | |
+| 22 | handle duplicate key errs | no | editor |
+| 23 | xactions commit | no | |
+| 24 | //External// effects applied | **yes** | xtype |
+| 25 | //Final// effects applied | no | editor |
+| 26 | "did commit" callback | **yes** | xtype |
+| 27 | Cache engine updates | no | extensions |
+| 28 | Herald rules | no | editor |
+| 29 | "did commit" part 2 | no | editor |
+| 30 | Email+feed processing hooks | no | editor |
+
+1. **Open transaction and lock object**
+
+If it's an existing object and this isn't a preview edit, then it's reloaded
+from the database, a db transaction is opened and the object is loaded with
+`SELECT .. FOR UPDATE` to prevent concurrent modification.
+
+2. **High level parameters of the edit are validated.**
+
+E.g., all the actions to perform are instances of the base Transaction DAO,
+that it's not a transaction that's already been applied.
+
+3. **Checks for MFA authentication requirements**
+
+If any xaction has such a requirement, a MFA xaction at the front of the
+transaction list. The presence of such a transaction configures edit forms to
+require MFA re-authentication to submit the form. An object or transaction type
+that requires MFA to edit/apply cannot be edited outside the web UI, unless the
+omnipotent viewer is used.
+
+4. **The object-under-edit and current viewer are attached to the xactions.**
+
+This is not helpful for implementing new types because it attaches them to the
+transaction objects for internal purposes, not the transaction //types.//
+Transaction types can always access the actor the editor is using
+@{method:PhabricatorModularTransactionType::getActor}.
+
+5. **Transactions are "expanded".**
+
+Which means that a transaction like "resign from diff" also means "remove
+myself as a reviewer." Hooks are provided but do not instantiate transaction
+types. Transaction expansion runs in the context of the editor.
+
+6. **Some implicit/automatic support transactions are added to the process**
+
+for things like where your transaction has some reMarkup changes, or the
+object has subscribers and those subscribers have changed... within some
+reMarkup.
+
+7. **Transactions are combined**
+
+To coalesce two updates of one field into one update. Has hook on transaction
+type objects, but only works if you have two of the same type in an edit.
+
+8. **Common attributes are added to the transactions.**
+
+**(NO HOOKS)** This is stuff like the author/actor, content source (e.g.,
+web), edit policy.
+
+9. **Transaction type validation logic is run.**
+
+The transactions are grouped by their type and then all of the xactions of
+that type are passed to the transaction type //once// for validation. Any state
+you set on the input transactions to the editor (expect builtin state like
+newObject) //will not be present.//
+
+10. **The editor gets the chance to validate every transaction.**
+
+This is presumably for domain specific editing logic.
+
+11. **Transaction editor //extensions// get to validate the transactions.**
+
+NOTE: Currently undocumented.
+
+12. **Missing field errors are checked for and processed.**
+
+These errors may not be raised if the editor is configured to not care.
+
+13. **New/old values generated + some legacy file attachment handling.**
+
+This is where new and old values are generated from the xtype as well as some
+custom logic for fixing up the values for file type transactions.
+
+14. **Capability checks are performed.**
+
+Transaction types are allowed to declare additional capabilities a user needs in
+order to perform the action.
+
+15. **Transactions are filtered for effect and special effects.**
+
+Transactions are allowed to define what "has an effect" means. This means that
+they can conditionally filter themselves out based on arbitrary logic. There is
+also a number of built-in filtering for comment and MFA transactions.
+
+16. **MFA requirement tested and if needed executed.**
+
+MFA requirements only work if the call is from conduit or web. Anything else
+simply can't use MFA and transaction editors.
+
+17. **Initial effects are executed.**
+
+These allow the editor to prepare state to handle subsequent phases, as well as
+other mysterious purposes. It's really important to note that
+`shouldApplyInitialEffect` will get called **TWICE** because of some weirdness
+around previewing.
+
+18. **Marks all the xactions as create if needed.**
+
+When an object is being created a special key in the transaction metadata is
+set to indicate that the transaction group was the creation txn.
+
+19. **Transactions are sorted for display purposes.**
+
+An opportunity is given to editors to reorder how the transactions will be
+committed to the database. There is also default behavior for comments.
+
+20. **Internal effects are executed.**
+
+Internal effects (defined on the transaction type) are where most
+transactions apply the new state to the object being worked on and other
+ancillary but closely related objects.
+
+21. **//The object is saved//.**
+
+All the internal effects have run successfully to build new object(s) state.
+The object is inserted/updated in the database.
+
+22. **The editor is given a chance to react to duplicate key errors.**
+
+This is nominally to allow the editor to process the exception and throw
+something else.
+
+23. **The xactions themselves are saved to the database.**
+
+This involves setting some final metadata such as the object PHID and
+transaction group id. There's some special case logic around a new EDGE type
+transaction format.
+
+24. **External effects are executed.**
+
+These effects (defined on the transaction types) are used to perform side
+effects on other objects, enqueue daemon jobs, or potentially talk to
+external services.
+
+25. **Final effects are executed.**
+
+This allows the editor to perform side final side effects before the overall
+database transaction is committed. Immediately after this is transaction
+commit, call it phase 25a.
+
+26. **A "did commit" callback is executed on the xactions.**
+
+Each transaction type is able to react to the fact that the overall database
+transaction has been applied successfully. This is typically used for
+notifying related applications of a change they need to respond to.
+
+27. **Cache engines are notified of the object change.**
+
+Someone ought to write some prose for this.
+
+28. **Herald rules are run.**
+
+This is kinda interesting. The editor can decide if there are herald rules
+that need running based on all the transactions applied. If there are any,
+then the editor must provide a @{class:HeraldAdapter} by some means. The
+adapter then runs it's rules and afterwards the editor can generate further
+transactions for the object for things like rules that automatically assign
+tasks with titles starting with "[LOL]" to the team's intern.
+Finally, the herald editor is run to commit those transactions.
+
+29. **Editors can handle the completion of the primary edit portion.**
+
+This doesn't include the major side effects of enqueueing the jobs to send
+email and publish feed stories.
+
+30. **Various hooks for email processing are called on the editor.**
+
+The hooks are for things like deciding if mail should be sent, whom they
+should be sent to, what mail content to create, queue final transactions to
+be run after all is said and done. This is a wild scenario because a copy of
+the editor will be created and then will be called all over again for the
+transactions it just generated.
+
+= Implementing an Editor
+
+The process for creating an editor is rather straightforward. The overwhelming
+majority of the logic is in the base class, and can't be overridden. In short
+you must:
+
+1. Create a subclass of @{class:PhabricatorApplicationTransactionEditor}
+2. Implement @{interface:PhabricatorApplicationTransactionInterface} on the
+ object types of your application. I.e., your storage objects that descend
+ from @{class:LiskDAO}.
+3. Implement zero or more transaction types by creating a subclass of
+ @{class:PhabricatorModularTransactionType} for each storage object type in
+ your application.
+4. Use the editor!
+
+If you need to exit an edit early, the only way out is to record an error in
+`xtype` validation logic, or throw an exception in one of the editor hooks.
+
+== Implementing Transaction Types
+
+For simple object types, the majority of the logic will go into the transaction
+types. There are a few methods that are largely mandatory to implement to have
+any kind of reasonable logic.
+
+The most important is
+@{method:PhabricatorModularTransactionType::validateTransactions}. This is where
+you'll ensure that the changes are well formed. Logic like ensuring a maximum
+length for a value, or that it's a PHID should go here. This method will be
+called with //all// of the transactions of this type that will be applied to the
+object, so this is also where you could ensure that only one "Title" transaction
+is applied.
+
+Next is @{method:PhabricatorModularTransactionType::generateOldValue}. Typically
+the implementation of this will just return the value already on the object, but
+can also always return `null` if that's challenging or not meaningful to do.
+
+There are two methods you can implement to actually perform mutations. The
+first, and most common is
+@{method:PhabricatorModularTransactionType::applyInternalEffects}. This method
+should be used to mutate the actual object being edited. The second is
+@{method:PhabricatorModularTransactionType::applyExternalEffects} which is where
+you should place mutations that affect other objects such as caches or internal
+state.
+
+NOTE: It's important that your transaction types are **stateless**! Because of
+how the types are cloned inside the base editor, it's very challenging or
+impossible to have stateful transaction types.
+
+= Next Steps
+
+Try reading a few transaction editors and their transaction types.
+@{class:PhrictionTransactionEditor} and @{class:PonderEditor} are both simple
+editors that are not too difficult to understand. A much more complex one is
+@{class:ManiphestTransactionEditor}.

File Metadata

Mime Type
text/plain
Expires
Wed, Feb 19, 04:50 (3 d, 12 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1174713
Default Alt Text
D25883.1739940629.diff (16 KB)

Event Timeline