Edit a Maniphest task programatically

#1

Hello all,

i am trying the following:

  • When saving Maniphest task A also edit Maniphest task B
    – Scenario: Task A has a “Blocked By” custom field which, when set to “Task B” should automatically edit the custom field “Blocks” of Task B to the value of Task A

I don’t want to do this with Conduit as i don’t want to need a Conduit API Token for this. So, is there any way to (at best easily) editing a custom field of Task B when saving Task A without Conduit?

Thanks for an answer.

Take care
Florian

#2

Have you tried Herald?

#3

Hey @avivey, thanks for your reply. I haven’t tried Herald yet, but as this should go into an extension which might be used by others i don’t want to have the user to set up some rules or anything. I was more thinking of using the applyApplicationTransactionInternalEffects function of my custom field or something like that. Or would this be a bad way? Can Herald rules be applied without the user actually have to add them first?

Thanks and take care
Florian

#4

Maybe applyApplicationTransactionExternalEffects would make sense in some case.

In your case, it sounds like “blocking” is a relation between tasks, so maybe it would be better represented by an Edge, and have the fields read the edges.

#5

Hello @avivey,

thank you for your suggestion. When i get it right saving it as an edge will make it NOT appear in the customFields section, right? As i would like it to be there.

In general i find it very complicated to work with custom fields when reading an object (Maniphest task for example), having to do something like:

$blocked_field_list = PhabricatorCustomField::getObjectFields(
                            $blocked_task,
                            PhabricatorCustomField::ROLE_EDIT
                        );
$blocked_field_list->setViewer($this->getViewer())
                        ->readFieldsFromStorage($blocked_task);
$blocked_fields = $blocked_field_list->getFields();$blocked_field = $blocked_fields['ganttchart:blocks'];

to get a list and values of attached custom fields for the task. Anyway, back to the edit case. When doing something like:

$blocks_task = id(new ManiphestTaskQuery())
                    ->setViewer($this->getViewer())
                    ->needProjectPHIDs(true)
                    ->withPHIDs(array($blocks_task_phid))
                    ->executeOne();
$blocks_task->setTitle('Foo');
$blocks_task-save()

works just fine, why isn’t it possible to do something like

$blocks_task->setCustomField('blockedBy', array('PHID-xxx'));
$blocks_task-save()

also? That would make it so much easier. But when i understand you right there is no such thing or way at the moment, right? I also started to fiddle with the ManiphestEditEngine or mimicking the function of PhabricatorEditEngine when saving edit data and tried creating a list of $xactions myself but as you might figure without any real luck.

Thanks for your answer and take care
Florian

#6

The edge will not automatically show up, you still need a CustomField, but the field will be backed by an Edge and its transaction will be an EdgeTransaction, which handles both directions of the edge.

When doing this:

$object = get_object(...);
$object->setTitle('title');
$object->save();

You can’t do policy checks, can’t trigger Herald, and can’t publish a feed event for the modification, which is why this flow has been replaces with Transactions flow:

$object = $get_object();
$xactions[] = new SetTitleTransaction()
    ->setNewValue('Title');
$engine->applyTransactions($object, $xactions);

which handles all those things.

#7

Hello @avivey,

about the Edges: alright, got it, thanks for clarifying.

about the save() method: i knew there was a catch to it, again thanks for clarifying it. Also i am not sure if there is any way to make this work with custom fields, i have found none.

about the TransactionEngine part: so i guess that would be the best way to go if i don’t want to use edges (as i still want the blockedBy and inverse blocks fields to be custom tokenizer fields and don’t want to fiddle with another complexity level). I will have a look into the PhabricatorEditEngine class and methods, maybe i can figure it out how to make it work with one custom field only. If we assume my custom field has a fieldKey myextension:blockedBy , how would the resulting new SetXYZTransaction() then be? new setMyextensionBlockedBy() or something else? And would i have to add another class in my extension, for example a setMyextensionBlockedBy class?

