@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

Allow Controllers to return a wider range of "response-like" objects

Summary:
Ref T1806. Ref T5752. Currently, `handleRequest()` needs to return an `AphrontResponse`, but sometimes it's really convenient to return some other object, like a Dialog, and let that convert into a response elsewhere.

Formalize this and clean up some of the existing hacks for it so there's less custom/magical code in Phabricator-specific classes and more general code in Aphront classes.

More broadly, I want to clean up T5752 before pursuing T9132, since I'm generally happy with how `SearchEngine` works except for how it interacts with side navs / application menus. I want to fix that first so a new Editor (which will have a lot in common with SearchEngine in terms of how controllers interact with it) doesn't make the problem twice as bad.

Test Plan:
- Loaded a bunch of normal pages.
- Loaded dialogs.
- Loaded proxy responses (submitted empty comments in Maniphest).

Reviewers: chad

Reviewed By: chad

Subscribers: joshuaspence

Maniphest Tasks: T1806, T5752

Differential Revision: https://secure.phabricator.com/D14032

+206 -36
+9 -2
src/__phutil_library_map__.php
··· 158 158 'AphrontRequest' => 'aphront/AphrontRequest.php', 159 159 'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php', 160 160 'AphrontResponse' => 'aphront/response/AphrontResponse.php', 161 + 'AphrontResponseProducerInterface' => 'aphront/interface/AphrontResponseProducerInterface.php', 161 162 'AphrontRoutingMap' => 'aphront/site/AphrontRoutingMap.php', 162 163 'AphrontRoutingResult' => 'aphront/site/AphrontRoutingResult.php', 163 164 'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php', ··· 3732 3733 'AphrontCursorPagerView' => 'AphrontView', 3733 3734 'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration', 3734 3735 'AphrontDialogResponse' => 'AphrontResponse', 3735 - 'AphrontDialogView' => 'AphrontView', 3736 + 'AphrontDialogView' => array( 3737 + 'AphrontView', 3738 + 'AphrontResponseProducerInterface', 3739 + ), 3736 3740 'AphrontException' => 'Exception', 3737 3741 'AphrontFileResponse' => 'AphrontResponse', 3738 3742 'AphrontFormCheckboxControl' => 'AphrontFormControl', ··· 3775 3779 'AphrontPageView' => 'AphrontView', 3776 3780 'AphrontPlainTextResponse' => 'AphrontResponse', 3777 3781 'AphrontProgressBarView' => 'AphrontBarView', 3778 - 'AphrontProxyResponse' => 'AphrontResponse', 3782 + 'AphrontProxyResponse' => array( 3783 + 'AphrontResponse', 3784 + 'AphrontResponseProducerInterface', 3785 + ), 3779 3786 'AphrontRedirectResponse' => 'AphrontResponse', 3780 3787 'AphrontRedirectResponseTestCase' => 'PhabricatorTestCase', 3781 3788 'AphrontReloadResponse' => 'AphrontRedirectResponse',
-4
src/aphront/AphrontController.php
··· 24 24 return; 25 25 } 26 26 27 - public function didProcessRequest($response) { 28 - return $response; 29 - } 30 - 31 27 public function handleRequest(AphrontRequest $request) { 32 28 if (method_exists($this, 'processRequest')) { 33 29 return $this->processRequest();
+149 -3
src/aphront/configuration/AphrontApplicationConfiguration.php
··· 1 1 <?php 2 2 3 3 /** 4 - * @task routing URI Routing 4 + * @task routing URI Routing 5 + * @task response Response Handling 5 6 */ 6 7 abstract class AphrontApplicationConfiguration extends Phobject { 7 8 ··· 234 235 if (!$response) { 235 236 $controller->willProcessRequest($uri_data); 236 237 $response = $controller->handleRequest($request); 238 + $this->validateControllerResponse($controller, $response); 237 239 } 238 240 } catch (Exception $ex) { 239 241 $original_exception = $ex; ··· 241 243 } 242 244 243 245 try { 244 - $response = $controller->didProcessRequest($response); 245 - $response = $this->willSendResponse($response, $controller); 246 + $response = $this->produceResponse($request, $response); 247 + $response = $controller->willSendResponse($response); 246 248 $response->setRequest($request); 247 249 248 250 $unexpected_output = PhabricatorStartup::endOutputCapture(); ··· 423 425 424 426 return $site; 425 427 } 428 + 429 + 430 + /* -( Response Handling )-------------------------------------------------- */ 431 + 432 + 433 + /** 434 + * Tests if a response is of a valid type. 435 + * 436 + * @param wild Supposedly valid response. 437 + * @return bool True if the object is of a valid type. 438 + * @task response 439 + */ 440 + private function isValidResponseObject($response) { 441 + if ($response instanceof AphrontResponse) { 442 + return true; 443 + } 444 + 445 + if ($response instanceof AphrontResponseProducerInterface) { 446 + return true; 447 + } 448 + 449 + return false; 450 + } 451 + 452 + 453 + /** 454 + * Verifies that the return value from an @{class:AphrontController} is 455 + * of an allowed type. 456 + * 457 + * @param AphrontController Controller which returned the response. 458 + * @param wild Supposedly valid response. 459 + * @return void 460 + * @task response 461 + */ 462 + private function validateControllerResponse( 463 + AphrontController $controller, 464 + $response) { 465 + 466 + if ($this->isValidResponseObject($response)) { 467 + return; 468 + } 469 + 470 + throw new Exception( 471 + pht( 472 + 'Controller "%s" returned an invalid response from call to "%s". '. 473 + 'This method must return an object of class "%s", or an object '. 474 + 'which implements the "%s" interface.', 475 + get_class($controller), 476 + 'handleRequest()', 477 + 'AphrontResponse', 478 + 'AphrontResponseProducerInterface')); 479 + } 480 + 481 + 482 + /** 483 + * Verifies that the erturn value from an 484 + * @{class:AphrontResponseProducerInterface} is of an allowed type. 485 + * 486 + * @param AphrontResponseProducerInterface Object which produced 487 + * this response. 488 + * @param wild Supposedly valid response. 489 + * @return void 490 + * @task response 491 + */ 492 + private function validateProducerResponse( 493 + AphrontResponseProducerInterface $producer, 494 + $response) { 495 + 496 + if ($this->isValidResponseObject($response)) { 497 + return; 498 + } 499 + 500 + throw new Exception( 501 + pht( 502 + 'Producer "%s" returned an invalid response from call to "%s". '. 503 + 'This method must return an object of class "%s", or an object '. 504 + 'which implements the "%s" interface.', 505 + get_class($producer), 506 + 'produceAphrontResponse()', 507 + 'AphrontResponse', 508 + 'AphrontResponseProducerInterface')); 509 + } 510 + 511 + 512 + /** 513 + * Resolves a response object into an @{class:AphrontResponse}. 514 + * 515 + * Controllers are permitted to return actual responses of class 516 + * @{class:AphrontResponse}, or other objects which implement 517 + * @{interface:AphrontResponseProducerInterface} and can produce a response. 518 + * 519 + * If a controller returns a response producer, invoke it now and produce 520 + * the real response. 521 + * 522 + * @param AphrontRequest Request being handled. 523 + * @param AphrontResponse|AphrontResponseProducerInterface Response, or 524 + * response producer. 525 + * @return AphrontResponse Response after any required production. 526 + * @task response 527 + */ 528 + private function produceResponse(AphrontRequest $request, $response) { 529 + $original = $response; 530 + 531 + // Detect cycles on the exact same objects. It's still possible to produce 532 + // infinite responses as long as they're all unique, but we can only 533 + // reasonably detect cycles, not guarantee that response production halts. 534 + 535 + $seen = array(); 536 + while (true) { 537 + // NOTE: It is permissible for an object to be both a response and a 538 + // response producer. If so, being a producer is "stronger". This is 539 + // used by AphrontProxyResponse. 540 + 541 + // If this response is a valid response, hand over the request first. 542 + if ($response instanceof AphrontResponse) { 543 + $response->setRequest($request); 544 + } 545 + 546 + // If this isn't a producer, we're all done. 547 + if (!($response instanceof AphrontResponseProducerInterface)) { 548 + break; 549 + } 550 + 551 + $hash = spl_object_hash($response); 552 + if (isset($seen[$hash])) { 553 + throw new Exception( 554 + pht( 555 + 'Failure while producing response for object of class "%s": '. 556 + 'encountered production cycle (identical object, of class "%s", '. 557 + 'was produced twice).', 558 + get_class($original), 559 + get_class($response))); 560 + } 561 + 562 + $seen[$hash] = true; 563 + 564 + $new_response = $response->produceAphrontResponse(); 565 + $this->validateProducerResponse($response, $new_response); 566 + $response = $new_response; 567 + } 568 + 569 + return $response; 570 + } 571 + 426 572 427 573 }
-4
src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
··· 273 273 return $response; 274 274 } 275 275 276 - public function willSendResponse(AphrontResponse $response) { 277 - return $response; 278 - } 279 - 280 276 public function build404Controller() { 281 277 return array(new Phabricator404Controller(), array()); 282 278 }
+24
src/aphront/interface/AphrontResponseProducerInterface.php
··· 1 + <?php 2 + 3 + /** 4 + * An object can implement this interface to allow it to be returned directly 5 + * from an @{class:AphrontController}. 6 + * 7 + * Normally, controllers must return an @{class:AphrontResponse}. Sometimes, 8 + * this is not convenient or requires an awkward API. If it's preferable to 9 + * return some other type of object which is equivalent to or describes a 10 + * valid response, that object can implement this interface and produce a 11 + * response later. 12 + */ 13 + interface AphrontResponseProducerInterface { 14 + 15 + 16 + /** 17 + * Produce the equivalent @{class:AphrontResponse} for this object. 18 + * 19 + * @return AphrontResponse Equivalent response. 20 + */ 21 + public function produceAphrontResponse(); 22 + 23 + 24 + }
+11 -1
src/aphront/response/AphrontProxyResponse.php
··· 8 8 * then constructing a real @{class:AphrontAjaxResponse} in 9 9 * @{method:reduceProxyResponse}. 10 10 */ 11 - abstract class AphrontProxyResponse extends AphrontResponse { 11 + abstract class AphrontProxyResponse 12 + extends AphrontResponse 13 + implements AphrontResponseProducerInterface { 12 14 13 15 private $proxy; 14 16 ··· 69 71 '%s must implement %s.', 70 72 __CLASS__, 71 73 'reduceProxyResponse()')); 74 + } 75 + 76 + 77 + /* -( AphrontResponseProducerInterface )----------------------------------- */ 78 + 79 + 80 + public function produceAphrontResponse() { 81 + return $this->reduceProxyResponse(); 72 82 } 73 83 74 84 }
+1 -21
src/applications/base/controller/PhabricatorController.php
··· 370 370 return $this->buildPageResponse($page); 371 371 } 372 372 373 - public function didProcessRequest($response) { 374 - // If a bare DialogView is returned, wrap it in a DialogResponse. 375 - if ($response instanceof AphrontDialogView) { 376 - $response = id(new AphrontDialogResponse())->setDialog($response); 377 - } 378 - 373 + public function willSendResponse(AphrontResponse $response) { 379 374 $request = $this->getRequest(); 380 - $response->setRequest($request); 381 - 382 - $seen = array(); 383 - while ($response instanceof AphrontProxyResponse) { 384 - $hash = spl_object_hash($response); 385 - if (isset($seen[$hash])) { 386 - $seen[] = get_class($response); 387 - throw new Exception( 388 - pht('Cycle while reducing proxy responses: %s', 389 - implode(' -> ', $seen))); 390 - } 391 - $seen[$hash] = get_class($response); 392 - 393 - $response = $response->reduceProxyResponse(); 394 - } 395 375 396 376 if ($response instanceof AphrontDialogResponse) { 397 377 if (!$request->isAjax() && !$request->isQuicksand()) {
+12 -1
src/view/AphrontDialogView.php
··· 1 1 <?php 2 2 3 - final class AphrontDialogView extends AphrontView { 3 + final class AphrontDialogView 4 + extends AphrontView 5 + implements AphrontResponseProducerInterface { 4 6 5 7 private $title; 6 8 private $shortTitle; ··· 369 371 $attributes, 370 372 $content); 371 373 } 374 + } 375 + 376 + 377 + /* -( AphrontResponseProducerInterface )----------------------------------- */ 378 + 379 + 380 + public function produceAphrontResponse() { 381 + return id(new AphrontDialogResponse()) 382 + ->setDialog($this); 372 383 } 373 384 374 385 }