Path: blob/master/src/applications/maniphest/editor/ManiphestTransactionEditor.php
12256 views
<?php12final class ManiphestTransactionEditor3extends PhabricatorApplicationTransactionEditor {45private $oldProjectPHIDs;6private $moreValidationErrors = array();78public function getEditorApplicationClass() {9return 'PhabricatorManiphestApplication';10}1112public function getEditorObjectsDescription() {13return pht('Maniphest Tasks');14}1516public function getTransactionTypes() {17$types = parent::getTransactionTypes();1819$types[] = PhabricatorTransactions::TYPE_COMMENT;20$types[] = PhabricatorTransactions::TYPE_EDGE;21$types[] = PhabricatorTransactions::TYPE_COLUMNS;22$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;23$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;2425return $types;26}2728public function getCreateObjectTitle($author, $object) {29return pht('%s created this task.', $author);30}3132public function getCreateObjectTitleForFeed($author, $object) {33return pht('%s created %s.', $author, $object);34}3536protected function getCustomTransactionOldValue(37PhabricatorLiskDAO $object,38PhabricatorApplicationTransaction $xaction) {3940switch ($xaction->getTransactionType()) {41case PhabricatorTransactions::TYPE_COLUMNS:42return null;43}44}4546protected function getCustomTransactionNewValue(47PhabricatorLiskDAO $object,48PhabricatorApplicationTransaction $xaction) {4950switch ($xaction->getTransactionType()) {51case PhabricatorTransactions::TYPE_COLUMNS:52return $xaction->getNewValue();53}54}5556protected function transactionHasEffect(57PhabricatorLiskDAO $object,58PhabricatorApplicationTransaction $xaction) {5960$old = $xaction->getOldValue();61$new = $xaction->getNewValue();6263switch ($xaction->getTransactionType()) {64case PhabricatorTransactions::TYPE_COLUMNS:65return (bool)$new;66}6768return parent::transactionHasEffect($object, $xaction);69}7071protected function applyCustomInternalTransaction(72PhabricatorLiskDAO $object,73PhabricatorApplicationTransaction $xaction) {7475switch ($xaction->getTransactionType()) {76case PhabricatorTransactions::TYPE_COLUMNS:77return;78}79}8081protected function applyCustomExternalTransaction(82PhabricatorLiskDAO $object,83PhabricatorApplicationTransaction $xaction) {8485switch ($xaction->getTransactionType()) {86case PhabricatorTransactions::TYPE_COLUMNS:87foreach ($xaction->getNewValue() as $move) {88$this->applyBoardMove($object, $move);89}90break;91}92}9394protected function applyFinalEffects(95PhabricatorLiskDAO $object,96array $xactions) {9798// When we change the status of a task, update tasks this tasks blocks99// with a message to the effect of "alincoln resolved blocking task Txxx."100$unblock_xaction = null;101foreach ($xactions as $xaction) {102switch ($xaction->getTransactionType()) {103case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:104$unblock_xaction = $xaction;105break;106}107}108109if ($unblock_xaction !== null) {110$blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(111$object->getPHID(),112ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);113if ($blocked_phids) {114// In theory we could apply these through policies, but that seems a115// little bit surprising. For now, use the actor's vision.116$blocked_tasks = id(new ManiphestTaskQuery())117->setViewer($this->getActor())118->withPHIDs($blocked_phids)119->needSubscriberPHIDs(true)120->needProjectPHIDs(true)121->execute();122123$old = $unblock_xaction->getOldValue();124$new = $unblock_xaction->getNewValue();125126foreach ($blocked_tasks as $blocked_task) {127$parent_xaction = id(new ManiphestTransaction())128->setTransactionType(129ManiphestTaskUnblockTransaction::TRANSACTIONTYPE)130->setOldValue(array($object->getPHID() => $old))131->setNewValue(array($object->getPHID() => $new));132133if ($this->getIsNewObject()) {134$parent_xaction->setMetadataValue('blocker.new', true);135}136137$this->newSubEditor()138->setContinueOnNoEffect(true)139->setContinueOnMissingFields(true)140->applyTransactions($blocked_task, array($parent_xaction));141}142}143}144145return $xactions;146}147148protected function shouldSendMail(149PhabricatorLiskDAO $object,150array $xactions) {151return true;152}153154protected function getMailSubjectPrefix() {155return pht('[Maniphest]');156}157158protected function getMailThreadID(PhabricatorLiskDAO $object) {159return 'maniphest-task-'.$object->getPHID();160}161162protected function getMailTo(PhabricatorLiskDAO $object) {163$phids = array();164165if ($object->getOwnerPHID()) {166$phids[] = $object->getOwnerPHID();167}168$phids[] = $this->getActingAsPHID();169170return $phids;171}172173public function getMailTagsMap() {174return array(175ManiphestTransaction::MAILTAG_STATUS =>176pht("A task's status changes."),177ManiphestTransaction::MAILTAG_OWNER =>178pht("A task's owner changes."),179ManiphestTransaction::MAILTAG_PRIORITY =>180pht("A task's priority changes."),181ManiphestTransaction::MAILTAG_CC =>182pht("A task's subscribers change."),183ManiphestTransaction::MAILTAG_PROJECTS =>184pht("A task's associated projects change."),185ManiphestTransaction::MAILTAG_UNBLOCK =>186pht("One of a task's subtasks changes status."),187ManiphestTransaction::MAILTAG_COLUMN =>188pht('A task is moved between columns on a workboard.'),189ManiphestTransaction::MAILTAG_COMMENT =>190pht('Someone comments on a task.'),191ManiphestTransaction::MAILTAG_OTHER =>192pht('Other task activity not listed above occurs.'),193);194}195196protected function buildReplyHandler(PhabricatorLiskDAO $object) {197return id(new ManiphestReplyHandler())198->setMailReceiver($object);199}200201protected function buildMailTemplate(PhabricatorLiskDAO $object) {202$id = $object->getID();203$title = $object->getTitle();204205return id(new PhabricatorMetaMTAMail())206->setSubject("T{$id}: {$title}");207}208209protected function getObjectLinkButtonLabelForMail(210PhabricatorLiskDAO $object) {211return pht('View Task');212}213214protected function buildMailBody(215PhabricatorLiskDAO $object,216array $xactions) {217218$body = parent::buildMailBody($object, $xactions);219220if ($this->getIsNewObject()) {221$body->addRemarkupSection(222pht('TASK DESCRIPTION'),223$object->getDescription());224}225226$body->addLinkSection(227pht('TASK DETAIL'),228$this->getObjectLinkButtonURIForMail($object));229230231$board_phids = array();232$type_columns = PhabricatorTransactions::TYPE_COLUMNS;233foreach ($xactions as $xaction) {234if ($xaction->getTransactionType() == $type_columns) {235$moves = $xaction->getNewValue();236foreach ($moves as $move) {237$board_phids[] = $move['boardPHID'];238}239}240}241242if ($board_phids) {243$projects = id(new PhabricatorProjectQuery())244->setViewer($this->requireActor())245->withPHIDs($board_phids)246->execute();247248foreach ($projects as $project) {249$body->addLinkSection(250pht('WORKBOARD'),251PhabricatorEnv::getProductionURI($project->getWorkboardURI()));252}253}254255256return $body;257}258259protected function shouldPublishFeedStory(260PhabricatorLiskDAO $object,261array $xactions) {262return true;263}264265protected function supportsSearch() {266return true;267}268269protected function shouldApplyHeraldRules(270PhabricatorLiskDAO $object,271array $xactions) {272return true;273}274275protected function buildHeraldAdapter(276PhabricatorLiskDAO $object,277array $xactions) {278279return id(new HeraldManiphestTaskAdapter())280->setTask($object);281}282283protected function adjustObjectForPolicyChecks(284PhabricatorLiskDAO $object,285array $xactions) {286287$copy = parent::adjustObjectForPolicyChecks($object, $xactions);288foreach ($xactions as $xaction) {289switch ($xaction->getTransactionType()) {290case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:291$copy->setOwnerPHID($xaction->getNewValue());292break;293default:294break;295}296}297298return $copy;299}300301protected function validateAllTransactions(302PhabricatorLiskDAO $object,303array $xactions) {304305$errors = parent::validateAllTransactions($object, $xactions);306307if ($this->moreValidationErrors) {308$errors = array_merge($errors, $this->moreValidationErrors);309}310311foreach ($this->getLockValidationErrors($object, $xactions) as $error) {312$errors[] = $error;313}314315return $errors;316}317318protected function expandTransactions(319PhabricatorLiskDAO $object,320array $xactions) {321322$actor = $this->getActor();323$actor_phid = $actor->getPHID();324325$results = parent::expandTransactions($object, $xactions);326327$is_unassigned = ($object->getOwnerPHID() === null);328329$any_assign = false;330foreach ($xactions as $xaction) {331if ($xaction->getTransactionType() ==332ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) {333$any_assign = true;334break;335}336}337338$is_open = !$object->isClosed();339340$new_status = null;341foreach ($xactions as $xaction) {342switch ($xaction->getTransactionType()) {343case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:344$new_status = $xaction->getNewValue();345break;346}347}348349if ($new_status === null) {350$is_closing = false;351} else {352$is_closing = ManiphestTaskStatus::isClosedStatus($new_status);353}354355// If the task is not assigned, not being assigned, currently open, and356// being closed, try to assign the actor as the owner.357if ($is_unassigned && !$any_assign && $is_open && $is_closing) {358$is_claim = ManiphestTaskStatus::isClaimStatus($new_status);359360// Don't assign the actor if they aren't a real user.361// Don't claim the task if the status is configured to not claim.362if ($actor_phid && $is_claim) {363$results[] = id(new ManiphestTransaction())364->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE)365->setNewValue($actor_phid);366}367}368369// Automatically subscribe the author when they create a task.370if ($this->getIsNewObject()) {371if ($actor_phid) {372$results[] = id(new ManiphestTransaction())373->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)374->setNewValue(375array(376'+' => array($actor_phid => $actor_phid),377));378}379}380381$send_notifications = PhabricatorNotificationClient::isEnabled();382if ($send_notifications) {383$this->oldProjectPHIDs = $this->loadProjectPHIDs($object);384}385386return $results;387}388389protected function expandTransaction(390PhabricatorLiskDAO $object,391PhabricatorApplicationTransaction $xaction) {392393$results = parent::expandTransaction($object, $xaction);394395$type = $xaction->getTransactionType();396switch ($type) {397case PhabricatorTransactions::TYPE_COLUMNS:398try {399$more_xactions = $this->buildMoveTransaction($object, $xaction);400foreach ($more_xactions as $more_xaction) {401$results[] = $more_xaction;402}403} catch (Exception $ex) {404$error = new PhabricatorApplicationTransactionValidationError(405$type,406pht('Invalid'),407$ex->getMessage(),408$xaction);409$this->moreValidationErrors[] = $error;410}411break;412case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:413// If this is a no-op update, don't expand it.414$old_value = $object->getOwnerPHID();415$new_value = $xaction->getNewValue();416if ($old_value === $new_value) {417break;418}419420// When a task is reassigned, move the old owner to the subscriber421// list so they're still in the loop.422if ($old_value) {423$results[] = id(new ManiphestTransaction())424->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)425->setIgnoreOnNoEffect(true)426->setNewValue(427array(428'+' => array($old_value => $old_value),429));430}431break;432}433434return $results;435}436437private function buildMoveTransaction(438PhabricatorLiskDAO $object,439PhabricatorApplicationTransaction $xaction) {440$actor = $this->getActor();441442$new = $xaction->getNewValue();443if (!is_array($new)) {444$this->validateColumnPHID($new);445$new = array($new);446}447448$relative_phids = array();449foreach ($new as $key => $value) {450if (!is_array($value)) {451$this->validateColumnPHID($value);452$value = array(453'columnPHID' => $value,454);455}456457PhutilTypeSpec::checkMap(458$value,459array(460'columnPHID' => 'string',461'beforePHIDs' => 'optional list<string>',462'afterPHIDs' => 'optional list<string>',463464// Deprecated older variations of "beforePHIDs" and "afterPHIDs".465'beforePHID' => 'optional string',466'afterPHID' => 'optional string',467));468469$value = $value + array(470'beforePHIDs' => array(),471'afterPHIDs' => array(),472);473474// Normalize the legacy keys "beforePHID" and "afterPHID" keys to the475// modern format.476if (!empty($value['afterPHID'])) {477if ($value['afterPHIDs']) {478throw new Exception(479pht(480'Transaction specifies both "afterPHID" and "afterPHIDs". '.481'Specify only "afterPHIDs".'));482}483$value['afterPHIDs'] = array($value['afterPHID']);484unset($value['afterPHID']);485}486487if (isset($value['beforePHID'])) {488if ($value['beforePHIDs']) {489throw new Exception(490pht(491'Transaction specifies both "beforePHID" and "beforePHIDs". '.492'Specify only "beforePHIDs".'));493}494$value['beforePHIDs'] = array($value['beforePHID']);495unset($value['beforePHID']);496}497498foreach ($value['beforePHIDs'] as $phid) {499$relative_phids[] = $phid;500}501502foreach ($value['afterPHIDs'] as $phid) {503$relative_phids[] = $phid;504}505506$new[$key] = $value;507}508509// We require that objects you specify in "beforePHIDs" or "afterPHIDs"510// are real objects which exist and which you have permission to view.511// If you provide other objects, we remove them from the specification.512513if ($relative_phids) {514$objects = id(new PhabricatorObjectQuery())515->setViewer($actor)516->withPHIDs($relative_phids)517->execute();518$objects = mpull($objects, null, 'getPHID');519} else {520$objects = array();521}522523foreach ($new as $key => $value) {524$value['afterPHIDs'] = $this->filterValidPHIDs(525$value['afterPHIDs'],526$objects);527$value['beforePHIDs'] = $this->filterValidPHIDs(528$value['beforePHIDs'],529$objects);530531$new[$key] = $value;532}533534$column_phids = ipull($new, 'columnPHID');535if ($column_phids) {536$columns = id(new PhabricatorProjectColumnQuery())537->setViewer($actor)538->withPHIDs($column_phids)539->execute();540$columns = mpull($columns, null, 'getPHID');541} else {542$columns = array();543}544545$board_phids = mpull($columns, 'getProjectPHID');546$object_phid = $object->getPHID();547548// Note that we may not have an object PHID if we're creating a new549// object.550$object_phids = array();551if ($object_phid) {552$object_phids[] = $object_phid;553}554555if ($object_phids) {556$layout_engine = id(new PhabricatorBoardLayoutEngine())557->setViewer($this->getActor())558->setBoardPHIDs($board_phids)559->setObjectPHIDs($object_phids)560->setFetchAllBoards(true)561->executeLayout();562}563564foreach ($new as $key => $spec) {565$column_phid = $spec['columnPHID'];566$column = idx($columns, $column_phid);567if (!$column) {568throw new Exception(569pht(570'Column move transaction specifies column PHID "%s", but there '.571'is no corresponding column with this PHID.',572$column_phid));573}574575$board_phid = $column->getProjectPHID();576577if ($object_phid) {578$old_columns = $layout_engine->getObjectColumns(579$board_phid,580$object_phid);581$old_column_phids = mpull($old_columns, 'getPHID');582} else {583$old_column_phids = array();584}585586$spec += array(587'boardPHID' => $board_phid,588'fromColumnPHIDs' => $old_column_phids,589);590591// Check if the object is already in this column, and isn't being moved.592// We can just drop this column change if it has no effect.593$from_map = array_fuse($spec['fromColumnPHIDs']);594$already_here = isset($from_map[$column_phid]);595596$is_reordering = ($spec['afterPHIDs'] || $spec['beforePHIDs']);597if ($already_here && !$is_reordering) {598unset($new[$key]);599} else {600$new[$key] = $spec;601}602}603604$new = array_values($new);605$xaction->setNewValue($new);606607608$more = array();609610// If we're moving the object into a column and it does not already belong611// in the column, add the appropriate board. For normal columns, this612// is the board PHID. For proxy columns, it is the proxy PHID, unless the613// object is already a member of some descendant of the proxy PHID.614615// The major case where this can happen is moves via the API, but it also616// happens when a user drags a task from the "Backlog" to a milestone617// column.618619if ($object_phid) {620$current_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(621$object_phid,622PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);623$current_phids = array_fuse($current_phids);624} else {625$current_phids = array();626}627628$add_boards = array();629foreach ($new as $move) {630$column_phid = $move['columnPHID'];631$board_phid = $move['boardPHID'];632$column = $columns[$column_phid];633$proxy_phid = $column->getProxyPHID();634635// If this is a normal column, add the board if the object isn't already636// associated.637if (!$proxy_phid) {638if (!isset($current_phids[$board_phid])) {639$add_boards[] = $board_phid;640}641continue;642}643644// If this is a proxy column but the object is already associated with645// the proxy board, we don't need to do anything.646if (isset($current_phids[$proxy_phid])) {647continue;648}649650// If this a proxy column and the object is already associated with some651// descendant of the proxy board, we also don't need to do anything.652$descendants = id(new PhabricatorProjectQuery())653->setViewer(PhabricatorUser::getOmnipotentUser())654->withAncestorProjectPHIDs(array($proxy_phid))655->execute();656657$found_descendant = false;658foreach ($descendants as $descendant) {659if (isset($current_phids[$descendant->getPHID()])) {660$found_descendant = true;661break;662}663}664665if ($found_descendant) {666continue;667}668669// Otherwise, we're moving the object to a proxy column which it is not670// a member of yet, so add an association to the column's proxy board.671672$add_boards[] = $proxy_phid;673}674675if ($add_boards) {676$more[] = id(new ManiphestTransaction())677->setTransactionType(PhabricatorTransactions::TYPE_EDGE)678->setMetadataValue(679'edge:type',680PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)681->setIgnoreOnNoEffect(true)682->setNewValue(683array(684'+' => array_fuse($add_boards),685));686}687688return $more;689}690691private function applyBoardMove($object, array $move) {692$board_phid = $move['boardPHID'];693$column_phid = $move['columnPHID'];694695$before_phids = $move['beforePHIDs'];696$after_phids = $move['afterPHIDs'];697698$object_phid = $object->getPHID();699700// We're doing layout with the omnipotent viewer to make sure we don't701// remove positions in columns that exist, but which the actual actor702// can't see.703$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();704705$select_phids = array($board_phid);706707$descendants = id(new PhabricatorProjectQuery())708->setViewer($omnipotent_viewer)709->withAncestorProjectPHIDs($select_phids)710->execute();711foreach ($descendants as $descendant) {712$select_phids[] = $descendant->getPHID();713}714715$board_tasks = id(new ManiphestTaskQuery())716->setViewer($omnipotent_viewer)717->withEdgeLogicPHIDs(718PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,719PhabricatorQueryConstraint::OPERATOR_ANCESTOR,720array($select_phids))721->execute();722723$board_tasks = mpull($board_tasks, null, 'getPHID');724$board_tasks[$object_phid] = $object;725726// Make sure tasks are sorted by ID, so we lay out new positions in727// a consistent way.728$board_tasks = msort($board_tasks, 'getID');729730$object_phids = array_keys($board_tasks);731732$engine = id(new PhabricatorBoardLayoutEngine())733->setViewer($omnipotent_viewer)734->setBoardPHIDs(array($board_phid))735->setObjectPHIDs($object_phids)736->executeLayout();737738// TODO: This logic needs to be revised when we legitimately support739// multiple column positions.740$columns = $engine->getObjectColumns($board_phid, $object_phid);741foreach ($columns as $column) {742$engine->queueRemovePosition(743$board_phid,744$column->getPHID(),745$object_phid);746}747748$engine->queueAddPosition(749$board_phid,750$column_phid,751$object_phid,752$after_phids,753$before_phids);754755$engine->applyPositionUpdates();756}757758759private function validateColumnPHID($value) {760if (phid_get_type($value) == PhabricatorProjectColumnPHIDType::TYPECONST) {761return;762}763764throw new Exception(765pht(766'When moving objects between columns on a board, columns must '.767'be identified by PHIDs. This transaction uses "%s" to identify '.768'a column, but that is not a valid column PHID.',769$value));770}771772773private function getLockValidationErrors($object, array $xactions) {774$errors = array();775776$old_owner = $object->getOwnerPHID();777$old_status = $object->getStatus();778779$new_owner = $old_owner;780$new_status = $old_status;781782$owner_xaction = null;783$status_xaction = null;784785foreach ($xactions as $xaction) {786switch ($xaction->getTransactionType()) {787case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:788$new_owner = $xaction->getNewValue();789$owner_xaction = $xaction;790break;791case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:792$new_status = $xaction->getNewValue();793$status_xaction = $xaction;794break;795}796}797798$actor_phid = $this->getActingAsPHID();799800$was_locked = ManiphestTaskStatus::areEditsLockedInStatus(801$old_status);802$now_locked = ManiphestTaskStatus::areEditsLockedInStatus(803$new_status);804805if (!$now_locked) {806// If we're not ending in an edit-locked status, everything is good.807} else if ($new_owner !== null) {808// If we ending the edit with some valid owner, this is allowed for809// now. We might need to revisit this.810} else {811// The edits end with the task locked and unowned. No one will be able812// to edit it, so we forbid this. We try to be specific about what the813// user did wrong.814815$owner_changed = ($old_owner && !$new_owner);816$status_changed = ($was_locked !== $now_locked);817$message = null;818819if ($status_changed && $owner_changed) {820$message = pht(821'You can not lock this task and unassign it at the same time '.822'because no one will be able to edit it anymore. Lock the task '.823'or remove the owner, but not both.');824$problem_xaction = $status_xaction;825} else if ($status_changed) {826$message = pht(827'You can not lock this task because it does not have an owner. '.828'No one would be able to edit the task. Assign the task to an '.829'owner before locking it.');830$problem_xaction = $status_xaction;831} else if ($owner_changed) {832$message = pht(833'You can not remove the owner of this task because it is locked '.834'and no one would be able to edit the task. Reassign the task or '.835'unlock it before removing the owner.');836$problem_xaction = $owner_xaction;837} else {838// If the task was already broken, we don't have a transaction to839// complain about so just let it through. In theory, this is840// impossible since policy rules should kick in before we get here.841}842843if ($message) {844$errors[] = new PhabricatorApplicationTransactionValidationError(845$problem_xaction->getTransactionType(),846pht('Lock Error'),847$message,848$problem_xaction);849}850}851852return $errors;853}854855private function filterValidPHIDs($phid_list, array $object_map) {856foreach ($phid_list as $key => $phid) {857if (isset($object_map[$phid])) {858continue;859}860861unset($phid_list[$key]);862}863864return array_values($phid_list);865}866867protected function didApplyTransactions($object, array $xactions) {868$send_notifications = PhabricatorNotificationClient::isEnabled();869if ($send_notifications) {870$old_phids = $this->oldProjectPHIDs;871$new_phids = $this->loadProjectPHIDs($object);872873// We want to emit update notifications for all old and new tagged874// projects, and all parents of those projects. For example, if an875// edit removes project "A > B" from a task, the "A" workboard should876// receive an update event.877878$project_phids = array_fuse($old_phids) + array_fuse($new_phids);879$project_phids = array_keys($project_phids);880881if ($project_phids) {882$projects = id(new PhabricatorProjectQuery())883->setViewer(PhabricatorUser::getOmnipotentUser())884->withPHIDs($project_phids)885->execute();886887$notify_projects = array();888foreach ($projects as $project) {889$notify_projects[$project->getPHID()] = $project;890foreach ($project->getAncestorProjects() as $ancestor) {891$notify_projects[$ancestor->getPHID()] = $ancestor;892}893}894895foreach ($notify_projects as $key => $project) {896if (!$project->getHasWorkboard()) {897unset($notify_projects[$key]);898}899}900901$notify_phids = array_keys($notify_projects);902903if ($notify_phids) {904$data = array(905'type' => 'workboards',906'subscribers' => $notify_phids,907);908909PhabricatorNotificationClient::tryToPostMessage($data);910}911}912}913914return $xactions;915}916917private function loadProjectPHIDs(ManiphestTask $task) {918if (!$task->getPHID()) {919return array();920}921922$edge_query = id(new PhabricatorEdgeQuery())923->withSourcePHIDs(array($task->getPHID()))924->withEdgeTypes(925array(926PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,927));928929$edge_query->execute();930931return $edge_query->getDestinationPHIDs();932}933934}935936937