One question still: is the applyApplicationTransactionInternalEffects method in my custom field class then the right place to go for such code or would you advice for another place/method?

Thanks again for your input and take care
Florian

#8

I think you’ll have a CustomFieldTransaction with setFieldId() and setNewValue(), but I’m not sure - I’m not sure I’ve ever written any. It’s possible that you write a custom Transaction class that extends CustomFieldTransaction, or something similar. There should be a few examples in the code, in Maniphest and/or Differential.

applyApplicationTransactionInternalEffects is for effects on the object being modified; You want applyApplicationTransactionExternalEffects (note External), which is for effects on other objects (the other end of the relation).

There’s no way to make save() work with custom fields - save() is part of the ORM layer, and custom fields are separate objects in that level. SearchEngine and the Custom Field Engine bring those together at a higher level.

#9

Hey @avivey,

thanks a lot for pointing me in the right direction and giving me a possible starting point for this. I will look into it and try to find a solution.

Also thank you for giving a much appreciated insight into the complexity of Phabricator and the way things work.

Take care
Florian

1 Like
#10

Hey @avivey,

so, finally the first problem is solved: editing Task B when Task A is saved. BUT with that another problem arises and i can’t get my head around it.

I solved the “saving Task A edits Task B” thing as follows (values are just for example):

public function applyApplicationTransactionExternalEffects(
        PhabricatorApplicationTransaction $xaction)
    {
        if ($this->getProxy()) {
 $this->applyBlocks($xaction);
return $this->getProxy()->applyApplicationTransactionExternalEffects($xaction);
        }
return;
    }

public function applyBlocks(PhabricatorApplicationTransaction $xaction)
    {
        $blocks_tasks_phids = json_decode($xaction->getNewValue());
        if (!empty($blocks_tasks_phids)) {
            foreach ($blocks_tasks_phids as $blocks_task_phid) {
                $blocks_task = id(new ManiphestTaskQuery())
                    ->setViewer($this->getViewer())
                    ->needProjectPHIDs(true)
                    ->withPHIDs(array($blocks_task_phid))
                    ->executeOne();

                $blocks_template = new ManiphestTransaction();

                $blocks_edit_engine = $blocks_task->getApplicationTransactionEditor();
                $blocks_edit_engine->setActor($this->getViewer())
                    ->setContinueOnMissingFields(true)
                    ->setContinueOnNoEffect(true);

                $blocks_transaction = clone $blocks_template;
                $blocks_transaction->setTransactionType(
                  PhabricatorTransactions::TYPE_CUSTOMFIELD);
                $blocks_transaction->setMetadataValue('customfield:key', 'ganttchart:blocks');
                $blocks_transaction->setOldValue(array('1234'));
                $blocks_transaction->setNewValue(array('5678'));
                $blocks_transactions[] = $blocks_transaction;

                $blocks_content_source = PhabricatorContentSource::newForSource(
                    PhabricatorWebContentSource::SOURCECONST
                );
                $blocks_edit_engine->setContentSource($blocks_content_source);
                $blocks_edit_engine->applyTransactions($blocks_task, $blocks_transactions);

            }
        }
    } 

That works and edits the blocks field of Task B. But as soon as i do it with $blocks_edit_engine->applyTransactions($blocks_task, $blocks_transactions); the changes i made in the blockedBy field of Task A are NOT saved. I see the same behaviour as soon as i put

$blocks_field_list = PhabricatorCustomField::getObjectFields(
                            $blocks_task,
                            PhabricatorCustomField::ROLE_EDIT
                        );

somewhere in the applyBlocks function. Any chance you might know why this is happening as i of course need to get the old value of Task B of field blocks before editing it and if i can’t do applyTransactions() at all i can’t edit Task B.

Thanks again for your help
Florian

#11

Sorry, I don’t know; I’ve literally never used applyExternalEffects.

You’re going to have to look for examples in the code base.

#12

Hi @avivey,

alright, thanks anyways!

Take care
Florian