for (;;);{"error":null,"payload":{"timeline":"\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_272\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-minor-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xfpo7764mzhw24fxdxpi\/PHID-FILE-anrie3fu7jeg4446msm3\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/EvanProdromou\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003ca name=\"5117224\" id=\"5117224\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-pencil phui-timeline-icon\" data-meta=\"0_271\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/EvanProdromou\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_0\"\u003eEvanProdromou\u003c\/a\u003e created this task.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5117224\" data-sigil=\"has-tooltip\" data-meta=\"0_270\"\u003e\u003cspan class=\"screen-only\"\u003eApr 17 2019, 12:32 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-17 00:32:32 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_275\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-minor-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xfpo7764mzhw24fxdxpi\/PHID-FILE-anrie3fu7jeg4446msm3\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/EvanProdromou\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003ca name=\"5131075\" id=\"5131075\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-link phui-timeline-icon\" data-meta=\"0_274\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/EvanProdromou\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_156\"\u003eEvanProdromou\u003c\/a\u003e added a project: \u003ca href=\"\/project\/view\/4013\/\" class=\"phui-handle handle-status-closed\" data-sigil=\"hovercard\" data-meta=\"0_157\"\u003ePlatform Engineering (Parsoid REST API in PHP (CDP2))\u003c\/a\u003e.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5131075\" data-sigil=\"has-tooltip\" data-meta=\"0_273\"\u003e\u003cspan class=\"screen-only\"\u003eApr 23 2019, 3:33 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-23 15:33:50 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_279\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-minor-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xfpo7764mzhw24fxdxpi\/PHID-FILE-anrie3fu7jeg4446msm3\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/EvanProdromou\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003ca name=\"5133576\" id=\"5133576\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-pencil phui-timeline-icon\" data-meta=\"0_277\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/EvanProdromou\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_158\"\u003eEvanProdromou\u003c\/a\u003e renamed this task from \u003cspan class=\"phui-timeline-value\"\u003eREST API Handler Interface RFC\u003c\/span\u003e to \u003cspan class=\"phui-timeline-value\"\u003eRoute Handler Interface RFC\u003c\/span\u003e.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5133576\" data-sigil=\"has-tooltip\" data-meta=\"0_276\"\u003e\u003cspan class=\"screen-only\"\u003eApr 24 2019, 1:50 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-24 01:50:57 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-pencil phui-timeline-icon\" data-meta=\"0_278\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/EvanProdromou\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_159\"\u003eEvanProdromou\u003c\/a\u003e updated the task description. \u003ca href=\"\/transactions\/detail\/PHID-XACT-TASK-vf3wyoel2o4png6\/\" data-sigil=\"workflow\"\u003e(Show Details)\u003c\/a\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_283\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-minor-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003ca name=\"5136061\" id=\"5136061\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-pencil phui-timeline-icon\" data-meta=\"0_281\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_160\"\u003eTgr\u003c\/a\u003e updated the task description. \u003ca href=\"\/transactions\/detail\/PHID-XACT-TASK-o3f3lt5bsfcjbid\/\" data-sigil=\"workflow\"\u003e(Show Details)\u003c\/a\u003e\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5136061\" data-sigil=\"has-tooltip\" data-meta=\"0_280\"\u003e\u003cspan class=\"screen-only\"\u003eApr 24 2019, 9:13 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-24 21:13:52 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-pencil phui-timeline-icon\" data-meta=\"0_282\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_161\"\u003eTgr\u003c\/a\u003e updated the task description. \u003ca href=\"\/transactions\/detail\/PHID-XACT-TASK-q6crgddzeffhxby\/\" data-sigil=\"workflow\"\u003e(Show Details)\u003c\/a\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_292\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5136073\" id=\"5136073\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-user-plus phui-timeline-icon\" data-meta=\"0_291\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_163\"\u003eTgr\u003c\/a\u003e subscribed.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5136073\" data-sigil=\"has-tooltip\" data-meta=\"0_290\"\u003e\u003cspan class=\"screen-only\"\u003eApr 24 2019, 9:20 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-24 21:20:15 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_288\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_289\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_162\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eWrt the handler, I think the more standard way to do request handling is that your handler receives a request and a response object, which are then easy to change without touching an interface that's implemented in a hundred extensions.\u003cbr \/\u003e\n(See also \u003ca href=\"\/T193613\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_2\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT193613: RFC: Establish stable interface policy for PHP code\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e on that topic.)\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_301\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5136076\" id=\"5136076\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_300\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_164\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5136076\" data-sigil=\"has-tooltip\" data-meta=\"0_299\"\u003e\u003cspan class=\"screen-only\"\u003eApr 24 2019, 9:21 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-24 21:21:43 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_297\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_298\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_165\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eFinding out what the major REST API frameworks \/ libraries are and what interface they provide would be a good first step.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_304\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-minor-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003ca name=\"5139310\" id=\"5139310\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-link phui-timeline-icon\" data-meta=\"0_303\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_166\"\u003eTgr\u003c\/a\u003e added a project: \u003ca href=\"\/tag\/mediawiki-rest-api\/\" class=\"phui-handle\" data-sigil=\"hovercard\" data-meta=\"0_167\"\u003eMediaWiki-REST-API\u003c\/a\u003e.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5139310\" data-sigil=\"has-tooltip\" data-meta=\"0_302\"\u003e\u003cspan class=\"screen-only\"\u003eApr 25 2019, 11:36 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-25 23:36:23 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_307\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-minor-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003ca name=\"5142713\" id=\"5142713\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-link phui-timeline-icon\" data-meta=\"0_306\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_168\"\u003etstarling\u003c\/a\u003e mentioned this in \u003ca href=\"\/T219959\" class=\"phui-handle handle-status-closed\" data-sigil=\"hovercard\" data-meta=\"0_169\"\u003eT219959: REST API Router\u003c\/a\u003e.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5142713\" data-sigil=\"has-tooltip\" data-meta=\"0_305\"\u003e\u003cspan class=\"screen-only\"\u003eApr 29 2019, 2:07 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-29 02:07:47 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_316\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5142766\" id=\"5142766\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-user-plus phui-timeline-icon\" data-meta=\"0_315\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_171\"\u003etstarling\u003c\/a\u003e subscribed.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5142766\" data-sigil=\"has-tooltip\" data-meta=\"0_314\"\u003e\u003cspan class=\"screen-only\"\u003eApr 29 2019, 4:17 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-29 04:17:57 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_312\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_313\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_170\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eLibrary surveys and some related thoughts.\u003c\/p\u003e\n\n\u003ch3 class=\"remarkup-header\"\u003eRequests and responses\u003c\/h3\u003e\n\n\u003cp\u003eDjango has \u003ca href=\"https:\/\/www.django-rest-framework.org\/api-guide\/views\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eone popular example\u003c\/a\u003e of a REST API framework. It provides two interface styles, which are representative of the other REST frameworks I've looked at. I'll convert them to PHP pseudocode here.\u003c\/p\u003e\n\n\u003cp\u003eClass-based views:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"k\"\u003eclass\u003c\/span\u003e \u003cspan class=\"no\"\u003eHandler\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003eget\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"k\"\u003enew\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"Response\"\u003eResponse\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"o\"\u003e[\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'message'\u003c\/span\u003e \u003cspan class=\"o\"\u003e=>\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'Hello'\u003c\/span\u003e \u003cspan class=\"o\"\u003e]\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\n \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003epost\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"doSomething\"\u003edoSomething\u003c\/span\u003e\u003cspan class=\"o\"\u003e();\u003c\/span\u003e\n \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"k\"\u003enew\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"Response\"\u003eResponse\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"o\"\u003e[\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'message'\u003c\/span\u003e \u003cspan class=\"o\"\u003e=>\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'done'\u003c\/span\u003e \u003cspan class=\"o\"\u003e]\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eFunction-based views:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003ehandler\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"k\"\u003enew\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"Response\"\u003eResponse\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"o\"\u003e[\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'message'\u003c\/span\u003e \u003cspan class=\"o\"\u003e=>\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'Hello'\u003c\/span\u003e \u003cspan class=\"o\"\u003e]\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eIn neither case is the URL template sufficient to identify the function being called. For class-based views, the HTTP method is mapped to the class method. For function-based views, only GET is accepted by default, and this is customised with decorators. In both cases, the framework is responsible for method name validation, before the handler is called.\u003c\/p\u003e\n\n\u003cp\u003eThe Action API has only mustBePosted() for method validation, it does not support any methods apart from GET and POST.\u003c\/p\u003e\n\n\u003cp\u003eDjango's handler indicates errors either by throwing an APIException, or by returning a Response with an error status. In the Action API a helper such as ApiBase::dieWithError() throws an exception.\u003c\/p\u003e\n\n\u003cp\u003eA slight modification would allow Response class names to be injected by the framework, similar to \u003ca href=\"https:\/\/www.eclipse.org\/jetty\/javadoc\/current\/org\/eclipse\/jetty\/server\/Handler.html#handle(java.lang.String,org.eclipse.jetty.server.Request,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ejetty\u003c\/a\u003e:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003ehandle\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"no\"\u003eResponse\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e \u003cspan class=\"o\"\u003e):\u003c\/span\u003e \u003cspan class=\"no\"\u003evoid\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e...\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eThis also allows the Response to do things on the HTTP layer, it's not just a value object. Jetty's Response class allows streaming, so it has getOutputStream() which returns a stream object which immediately sends things to the network via a buffer. Similar flexibility would be useful for us, since we do stream large files from some of our endpoints.\u003c\/p\u003e\n\n\u003cp\u003eSome libraries have more of a bias towards usability for the developer in common cases. For example Flask \u003ca href=\"http:\/\/flask.pocoo.org\/docs\/1.0\/quickstart\/#variable-rules\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003emaps template variables from the URI to positional parameters\u003c\/a\u003e, and has \u003ca href=\"http:\/\/flask.pocoo.org\/docs\/1.0\/quickstart\/#about-responses\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003econventions for interpreting mixed-type return values\u003c\/a\u003e, making simple handlers very brief indeed.\u003c\/p\u003e\n\n\u003cp\u003eWikimedia's HyperSwitch (used by RESTBase) passes to handlers a framework object, which has HTTP client methods, allowing for proxying. Handlers return a plain object, or a promise which resolves to a plain object. The body member is serialized to JSON before writing it to the output. In PHP pseudocode:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003ehandle\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eHyper\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$hyper\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"o\"\u003e[\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'status'\u003c\/span\u003e \u003cspan class=\"o\"\u003e=>\u003c\/span\u003e \u003cspan class=\"mi\"\u003e200\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'body'\u003c\/span\u003e \u003cspan class=\"o\"\u003e=>\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"no\"\u003eobject\u003c\/span\u003e\u003cspan class=\"o\"\u003e)[\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'message'\u003c\/span\u003e \u003cspan class=\"o\"\u003e=>\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'hello'\u003c\/span\u003e \u003cspan class=\"o\"\u003e]\u003c\/span\u003e \u003cspan class=\"o\"\u003e];\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eThe nearest equivalent in Flask is the instance of the Flask class conventionally stored in the "app" global variable, or the various global functions. The nearest equivalent in the Action API is APIBase, i.e. the base class of all API modules.\u003c\/p\u003e\n\n\u003cp\u003eIn the Action API, responses are done using ApiBase::getResult(), which returns a mutable object.\u003c\/p\u003e\n\n\u003ch3 class=\"remarkup-header\"\u003eCaching\u003c\/h3\u003e\n\n\u003cp\u003eHandler::getLastModified(), getETag() and getExpires() in Evan's proposal are apparently intended to support implementation of If-Modified-Since and If-Match in the framework. Flask and HyperSwitch lack specialised cache features. Django has its own memcached-based cache module, configured using decorators, but it supports only fixed expiry times, not revalidation.\u003c\/p\u003e\n\n\u003cp\u003eThe Action API has relatively sophisticated cache handling. For response headers it has setCacheMode(), setCacheControl() and setCacheMaxAge(). For If-Modified-Since and If-None-Match handling, it has the module-specific getConditionalRequestData(), which is called by ApiMain prior to execute().\u003c\/p\u003e\n\n\u003ch3 class=\"remarkup-header\"\u003eRoute specifications\u003c\/h3\u003e\n\n\u003cp\u003e\u003ca href=\"https:\/\/swagger.io\/specification\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eSwagger\u003c\/a\u003e uses RFC 6570 as a serialization format, not as an input format, which I think is sensible. RFC 6570 is complex and potentially ambiguous. Swagger's path templates are comparable to RFC 6570's level 1 templates, which are easy to describe in a doc comment without reference to the RFC. Given that we will probably have a large number of endpoints, I would like modules to have common path prefixes, so that we can extract a prefix hashtable and identify at least a shorter list of candidate handlers without running an RFC 6570 parser on the URL for every registered endpoint.\u003c\/p\u003e\n\n\u003cp\u003eHyperSwitch has modules, but the path templates are still in a flat namespace. I would consider making the module name be an explicit part of the URL path, with the route templates for specific handlers being relative to the module name. The mathoid module in RESTBase registers templates \u003ctt class=\"remarkup-monospaced\"\u003e\/math\/check\/{type}\u003c\/tt\u003e, \u003ctt class=\"remarkup-monospaced\"\u003e\/math\/formula\/{hash}\u003c\/tt\u003e and \u003ctt class=\"remarkup-monospaced\"\u003e\/math\/render\/{format}\/{hash}\u003c\/tt\u003e. Instead we could have a \u003ctt class=\"remarkup-monospaced\"\u003e\/math\u003c\/tt\u003e module which registers templates \u003ctt class=\"remarkup-monospaced\"\u003e\/check\/{type}\u003c\/tt\u003e etc. For backwards compatibility, we could allow registration of full path templates as well, but I don't think this should be encouraged for new code.\u003c\/p\u003e\n\n\u003ch3 class=\"remarkup-header\"\u003eParameter validation\u003c\/h3\u003e\n\n\u003cp\u003eIn Flask, parameter types are specified within the path template. In HyperSwitch, parameter types are described in the Swagger spec. HyperSwitch's parameter validation is fully featured, comparable with the parameter validation in MediaWiki's Action API. I think it would be good to have declarative parameter validation, implemented in the framework, rather than relying on handlers to validate their parameters.\u003c\/p\u003e\n\n\u003ch3 class=\"remarkup-header\"\u003eVersioning\u003c\/h3\u003e\n\n\u003cp\u003eRESTBase has a global version number "v1" which is used as a prefix for everything. According to \u003ca href=\"https:\/\/www.mediawiki.org\/wiki\/API_versioning\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eAPI versioning\u003c\/a\u003e, this is a semantic version, and if any "stable" module ever changes in a way that breaks backwards compatibility, the global version would have to be updated. This may work for RESTBase with its monolithic "v1" directory, but for MediaWiki it's impossible to guarantee the stability of routes registered by extensions.\u003c\/p\u003e\n\n\u003cp\u003eThe Action API has a global "formatversion" which modifies the JSON encoder. By analogy, we could have a global version in the REST API which modifies the behaviour of shared components such as serialization or parameter validation. But I'm not sure if it's necessary.\u003c\/p\u003e\n\n\u003cp\u003eWe discussed versioning of API modules once, I think in the context of the 2014 \u003ca href=\"https:\/\/www.mediawiki.org\/wiki\/Requests_for_comment\/API_roadmap\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eAPI roadmap\u003c\/a\u003e. Analogies to DirectX were made. It is a hard problem. I don't think there is any benefit to having a module version concept in core. Modules could include a version number in their path templates if they wish to do so. Perhaps it should be policy to do so.\u003c\/p\u003e\n\n\u003cp\u003eWhat core certainly does need is a concept of deprecation. The Action API has various kinds of deprecation within its parameter validation system, and it has addDeprecation() as a way to convey deprecation to clients. It's assisted in this by having a standard JSON response format for errors and warnings, which RESTBase lacks. What the Action API lacks is an API key which would allow developers to be contacted directly when a deprecated API is used.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_325\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5142785\" id=\"5142785\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-user-plus phui-timeline-icon\" data-meta=\"0_324\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_172\"\u003etstarling\u003c\/a\u003e added subscribers: \u003ca href=\"\/p\/Anomie\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_173\"\u003eAnomie\u003c\/a\u003e, \u003ca href=\"\/p\/Legoktm\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_174\"\u003eLegoktm\u003c\/a\u003e.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5142785\" data-sigil=\"has-tooltip\" data-meta=\"0_323\"\u003e\u003cspan class=\"screen-only\"\u003eApr 29 2019, 5:27 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-29 05:27:42 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_321\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_322\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_175\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eThere's no single accepted definition of a REST API. Is this a fair summary of what we are building here?\u003c\/p\u003e\n\n\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eResources identified using a path hierarchy.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eCreative use of HTTP methods and HTTP status codes.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eRaw, module-specific response formats. Content negotiation, using the Accept header.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eJSON as a preferred format for textual structured data in response bodies and POST request bodies.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eCustom HTTP headers for optional request and response metadata.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eDirectory indexes as documentation, e.g. \u003ca href=\"https:\/\/en.wikipedia.org\/api\/rest_v1\/media\/math\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ehttps:\/\/en.wikipedia.org\/api\/rest_v1\/media\/math\/\u003c\/a\u003e (an attempt at HATEOAS)\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003cp\u003eOne big question is: how much should be in extension.json? It seems neat, but if you put everything there, you lose the ability to configure things dynamically. For example ApiParse::getAllowedParams() calls Skin::getAllowedSkins(), should that kind of thing be allowed in REST?\u003c\/p\u003e\n\n\u003cp\u003eI think what I am leaning towards is for extension.json to have only a map of module URL paths to class names. The module object will be constructed only if the prefix matches, or for documentation. The module object provides a list of path templates. The module prefix would not need to be unique. For legacy URLs you could register "\/" as a module prefix and thus provide a template list on every request.\u003c\/p\u003e\n\n\u003cp\u003eI'm still figuring out the interface details, but I think we can have some syntactic sugar when constructing the handler map, very similar to Flask's decorators, allowing either closure or class-based handlers.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_336\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/ms665yc73j6nadjitytk\/PHID-FILE-mocohumlrcbe2lcuplml\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Anomie\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-badges\"\u003e\u003cul class=\"phui-badge-flex-view grouped flex-view-collapsed \"\u003e\u003cli class=\"phui-badge-flex-item\"\u003e\u003ca class=\"phui-badge-mini phui-badge-mini-orange \" href=\"\/badges\/view\/5\/\" data-sigil=\"has-tooltip\" data-meta=\"0_334\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-rocket\" data-meta=\"0_335\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5143725\" id=\"5143725\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_333\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Anomie\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_176\"\u003eAnomie\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5143725\" data-sigil=\"has-tooltip\" data-meta=\"0_332\"\u003e\u003cspan class=\"screen-only\"\u003eApr 29 2019, 2:29 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-29 14:29:32 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_330\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_331\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_177\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5142785\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_3\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5142785\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_5\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eThere's no single accepted definition of a REST API. Is this a fair summary of what we are building here?\u003c\/p\u003e\n\n\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eResources identified using a path hierarchy.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eCreative use of HTTP methods and HTTP status codes.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eSounds right to me.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eRaw, module-specific response formats. Content negotiation, using the Accept header.\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003cp\u003e[...]\u003c\/p\u003e\n\n\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eCustom HTTP headers for optional request and response metadata.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eAt the level of the Router interface, I'd say it shouldn't care about either of these. It should just route based on the URL, and maybe the HTTP method. But the Handlers routed to will need the ability to read and return arbitrary HTTP headers and content formats.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eJSON as a preferred format for textual structured data in response bodies and POST request bodies.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eGiven the above bullet, I think this might be out of scope for the interface we're talking about here other than as part of the "how to implement a Handler" documentation. But, yes, we seem to prefer JSON for both of these uses in things currently using Restbase.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eDirectory indexes as documentation, e.g. \u003ca href=\"https:\/\/en.wikipedia.org\/api\/rest_v1\/media\/math\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ehttps:\/\/en.wikipedia.org\/api\/rest_v1\/media\/math\/\u003c\/a\u003e (an attempt at HATEOAS)\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eI don't know what I'm supposed to be seeing there? The endpoint returns an array of three strings, but I see nothing obvious to do with the results. My best guess, though, is that this is another thing that might only be a part of the "how to implement a Handler" documentation.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eOne big question is: how much should be in extension.json? It seems neat, but if you put everything there, you lose the ability to configure things dynamically. For example ApiParse::getAllowedParams() calls Skin::getAllowedSkins(), should that kind of thing be allowed in REST?\u003c\/p\u003e\n\n\u003cp\u003eI think what I am leaning towards is for extension.json to have only a map of module URL paths to class names. The module object will be constructed only if the prefix matches, or for documentation. The module object provides a list of path templates. The module prefix would not need to be unique. For legacy URLs you could register "\/" as a module prefix and thus provide a template list on every request.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eFrom the "ApiParse::getAllowedParams() calls Skin::getAllowedSkins()" example, I guess one thing you're asking about here is whether something like the definition mentioned in \u003ca href=\"\/T221741\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_4\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221741: Define REST API interface in MediaWiki using OpenAPI 3.0 definition\u003c\/span\u003e\u003c\/a\u003e would be statically defined (directly in extension.json or as reference to a static file) versus being returned from a method called on the Handler? I suppose that depends on whether we want to allow Handlers to include enumerations derived from the wiki configuration in their documentation and sandbox, or if such fields would have to be declared as general "string" types with reference to finding the available values in the documentation.\u003c\/p\u003e\n\n\u003cp\u003eI wonder whether it would be more straightforward to use the templates directly in extension.json, instead of prefixes and then having the Handlers return templates. We could derive prefixes from the templates by taking everything up to the first variable, and we might save having to instantiate a bunch of Handlers to decide which one to actually use. Having the templates themselves in extension.json could be a benefit (clarity in config as to what exactly is handled) and a drawback (more entries in the config).\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003ebut I think we can have some syntactic sugar when constructing the handler map, very similar to Flask's decorators, allowing either closure or class-based handlers.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eI'll look forward to reading the details. I tried to look up Flask decorators, but I didn't find anything that seemed to match with the discussion here.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_345\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5145418\" id=\"5145418\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_344\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_178\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5145418\" data-sigil=\"has-tooltip\" data-meta=\"0_343\"\u003e\u003cspan class=\"screen-only\"\u003eApr 29 2019, 11:09 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-29 23:09:51 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_341\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_342\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_179\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003e\u003ca href=\"https:\/\/www.php-fig.org\/psr\/psr-15\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ePSR-15\u003c\/a\u003e defines the request handler interface as\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"k\"\u003einterface\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequestHandlerInterface\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003ehandle\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"no\"\u003eServerRequestInterface\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e):\u003c\/span\u003e \u003cspan class=\"no\"\u003eResponseInterface\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003cspan class=\"k\"\u003einterface\u003c\/span\u003e \u003cspan class=\"no\"\u003eMiddlewareInterface\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003eprocess\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"no\"\u003eServerRequestInterface\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequestHandlerInterface\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$handler\u003c\/span\u003e\u003cspan class=\"o\"\u003e):\u003c\/span\u003e \u003cspan class=\"no\"\u003eResponseInterface\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003ewith ServerRequestInterface and ResponseInterface defined in \u003ca href=\"https:\/\/www.php-fig.org\/psr\/psr-7\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ePSR-7\u003c\/a\u003e. The request interface is, like WebRequest, mostly a wrapper for the superglobals, except it does not handle sessions. It has a generic internal data storage mechanism (attributes) which middleware can use to communicate. Awkwardly, both request and response are immutable and changing them is done by creating a new object.\u003c\/p\u003e\n\n\u003cp\u003eThe PSR standards also have design decision documents: \u003ca href=\"https:\/\/www.php-fig.org\/psr\/psr-7\/meta\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ePSR-7\u003c\/a\u003e, \u003ca href=\"https:\/\/www.php-fig.org\/psr\/psr-15\/meta\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ePSR-15\u003c\/a\u003e. The latter gives some overview of handlers in various PHP framework (although it mainly focuses on how they interact with middleware).\u003c\/p\u003e\n\n\u003cp\u003eThere is value in being able to use PSR-17 compatible middleware, there are plenty of useful things out there. Probably possible to achieve with some compatibility wrapper regardless of what interface we end up with, though.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_354\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5145523\" id=\"5145523\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_353\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_180\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5145523\" data-sigil=\"has-tooltip\" data-meta=\"0_352\"\u003e\u003cspan class=\"screen-only\"\u003eApr 29 2019, 11:55 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-29 23:55:36 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_350\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_351\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_181\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5142785\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_6\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5142785\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_8\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eResources identified using a path hierarchy.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eNote this conflicts with module prefixes somewhat. The typical REST API approach is something like \u003ctt class=\"remarkup-monospaced\"\u003e\/<resource type>\/<representation>\/<id>\u003c\/tt\u003e, e.g. \u003ctt class=\"remarkup-monospaced\"\u003e\/page\/html\/Barack_Obama\u003c\/tt\u003e. But a large number of APIs will provide functionality for the same small set of resource types (page, revision, user etc) and often an extension will provide functionality for several different resource types.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eDirectory indexes as documentation, e.g. \u003ca href=\"https:\/\/en.wikipedia.org\/api\/rest_v1\/media\/math\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ehttps:\/\/en.wikipedia.org\/api\/rest_v1\/media\/math\/\u003c\/a\u003e (an attempt at HATEOAS)\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eIs there any use case for this? Personally I would rather use these for human-readable documentation rather than traversal support.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eOne big question is: how much should be in extension.json? It seems neat, but if you put everything there, you lose the ability to configure things dynamically. For example ApiParse::getAllowedParams() calls Skin::getAllowedSkins(), should that kind of thing be allowed in REST?\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eOne big benefit of having the path definitions in a static file (which might or might not be extension.json) is that you can generate documentation without having to install and execute the (potentially untrusted) extension. That would be useful on mediawiki.org. OTOH it would probably mean that the documentation is less accurate \/ useful.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5142765\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_7\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5142765\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_9\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eI think it would be good to have declarative parameter validation, implemented in the framework, rather than relying on handlers to validate their parameters.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eDeclarative parameter validation is needed for good sandbox UX, autogenerated documentation, machine-readable documentation (action=paraminfo style). It should probably be extensible though, so that extensions providing new resource types can also provide their own validators.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eRESTBase has a global version number "v1" which is used as a prefix for everything. According to \u003ca href=\"https:\/\/www.mediawiki.org\/wiki\/API_versioning\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eAPI versioning\u003c\/a\u003e, this is a semantic version, and if any "stable" module ever changes in a way that breaks backwards compatibility, the global version would have to be updated.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eIn the industry this tends to be used for complete API rewrites \/ paradigm changes (e.g. Github switched from v2 to v3 when they went for GraphQL). As long as we distribute MediaWiki as a tarball and setting up a new endpoint requires a bunch of manual effort from all site admins, seems like a reasonable insurance to have.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eI don't think there is any benefit to having a module version concept in core.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eMost versioning can happen via content negotiation; there's not much the core framework can do to facilitate that.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003e[the Action API is] assisted in [deprecation] by having a standard JSON response format for errors and warnings, which RESTBase lacks. What the Action API lacks is an API key which would allow developers to be contacted directly when a deprecated API is used.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eRESTBase sort of has a standard error format, although it's rather minimal (it has a message and a topic, and not much else). It doesn't help that RESTBase tends to act as a proxy for independently operated services, plus it does a lot of proxying to MediaWiki, so most errors originate outside it. The MediaWiki REST framework has the benefit of doing everything in-process, so it would make sense to standardize on exceptions as the standard error reporting mechanism, and let the framework do the formatting.\u003cbr \/\u003e\nOne area where the Action API does better than RESTBase is localization; for explicitly non-cacheable responses we might want to preserve that. OTOH errors are not necessarily non-cacheable and we probably don't want to split the cache by language... In general, to what extent we should do automatic accept-language negotiation vs. the Action APIs explicit uselang parameter probably deserves its own task.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_363\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5145852\" id=\"5145852\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-user-plus phui-timeline-icon\" data-meta=\"0_362\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_182\"\u003etstarling\u003c\/a\u003e added a subscriber: \u003ca href=\"\/p\/mobrovac\/\" class=\"phui-handle handle-availability-disabled phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_183\"\u003e\u003cspan class=\"perfect-circle\"\u003e\u2022\u003c\/span\u003e mobrovac\u003c\/a\u003e.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5145852\" data-sigil=\"has-tooltip\" data-meta=\"0_361\"\u003e\u003cspan class=\"screen-only\"\u003eApr 30 2019, 5:35 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-30 05:35:37 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_359\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_360\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_184\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5145523\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_11\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5145523\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_15\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5142785\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_10\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5142785\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_14\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eResources identified using a path hierarchy.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eNote this conflicts with module prefixes somewhat. The typical REST API approach is something like \u003ctt class=\"remarkup-monospaced\"\u003e\/<resource type>\/<representation>\/<id>\u003c\/tt\u003e, e.g. \u003ctt class=\"remarkup-monospaced\"\u003e\/page\/html\/Barack_Obama\u003c\/tt\u003e. But a large number of APIs will provide functionality for the same small set of resource types (page, revision, user etc) and often an extension will provide functionality for several different resource types.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eOK, let's not do module prefixes then.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5143725\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_12\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5143725\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_16\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eI wonder whether it would be more straightforward to use the templates directly in extension.json, instead of prefixes and then having the Handlers return templates. We could derive prefixes from the templates by taking everything up to the first variable, and we might save having to instantiate a bunch of Handlers to decide which one to actually use. Having the templates themselves in extension.json could be a benefit (clarity in config as to what exactly is handled) and a drawback (more entries in the config).\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eRight, OK, no module prefixes. As Tgr says, prefixes might not help that much. What I want is for the path router to complete in ~100us with 1000 registered templates. Maybe it won't be so difficult. We could split the path into components and then use it to traverse a precomputed tree. For example if you have \/page\/pdf\/{title} and \/page\/html\/{title} it precomputes a tree like ['page' => [ 'pdf' => $handler1, 'html' => $handler2 ] ]\u003c\/p\u003e\n\n\u003cp\u003eShould ambiguous templates be allowed? For example, if you have \/foo\/{var} and \/foo\/bar should that throw an exception at extension registration?\u003c\/p\u003e\n\n\u003cp\u003eShould optional parameters be allowed, as deprecated by \u003ca href=\"\/T217881\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_13\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT217881\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, for compatibility with existing Parsoid routes?\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_374\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/ms665yc73j6nadjitytk\/PHID-FILE-mocohumlrcbe2lcuplml\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Anomie\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-badges\"\u003e\u003cul class=\"phui-badge-flex-view grouped flex-view-collapsed \"\u003e\u003cli class=\"phui-badge-flex-item\"\u003e\u003ca class=\"phui-badge-mini phui-badge-mini-orange \" href=\"\/badges\/view\/5\/\" data-sigil=\"has-tooltip\" data-meta=\"0_372\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-rocket\" data-meta=\"0_373\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5147230\" id=\"5147230\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_371\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Anomie\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_185\"\u003eAnomie\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5147230\" data-sigil=\"has-tooltip\" data-meta=\"0_370\"\u003e\u003cspan class=\"screen-only\"\u003eApr 30 2019, 2:47 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-30 14:47:11 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_368\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_369\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_186\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eI didn't see this post earlier, I went straight to the latest one. :(\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5142765\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_17\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5142765\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_19\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eIn neither case is the URL template sufficient to identify the function being called.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eIt seems in both cases it's sufficient to identify the Handler (with the template + HTTP method identifying the specific function on the Handler), and for the function-based views it also identifies the function?\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eThe Action API has only mustBePosted() for method validation, it does not support any methods apart from GET and POST.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eMore specifically, the Action API doesn't differentiate between GET and POST at all other than requiring that some modules cannot be used with GET. A module could in theory do something different based on the method, but I'd consider that a wrong behavior.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eIn the Action API a helper such as ApiBase::dieWithError() throws an exception.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eAn Action API module can indicate an error to the "framework" by using such a helper, and the "framework" will turn that into an error response (that is still typically returned with an HTTP 200 response code). Alternatively, the module could set data in the ApiResult to indicate the error in some way, either imitating what the "framework" would do (although I wouldn't recommend that) or in some other way, which the "framework" would treat as a successful response.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eThe Action API has relatively sophisticated cache handling. For response headers it has setCacheMode(), setCacheControl() and setCacheMaxAge(). For If-Modified-Since and If-None-Match handling, it has the module-specific getConditionalRequestData(), which is called by ApiMain prior to execute().\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eUnfortunately I don't think anything uses the Action API's If-Modified-Since and If-None-Match handling :(\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eI think it would be good to have declarative parameter validation, implemented in the framework, rather than relying on handlers to validate their parameters.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003e+1. This also really helps when generating a sandbox UI.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eThe Action API has a global "formatversion" which modifies the JSON encoder. By analogy, we could have a global version in the REST API which modifies the behaviour of shared components such as serialization or parameter validation.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eIt's not global, it's a parameter on the ApiFormatJson formatting module. The action modules are supposed to be entirely format-agnostic.\u003c\/p\u003e\n\n\u003cp\u003eThe closest thing to actual global parameters in the Action API are the parameters to ApiMain, although currently none of them resemble a version. "Versioning" for Action API modules is generally handled by having request flags to activate specific new features.\u003c\/p\u003e\n\n\u003cp\u003eFor example, we have \u003ctt class=\"remarkup-monospaced\"\u003euselang\u003c\/tt\u003e which configures the UI language, \u003ctt class=\"remarkup-monospaced\"\u003eerrorformat\u003c\/tt\u003e and a few others which affect how the error formatter formats those exceptions from \u003ctt class=\"remarkup-monospaced\"\u003eApiBase::dieWithError()\u003c\/tt\u003e (and also warnings), and \u003ctt class=\"remarkup-monospaced\"\u003emaxage\u003c\/tt\u003e\/\u003ctt class=\"remarkup-monospaced\"\u003esmaxage\u003c\/tt\u003e which affect caching.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eI don't think there is any benefit to having a module version concept in core. Modules could include a version number in their path templates if they wish to do so.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003e+1. I have no opinion on whether it should be policy or not.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eWhat core certainly does need is a concept of deprecation. The Action API has various kinds of deprecation within its parameter validation system, and it has addDeprecation() as a way to convey deprecation to clients. It's assisted in this by having a standard JSON response format for errors and warnings, which RESTBase lacks.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eMy vague impression is that a REST API would more likely make use of HTTP headers for standardized reporting of deprecations, particularly if endpoints are allowed to return direct HTML (like \u003ca href=\"https:\/\/en.wikipedia.org\/api\/rest_v1\/page\/html\/Main_Page\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ehttps:\/\/en.wikipedia.org\/api\/rest_v1\/page\/html\/Main_Page\u003c\/a\u003e) rather than wrapping it in JSON.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eWhat the Action API lacks is an API key which would allow developers to be contacted directly when a deprecated API is used.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eSee discussion in \u003ca href=\"\/T221161\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_18\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221161: API keys\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e. I'm skeptical that well-meaning but less-clueful (or, depending on what else the keys do besides serve as a contact mechanism, malicious) developers wouldn't wind up copying keys from existing apps rather than registering their own.\u003c\/p\u003e\n\n\u003cp\u003eMore workable for contact information, IMO, would be to enforce a \u003ca href=\"https:\/\/meta.wikimedia.org\/wiki\/User-Agent_policy\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eUser-Agent policy\u003c\/a\u003e that states that clients must include "(Developer: WikiUserName-or-email@address)" in the agent, or require a separate custom HTTP request header for the same, or something like that.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_385\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/ms665yc73j6nadjitytk\/PHID-FILE-mocohumlrcbe2lcuplml\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Anomie\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-badges\"\u003e\u003cul class=\"phui-badge-flex-view grouped flex-view-collapsed \"\u003e\u003cli class=\"phui-badge-flex-item\"\u003e\u003ca class=\"phui-badge-mini phui-badge-mini-orange \" href=\"\/badges\/view\/5\/\" data-sigil=\"has-tooltip\" data-meta=\"0_383\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-rocket\" data-meta=\"0_384\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5147257\" id=\"5147257\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_382\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Anomie\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_187\"\u003eAnomie\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5147257\" data-sigil=\"has-tooltip\" data-meta=\"0_381\"\u003e\u003cspan class=\"screen-only\"\u003eApr 30 2019, 2:51 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-30 14:51:36 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_379\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_380\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_188\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5145852\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_20\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5145852\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_25\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eShould ambiguous templates be allowed? For example, if you have \/foo\/{var} and \/foo\/bar should that throw an exception at extension registration?\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eIdeally we should throw an exception if there's a conflict. On the other hand, if \u003ctt class=\"remarkup-monospaced\"\u003e{var}\u003c\/tt\u003e is an integer, an enum that doesn't allow "bar" as a value, or something like that, that might not be considered a conflict.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eShould optional parameters be allowed, as deprecated by \u003ca href=\"\/T217881\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_21\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT217881\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, for compatibility with existing Parsoid routes?\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eIt looks like \u003ca href=\"\/T217881\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_22\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT217881\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e is specifically about Swagger specs, and notes that optional parameters are just syntactic sugar for defining routes with and without the optional parameter. If we're not planning on using Swagger or some other standardized spec for defining our routes, I don't think \u003ca href=\"\/T217881\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_23\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT217881\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e is really relevant and we can decide on our own whether the sugar is worthwhile.\u003c\/p\u003e\n\n\u003cp\u003eI note that for a sandbox UI, having fewer distinct paths can be a benefit, but the widgets used for entering the optional parameters may have to differentiate between "not specified" and "empty string" (cf. \u003ca href=\"\/T130494\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_24\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT130494\u003c\/span\u003e\u003c\/a\u003e).\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_394\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5148349\" id=\"5148349\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_393\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_189\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5148349\" data-sigil=\"has-tooltip\" data-meta=\"0_392\"\u003e\u003cspan class=\"screen-only\"\u003eApr 30 2019, 7:08 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-30 19:08:07 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_390\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_391\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_190\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5145852\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_26\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5145852\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_27\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eWhat I want is for the path router to complete in ~100us with 1000 registered templates.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eSeems like a not very impactful performance optimization, given that the API module itself will probably be 2-3 magnitudes slower. Or are you thinking of a scenario where the router also handles caching?\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eShould ambiguous templates be allowed? For example, if you have \/foo\/{var} and \/foo\/bar should that throw an exception at extension registration?\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eReal ambiguity is pretty hard to detect, in both directions:\u003c\/p\u003e\n\n\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003ethe template pair \u003ctt class=\"remarkup-monospaced\"\u003e\/foo\/{var}\/baz\u003c\/tt\u003e and \u003ctt class=\"remarkup-monospaced\"\u003e\/foo\/bar\/{var}\u003c\/tt\u003e is ambiguous, but short of turning one into a regexp, matching against the other, and doing that for all pairings of templates, I don't think you can detect it.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ethe template pair \u003ctt class=\"remarkup-monospaced\"\u003e\/foo\/{var:int}\u003c\/tt\u003e and \u003ctt class=\"remarkup-monospaced\"\u003e\/foo\/bar\u003c\/tt\u003e is not ambiguous but once again you'd have to get into pairwise regex matching to verify that.\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003cp\u003eOne option is to be more strict than necessary and require all path templates to have unique static prefixes. Another is to allow full flexibility (including parameter types), use regular expressions to match the URL, but then the best we can do is to check whether actual requests are ambiguous.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_404\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xfpo7764mzhw24fxdxpi\/PHID-FILE-anrie3fu7jeg4446msm3\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/EvanProdromou\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5148500\" id=\"5148500\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_403\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/EvanProdromou\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_191\"\u003eEvanProdromou\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003eEdited\u003cspan class=\"visual-only\" aria-hidden=\"true\"\u003e \u00b7 \u003c\/span\u003e\u003ca href=\"#5148500\" data-sigil=\"has-tooltip\" data-meta=\"0_402\"\u003e\u003cspan class=\"screen-only\"\u003eApr 30 2019, 8:03 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-04-30 20:03:36 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_400\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_401\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_192\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eI'd rather have URL formats by convention rather than fixed by the internal structure, since in at least the case of Parsoid we need to match the existing URL structure, even if it's not "right".\u003c\/p\u003e\n\n\u003cp\u003eI for one prefer \/{type}\/{id}.{format}, like \/page\/Main_Page.json. And having URLs for related resources under that "directory", like \/page\/Main_Page\/revisions. It's tricky, since in this case our page titles allow "\/", but I think that can be fudged...?\u003c\/p\u003e\n\n\u003cp\u003eAnother nit: I vastly prefer type-as-collection to type-with-documentation. So, "\/user" is a way to get a (paginated!) collection of all users, not documentation about the user branch.\u003c\/p\u003e\n\n\u003cp\u003eFinally, I don't think we need to define these URL structures initially.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_413\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5149229\" id=\"5149229\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_412\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_193\"\u003etstarling\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5149229\" data-sigil=\"has-tooltip\" data-meta=\"0_411\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 12:22 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 00:22:33 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_409\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_410\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_194\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5148500\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_28\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5148500\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/EvanProdromou\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_29\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@EvanProdromou\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eI for one prefer \/{type}\/{id}.{format}, like \/page\/Main_Page.json. And having URLs for related resources under that "directory", like \/page\/Main_Page\/revisions. It's tricky, since in this case our page titles allow "\/", but I think that can be fudged...?\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eNormally you use %2F for slashes within path parameters. But normally dots would not be encoded in that way, so {id}.{format} is ambiguous. You can have a page called Main_Page.json. It's simpler to always use slash to delimit parameters.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eAnother nit: I vastly prefer type-as-collection to type-with-documentation. So, "\/user" is a way to get a (paginated!) collection of all users, not documentation about the user branch.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eIt makes sense for \/user\/ to show all users if you have \/user\/{name}\/profile etc. as your endpoint, as opposed to \/user\/profile\/{name}. Existing RESTBase endpoints mostly use the latter convention. The big question here is whether we want to use RESTBase as a starting point, or if we want to reorganise. If we reorganise, we may need backwards compatible Parsoid endpoints, even if they are deprecated from the outset.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_416\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-minor-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/bxdymb7zqtipuiytbor5\/PHID-FILE-hh7qwoflwjmrunovgr5a\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Jdforrester-WMF\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003ca name=\"5149230\" id=\"5149230\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-user-plus phui-timeline-icon\" data-meta=\"0_415\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Jdforrester-WMF\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_195\"\u003eJdforrester-WMF\u003c\/a\u003e subscribed.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5149230\" data-sigil=\"has-tooltip\" data-meta=\"0_414\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 12:24 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 00:24:32 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_427\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/lmqso2pdiuhzvrx7fltm\/PHID-FILE-g4ayg62xazjsnsxkgiqt\/alphanumeric_lato-dark_L.png-_48a3ba-0%2C0%2C0%2C0.png)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Legoktm\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-badges\"\u003e\u003cul class=\"phui-badge-flex-view grouped flex-view-collapsed \"\u003e\u003cli class=\"phui-badge-flex-item\"\u003e\u003ca class=\"phui-badge-mini phui-badge-mini-orange \" href=\"\/badges\/view\/17\/\" data-sigil=\"has-tooltip\" data-meta=\"0_425\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-rocket\" data-meta=\"0_426\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5149280\" id=\"5149280\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_424\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Legoktm\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_196\"\u003eLegoktm\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5149280\" data-sigil=\"has-tooltip\" data-meta=\"0_423\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 1:03 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 01:03:46 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_421\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_422\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_197\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5142785\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_30\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5142785\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_31\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eOne big question is: how much should be in extension.json? It seems neat, but if you put everything there, you lose the ability to configure things dynamically. For example ApiParse::getAllowedParams() calls Skin::getAllowedSkins(), should that kind of thing be allowed in REST?\u003c\/p\u003e\n\n\u003cp\u003eI think what I am leaning towards is for extension.json to have only a map of module URL paths to class names. The module object will be constructed only if the prefix matches, or for documentation. The module object provides a list of path templates. The module prefix would not need to be unique. For legacy URLs you could register "\/" as a module prefix and thus provide a template list on every request.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eI think the biggest dynamic configuration people end up wanting to do is a feature flag on\/off. But instead of \u003ctt class=\"remarkup-monospaced\"\u003e$wgEnableFooRoute = true;\u003c\/tt\u003e, we could have a standard \u003ctt class=\"remarkup-monospaced\"\u003e$wgEnableRoutes['foo'] = true;\u003c\/tt\u003e, so you don't need dynamicness for most use cases.\u003c\/p\u003e\n\n\u003cp\u003eGenerally for most things in extension,json, we also have an accompanying hook if people really need dynamicness.\u003c\/p\u003e\n\n\u003cp\u003eI don't have specific comments for the rest since I haven't fully read it, but Flask is my favorite Python web framework, and I love its routing system. The decorators are so simple that I've been able to teach people how to use them in under an hour. <\/off-topic>\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_437\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xfpo7764mzhw24fxdxpi\/PHID-FILE-anrie3fu7jeg4446msm3\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/EvanProdromou\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5149288\" id=\"5149288\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_436\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/EvanProdromou\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_198\"\u003eEvanProdromou\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003eEdited\u003cspan class=\"visual-only\" aria-hidden=\"true\"\u003e \u00b7 \u003c\/span\u003e\u003ca href=\"#5149288\" data-sigil=\"has-tooltip\" data-meta=\"0_435\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 1:40 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 01:40:20 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_433\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_434\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_199\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eOne thing I brought up tonight in the meeting is that \u003ca href=\"https:\/\/www.mediawiki.org\/wiki\/HyperSwitch\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eHyperSwitch\u003c\/a\u003e uses \u003ca href=\"https:\/\/swagger.io\/docs\/specification\/about\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eOpenAPI 3.0\u003c\/a\u003e to define routes. It's a unique architecture in my experience!\u003c\/p\u003e\n\n\u003cp\u003eUPDATE: It's actually a pretty well-established architecture; it's how \u003ca href=\"https:\/\/swagger.io\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eSwagger\u003c\/a\u003e works, after all.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_446\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5149306\" id=\"5149306\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_445\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_200\"\u003etstarling\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5149306\" data-sigil=\"has-tooltip\" data-meta=\"0_444\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 2:29 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 02:29:16 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_442\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_443\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_201\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5148349\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_32\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5148349\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_33\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eReal ambiguity is pretty hard to detect, in both directions:\u003c\/p\u003e\n\n\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003ethe template pair \u003ctt class=\"remarkup-monospaced\"\u003e\/foo\/{var}\/baz\u003c\/tt\u003e and \u003ctt class=\"remarkup-monospaced\"\u003e\/foo\/bar\/{var}\u003c\/tt\u003e is ambiguous, but short of turning one into a regexp, matching against the other, and doing that for all pairings of templates, I don't think you can detect it.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eNo, I don't think it's that hard. It's O(N^2) in the worst case, but most pairs don't need to be considered. I wrote a proof of concept script to test my algorithm, see below.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003ethe template pair \u003ctt class=\"remarkup-monospaced\"\u003e\/foo\/{var:int}\u003c\/tt\u003e and \u003ctt class=\"remarkup-monospaced\"\u003e\/foo\/bar\u003c\/tt\u003e is not ambiguous but once again you'd have to get into pairwise regex matching to verify that.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eRESTBase does not have that kind of type matching, as far as I can see. My proposal is to have parameters which are just a wildcard, like the regex [^\/]*, and to have route-specific parameter validation after the route is thus identified.\u003c\/p\u003e\n\n\u003cp\u003eMy algorithm is as follows. Consider paths to be a vector of path components. Two templates conflict they have the same number of path components, and all their path components conflict when considered in pairs at the same component index. That is, template A conflicts with template B if A[i] conflicts with B[i] for all i. Two path components conflict if either they are the same string literal, or if at least one of the path components is a wildcard.\u003c\/p\u003e\n\n\u003cp\u003eIn the script below, I check for conflicts while simultaneously building a routing tree. If one path component is found to not conflict, traversal of that node's children can be omitted, that's why it typically takes much less than N^2 steps to add N routes.\u003c\/p\u003e\n\n\u003cp\u003e\u003cdiv class=\"paste-embed\"\u003e\u003cdiv class=\"paste-embed-head\"\u003e\u003ca href=\"\/P8463\"\u003eP8463 detect-template-ambiguity.php\u003c\/a\u003e\u003c\/div\u003e\u003cdiv class=\"paste-embed-body\" style=\"max-height: 27.6em;\"\u003e\u003cdiv class=\"phabricator-source-code-container\"\u003e\u003ctable class=\"phabricator-source-code-view remarkup-code PhabricatorMonospaced\" data-sigil=\"phabricator-source has-symbols\" data-meta=\"0_34\"\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e1\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\u003cspan class=\"cp\"\u003e<?php\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e2\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e3\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\u003cspan class=\"k\"\u003eclass\u003c\/span\u003e \u003cspan class=\"no\"\u003eTemplates\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e4\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\u003cspan class=\"k\"\u003eprivate\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$treesByLength\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"o\"\u003e[];\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e5\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e6\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e7\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003efindConflict\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$node\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$parts\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$index\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"mi\"\u003e0\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e8\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$index\u003c\/span\u003e \u003cspan class=\"o\"\u003e>=\u003c\/span\u003e \u003cspan class=\"nf\" data-symbol-name=\"count\"\u003ecount\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$parts\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e9\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"c\"\u003e\/\/ If we reached the leaf node then a conflict is detected\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e10\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"s1\"\u003e''\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e11\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e12\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"nv\"\u003e$part\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$parts\u003c\/span\u003e\u003cspan class=\"o\"\u003e[\u003c\/span\u003e\u003cspan class=\"nv\"\u003e$index\u003c\/span\u003e\u003cspan class=\"o\"\u003e];\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e13\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"nv\"\u003e$result\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e14\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$part\u003c\/span\u003e \u003cspan class=\"o\"\u003e===\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'*'\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e15\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"k\"\u003eforeach\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$node\u003c\/span\u003e \u003cspan class=\"k\"\u003eas\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$key\u003c\/span\u003e \u003cspan class=\"o\"\u003e=>\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$childNode\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e16\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\u003cspan class=\"nv\"\u003e$result\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"findConflict\"\u003efindConflict\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$childNode\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$parts\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$index\u003c\/span\u003e \u003cspan class=\"o\"\u003e+\u003c\/span\u003e \u003cspan class=\"mi\"\u003e1\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e17\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$result\u003c\/span\u003e \u003cspan class=\"o\"\u003e!==\u003c\/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e18\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\t\u003cspan class=\"nv\"\u003e$result\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"$key\/$result"\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e19\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e20\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e21\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e \u003cspan class=\"k\"\u003eelse\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e22\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nf\" data-symbol-name=\"isset\"\u003eisset\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$node\u003c\/span\u003e\u003cspan class=\"o\"\u003e[\u003c\/span\u003e\u003cspan class=\"nv\"\u003e$part\u003c\/span\u003e\u003cspan class=\"o\"\u003e]\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e23\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\u003cspan class=\"nv\"\u003e$result\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"findConflict\"\u003efindConflict\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$node\u003c\/span\u003e\u003cspan class=\"o\"\u003e[\u003c\/span\u003e\u003cspan class=\"nv\"\u003e$part\u003c\/span\u003e\u003cspan class=\"o\"\u003e],\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$parts\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$index\u003c\/span\u003e \u003cspan class=\"o\"\u003e+\u003c\/span\u003e \u003cspan class=\"mi\"\u003e1\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e24\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$result\u003c\/span\u003e \u003cspan class=\"o\"\u003e!==\u003c\/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e25\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\t\u003cspan class=\"nv\"\u003e$result\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"$part\/$result"\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e26\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e27\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e28\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$result\u003c\/span\u003e \u003cspan class=\"o\"\u003e===\u003c\/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c\/span\u003e \u003cspan class=\"o\"\u003e&&\u003c\/span\u003e \u003cspan class=\"nf\" data-symbol-name=\"isset\"\u003eisset\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$node\u003c\/span\u003e\u003cspan class=\"o\"\u003e[\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'*'\u003c\/span\u003e\u003cspan class=\"o\"\u003e]\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e29\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\u003cspan class=\"nv\"\u003e$result\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"findConflict\"\u003efindConflict\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$node\u003c\/span\u003e\u003cspan class=\"o\"\u003e[\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'*'\u003c\/span\u003e\u003cspan class=\"o\"\u003e],\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$parts\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$index\u003c\/span\u003e \u003cspan class=\"o\"\u003e+\u003c\/span\u003e \u003cspan class=\"mi\"\u003e1\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e30\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$result\u003c\/span\u003e \u003cspan class=\"o\"\u003e!==\u003c\/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e31\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\t\u003cspan class=\"nv\"\u003e$result\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"*\/$result"\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e32\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e33\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e34\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e35\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$result\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e36\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e37\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e38\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003eadd\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$template\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e39\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"nv\"\u003e$parts\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nf\" data-symbol-name=\"explode\"\u003eexplode\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'\/'\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$template\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e40\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"nv\"\u003e$length\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nf\" data-symbol-name=\"count\"\u003ecount\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$parts\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e41\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"o\"\u003e!\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-name=\"isset\"\u003eisset\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"treesByLength\"\u003etreesByLength\u003c\/span\u003e\u003cspan class=\"o\"\u003e[\u003c\/span\u003e\u003cspan class=\"nv\"\u003e$length\u003c\/span\u003e\u003cspan class=\"o\"\u003e]\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e42\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"treesByLength\"\u003etreesByLength\u003c\/span\u003e\u003cspan class=\"o\"\u003e[\u003c\/span\u003e\u003cspan class=\"nv\"\u003e$length\u003c\/span\u003e\u003cspan class=\"o\"\u003e]\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"o\"\u003e[];\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e43\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e44\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"nv\"\u003e$tree\u003c\/span\u003e \u003cspan class=\"o\"\u003e=&\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"treesByLength\"\u003etreesByLength\u003c\/span\u003e\u003cspan class=\"o\"\u003e[\u003c\/span\u003e\u003cspan class=\"nv\"\u003e$length\u003c\/span\u003e\u003cspan class=\"o\"\u003e];\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e45\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"nv\"\u003e$conflict\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"findConflict\"\u003efindConflict\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$tree\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$parts\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e46\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$conflict\u003c\/span\u003e \u003cspan class=\"o\"\u003e!==\u003c\/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e47\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"no\"\u003eecho\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"Found conflict in template $template with existing template $conflict\u003c\/span\u003e\u003cspan class=\"k\"\u003e\\n\u003c\/span\u003e\u003cspan class=\"s2\"\u003e"\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e48\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e49\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e50\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e51\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"k\"\u003eforeach\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$parts\u003c\/span\u003e \u003cspan class=\"k\"\u003eas\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$index\u003c\/span\u003e \u003cspan class=\"o\"\u003e=>\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$part\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e52\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"o\"\u003e!\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-name=\"isset\"\u003eisset\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$tree\u003c\/span\u003e\u003cspan class=\"o\"\u003e[\u003c\/span\u003e\u003cspan class=\"nv\"\u003e$part\u003c\/span\u003e\u003cspan class=\"o\"\u003e]\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e53\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\u003cspan class=\"nv\"\u003e$tree\u003c\/span\u003e\u003cspan class=\"o\"\u003e[\u003c\/span\u003e\u003cspan class=\"nv\"\u003e$part\u003c\/span\u003e\u003cspan class=\"o\"\u003e]\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"o\"\u003e[];\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e54\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e55\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"nv\"\u003e$tree\u003c\/span\u003e \u003cspan class=\"o\"\u003e=&\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$tree\u003c\/span\u003e\u003cspan class=\"o\"\u003e[\u003c\/span\u003e\u003cspan class=\"nv\"\u003e$part\u003c\/span\u003e\u003cspan class=\"o\"\u003e];\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e56\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e57\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e58\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e59\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\u003cspan class=\"k\"\u003eprivate\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003econvertTreeToRoutes\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$tree\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e60\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$tree\u003c\/span\u003e \u003cspan class=\"o\"\u003e===\u003c\/span\u003e \u003cspan class=\"o\"\u003e[]\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e61\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"o\"\u003e[\u003c\/span\u003e \u003cspan class=\"s1\"\u003e''\u003c\/span\u003e \u003cspan class=\"o\"\u003e];\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e62\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e63\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"nv\"\u003e$routes\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"o\"\u003e[];\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e64\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"k\"\u003eforeach\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$tree\u003c\/span\u003e \u003cspan class=\"k\"\u003eas\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$key\u003c\/span\u003e \u003cspan class=\"o\"\u003e=>\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$node\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e65\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"nv\"\u003e$childRoutes\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"convertTreeToRoutes\"\u003econvertTreeToRoutes\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$node\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e66\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"k\"\u003eforeach\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$childRoutes\u003c\/span\u003e \u003cspan class=\"k\"\u003eas\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$route\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e67\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\t\u003cspan class=\"nv\"\u003e$routes\u003c\/span\u003e\u003cspan class=\"o\"\u003e[]\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"$key\/$route"\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e68\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e69\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e70\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$routes\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e71\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e72\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e73\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003egetRouteList\u003c\/span\u003e\u003cspan class=\"o\"\u003e()\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e74\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"nv\"\u003e$routes\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"o\"\u003e[];\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e75\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"k\"\u003eforeach\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"treesByLength\"\u003etreesByLength\u003c\/span\u003e \u003cspan class=\"k\"\u003eas\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$tree\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e76\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\t\u003cspan class=\"nv\"\u003e$routes\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nf\" data-symbol-name=\"array_merge\"\u003earray_merge\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$routes\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"convertTreeToRoutes\"\u003econvertTreeToRoutes\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$tree\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e77\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e78\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$routes\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e79\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e80\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e81\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e82\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\u003cspan class=\"nv\"\u003e$templates\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"k\"\u003enew\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"Templates\"\u003eTemplates\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e83\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\u003cspan class=\"k\"\u003ewhile\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c\/span\u003e \u003cspan class=\"o\"\u003e!==\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$line\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nf\" data-symbol-name=\"fgets\"\u003efgets\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eSTDIN\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e84\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\t\u003cspan class=\"nv\"\u003e$templates\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"add\"\u003eadd\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nf\" data-symbol-name=\"trim\"\u003etrim\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$line\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e85\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e86\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e87\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\u003cspan class=\"no\"\u003eprint\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"\u003c\/span\u003e\u003cspan class=\"k\"\u003e\\n\u003c\/span\u003e\u003cspan class=\"s2\"\u003eCompiled route list:\u003c\/span\u003e\u003cspan class=\"k\"\u003e\\n\u003c\/span\u003e\u003cspan class=\"s2\"\u003e"\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e88\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\u003cspan class=\"no\"\u003eprint\u003c\/span\u003e \u003cspan class=\"nf\" data-symbol-name=\"implode\"\u003eimplode\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"\u003c\/span\u003e\u003cspan class=\"k\"\u003e\\n\u003c\/span\u003e\u003cspan class=\"s2\"\u003e"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$templates\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getRouteList\"\u003egetRouteList\u003c\/span\u003e\u003cspan class=\"o\"\u003e()\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e.\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"\u003c\/span\u003e\u003cspan class=\"k\"\u003e\\n\u003c\/span\u003e\u003cspan class=\"s2\"\u003e"\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003c\/table\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cbr \/\u003e\n\u003cdiv class=\"paste-embed\"\u003e\u003cdiv class=\"paste-embed-head\"\u003e\u003ca href=\"\/P8464\"\u003eP8464 detect-template-ambiguity sample\u003c\/a\u003e\u003c\/div\u003e\u003cdiv class=\"paste-embed-body\" style=\"max-height: 27.6em;\"\u003e\u003cdiv class=\"phabricator-source-code-container\"\u003e\u003ctable class=\"phabricator-source-code-view remarkup-code PhabricatorMonospaced\" data-sigil=\"phabricator-source has-symbols\" data-meta=\"0_35\"\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e1\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/foo\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e2\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/bar\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e3\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/*\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e4\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003eFound conflict in template \/* with existing template \/bar\/\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e5\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/baz\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e6\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/*\/foo\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e7\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/*\/bar\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e8\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/foo\/bar\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e9\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003eFound conflict in template \/foo\/bar with existing template \/*\/bar\/\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e10\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/foo\/baz\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e11\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/foo\/*\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e12\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003eFound conflict in template \/foo\/* with existing template \/foo\/baz\/\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e13\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/a\/*\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e14\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003eFound conflict in template \/a\/* with existing template \/*\/bar\/\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e15\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e16\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003eCompiled route list:\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e17\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/foo\/\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e18\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/bar\/\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e19\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/baz\/\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e20\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/*\/foo\/\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e21\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/*\/bar\/\n\u003c\/td\u003e\u003c\/tr\u003e\u003ctr\u003e\u003cth class=\"phabricator-source-line\"\u003e\u003cspan\u003e22\u003c\/span\u003e\u003c\/th\u003e\u003ctd class=\"phabricator-source-code\"\u003e\/foo\/baz\/\n\u003c\/td\u003e\u003c\/tr\u003e\u003c\/table\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_455\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5149333\" id=\"5149333\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_454\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_202\"\u003etstarling\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5149333\" data-sigil=\"has-tooltip\" data-meta=\"0_453\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 3:27 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 03:27:19 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_451\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_452\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_203\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5149288\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_36\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5149288\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/EvanProdromou\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_37\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@EvanProdromou\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eOne thing I brought up tonight in the meeting is that \u003ca href=\"https:\/\/www.mediawiki.org\/wiki\/HyperSwitch\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eHyperSwitch\u003c\/a\u003e uses \u003ca href=\"https:\/\/swagger.io\/docs\/specification\/about\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eOpenAPI 3.0\u003c\/a\u003e to define routes. It's a unique architecture in my experience!\u003c\/p\u003e\n\n\u003cp\u003eUPDATE: It's actually a pretty well-established architecture; it's how \u003ca href=\"https:\/\/swagger.io\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eSwagger\u003c\/a\u003e works, after all.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eI don't want to repeat RESTBase's mistake of mixing configuration with registration and business logic, ultimately requiring the user to edit enormous distributed YAML files. If we follow conventions from the Action API, we can have automatically generated documentation which is localised and integrates all currently-enabled extensions. We can generate an OpenAPI spec if necessary.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_464\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5149393\" id=\"5149393\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_463\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_204\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5149393\" data-sigil=\"has-tooltip\" data-meta=\"0_462\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 4:29 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 04:29:25 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_460\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_461\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_205\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5149229\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_38\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5149229\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_41\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eIf we reorganise, we may need backwards compatible Parsoid endpoints, even if they are deprecated from the outset.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eWe might need those anyway since raw Parsoid endpoints are different from RESTBase Parsoid endpoints, and we'll do the traffic switch in RESTBase not Varnish, so we need to match the URLs RESTBase sends to. See the discussion in \u003ca href=\"\/T221738\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_39\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221738: Parsoid REST API routes in MediaWiki\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5149288\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_40\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5149288\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/EvanProdromou\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_42\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@EvanProdromou\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eOne thing I brought up tonight in the meeting is that \u003ca href=\"https:\/\/www.mediawiki.org\/wiki\/HyperSwitch\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eHyperSwitch\u003c\/a\u003e uses \u003ca href=\"https:\/\/swagger.io\/docs\/specification\/about\/\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eOpenAPI 3.0\u003c\/a\u003e to define routes. It's a unique architecture in my experience!\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eOpenAPI is meant to be used for defining routes, although that can take other forms (like dynamic code generation or scaffolding). And there are plenty of examples of the other direction as well, generating OpenAPI from phpdoc or whatever. (The benefit of having an OpenAPI spec, even if your routing system does not use it, is that it can also be used for generating clients; and given the huge variety of languages, frameworks, coding styles etc. that's not something where we could roll our own tool. It can also generate doc pages and sandboxes, although there we can probably do better.)\u003c\/p\u003e\n\n\u003cp\u003eI'm with Tim on using it as the source of truth: it tries to be the source for both API spec, documentation and tests, and the end result is bloated, hard to navigate and kind of mediocre at all three (most notably there's not much i18n support). Plus, YAML. It would be nice if we could generate it, though.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_475\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/ms665yc73j6nadjitytk\/PHID-FILE-mocohumlrcbe2lcuplml\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Anomie\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-badges\"\u003e\u003cul class=\"phui-badge-flex-view grouped flex-view-collapsed \"\u003e\u003cli class=\"phui-badge-flex-item\"\u003e\u003ca class=\"phui-badge-mini phui-badge-mini-orange \" href=\"\/badges\/view\/5\/\" data-sigil=\"has-tooltip\" data-meta=\"0_473\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-rocket\" data-meta=\"0_474\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5149843\" id=\"5149843\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_472\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Anomie\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_206\"\u003eAnomie\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5149843\" data-sigil=\"has-tooltip\" data-meta=\"0_471\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 2:06 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 14:06:56 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_469\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_470\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_207\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5149393\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_43\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5149393\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_46\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003e(The benefit of having an OpenAPI spec, even if your routing system does not use it, is that it can also be used for generating clients; and given the huge variety of languages, frameworks, coding styles etc. that's not something where we could roll our own tool.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003e\u003ca href=\"https:\/\/en.wikipedia.org\/wiki\/SOAP\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eI think I've heard that one before...\u003c\/a\u003e\u003c\/p\u003e\n\n\u003cp\u003eBut if it turns out to be easy to auto-generate, sure, go for it.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5149306\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_45\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5149306\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_48\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5148349\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_44\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5148349\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_47\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003ethe template pair \u003ctt class=\"remarkup-monospaced\"\u003e\/foo\/{var:int}\u003c\/tt\u003e and \u003ctt class=\"remarkup-monospaced\"\u003e\/foo\/bar\u003c\/tt\u003e is not ambiguous but once again you'd have to get into pairwise regex matching to verify that.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eRESTBase does not have that kind of type matching, as far as I can see.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eWe don't necessarily have to limit ourselves to just what Restbase already has. I think we could plug in some simple typed parameters into your algorithm without much trouble:\u003c\/p\u003e\n\n\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003e\u003ctt class=\"remarkup-monospaced\"\u003e{var:string}\u003c\/tt\u003e conflicts with any literal string and with any other var (regardless of type).\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003e\u003ctt class=\"remarkup-monospaced\"\u003e{var:enum}\u003c\/tt\u003e can be tested against other things by comparing each of the enum values as if it were a literal string. If any value conflicts, the enum as a whole conflicts.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003e\u003ctt class=\"remarkup-monospaced\"\u003e{var:int}\u003c\/tt\u003e conflicts with literal strings matching an integer and other \u003ctt class=\"remarkup-monospaced\"\u003eint\u003c\/tt\u003e vars.\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eThe \u003ctt class=\"remarkup-monospaced\"\u003eenum\u003c\/tt\u003e "each value as if it were a literal string" rule meshes here nicely.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003cp\u003eOn the other hand, just because we could doesn't necessarily mean we should. In the future we could add \u003ctt class=\"remarkup-monospaced\"\u003e{var:int}\u003c\/tt\u003e and such easily enough once we have a real use case.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_485\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/ibwvwnryaftpycp36sta\/PHID-FILE-c33l454n2a6u7jgrvqim\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/TheDJ\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5150131\" id=\"5150131\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-user-plus phui-timeline-icon\" data-meta=\"0_484\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/TheDJ\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_209\"\u003eTheDJ\u003c\/a\u003e subscribed.\u003cspan class=\"phui-timeline-extra\"\u003eEdited\u003cspan class=\"visual-only\" aria-hidden=\"true\"\u003e \u00b7 \u003c\/span\u003e\u003ca href=\"#5150131\" data-sigil=\"has-tooltip\" data-meta=\"0_483\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 3:32 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 15:32:12 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_481\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_482\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_208\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eI'd like to show some separation of concerns that Laravel has been applying:\u003c\/p\u003e\n\n\u003col class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003e\u003ca href=\"https:\/\/laravel.com\/docs\/5.8\/routing\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eRoute\u003c\/a\u003e definitions\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003ecan contain input variables (non typed, named only)\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ename path variable can have 'constraints' (regex). \u003ctt class=\"remarkup-monospaced\"\u003eRoute::get('user\/{name}')->where('name', '[A-Za-z]+');\u003c\/tt\u003e\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eroutes can be route grouped, prefixed etc etc\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eroutes can have middleware applied to them to do processing (but middle ware can also globally be set, or programatically triggered)\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eroutes are matched in order of registration\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eroute table can be exported\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003e\u003ca href=\"https:\/\/laravel.com\/docs\/5.8\/controllers\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eControllers\u003c\/a\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eprocess \u003ca href=\"https:\/\/laravel.com\/docs\/5.8\/requests\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003erequests\u003c\/a\u003e\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eretrieve input from request (be it form, json or path input [has already been unpacked by middleware])\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ethen apply validation, do your own work etc modify the models etc.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ereturn \u003ca href=\"https:\/\/laravel.com\/docs\/5.8\/responses\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ereponses\u003c\/a\u003e\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eControllers (can) use PSR-7\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/li\u003e\n\u003c\/ol\u003e\n\n\u003cp\u003eThis is simple and gives a lot of flexibility. All restrictions are then added via either traits on or subclass implementation of Controllers\/Handlers or by the middleware. You could have a JSON schema middleware for instance, an AdminController and a UserController base class etc.\u003c\/p\u003e\n\n\u003cp\u003eOne of those sets of specialisations they have, are called \u003ca href=\"https:\/\/laravel.com\/docs\/5.8\/eloquent-resources\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eAPI Resources\u003c\/a\u003e. Resources are the stricter REST representations\/reflections of models (Here you can transform the json representation for instance. but they also provide a default pagination implementation for instance). Resources can also have other, nested, resources. On their routing level, they have a specific apiResource registration, which basically allows you to in one go, register all the http operations for a resource (with override possibilities if you need them). A default controller does the most basic Model operations, but you can adapt almost everything about this.\u003c\/p\u003e\n\n\u003cp\u003eThis is a (rewritten) example of routing definition I recently created. It mixes some of the conventions that laravel allowed me. It's messy, as real world systems for many reasons often are\/get.\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"nc\" data-symbol-name=\"Route\"\u003eRoute\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"Route\" data-symbol-name=\"prefix\"\u003eprefix\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'\/v1'\u003c\/span\u003e\u003cspan class=\"o\"\u003e)\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"middleware\"\u003emiddleware\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'jsonContentType'\u003c\/span\u003e\u003cspan class=\"o\"\u003e)\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"group\"\u003egroup\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e\u003cspan class=\"o\"\u003e()\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"nc\" data-symbol-name=\"Route\"\u003eRoute\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"Route\" data-symbol-name=\"apiResource\"\u003eapiResource\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'events'\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'EventController'\u003c\/span\u003e\u003cspan class=\"o\"\u003e);\u003c\/span\u003e \u003cspan class=\"c\"\u003e\/\/ very strict REST resource using a subclass of ResourceController\u003c\/span\u003e\n \u003cspan class=\"nc\" data-symbol-name=\"Route\"\u003eRoute\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"Route\" data-symbol-name=\"prefix\"\u003eprefix\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'\/data'\u003c\/span\u003e\u003cspan class=\"o\"\u003e)\u003c\/span\u003e\n \u003cspan class=\"c\"\u003e\/\/ These operations directly operate on the old system that we need to deprecate\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"group\"\u003egroup\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"k\"\u003efunction\u003c\/span\u003e\u003cspan class=\"o\"\u003e(){\u003c\/span\u003e\n \u003cspan class=\"nc\" data-symbol-name=\"Route\"\u003eRoute\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"Route\" data-symbol-name=\"get\"\u003eget\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'transactions'\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'DataController@transactionsIndex'\u003c\/span\u003e\u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"nc\" data-symbol-name=\"Route\"\u003eRoute\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"Route\" data-symbol-name=\"get\"\u003eget\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'tickets\/{ticket_id}'\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'DataController@showTicket'\u003c\/span\u003e\u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"nc\" data-symbol-name=\"Route\"\u003eRoute\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"Route\" data-symbol-name=\"get\"\u003eget\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'tickets\/{ticket_id}\/usage'\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'DataController@usage'\u003c\/span\u003e\u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"nc\" data-symbol-name=\"Route\"\u003eRoute\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"Route\" data-symbol-name=\"get\"\u003eget\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'events\/{event_id}\/count'\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'DataController@count'\u003c\/span\u003e\u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e});\u003c\/span\u003e\n \u003cspan class=\"nc\" data-symbol-name=\"Route\"\u003eRoute\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"Route\" data-symbol-name=\"post\"\u003epost\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'\/transactions'\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'TransactionsController@sync'\u003c\/span\u003e\u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"nc\" data-symbol-name=\"Route\"\u003eRoute\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"Route\" data-symbol-name=\"prefix\"\u003eprefix\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'\/properties'\u003c\/span\u003e\u003cspan class=\"o\"\u003e)\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"group\"\u003egroup\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"k\"\u003efunction\u003c\/span\u003e\u003cspan class=\"o\"\u003e(){\u003c\/span\u003e\n \u003cspan class=\"nc\" data-symbol-name=\"Route\"\u003eRoute\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"Route\" data-symbol-name=\"post\"\u003epost\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'sync'\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'PropertiesController@sync'\u003c\/span\u003e\u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"nc\" data-symbol-name=\"Route\"\u003eRoute\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"Route\" data-symbol-name=\"get\"\u003eget\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\u003cspan class=\"s1\"\u003e'types'\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'PropertiesController@typesIndex'\u003c\/span\u003e\u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e});\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e});\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eThe only proper REST thing here is the Events resources\/models and EventController. The other stuff is.. a poor excuse for an api...And I think that's my point.. routing isn't REST, input specification\/validation or output specification isn't REST either. REST definitions should be 'syntactic sugar' on top of these more fundamental concepts. They are traits, subclasses, phpdoc annotations, etc. which then provide either more semantic context, more restrictions and more features as required. We shouldn't mix those things and bring them together at one point. We should have a RESTRegistrationTrait, and a RestResource, a RESTController and maybe a REST Json schema base middleware validating input, and there could be a RESTOpenAPIExporter etc..\u003c\/p\u003e\n\n\u003cp\u003eLastly OpenAPI is really mature, unlike it's predecessor Swagger v2, so far i haven't been 'blocked' by it's capabilities in providing accurate HTTP documentation specs. However I'd say that while it might be possible to generate clients based of them, i'd never rely on those clients for production work work ;)\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_496\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/ms665yc73j6nadjitytk\/PHID-FILE-mocohumlrcbe2lcuplml\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Anomie\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-badges\"\u003e\u003cul class=\"phui-badge-flex-view grouped flex-view-collapsed \"\u003e\u003cli class=\"phui-badge-flex-item\"\u003e\u003ca class=\"phui-badge-mini phui-badge-mini-orange \" href=\"\/badges\/view\/5\/\" data-sigil=\"has-tooltip\" data-meta=\"0_494\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-rocket\" data-meta=\"0_495\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5150416\" id=\"5150416\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_493\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Anomie\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_210\"\u003eAnomie\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5150416\" data-sigil=\"has-tooltip\" data-meta=\"0_492\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 4:45 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 16:45:47 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_490\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_491\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_211\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5150130\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_49\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5150130\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/TheDJ\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_50\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@TheDJ\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003col class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003e\u003ca href=\"https:\/\/laravel.com\/docs\/5.8\/routing\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eRoute\u003c\/a\u003e definitions\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003ecan contain input variables (non typed, named only)\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ename path variable can have 'constraints' (regex). \u003ctt class=\"remarkup-monospaced\"\u003eRoute::get('user\/{name}')->where('name', '[A-Za-z]+');\u003c\/tt\u003e\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/li\u003e\n\u003c\/ol\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eSo they're typed, but typed via regex rather than predefined keywords. I doubt we'd go that route since it would make registration-time detection of routing collisions difficult.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eroutes can be route grouped, prefixed etc etc\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eThis seems like something included to make the procedural interface in your code sample work better. I think we'd prefer a more declarative interface.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eroutes can have middleware applied to them to do processing (but middle ware can also globally be set, or programatically triggered)\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eFrom your examples, it sounds like "middleware" is something along the lines of "request munging before the Handler sees it". Possibly doing some access control checks. Is that right?\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eroutes are matched in order of registration\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eWhen pieces come from various independent MediaWiki extensions, order of registration can wind up being confusing. We've been discussing specifically disallowing ambiguous routing declarations at registration time instead.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eroute table can be exported\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eWith a declarative definition, there's probably not so much need as the combined declarations are probably already a decent table.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003col class=\"remarkup-list\" start=\"2\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003e\u003ca href=\"https:\/\/laravel.com\/docs\/5.8\/controllers\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eControllers\u003c\/a\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eprocess \u003ca href=\"https:\/\/laravel.com\/docs\/5.8\/requests\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003erequests\u003c\/a\u003e\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eretrieve input from request (be it form, json or path input [has already been unpacked by middleware])\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ethen apply validation, do your own work etc modify the models etc.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ereturn \u003ca href=\"https:\/\/laravel.com\/docs\/5.8\/responses\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ereponses\u003c\/a\u003e\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eControllers (can) use PSR-7\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/li\u003e\n\u003c\/ol\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eThat's the Handler described above. Gerg\u0151 suggested using PSR-7, but we might just go with MediaWiki's existing WebRequest and WebResponse classes instead.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eOne of those sets of specialisations they have, are called \u003ca href=\"https:\/\/laravel.com\/docs\/5.8\/eloquent-resources\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eAPI Resources\u003c\/a\u003e. Resources are the stricter REST representations\/reflections of models (Here you can transform the json representation for instance. but they also provide a default pagination implementation for instance). Resources can also have other, nested, resources. On their routing level, they have a specific apiResource registration, which basically allows you to in one go, register all the http operations for a resource (with override possibilities if you need them). A default controller does the most basic Model operations, but you can adapt almost everything about this.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eThis, on the other hand, sounds like you're getting close to building something like GraphQL rather than an interface for accessing disparate services. A GraphQL endpoint as a Handler may not be out of place, but I'm not sure we'd want to bake it into the Router.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_505\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5151276\" id=\"5151276\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_504\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_212\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5151276\" data-sigil=\"has-tooltip\" data-meta=\"0_503\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 8:50 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 20:50:18 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_501\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_502\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_213\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5149843\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_51\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5149843\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_53\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003e\u003ca href=\"https:\/\/en.wikipedia.org\/wiki\/SOAP\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eI think I've heard that one before...\u003c\/a\u003e\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eAutomatic interface generation was actually a nice aspect of SOAP. (Too bad most others were horrible.)\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5150130\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_52\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5150130\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/TheDJ\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_54\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@TheDJ\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eOne of those sets of specialisations they have, are called \u003ca href=\"https:\/\/laravel.com\/docs\/5.8\/eloquent-resources\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eAPI Resources\u003c\/a\u003e. Resources are the stricter REST representations\/reflections of models (Here you can transform the json representation for instance. but they also provide a default pagination implementation for instance). Resources can also have other, nested, resources.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eHaving some kind of reusable model -> JSON-friendly object converter is definitely a good idea (although calling it resource seems like a particularly confusing choice). Automatic handling of nested models (and lots of automation, in general) is good for rapid development of low-usage, not too heavily customized websites where you can use it with ORM and scaffolding to auto-generate almost everything from an object model. I don't think it's particularly useful for something like MediaWiki.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eLastly OpenAPI is really mature, unlike it's predecessor Swagger v2, so far i haven't been 'blocked' by it's capabilities in providing accurate HTTP documentation specs.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eSwagger v2 is OpenAPI 2 so I guess you mean OpenAPI 3? I'm not too familiar with the differences, but in terms of documentation, IMO context-switching between wikitext\/JSON and markdown\/YAML would get annoying quickly. Also AFAIK doc localization is explicitly not supported at this point (\u003ca href=\"https:\/\/github.com\/OAI\/OpenAPI-Specification\/issues\/274\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003e#274\u003c\/a\u003e seems to be the relevant issue). But my main issue with it is that they just want to cram too many things into a single file, making it hard to navigate (granted extension.json also suffers from that somewhat).\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_514\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5151430\" id=\"5151430\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_513\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_214\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5151430\" data-sigil=\"has-tooltip\" data-meta=\"0_512\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 9:53 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 21:53:29 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_510\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_511\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_215\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eRe: pagination, for Wikimedia scale it tends to be surprisingly involved; we definitely want to avoid doing it separately for API and web. The current Pager classes mix DB and presentation logic, and tend to use the horrible pattern of being constructed around a SpecialPage object, but the DB aspect of those classes is probably something we want to keep in the long term, at which point all an API pagination helper can do is read\/write continuation data in a standard format, like ApiBase does. Something like that will probably be useful for REST APIs as well.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_523\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5151446\" id=\"5151446\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_522\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_216\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5151446\" data-sigil=\"has-tooltip\" data-meta=\"0_521\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 10:06 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 22:06:56 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_519\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_520\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_217\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5150416\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_55\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5150416\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_56\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eFrom your examples, it sounds like "middleware" is something along the lines of "request munging before the Handler sees it". Possibly doing some access control checks. Is that right?\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eMiddleware means decorator pattern, basically. It can munge the request before the handler sees it, munge the response after the handler has returned it, or choose to handle the request directly and not invoke the handler at all. (Also, the handler can be another middleware - it's an onion structure.) It's a bit less random than the sea of hooks we tend to use, and easier understood by anyone familiar with the paradigm, IMO we should go for it. (In the very long term, it would be nice to use it in MediaWiki in general, too.)\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eThat's the Handler described above. Gerg\u0151 suggested using PSR-7, but we might just go with MediaWiki's existing WebRequest and WebResponse classes instead.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eWebRequest is mainly designed for web forms where you don't really care whether something comes from URL query parameters or POST body, and does not deal with other verbs or JSON bodies at all. Also, neither of those objects were written with a middleware use case in mind. I'm not sure we aren't better off by starting clean.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_532\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5151601\" id=\"5151601\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_531\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_218\"\u003etstarling\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5151601\" data-sigil=\"has-tooltip\" data-meta=\"0_530\"\u003e\u003cspan class=\"screen-only\"\u003eMay 1 2019, 11:56 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-01 23:56:28 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_528\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_529\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_219\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eSeveral REST libraries have "reverse routing", i.e. serialization of parameters into a URL. That requires named routes, or some other way to refer to a route, since you don't want to have to provide the full template in order to serialize parameters. Do we need that?\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_541\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5151701\" id=\"5151701\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_540\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_220\"\u003etstarling\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5151701\" data-sigil=\"has-tooltip\" data-meta=\"0_539\"\u003e\u003cspan class=\"screen-only\"\u003eMay 2 2019, 1:21 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-02 01:21:37 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_537\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_538\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_221\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eI went through a few iterations on the API in the last couple of days. Here's my current favourite. First the verbose interface:\u003c\/p\u003e\n\n\u003cp\u003eextension.json:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"json\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"s\"\u003e"RESTRoutes"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"o\"\u003e[\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"method"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"GET"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"path"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"\/user\/{name}\/hello"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"class"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"HelloHandler"\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e]\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eThe handler class:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"k\"\u003eclass\u003c\/span\u003e \u003cspan class=\"no\"\u003eHelloHandler\u003c\/span\u003e \u003cspan class=\"k\"\u003eextends\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"Handler\"\u003eHandler\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003eexecute\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"no\"\u003eResponse\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"write\"\u003ewrite\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'Hello '\u003c\/span\u003e \u003cspan class=\"o\"\u003e.\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getParam\"\u003egetParam\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'name'\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e.\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'!'\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\n\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003egetLastModified\u003c\/span\u003e\u003cspan class=\"o\"\u003e()\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"nf\" data-symbol-name=\"filemtime\"\u003efilemtime\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"kc\"\u003e__FILE__\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\n\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003evalidateParams\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\u003cspan class=\"nv\"\u003e$validator\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getValidator\"\u003egetValidator\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\t\t\u003cspan class=\"nv\"\u003e$validator\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"assertParamRegex\"\u003eassertParamRegex\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'name'\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'\/^[a-z]+$\/'\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eThere's always one handler class per route, and the interface is similar to the one proposed by \u003ca href=\"\/p\/EvanProdromou\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_57\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@EvanProdromou\u003c\/span\u003e\u003c\/a\u003e. We have one handler class per route, allowing every route to dynamically override its configuration. The Handler takes the configuration array from extension.json as a constructor parameter, and uses it to provide a default implementation of parameter validation. I agree with splitting ApiBase::getConditionalRequestData() into getLastModified() and getETag(). Evan's $templateParams is subsumed into the Request object. Streaming is supported by adding the Response parameter. Evan proposed having the Request as a constructor parameter to the Handler, but sending it in via execute() instead has a few benefits. It makes it possible to construct Handler objects without having an active request, which I think will be useful.\u003c\/p\u003e\n\n\u003cp\u003eWithout loss of flexibility, I also introduce the subclass SimpleHandler, which implements its own execute(), copying the Request and Response objects to object properties to provide getRequest() and getResponse(). It uses the template parameters as formal parameters to the overridden method run(). It interprets the return value from run() and writes it to the Response. Thus:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"k\"\u003eclass\u003c\/span\u003e \u003cspan class=\"no\"\u003eHelloHandler\u003c\/span\u003e \u003cspan class=\"k\"\u003eextends\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"SimpleHandler\"\u003eSimpleHandler\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003erun\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$name\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"Hello $name!"\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eFinally, I wanted extensions to be able to implement their own handler factory. This allows handlers to be implemented as anonymous classes. We could have\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"json\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"s\"\u003e"RESTRoutes"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"o\"\u003e[\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"method"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"GET"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"path"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"\/user\/{name}\/hello"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"factory"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"MyExtension::hello"\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e]\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eIf we need named routes anyway, perhaps this could be abbreviated to\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"json\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"s\"\u003e"RESTRoutes"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"o\"\u003e[\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"method"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"GET"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"path"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"\/user\/{name}\/hello"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"name"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"hello"\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"RESTFactoryClass"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"MyExtension"\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e]\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eIn this latter extension.json fragment, I am proposing to build the name of a static method from the extension's RESTFactoryClass and the route name. Either way, you can then have:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"k\"\u003eclass\u003c\/span\u003e \u003cspan class=\"no\"\u003eMyExtension\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003estatic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003ehello\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$config\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"k\"\u003enew\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"class\"\u003eclass\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$config\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"k\"\u003eextends\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"SimpleHandler\"\u003eSimpleHandler\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003erun\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$name\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"Hello $name!"\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\t\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eThis allows many route handlers to be defined in a single file. Using an anonymous class is better for this use case than defining the run() method in a closure, since the anonymous class's run() trivially has access to $this->getRequest(), $this->getResponse(), etc. Also, you can see now the full benefit of passing only $config to the Handler constructor, and sending Request and Response via execute(): the factory function does not need to be aware of Request and Response at all. It would almost be possible to override execute() with a closure, but I think it's not so nice:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"k\"\u003eclass\u003c\/span\u003e \u003cspan class=\"no\"\u003eMyExtension\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003estatic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003ehello\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$config\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"no\"\u003eResponse\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"k\"\u003euse\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$config\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"write\"\u003ewrite\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'Hello '\u003c\/span\u003e \u003cspan class=\"o\"\u003e.\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getParam\"\u003egetParam\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'name'\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e.\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'!'\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\t\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eYou drop an indenting level, but at the cost of losing the ability to access any methods of the Handler base class. You could pass the Handler as a parameter to the closure, but then its protected methods would have to become public.\u003c\/p\u003e\n\n\u003cp\u003eRequest here is not WebRequest, it is MediaWiki\\Rest\\Request, which will provide getParams() for access to the template parameters. Other methods may end up being wrappers around a contained WebRequest.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_550\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5152090\" id=\"5152090\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_549\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_222\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5152090\" data-sigil=\"has-tooltip\" data-meta=\"0_548\"\u003e\u003cspan class=\"screen-only\"\u003eMay 2 2019, 7:10 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-02 07:10:59 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_546\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_547\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_223\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eI'd prefer something more DI-friendly. Handlers (or handler factories) should be constructed by MediaWikiServices or a similar container, otherwise they can't receive dependencies on instantiation. This is a generic problem (action API modules have it, special pages have it, hook handlers have it, Action classes would probably have it if the internals involved in \u003ctt class=\"remarkup-monospaced\"\u003eindex.php?action=\u003c\/tt\u003e would be less messy and followed a proper controller pattern), so it would be nice to come up with a generic solution that can be used elsewhere. IMO that would be passing the parameters to ObjectFactory (which already understands \u003ctt class=\"remarkup-monospaced\"\u003eclass\u003c\/tt\u003e and \u003ctt class=\"remarkup-monospaced\"\u003efactory\u003c\/tt\u003e parameters) and teach ObjectFactory to also understand \u003ctt class=\"remarkup-monospaced\"\u003eservice\u003c\/tt\u003e (which would be the preferred method, just get the service by that name from MediaWikiServices), and then decide based on what interface the returned object implements whether it's a handler or a factory.\u003c\/p\u003e\n\n\u003cp\u003eSo you'd write something like\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"c\"\u003e\/\/ extension.json\u003c\/span\u003e\n\u003cspan class=\"s2\"\u003e"RESTRoutes"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"o\"\u003e[\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"s2\"\u003e"method"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"GET"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e\n \u003cspan class=\"s2\"\u003e"path"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"\/user\/{name}\/hello"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e\n \u003cspan class=\"s2\"\u003e"name"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"myextension-hello"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e\n \u003cspan class=\"s2\"\u003e"service"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"MyExtensionHandlerFactory"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e]\u003c\/span\u003e\n\n\u003cspan class=\"c\"\u003e\/\/ ServiceWiring.php\u003c\/span\u003e\n\u003cspan class=\"s1\"\u003e'MyExtensionHandlerFactory'\u003c\/span\u003e \u003cspan class=\"o\"\u003e=>\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$services\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"k\"\u003enew\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"MediaWiki\\Extensions\\MyExtension\\HandlerFactory\"\u003eMediaWiki\\Extensions\\MyExtension\\HandlerFactory\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\n \u003cspan class=\"nv\"\u003e$services\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"get\"\u003eget\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'MyExtensionStore'\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e},\u003c\/span\u003e\n\n\u003cspan class=\"c\"\u003e\/\/ HandlerFactory.php\u003c\/span\u003e\n\u003cspan class=\"k\"\u003eclass\u003c\/span\u003e \u003cspan class=\"no\"\u003eHandlerFactlory\u003c\/span\u003e \u003cspan class=\"k\"\u003eimplements\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"MediaWiki\\Rest\\HandlerFactory\"\u003eMediaWiki\\Rest\\HandlerFactory\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003ecreate\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$name\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$verb\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"nf\" data-symbol-name=\"assert\"\u003eassert\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$name\u003c\/span\u003e \u003cspan class=\"o\"\u003e===\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'myextension-hello'\u003c\/span\u003e \u003cspan class=\"o\"\u003e&&\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$verb\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'GET'\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"k\"\u003enew\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"HelloGetHandler\"\u003eHelloGetHandler\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"store\"\u003estore\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eI wouldn't mind some terser notation, so that routes are easy to scan, like\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"c\"\u003e\/\/ extension.json\u003c\/span\u003e\n\u003cspan class=\"s2\"\u003e"RESTRoutes"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"s2\"\u003e"myextension-foo"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"o\"\u003e[\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"GET"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"\/user\/{name}\/foo"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"o\"\u003e...\u003c\/span\u003e \u003cspan class=\"o\"\u003e],\u003c\/span\u003e\n \u003cspan class=\"s2\"\u003e"myextension-bar"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"o\"\u003e[\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"GET"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"\/user\/{name}\/bar"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"o\"\u003e...\u003c\/span\u003e \u003cspan class=\"o\"\u003e],\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003ebut making handler instantiation definitions more terse seems awkward.\u003c\/p\u003e\n\n\u003chr class=\"remarkup-hr\" \/\u003e\n\n\u003cp\u003eParameter types should be expressed in a declarative way, as that's needed for good sandbox UX and good autogenerated help pages. That could in the route like `"\/user\/{name:user}\/foo", or could be some method on the handler that returns a descriptor array; the second is probably a better way as we might want to add more information than just the type (min\/max, enum values etc. etc.)\u003c\/p\u003e\n\n\u003chr class=\"remarkup-hr\" \/\u003e\n\n\u003cp\u003eI assume \u003ctt class=\"remarkup-monospaced\"\u003erun()\u003c\/tt\u003e in the examples is meant to be protected, not public?\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_560\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/ibwvwnryaftpycp36sta\/PHID-FILE-c33l454n2a6u7jgrvqim\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/TheDJ\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5152457\" id=\"5152457\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_559\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/TheDJ\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_224\"\u003eTheDJ\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003eEdited\u003cspan class=\"visual-only\" aria-hidden=\"true\"\u003e \u00b7 \u003c\/span\u003e\u003ca href=\"#5152457\" data-sigil=\"has-tooltip\" data-meta=\"0_558\"\u003e\u003cspan class=\"screen-only\"\u003eMay 2 2019, 10:00 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-02 10:00:55 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_556\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_557\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_225\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5150416\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_58\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5150416\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_67\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eSo they're typed, but typed via regex rather than predefined keywords. I doubt we'd go that route since it would make registration-time detection of routing collisions difficult.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eNo, they are not typed, because urls themselves are not typed, they are simply text. My point was, stay close to what this is. Matching a route, same as in apache or varnish etc. Urls don't have number components, so we shouldn't add them in our route matching.\u003c\/p\u003e\n\n\u003cp\u003eThe regex pattern is there to make it easier to define things like:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"text\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\/resource\/create\n\/resource\/{command}\n\/resource\/{resource_id}\n\/resource\/{resource_id}\/subresource\/{subid}\/create\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eTo enhance _matching_ capabilities. But variable value or type validation\/specification\/documentation are other concerns at another level of the logic. While there might be some overlap between matching a number and something having to be a number, they are not the same concern and mixing it makes you less flexible than reality might potentially require.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5150416\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_59\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5150416\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_68\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eThis seems like something included to make the procedural interface in your code sample work better. I think we'd prefer a more declarative interface.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eNot just for that, groups can also have middleware for instance prefix(\/admin) group with middleware(admin authentication) is a very powerful and readable structure, which avoids making mistakes like accidentally adding a route with no admin authentication underneath a \/admin path.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5150416\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_60\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5150416\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_69\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eWhen pieces come from various independent MediaWiki extensions, order of registration can wind up being confusing. We've been discussing specifically disallowing ambiguous routing declarations at registration time instead.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eI have no particular problem with that, i think this is actually one of the few weak areas of laravel apps.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5150416\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_61\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5150416\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_70\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eThat's the Handler described above. Gerg\u0151 suggested using PSR-7, but we might just go with MediaWiki's existing WebRequest and WebResponse classes instead.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eMostly yes. But I think that it's mixing response and handler for instance. lastmodified as mentioned in this proposal, is a response property and thus does not belong in a standard Handler class. In Laravel, you would 1 of three things:\u003c\/p\u003e\n\n\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003ecreate a MySpecializedResponse, initialized (incl. LastModified) by a factory based on your model\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003esay that your Handler uses LastModifiedTrait to provide the getLastModified and then Router checks for the function being present and then enforcing it on the response.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eadd a MyETAGMiddleware to conditionally check if your ModelType requires setting the eTAG and then doing it.\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5150416\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_62\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5150416\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_71\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eThis, on the other hand, sounds like you're getting close to building something like GraphQL rather than an interface for accessing disparate services. A GraphQL endpoint as a Handler may not be out of place, but I'm not sure we'd want to bake it into the Router.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eI'm not familiar with graphQL, but basically, it switches out the base behavior of the Router with a more specialized behavior. Nothing more.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5151276\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_63\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5151276\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_72\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003e(although calling it resource seems like a particularly confusing choice).\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eDepends on your frame of reference: "The fundamental concept in any RESTful API is the resource" \u003ca href=\"https:\/\/restful-api-design.readthedocs.io\/en\/latest\/resources.html\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ehttps:\/\/restful-api-design.readthedocs.io\/en\/latest\/resources.html\u003c\/a\u003e\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5151276\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_64\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5151276\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_73\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eSwagger v2 is OpenAPI 2 so I guess you mean OpenAPI 3?\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eOpenAPI 3 was the first official release of OpenApi, so that's what I often drop that version number ;)\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5151276\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_65\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5151276\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_74\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eAlso AFAIK doc localization is explicitly not supported at this point (\u003ca href=\"https:\/\/github.com\/OAI\/OpenAPI-Specification\/issues\/274\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003e#274\u003c\/a\u003e seems to be the relevant issue). But my main issue with it is that they just want to cram too many things into a single file, making it hard to navigate (granted extension.json also suffers from that somewhat).\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eWhen you build complexity, complexity is pretty unavoidable ;) I think it would be great if you can just generate a OpenAPI spec from whatever we use internally format=openapi&formatversion=v3. Just being able to import that into Postman to toy around with is SOO powerful and should not be underestimated !!!\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5151446\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_66\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5151446\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_75\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eMiddleware means decorator pattern, basically. It can munge the request before the handler sees it, munge the response after the handler has returned it, or choose to handle the request directly and not invoke the handler at all. (Also, the handler can be another middleware - it's an onion structure.) It's a bit less random than the sea of hooks we tend to use, and easier understood by anyone familiar with the paradigm, IMO we should go for it. (In the very long term, it would be nice to use it in MediaWiki in general, too.)\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eMore important.... easily reusable components to do that (which is why it works so well for route groups). If you've ever needed to have written a guzzleclient for another api, that comes with lots of business rules (think throttling, token refresh, automatic retry etc) you'll appreciate this type of layer.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_570\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/ibwvwnryaftpycp36sta\/PHID-FILE-c33l454n2a6u7jgrvqim\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/TheDJ\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5152532\" id=\"5152532\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_569\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/TheDJ\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_226\"\u003eTheDJ\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003eEdited\u003cspan class=\"visual-only\" aria-hidden=\"true\"\u003e \u00b7 \u003c\/span\u003e\u003ca href=\"#5152532\" data-sigil=\"has-tooltip\" data-meta=\"0_568\"\u003e\u003cspan class=\"screen-only\"\u003eMay 2 2019, 10:19 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-02 10:19:33 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_566\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_567\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_227\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5151701\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_76\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5151701\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_77\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eThis allows many route handlers to be defined in a single file. Using an anonymous class is better for this use case than defining the run() method in a closure, since the anonymous class's run() trivially has access to $this->getRequest(), $this->getResponse(), etc. Also, you can see now the full benefit of passing only $config to the Handler constructor, and sending Request and Response via execute(): the factory function does not need to be aware of Request and Response at all. It would almost be possible to override execute() with a closure, but I think it's not so nice:\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eSo what Laravel does is that it defines the name of the run\/execute at the route declaration level, with a fallback that the Router will use when unspecified. Then for REST resources for instance, it adds the following 'sugar' in the resource handling: A single handler can do all handling for 1 type of resources, and has default function names for the various resource operations so:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-table-wrap\"\u003e\u003ctable class=\"remarkup-table\"\u003e\n\u003ctr\u003e\u003ctd\u003eVerb\u003c\/td\u003e\u003ctd\u003eURI\u003c\/td\u003e\u003ctd\u003eAction\u003c\/td\u003e\u003c\/tr\u003e\n\u003ctr\u003e\u003ctd\u003eGET\u003c\/td\u003e\u003ctd\u003e\/photos\u003c\/td\u003e\u003ctd\u003eindex\u003c\/td\u003e\u003c\/tr\u003e\n\u003ctr\u003e\u003ctd\u003eGET\u003c\/td\u003e\u003ctd\u003e\/photos\/create\u003c\/td\u003e\u003ctd\u003ecreate\u003c\/td\u003e\u003c\/tr\u003e\n\u003ctr\u003e\u003ctd\u003ePOST\u003c\/td\u003e\u003ctd\u003e\/photos\/\u003c\/td\u003e\u003ctd\u003estore\u003c\/td\u003e\u003c\/tr\u003e\n\u003ctr\u003e\u003ctd\u003eGET\u003c\/td\u003e\u003ctd\u003e\/photos\/{photo}\u003c\/td\u003e\u003ctd\u003eshow\u003c\/td\u003e\u003c\/tr\u003e\n\u003ctr\u003e\u003ctd\u003eGET\u003c\/td\u003e\u003ctd\u003e\/photos\/{photo}\/edit\u003c\/td\u003e\u003ctd\u003eedit\u003c\/td\u003e\u003c\/tr\u003e\n\u003ctr\u003e\u003ctd\u003ePUT\/PATCH\u003c\/td\u003e\u003ctd\u003e\/photos\/{photo}\u003c\/td\u003e\u003ctd\u003eupdate\u003c\/td\u003e\u003c\/tr\u003e\n\u003ctr\u003e\u003ctd\u003eDELETE\u003c\/td\u003e\u003ctd\u003e\/photos\/{photo}\u003c\/td\u003e\u003ctd\u003edestroy\u003c\/td\u003e\u003c\/tr\u003e\n\u003ctr\u003e\u003c\/tr\u003e\n\u003c\/table\u003e\u003c\/div\u003e\n\n\u003cp\u003eAnd you would have a PhotosController implementing a ResourceController with the default handler methods \u003ctt class=\"remarkup-monospaced\"\u003eindex, create, store, show, edit, update, destroy\u003c\/tt\u003e for each of those resource actions that the Router will automatically will know to use if your registered an apiResource route. Since all photos share significant amounts of non-Model logic, this is useful. Again, overrides are possible for those who want to, and you can split all of this out in separate handlers if you want to, but 90% of the time this works really well and makes things rather readable.\u003c\/p\u003e\n\n\u003cp\u003e*NOTE:* This totally goes against PSR-15 btw.. and Laravel has not yet stated what their idea is for the future on this particular part of their implementation (as far as I'm aware)\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_573\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-minor-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003ca name=\"5152610\" id=\"5152610\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-user-plus phui-timeline-icon\" data-meta=\"0_572\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_228\"\u003etstarling\u003c\/a\u003e added a subscriber: \u003ca href=\"\/p\/daniel\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_229\"\u003edaniel\u003c\/a\u003e.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5152610\" data-sigil=\"has-tooltip\" data-meta=\"0_571\"\u003e\u003cspan class=\"screen-only\"\u003eMay 2 2019, 10:33 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-02 10:33:56 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_582\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5152650\" id=\"5152650\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_581\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_230\"\u003etstarling\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5152650\" data-sigil=\"has-tooltip\" data-meta=\"0_580\"\u003e\u003cspan class=\"screen-only\"\u003eMay 2 2019, 11:01 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-02 11:01:52 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_578\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_579\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_231\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5152457\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_78\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5152457\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/TheDJ\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_79\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@TheDJ\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eMostly yes. But I think that it's mixing response and handler for instance. lastmodified as mentioned in this proposal, is a response property and thus does not belong in a standard Handler class. In Laravel, you would 1 of three things:\u003c\/p\u003e\n\n\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003ecreate a MySpecializedResponse, initialized (incl. LastModified) by a factory based on your model\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003esay that your Handler uses LastModifiedTrait to provide the getLastModified and then Router checks for the function being present and then enforcing it on the response.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eadd a MyETAGMiddleware to conditionally check if your ModelType requires setting the eTAG and then doing it.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eThe reason it's not a response property is because it exists only for Is-Modified-Since handling. It's an alternative entry point. If the resource has not been modified since the date specified in the header, a 304 response is sent and execute() is not called. My proposal should have had a Request parameter to getLastModified(), which is needed to identify the resource being queried. I don't think the value returned by getLastModified() should be used to set response headers, since it's really meant to be equivalent to ApiBase::getConditionalRequestData(), the name of which is awkward but at least avoids this confusion.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_592\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/ibwvwnryaftpycp36sta\/PHID-FILE-c33l454n2a6u7jgrvqim\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/TheDJ\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5152708\" id=\"5152708\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_591\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/TheDJ\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_232\"\u003eTheDJ\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003eEdited\u003cspan class=\"visual-only\" aria-hidden=\"true\"\u003e \u00b7 \u003c\/span\u003e\u003ca href=\"#5152708\" data-sigil=\"has-tooltip\" data-meta=\"0_590\"\u003e\u003cspan class=\"screen-only\"\u003eMay 2 2019, 11:30 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-02 11:30:14 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_588\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_589\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_233\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5152650\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_80\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5152650\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_81\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eThe reason it's not a response property is because it exists only for Is-Modified-Since handling.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eAh, that makes more sense. For this Laravel would always do the Trait approach I suspect (trait, because optional behavior of HTTP and your Router, where required stuff tends to go into an Interface) and then have the Router conditionally apply that HTTP logic. Then if that is used a lot, you would add the Trait to some sort of BaseController that other Controllers extend.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_603\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/v5wmnfiaelrd6nztxd3a\/PHID-FILE-s57znldbv3qchtewuryd\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/daniel\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-badges\"\u003e\u003cul class=\"phui-badge-flex-view grouped flex-view-collapsed \"\u003e\u003cli class=\"phui-badge-flex-item\"\u003e\u003ca class=\"phui-badge-mini phui-badge-mini-indigo \" href=\"\/badges\/view\/14\/\" data-sigil=\"has-tooltip\" data-meta=\"0_601\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-empire\" data-meta=\"0_602\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5152853\" id=\"5152853\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_600\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/daniel\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_234\"\u003edaniel\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5152853\" data-sigil=\"has-tooltip\" data-meta=\"0_599\"\u003e\u003cspan class=\"screen-only\"\u003eMay 2 2019, 1:06 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-02 13:06:12 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_597\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_598\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_235\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5152090\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_82\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5152090\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_83\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eI'd prefer something more DI-friendly. Handlers (or handler factories) should be constructed by MediaWikiServices or a similar container, otherwise they can't receive dependencies on instantiation. This is a generic problem (action API modules have it, special pages have it, hook handlers have it, Action classes would probably have it if the internals involved in \u003ctt class=\"remarkup-monospaced\"\u003eindex.php?action=\u003c\/tt\u003e would be less messy and followed a proper controller pattern), so it would be nice to come up with a generic solution that can be used elsewhere.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eOh yes.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eIMO that would be passing the parameters to ObjectFactory (which already understands \u003ctt class=\"remarkup-monospaced\"\u003eclass\u003c\/tt\u003e and \u003ctt class=\"remarkup-monospaced\"\u003efactory\u003c\/tt\u003e parameters) and teach ObjectFactory to also understand \u003ctt class=\"remarkup-monospaced\"\u003eservice\u003c\/tt\u003e (which would be the preferred method, just get the service by that name from MediaWikiServices), and then decide based on what interface the returned object implements whether it's a handler or a factory.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eThat's probably a sensible approach. It should be noted that ObjectFactory acts as a "static entry point" in this context, and will have to rely on global state here (by accessing MediaWikiServices::getInstance), unless it itself becomes a service (which sounds nice, but I vaguely recall issues with the order of initialization when bootstrapping MediaWikiServices).\u003c\/p\u003e\n\n\u003cp\u003eIn any case, supporting a "service" key in ObjectFactory would certainly be useful, but I'd suggest to also suggest to explicitly support a "factory" key, as in:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"text\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e"RESTRoutes": [\n{\n "method": "GET",\n "path": "\/user\/{name}\/hello",\n "name": "myextension-hello",\n "factory": {\n "service": "MyExtensionHandlerFactory",\n "method": "newRouteHandler",\n }\n}\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eBut really, this does not provide a huge advantage over something like\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"text\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e"factory": "MyExtension::hello"\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eThe static method would act as an instantiator callback, internally relying on MediaWikiServices::getInstance(). Considering that the extension would need a (static method) hook handler to put MyExtensionHandlerFactory into place in MediaWikiServices, this doesn't seem too terrible. We could improve this further by defining that the service container is passed to the factory method as a parameter (this, being wiring code, representing one of the very few places where it's ok to pass around a service container).\u003c\/p\u003e\n\n\u003cp\u003eAlternatively, we could do what we have done with SlotRoleRegistry: define a registry service, (let's say, RESTRouteRegistry), and have extensions register instantiator callback with the registry. Extensions can then use MediaWikiServices::addServiceManipulator() to attach code that will register their handler. In the route declaration, the handler would be referred to by name (either the route name, "myextension-hello" in the example, or a separate handler name, if we want to be able to use the same handler instance for multiple routes).\u003c\/p\u003e\n\n\u003cp\u003eThis technique is the cleanest in my opinion, but not obvious, and currently not documented anywhere. It involves two levels of indirection via callbacks, to enable two levels of lazy initialization. Perhaps that's overkill.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_614\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/ms665yc73j6nadjitytk\/PHID-FILE-mocohumlrcbe2lcuplml\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Anomie\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-badges\"\u003e\u003cul class=\"phui-badge-flex-view grouped flex-view-collapsed \"\u003e\u003cli class=\"phui-badge-flex-item\"\u003e\u003ca class=\"phui-badge-mini phui-badge-mini-orange \" href=\"\/badges\/view\/5\/\" data-sigil=\"has-tooltip\" data-meta=\"0_612\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-rocket\" data-meta=\"0_613\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5153831\" id=\"5153831\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_611\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Anomie\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_236\"\u003eAnomie\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5153831\" data-sigil=\"has-tooltip\" data-meta=\"0_610\"\u003e\u003cspan class=\"screen-only\"\u003eMay 2 2019, 6:01 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-02 18:01:43 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_608\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_609\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_237\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5151430\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_84\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5151430\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_95\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eRe: pagination, for Wikimedia scale it tends to be surprisingly involved; we definitely want to avoid doing it separately for API and web. The current Pager classes mix DB and presentation logic, and tend to use the horrible pattern of being constructed around a SpecialPage object, but the DB aspect of those classes is probably something we want to keep in the long term, at which point all an API pagination helper can do is read\/write continuation data in a standard format, like ApiBase does. Something like that will probably be useful for REST APIs as well.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eI just posted a brief criticism of Pager yesterday at \u003ca href=\"\/T221449#5150357\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_85\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221449#5150357\u003c\/span\u003e\u003c\/a\u003e.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5151446\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_86\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5151446\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_96\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eMiddleware means decorator pattern, basically. It can munge the request before the handler sees it, munge the response after the handler has returned it, or choose to handle the request directly and not invoke the handler at all. (Also, the handler can be another middleware - it's an onion structure.)\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eSo a "decorator pattern" is basically a proxy object.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eIt's a bit less random than the sea of hooks we tend to use, and easier understood by anyone familiar with the paradigm, IMO we should go for it. (In the very long term, it would be nice to use it in MediaWiki in general, too.)\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eHooks let you change things at specific points inside a code flow, while a proxy only lets you affect things before\/after an external call or replace the call entirely with little opportunity for logic sharing. While I could see some potential use cases for proxying (e.g. CachedBagOStuff), I'm skeptical that it could generally replace hooks.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5151601\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_87\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5151601\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_97\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eSeveral REST libraries have "reverse routing", i.e. serialization of parameters into a URL. That requires named routes, or some other way to refer to a route, since you don't want to have to provide the full template in order to serialize parameters.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eWhy don't you want to?\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eDo we need that?\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eWhat's the use case?\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5151701\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_88\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5151701\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_98\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eThe handler class:\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003e[...]\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eThere's always one handler class per route, and the interface is similar to the one proposed by \u003ca href=\"\/p\/EvanProdromou\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_99\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@EvanProdromou\u003c\/span\u003e\u003c\/a\u003e. We have one handler class per route, allowing every route to dynamically override its configuration. The Handler takes the configuration array from extension.json as a constructor parameter, and uses it to provide a default implementation of parameter validation. I agree with splitting ApiBase::getConditionalRequestData() into getLastModified() and getETag(). Evan's $templateParams is subsumed into the Request object. Streaming is supported by adding the Response parameter. Evan proposed having the Request as a constructor parameter to the Handler, but sending it in via execute() instead has a few benefits. It makes it possible to construct Handler objects without having an active request, which I think will be useful.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eLooks sensible to me.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eWithout loss of flexibility, I also introduce the subclass SimpleHandler, which implements its own execute(), copying the Request and Response objects to object properties to provide getRequest() and getResponse(). It uses the template parameters as formal parameters to the overridden method run(). It interprets the return value from run() and writes it to the Response. Thus:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"k\"\u003eclass\u003c\/span\u003e \u003cspan class=\"no\"\u003eHelloHandler\u003c\/span\u003e \u003cspan class=\"k\"\u003eextends\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"SimpleHandler\"\u003eSimpleHandler\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003erun\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$name\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"Hello $name!"\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eDoes PHP not complain that you're redefining \u003ctt class=\"remarkup-monospaced\"\u003erun()\u003c\/tt\u003e with an incompatible parameter list? Other than that, it seems sensible enough with the caveat that trying to recursively call \u003ctt class=\"remarkup-monospaced\"\u003eexecute()\u003c\/tt\u003e might wind up breaking (I have no idea for a use case that would require that, so I wouldn't worry about it).\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eFinally, I wanted extensions to be able to implement their own handler factory. This allows handlers to be implemented as anonymous classes. We could have\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eThe current push towards DI everywhere will require that too, so all the dependencies can be injected into the Handler instance.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eIf we need named routes anyway, perhaps this could be abbreviated to\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"json\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"s\"\u003e"RESTRoutes"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"o\"\u003e[\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"method"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"GET"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"path"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"\/user\/{name}\/hello"\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"name"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"hello"\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n \u003cspan class=\"s\"\u003e"RESTFactoryClass"\u003c\/span\u003e\u003cspan class=\"o\"\u003e:\u003c\/span\u003e \u003cspan class=\"s\"\u003e"MyExtension"\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e]\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eThat's not valid JSON. Also I'd personally prefer the more flexible version above rather than requiring exactly one factory class per extension.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eIt would almost be possible to override execute() with a closure, but I think it's not so nice:\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eI agree, not so nice. Although you could use an anonymous class to override \u003ctt class=\"remarkup-monospaced\"\u003eexecute()\u003c\/tt\u003e easily enough, just like in the earlier anonymous class example.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5152090\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_89\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5152090\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_100\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eI'd prefer something more DI-friendly. Handlers (or handler factories) should be constructed by MediaWikiServices or a similar container, otherwise they can't receive dependencies on instantiation. This is a generic problem (action API modules have it, special pages have it, hook handlers have it, Action classes would probably have it if the internals involved in \u003ctt class=\"remarkup-monospaced\"\u003eindex.php?action=\u003c\/tt\u003e would be less messy and followed a proper controller pattern), so it would be nice to come up with a generic solution that can be used elsewhere.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eThe generic solution is, IMO, to point out that the static method pointed to as the value of "factory" is wiring so it's allowed to use MediaWikiServices as a service locator. It's just wiring done by supplying the PHP code via reference to a named static method declared elsewhere rather than using a closure. (I \u003cem\u003ethink\u003c\/em\u003e \u003ca href=\"\/p\/daniel\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_101\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@daniel\u003c\/span\u003e\u003c\/a\u003e is saying the same thing in \u003ca href=\"\/T221177#5152853\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_90\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5152853\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e)\u003c\/p\u003e\n\n\u003cp\u003eBeyond that, if you want \u003ca href=\"\/T171515\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_91\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT171515\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e it's probably better to do that there.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eand then decide based on what interface the returned object implements whether it's a handler or a factory.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eUgh.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eParameter types should be expressed in a declarative way, as that's needed for good sandbox UX and good autogenerated help pages. That could in the route like `"\/user\/{name:user}\/foo", or could be some method on the handler that returns a descriptor array; the second is probably a better way as we might want to add more information than just the type (min\/max, enum values etc. etc.)\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eThat doesn't need to be done in the routing template, though. It could as well be a method on the Handler, similar to the Action API's \u003ctt class=\"remarkup-monospaced\"\u003egetAllowedParams()\u003c\/tt\u003e.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5152457\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_93\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5152457\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/TheDJ\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_103\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@TheDJ\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5150416\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_92\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5150416\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_102\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eSo they're typed, but typed via regex rather than predefined keywords. I doubt we'd go that route since it would make registration-time detection of routing collisions difficult.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eNo, they are not typed, [...]\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eWhether you define it as \u003ctt class=\"remarkup-monospaced\"\u003eint\u003c\/tt\u003e or \u003ctt class=\"remarkup-monospaced\"\u003e\/^[+-]?\\d+$\/\u003c\/tt\u003e, it's still restricting the allowed values of the parameter to conform to a limited set of valid input.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eNot just for that, groups can also have middleware for instance prefix(\/admin) group with middleware(admin authentication) is a very powerful and readable structure, which avoids making mistakes like accidentally adding a route with no admin authentication underneath a \/admin path.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eOTOH, it also means that it may not be obvious that \/admin\/foo\/bar\/baz is inheriting some middleware defined on \/admin.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eBut I think that it's mixing response and handler for instance. lastmodified as mentioned in this proposal, is a response property and thus does not belong in a standard Handler class.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eThe problem is that you probably don't want to construct the whole response before being able to check whether If-Modified-Since or If-None-Match applies, so blindly declaring it "a response property" doesn't really work that well. The actual setting of the Last-Modified or ETag headers in the response is fairly trivial.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5152853\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_94\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5152853\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/daniel\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_104\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@daniel\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eAlternatively, we could do what we have done with SlotRoleRegistry: define a registry service, (let's say, RESTRouteRegistry), and have extensions register instantiator callback with the registry.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eThat seems somewhat orthogonal to me, actually. It doesn't matter too much whether the extension has data in extension.json that results in MediaWiki calling a \u003ctt class=\"remarkup-monospaced\"\u003eregister()\u003c\/tt\u003e method on the registry versus the extension using addServiceManipulator() or some other hook to call \u003ctt class=\"remarkup-monospaced\"\u003eregister()\u003c\/tt\u003e directly. Conversely, it also doesn't matter too much whether the data from extension.json is passed to a formal registry service fetched from MediaWikiServices or if the registry is internal to some larger service.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_625\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/v5wmnfiaelrd6nztxd3a\/PHID-FILE-s57znldbv3qchtewuryd\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/daniel\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-badges\"\u003e\u003cul class=\"phui-badge-flex-view grouped flex-view-collapsed \"\u003e\u003cli class=\"phui-badge-flex-item\"\u003e\u003ca class=\"phui-badge-mini phui-badge-mini-indigo \" href=\"\/badges\/view\/14\/\" data-sigil=\"has-tooltip\" data-meta=\"0_623\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-empire\" data-meta=\"0_624\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5154488\" id=\"5154488\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_622\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/daniel\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_238\"\u003edaniel\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5154488\" data-sigil=\"has-tooltip\" data-meta=\"0_621\"\u003e\u003cspan class=\"screen-only\"\u003eMay 2 2019, 9:28 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-02 21:28:10 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_619\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_620\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_239\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5153831\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_106\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5153831\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_109\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eThe generic solution is, IMO, to point out that the static method pointed to as the value of "factory" is wiring so it's allowed to use MediaWikiServices as a service locator. It's just wiring done by supplying the PHP code via reference to a named static method declared elsewhere rather than using a closure. (I \u003cem\u003ethink\u003c\/em\u003e \u003ca href=\"\/p\/daniel\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_108\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@daniel\u003c\/span\u003e\u003c\/a\u003e is saying the same thing in \u003ca href=\"\/T221177#5152853\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_105\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5152853\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e)\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eIndeed.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5152853\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_107\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5152853\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/daniel\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_110\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@daniel\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eAlternatively, we could do what we have done with SlotRoleRegistry: define a registry service, (let's say, RESTRouteRegistry), and have extensions register instantiator callback with the registry.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eThat seems somewhat orthogonal to me, actually. It doesn't matter too much whether the extension has data in extension.json that results in MediaWiki calling a \u003ctt class=\"remarkup-monospaced\"\u003eregister()\u003c\/tt\u003e method on the registry versus the extension using addServiceManipulator() or some other hook to call \u003ctt class=\"remarkup-monospaced\"\u003eregister()\u003c\/tt\u003e directly.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eThe only difference is how lazy the initialization is. If these things are only needed for REST API routing, we should avoid instantiating them for requests that hit index.php (or api.php, for that matter).\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eConversely, it also doesn't matter too much whether the data from extension.json is passed to a formal registry service fetched from MediaWikiServices or if the registry is internal to some larger service.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eIndeed. The relevant question is how extensions inject instantiation logic into that registry. Do they call a method on a registry service (deferred via the service manipulator mechanism), or is a static method named as the instantiator in some configuration variable? Both approaches seem fine to me.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_634\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5154489\" id=\"5154489\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_633\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_240\"\u003etstarling\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5154489\" data-sigil=\"has-tooltip\" data-meta=\"0_632\"\u003e\u003cspan class=\"screen-only\"\u003eMay 2 2019, 9:28 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-02 21:28:18 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_630\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_631\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_241\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eOK, let's use ObjectFactory.\u003c\/p\u003e\n\n\u003cp\u003eWith ObjectFactory it doesn't really make sense to pass $config to the handler factory anymore. This had two purposes: to identify the route to the factory so that factories can be shared between routes, and to allow $config to be passed down to the constructor to help with implementing default behaviour. For the former we can use $spec['args'] instead. For the latter we can have Handler::setConfig(), called after the factory returns.\u003c\/p\u003e\n\n\u003cp\u003eBy the way, it would be possible to have Handler::setRequest() and Handler::setResponse() instead of passing these objects in separately to every entry point (execute, getLastModified, etc.). At least then the stateful nature of Handler would be consistent, instead of becoming more stateful in SimpleHandler. Handler::getRequest() would throw an exception if Handler::setRequest() has not been called, and similarly for getConfig() and getResponse().\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5153831\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_111\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5153831\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_112\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eDoes PHP not complain that you're redefining \u003ctt class=\"remarkup-monospaced\"\u003erun()\u003c\/tt\u003e with an incompatible parameter list?\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003erun() has to be undeclared in the parent class.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eOther than that, it seems sensible enough with the caveat that trying to recursively call execute() might wind up breaking (I have no idea for a use case that would require that, so I wouldn't worry about it).\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eThe effect would be more obvious with setRequest() and setResponse(), and an execute() that takes no parameters. In practice, subrequests would be done by calling the router, which would make a new Handler object.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_643\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5154635\" id=\"5154635\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_642\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_242\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5154635\" data-sigil=\"has-tooltip\" data-meta=\"0_641\"\u003e\u003cspan class=\"screen-only\"\u003eMay 2 2019, 10:34 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-02 22:34:12 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_639\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_640\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_243\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5152457\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_113\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5152457\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/TheDJ\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_120\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@TheDJ\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eNot just for that, groups can also have middleware for instance prefix(\/admin) group with middleware(admin authentication) is a very powerful and readable structure, which avoids making mistakes like accidentally adding a route with no admin authentication underneath a \/admin path.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eThat's a good use case (although one could complain about it not being very restful) but not one that MediaWiki has, and I struggle to think of others. In general I think most microframeworks are optimized for rapid development of simple websites, which means there are a bunch of assumptions like two significant user levels (normal and admin) or interfaces being organized around models with simple CRUD interactions, which do not hold for complex social software.\u003cbr \/\u003e\n(Granted we are planning to have an admin panel in MediaWiki, so maybe we'll have separate concept of sysadmin login at some point.)\u003cbr \/\u003e\nUsing grouping to avoid redefining the same handler factory on every route would be nice, though.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5152650\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_114\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5152650\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_121\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eThe reason it's not a response property is because it exists only for Is-Modified-Since handling. It's an alternative entry point. If the resource has not been modified since the date specified in the header, a 304 response is sent and execute() is not called.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eThat's the kind of thing you'd use a middleware for.\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"k\"\u003eclass\u003c\/span\u003e \u003cspan class=\"no\"\u003eCachingMiddleware\u003c\/span\u003e \u003cspan class=\"k\"\u003eextends\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"Middleware\"\u003eMiddleware\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003egetLastModified\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e \u003cspan class=\"cm\"\u003e\/* override me *\/\u003c\/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n \u003cspan class=\"k\"\u003eabstract\u003c\/span\u003e \u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003egetETag\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e \u003cspan class=\"cm\"\u003e\/* override me *\/\u003c\/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n \u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003eprocess\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"no\"\u003eHandler\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$handler\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"nv\"\u003e$eTag\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getETag\"\u003egetETag\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$eTag\u003c\/span\u003e \u003cspan class=\"o\"\u003e&&\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$eTag\u003c\/span\u003e \u003cspan class=\"o\"\u003e===\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getHeader\"\u003egetHeader\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'ETag'\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"k\"\u003enew\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"NotModifiedResponse\"\u003eNotModifiedResponse\u003c\/span\u003e\u003cspan class=\"o\"\u003e();\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\n \u003cspan class=\"nv\"\u003e$lastModified\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getLastModified\"\u003egetLastModified\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$lastModified\u003c\/span\u003e \u003cspan class=\"o\"\u003e&&\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"compareDates\"\u003ecompareDates\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$lastModified\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getHeader\"\u003egetHeader\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'If-Modified-Since'\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e<=\u003c\/span\u003e \u003cspan class=\"mi\"\u003e0\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"k\"\u003enew\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"NotModifiedResponse\"\u003eNotModifiedResponse\u003c\/span\u003e\u003cspan class=\"o\"\u003e();\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\n \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$handler\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"process\"\u003eprocess\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eKeeps the framework simple, keeps handlers flexible in case they need to do something strange (e.g. do the date \/ etag check in some unusual way).\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5152853\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_115\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5152853\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/daniel\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_122\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@daniel\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eIt should be noted that ObjectFactory acts as a "static entry point" in this context, and will have to rely on global state here (by accessing MediaWikiServices::getInstance), unless it itself becomes a service (which sounds nice, but I vaguely recall issues with the order of initialization when bootstrapping MediaWikiServices).\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eNot necessarily, I'd imagine something like this in \u003ctt class=\"remarkup-monospaced\"\u003erest.php\u003c\/tt\u003e:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"c\"\u003e\/\/ WebStart etc.\u003c\/span\u003e\n\u003cspan class=\"nv\"\u003e$services\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"MediaWikiServices\"\u003eMediaWikiServices\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"MediaWikiServices\" data-symbol-name=\"newFromWiring\"\u003enewFromWiring\u003c\/span\u003e\u003cspan class=\"o\"\u003e();\u003c\/span\u003e\n\u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$services\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getRequestFactory\"\u003egetRequestFactory\u003c\/span\u003e\u003cspan class=\"o\"\u003e()->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"makeRequest\"\u003emakeRequest\u003c\/span\u003e\u003cspan class=\"o\"\u003e();\u003c\/span\u003e\n\u003cspan class=\"nv\"\u003e$router\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$services\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getRouter\"\u003egetRouter\u003c\/span\u003e\u003cspan class=\"o\"\u003e();\u003c\/span\u003e\n\u003cspan class=\"nv\"\u003e$routeConfig\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$router\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getRouteConfig\"\u003egetRouteConfig\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003cspan class=\"nv\"\u003e$handler\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"ObjectFactory\"\u003eObjectFactory\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"ObjectFactory\" data-symbol-name=\"getObjectFromSpec\"\u003egetObjectFromSpec\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$routeConfig\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$services\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003cspan class=\"nv\"\u003e$router\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"handle\"\u003ehandle\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$handler\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003ePassing the service container to a static utility method which has the purpose of being an object creation helper is fine, but it's nice to avoid that object accessing it directly, so that we can eventually make MediaWikiServices not a singleton, to further discourage people from accessing it at random points in their application. But I guess I'm bikeshedding.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eBut really, this does not provide a huge advantage over something like\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"text\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e"factory": "MyExtension::hello"\u003c\/pre\u003e\u003c\/div\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eBesides eventually getting rid of the singleton access to the service locator, I think it's nice to have all service access logic in the entry points + wiring files - makes it easier to understand the dependency graph.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eConsidering that the extension would need a (static method) hook handler to put MyExtensionHandlerFactory into place in MediaWikiServices\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eThis does not seem relevant to how route handler dependency injection should be done, but FWIW the extension would use a wiring file and list it under \u003ctt class=\"remarkup-monospaced\"\u003eServiceWiringFiles\u003c\/tt\u003e in \u003ctt class=\"remarkup-monospaced\"\u003eextension.json\u003c\/tt\u003e, so no need for a hook.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eAlternatively, we could do what we have done with SlotRoleRegistry: define a registry service, (let's say, RESTRouteRegistry), and have extensions register instantiator callback with the registry. Extensions can then use MediaWikiServices::addServiceManipulator() to attach code that will register their handler. In the route declaration, the handler would be referred to by name (either the route name, "myextension-hello" in the example, or a separate handler name, if we want to be able to use the same handler instance for multiple routes).\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eThat's a lot of stuff happening when we might not even be in an API request. I thought we were trying to move towards static configuration to avoid that kind of thing.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5153831\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_117\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5153831\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_123\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eI just posted a brief criticism of Pager yesterday at \u003ca href=\"\/T221449#5150357\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_116\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221449#5150357\u003c\/span\u003e\u003c\/a\u003e.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eThat's a criticism of the implementation rather than the use case. The Pager could certainly use improvement; but we still want to have some kind of pager class doing DB-level continuation, shared by web UI, action API and REST API, right?\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5151446\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_118\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5151446\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_124\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eHooks let you change things at specific points inside a code flow, while a proxy only lets you affect things before\/after an external call or replace the call entirely with little opportunity for logic sharing. While I could see some potential use cases for proxying (e.g. CachedBagOStuff), I'm skeptical that it could generally replace hooks.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eIt certainly couldn't; I just meant for this specific use case (doing things before \/ after \/ instead of calling the API handler).\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eWhat's the use case [for reverse routing]?\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eLook at something like \u003ca href=\"https:\/\/en.wikipedia.org\/api\/rest_v1\/page\/summary\/Representational_state_transfer\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ehttps:\/\/en.wikipedia.org\/api\/rest_v1\/page\/summary\/Representational_state_transfer\u003c\/a\u003e , it has an entire API URL section for generating links to other APIs. I'm not convinced how useful that pattern is, but it is there because app developers have asked for it, so the use case does exist.\u003c\/p\u003e\n\n\u003cp\u003e(That said I think the main reason libraries tend to have this is that they don't differentiate sharply between API and web; web pages are just API requests served in a HTML format, and then you can use reverse routing for link generation.)\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eBeyond that, if you want \u003ca href=\"\/T171515\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_119\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT171515\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e it's probably better to do that there.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eI want to standardize how object creation is specified in static declarations; currently we have one for LoggerFactory, another for ObjectCache, a third for the action API, a fourth one for AuthManager, a fifth one for hooks, and are planning a sixth here (and will need probably 2-3 more in the future), all with similar but not quite identical syntax. It's poor DX, and ObjectFactory is dedicated for this purpose so it seems to be the logical thing to standardize on.\u003cbr \/\u003e\nSwitching over hooks to use it is another discussion.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eThat doesn't need to be done in the routing template, though. It could as well be a method on the Handler, similar to the Action API's \u003ctt class=\"remarkup-monospaced\"\u003egetAllowedParams()\u003c\/tt\u003e.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eIt could even happen in an object separate from the handler, if we want to be able to generate docs \/ sandboxes without having to initialize all the services that the handlers need to actually run (probably a good idea if we want something similar to \u003ctt class=\"remarkup-monospaced\"\u003eaction=help&recursivesubmodules=1\u003c\/tt\u003e). As long as we make sure it happens *somewhere*.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_652\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5154753\" id=\"5154753\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_651\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_244\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5154753\" data-sigil=\"has-tooltip\" data-meta=\"0_650\"\u003e\u003cspan class=\"screen-only\"\u003eMay 2 2019, 11:36 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-02 23:36:47 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_648\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_649\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_245\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5154635\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_125\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5154635\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_127\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eI want to standardize how object creation is specified in static declarations; currently we have one for LoggerFactory, another for ObjectCache, a third for the action API, a fourth one for AuthManager, a fifth one for hooks, and are planning a sixth here (and will need probably 2-3 more in the future), all with similar but not quite identical syntax. It's poor DX, and ObjectFactory is dedicated for this purpose so it seems to be the logical thing to standardize on.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eCreated a separate task for this thread: \u003ca href=\"\/T222409\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_126\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT222409: Standardize declarative object construction in MediaWiki\u003c\/span\u003e\u003c\/a\u003e.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_661\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5154940\" id=\"5154940\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_660\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_246\"\u003etstarling\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5154940\" data-sigil=\"has-tooltip\" data-meta=\"0_659\"\u003e\u003cspan class=\"screen-only\"\u003eMay 3 2019, 3:29 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-03 03:29:12 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_657\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_658\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_247\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5154635\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_128\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5154635\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_129\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eThat's the kind of thing you'd use a middleware for.\u003cbr \/\u003e\n...\u003cbr \/\u003e\nKeeps the framework simple, keeps handlers flexible in case they need to do something strange (e.g. do the date \/ etag check in some unusual way).\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eI don't think a simple framework is a goal. We want simple handlers, the framework can be as complex as it needs to be to support that simplicity. We especially want cacheability to be easy to achieve in a handler since that is supposed to be a primary goal of introducing the REST API. I don't think splitting every class that implements IMS logic into two classes makes anything easier. The flexibility of handlers is the same either way. If getLastModified() is not overridden, a 304 will not be sent before execute(), but execute() is still free to send one if it wishes.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_670\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5154992\" id=\"5154992\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_669\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_248\"\u003etstarling\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5154992\" data-sigil=\"has-tooltip\" data-meta=\"0_668\"\u003e\u003cspan class=\"screen-only\"\u003eMay 3 2019, 5:33 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-03 05:33:55 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_666\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_667\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_249\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eI've been reading PSR-7. We already have Guzzle's PSR-7 library as a core dependency, so we could use GuzzleHttp\\Psr7\\ServerRequest as the parent class of our Rest\\Request. \u003ca href=\"https:\/\/github.com\/guzzle\/psr7\/blob\/b33127dd48a6d48e29a3561d4f5d11809936d12a\/src\/ServerRequest.php#L168\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eServerRequest::fromGlobals()\u003c\/a\u003e is wrong for us, but we could have an implementation that gets data from WebRequest. There is ServerRequest::getAttributes() which, PSR-7 strongly suggests, is supposed to have template variables from the path, so that would replace my Request::getParams().\u003c\/p\u003e\n\n\u003cp\u003ePSR-15's idea of responses and streaming is awkward. The return value of a handler is a ResponseInterface. PSR-15 says:\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eIt is RECOMMENDED that any middleware or request handler that generates a response will either compose a prototype of a PSR-7 ResponseInterface or a factory capable of generating a ResponseInterface instance in order to prevent dependence on a specific HTTP message implementation.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eI'm not exactly sure what that means. I hope it means that in the handler you will be able to call $this->createResponse() and the base class will provide one for you, because that would kind of work. An instance of ResponseInterface is immutable, which is inefficient and a nuisance, it requires the whole object to be cloned every time you set a header (see \u003ca href=\"https:\/\/github.com\/guzzle\/psr7\/blob\/b33127dd48a6d48e29a3561d4f5d11809936d12a\/src\/MessageTrait.php#L67\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eGuzzle's withHeader()\u003c\/a\u003e). At least ResponseInterface::getBody() returns a stream which you can write to without counting as mutation. We could always wrap the execute() function:\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"k\"\u003eclass\u003c\/span\u003e \u003cspan class=\"no\"\u003eHandler\u003c\/span\u003e \u003cspan class=\"k\"\u003eimplements\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"RequestHandlerInterface\"\u003eRequestHandlerInterface\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003ecreateImmutableResponse\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eMutableResponse\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"k\"\u003enew\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"Response\"\u003eResponse\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\n\t\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getStatusCode\"\u003egetStatusCode\u003c\/span\u003e\u003cspan class=\"o\"\u003e(),\u003c\/span\u003e\n\t\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getHeaders\"\u003egetHeaders\u003c\/span\u003e\u003cspan class=\"o\"\u003e(),\u003c\/span\u003e\n\t\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getBody\"\u003egetBody\u003c\/span\u003e\u003cspan class=\"o\"\u003e(),\u003c\/span\u003e\n\t\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getProtocolVersion\"\u003egetProtocolVersion\u003c\/span\u003e\u003cspan class=\"o\"\u003e(),\u003c\/span\u003e\n\t\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getReasonPhrase\"\u003egetReasonPhrase\u003c\/span\u003e\u003cspan class=\"o\"\u003e()\u003c\/span\u003e\n\t\t\u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\t\n\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003eprocess\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eServerRequestInterface\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e):\u003c\/span\u003e \u003cspan class=\"no\"\u003eResponseInterface\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"execute\"\u003eexecute\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"createImmutableResponse\"\u003ecreateImmutableResponse\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\n\u003cspan class=\"k\"\u003eclass\u003c\/span\u003e \u003cspan class=\"no\"\u003eHelloHandler\u003c\/span\u003e \u003cspan class=\"k\"\u003eextends\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"Handler\"\u003eHandler\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003eexecute\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eServerRequestInterface\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e):\u003c\/span\u003e \u003cspan class=\"no\"\u003eMutableResponse\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"createResponse\"\u003ecreateResponse\u003c\/span\u003e\u003cspan class=\"o\"\u003e();\u003c\/span\u003e\n\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"setHeader\"\u003esetHeader\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'name1'\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'value1'\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"setHeader\"\u003esetHeader\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'name2'\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'value2'\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getBody\"\u003egetBody\u003c\/span\u003e\u003cspan class=\"o\"\u003e()->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"write\"\u003ewrite\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s2\"\u003e"Hello "\u003c\/span\u003e \u003cspan class=\"o\"\u003e.\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getAttribute\"\u003egetAttribute\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'user'\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e.\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'!'\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e\n\t\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\n\n\u003cp\u003eIn this example, Response is the Guzzle class and MutableResponse is exactly the same except with setHeader().\u003c\/p\u003e\n\n\u003cp\u003eOf course, it's technically possible to add setHeaders() to a class that implements ResponseInterface. It just violates a MUST clause of PSR-7.\u003c\/p\u003e\n\n\u003cp\u003eSummary: meh.\u003c\/p\u003e\n\n\u003cp\u003eAdvantages:\u003c\/p\u003e\n\n\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eGuzzle provides some nice convenience functions.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eWe get to say we are following standards.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eMaybe something needs PSR-7... not sure what.\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003cp\u003eDisadvantages:\u003c\/p\u003e\n\n\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eYou end up with stupid problems like the inability to change a single line of code in your response class.\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003cp\u003eAlternatives:\u003c\/p\u003e\n\n\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eFork PSR-7 and liberally violate it.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eHave our own request and response classes. Provide a PSR-15 handler adaptor so that PSR-15 handlers can be called, assuming something actually needs that.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_681\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/ms665yc73j6nadjitytk\/PHID-FILE-mocohumlrcbe2lcuplml\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Anomie\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-badges\"\u003e\u003cul class=\"phui-badge-flex-view grouped flex-view-collapsed \"\u003e\u003cli class=\"phui-badge-flex-item\"\u003e\u003ca class=\"phui-badge-mini phui-badge-mini-orange \" href=\"\/badges\/view\/5\/\" data-sigil=\"has-tooltip\" data-meta=\"0_679\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-rocket\" data-meta=\"0_680\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5155921\" id=\"5155921\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_678\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Anomie\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_250\"\u003eAnomie\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5155921\" data-sigil=\"has-tooltip\" data-meta=\"0_677\"\u003e\u003cspan class=\"screen-only\"\u003eMay 3 2019, 2:40 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-03 14:40:43 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_675\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_676\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_251\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5154489\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_130\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5154489\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_134\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eBy the way, it would be possible to have Handler::setRequest() and Handler::setResponse() instead of passing these objects in separately to every entry point (execute, getLastModified, etc.). At least then the stateful nature of Handler would be consistent, instead of becoming more stateful in SimpleHandler. Handler::getRequest() would throw an exception if Handler::setRequest() has not been called, and similarly for getConfig() and getResponse().\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eI like that. Although, if you are always going to want to set the request and response in pairs, you might do \u003ctt class=\"remarkup-monospaced\"\u003esetRequestAndResponse()\u003c\/tt\u003e instead. If we want to get fancier, we could also have it throw if called again when a request\/response are already set, and maybe use a ScopedCallback to clear the request and response automatically.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5154635\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_131\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5154635\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_135\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eThat's the kind of thing you'd use a middleware for.\u003c\/p\u003e\n\n\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"k\"\u003eclass\u003c\/span\u003e \u003cspan class=\"no\"\u003eCachingMiddleware\u003c\/span\u003e \u003cspan class=\"k\"\u003eextends\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"Middleware\"\u003eMiddleware\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003egetLastModified\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e \u003cspan class=\"cm\"\u003e\/* override me *\/\u003c\/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n \u003cspan class=\"k\"\u003eabstract\u003c\/span\u003e \u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003egetETag\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e \u003cspan class=\"cm\"\u003e\/* override me *\/\u003c\/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c\/span\u003e\u003cspan class=\"o\"\u003e;\u003c\/span\u003e \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n \u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003eprocess\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eRequest\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"no\"\u003eHandler\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$handler\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"nv\"\u003e$eTag\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getETag\"\u003egetETag\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$eTag\u003c\/span\u003e \u003cspan class=\"o\"\u003e&&\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$eTag\u003c\/span\u003e \u003cspan class=\"o\"\u003e===\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getHeader\"\u003egetHeader\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'ETag'\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"k\"\u003enew\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"NotModifiedResponse\"\u003eNotModifiedResponse\u003c\/span\u003e\u003cspan class=\"o\"\u003e();\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\n \u003cspan class=\"nv\"\u003e$lastModified\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getLastModified\"\u003egetLastModified\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"k\"\u003eif\u003c\/span\u003e \u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$lastModified\u003c\/span\u003e \u003cspan class=\"o\"\u003e&&\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$this\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"compareDates\"\u003ecompareDates\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$lastModified\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getHeader\"\u003egetHeader\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"s1\"\u003e'If-Modified-Since'\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e<=\u003c\/span\u003e \u003cspan class=\"mi\"\u003e0\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"k\"\u003enew\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"NotModifiedResponse\"\u003eNotModifiedResponse\u003c\/span\u003e\u003cspan class=\"o\"\u003e();\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\n\n \u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$handler\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"process\"\u003eprocess\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n \u003cspan class=\"o\"\u003e}\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eThat seems unlikely to actually work very well. How would this middleware generically know what to have inside getLastModified() and getETag()? You'd likely have to override it for each Handler, at which point it there's little point in having the middleware versus putting the minimal logic in the Router itself.\u003c\/p\u003e\n\n\u003cp\u003eNote that it can't necessarily look up a cached Response, unless you have code elsewhere to invalidate that cache on every change. And in that case you may as well also throw in returning the cached Response instead of calling \u003ctt class=\"remarkup-monospaced\"\u003e$handler->execute()\u003c\/tt\u003e if the cached Response exists but doesn't match the conditional request headers.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"nv\"\u003e$router\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$services\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getRouter\"\u003egetRouter\u003c\/span\u003e\u003cspan class=\"o\"\u003e();\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eWhen we have a "service" that is actually the implementation of an entry point, having it as a formal service seems like it would encourage callers to write code using the entry point rather than using the business logic classes that back the entry point. For the action API, at least, I've long considered that a code smell.[1] Would it be better to have the wiring of the entry point "service" be done in the entry point file itself instead?\u003c\/p\u003e\n\n\u003cp\u003e[1]: The fact that "business logic classes" don't actually exist for a lot of old code is \u003ca href=\"\/T156872\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_132\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT156872\u003c\/span\u003e\u003c\/a\u003e, BTW.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"nv\"\u003e$routeConfig\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$router\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getRouteConfig\"\u003egetRouteConfig\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003cspan class=\"nv\"\u003e$handler\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"ObjectFactory\"\u003eObjectFactory\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"ObjectFactory\" data-symbol-name=\"getObjectFromSpec\"\u003egetObjectFromSpec\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$routeConfig\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$services\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003cspan class=\"nv\"\u003e$router\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"handle\"\u003ehandle\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$handler\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eDoes doing DI really require doing roundabout code like this? There doesn't seem to be any advantage in having to fetch the data for constructing the Handler from $router only to immediately pass that Handler back in.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eThat's a criticism of the implementation rather than the use case. The Pager could certainly use improvement; but we still want to have some kind of pager class doing DB-level continuation, shared by web UI, action API and REST API, right?\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eMy point was that Pager itself is ill-suited to that purpose.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eIt could even happen in an object separate from the handler, if we want to be able to generate docs \/ sandboxes without having to initialize all the services that the handlers need to actually run (probably a good idea if we want something similar to \u003ctt class=\"remarkup-monospaced\"\u003eaction=help&recursivesubmodules=1\u003c\/tt\u003e). As long as we make sure it happens *somewhere*.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eThat's a drawback to DI that I hadn't thought of before, thanks for pointing that out. Although I suppose the DI answer would be the FactoryFactoryFactory pattern,[2] i.e. add more layers of factories until you get one that's lightweight enough.\u003c\/p\u003e\n\n\u003cp\u003e[2]: Yes, that's hyperbole.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5154992\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_133\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5154992\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_136\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eAn instance of ResponseInterface is immutable, which is inefficient and a nuisance,\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003e+1.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\t\u003cspan class=\"k\"\u003epublic\u003c\/span\u003e \u003cspan class=\"k\"\u003efunction\u003c\/span\u003e \u003cspan class=\"no\"\u003ecreateImmutableResponse\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"no\"\u003eMutableResponse\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e \u003cspan class=\"o\"\u003e)\u003c\/span\u003e \u003cspan class=\"o\"\u003e{\u003c\/span\u003e\n\t\t\u003cspan class=\"k\"\u003ereturn\u003c\/span\u003e \u003cspan class=\"k\"\u003enew\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"Response\"\u003eResponse\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e\n\t\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getStatusCode\"\u003egetStatusCode\u003c\/span\u003e\u003cspan class=\"o\"\u003e(),\u003c\/span\u003e\n\t\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getHeaders\"\u003egetHeaders\u003c\/span\u003e\u003cspan class=\"o\"\u003e(),\u003c\/span\u003e\n\t\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getBody\"\u003egetBody\u003c\/span\u003e\u003cspan class=\"o\"\u003e(),\u003c\/span\u003e\n\t\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getProtocolVersion\"\u003egetProtocolVersion\u003c\/span\u003e\u003cspan class=\"o\"\u003e(),\u003c\/span\u003e\n\t\t\t\u003cspan class=\"nv\"\u003e$response\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getReasonPhrase\"\u003egetReasonPhrase\u003c\/span\u003e\u003cspan class=\"o\"\u003e()\u003c\/span\u003e\n\t\t\u003cspan class=\"o\"\u003e);\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eI'd just make that a method on MutableResponse.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eOf course, it's technically possible to add setHeaders() to a class that implements ResponseInterface. It just violates a MUST clause of PSR-7.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eIt's not clear to me whether the "all methods that might change state MUST be implemented such that they retain the internal state of the current message and return an instance that contains the changed state" applies just to the methods defined in the interface (e.g. \u003ctt class=\"remarkup-monospaced\"\u003ewithHeader()\u003c\/tt\u003e), or to the class as a whole. They take pains to reiterate that on all the "withFoo" methods.\u003c\/p\u003e\n\n\u003cp\u003eEven if the latter, we could still have an explicitly non-compliant MutableResponse that's compliant if only the ResponseInterface methods are used, with any caveats documented. Or just have easy to use methods for converting between immutable ResponseInterface and a MutableResponse that implements the interface in everything but instanceof.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_690\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5158558\" id=\"5158558\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_689\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_252\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5158558\" data-sigil=\"has-tooltip\" data-meta=\"0_688\"\u003e\u003cspan class=\"screen-only\"\u003eMay 6 2019, 2:36 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-06 02:36:58 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_686\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_687\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_253\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eYeah, OK, putting the caching methods in the handler makes more sense, fair points on that. The antipattern I'd like to avoid is adding all kinds of functionality to the handler interface overtime so it ends up as a kitchen sink like ApiBase did and no one has any idea what methods to use + mocking it for tests becomes a nightmare. (Granted, mocking is more of an issue for helper methods than for entry points.)\u003c\/p\u003e\n\n\u003cp\u003eI don't really have an opinion on get\/setRequest vs. functional style (the latter is maybe a little easier to test), but I don't see much point in trying to do both at the same time.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5155921\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_137\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5155921\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_139\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eNote that it can't necessarily look up a cached Response, unless you have code elsewhere to invalidate that cache on every change.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eGood point - is that something we want to support in the framework? That would presumably involve some kind of cache tagging interface on the response, which can be used for tag-based invalidation (e.g. \u003ctt class=\"remarkup-monospaced\"\u003e$response->addCacheTag( 'page', $pageId )\u003c\/tt\u003e and then the MediaWiki code that gets invoked for page save can use some kind of cache invalidation service to purge all the cache entries labeled with a certain page ID). We had low-priority plans like that for the action API (\u003ca href=\"\/T122867\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_138\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT122867\u003c\/span\u003e\u003c\/a\u003e - not sure where the switch from Varnish to ATS leaves those plans) but caching was never a pressing concern there. RESTBase uses the \u003ca href=\"https:\/\/www.mediawiki.org\/wiki\/Change_propagation\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003echange propagation\u003c\/a\u003e service which has basically \u003ca href=\"https:\/\/github.com\/wikimedia\/change-propagation\/blob\/master\/config.example.wikimedia.yaml\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003ehandwritten rules\u003c\/a\u003e on what to invalidate when - flexible but hard to follow and probably does not scale well if you have lots of APIs \/ business object types.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eWhen we have a "service" that is actually the implementation of an entry point, having it as a formal service seems like it would encourage callers to write code using the entry point rather than using the business logic classes that back the entry point.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eWe probably want very little code in the entry point though (can't test it etc), and a sufficiently motivated developer can always invoke code that's in a class (it's done with the action API, despite not being available as a service). If that's a concern (and I agree about the code smell, but you can always just rely on people not doing things \/ not being allowed to do things that are against best practices or architecture principles; granted the same could be said for DI), I suppose the entry point could define some constant and the router could refuse to work without it?\u003c\/p\u003e\n\n\u003cp\u003eIf we end up defining the API classes\/factories\/whatever as services, the API could even have its own wiring file (why load it on requests which won't use it) so the router doesn't even have to be available on normal requests.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cblockquote\u003e\u003cdiv class=\"remarkup-code-block\" data-code-lang=\"php\" data-sigil=\"remarkup-code-block\"\u003e\u003cpre class=\"remarkup-code\"\u003e\u003cspan class=\"nv\"\u003e$routeConfig\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$router\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"getRouteConfig\"\u003egetRouteConfig\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003cspan class=\"nv\"\u003e$handler\u003c\/span\u003e \u003cspan class=\"o\"\u003e=\u003c\/span\u003e \u003cspan class=\"nc\" data-symbol-name=\"ObjectFactory\"\u003eObjectFactory\u003c\/span\u003e\u003cspan class=\"o\"\u003e::\u003c\/span\u003e\u003cspan class=\"nf\" data-symbol-context=\"ObjectFactory\" data-symbol-name=\"getObjectFromSpec\"\u003egetObjectFromSpec\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$routeConfig\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$services\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\n\u003cspan class=\"nv\"\u003e$router\u003c\/span\u003e\u003cspan class=\"o\"\u003e->\u003c\/span\u003e\u003cspan class=\"na\" data-symbol-name=\"handle\"\u003ehandle\u003c\/span\u003e\u003cspan class=\"o\"\u003e(\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$request\u003c\/span\u003e\u003cspan class=\"o\"\u003e,\u003c\/span\u003e \u003cspan class=\"nv\"\u003e$handler\u003c\/span\u003e \u003cspan class=\"o\"\u003e);\u003c\/span\u003e\u003c\/pre\u003e\u003c\/div\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eDoes doing DI really require doing roundabout code like this? There doesn't seem to be any advantage in having to fetch the data for constructing the Handler from $router only to immediately pass that Handler back in.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eI don't think there's anything roundabout it. Having methods which do narrow well-defined tasks makes the code much easier to test than large methods doing the entire processing from beginning to end.\u003c\/p\u003e\n\n\u003cp\u003eIn any case, no, it's not uncommon to inject the DI container into classes which are intended for initialization or top-level control (like the App class in many frameworks, or sometimes even the controllers).\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eThat's a drawback to DI that I hadn't thought of before, thanks for pointing that out. Although I suppose the DI answer would be the FactoryFactoryFactory pattern,[2] i.e. add more layers of factories until you get one that's lightweight enough.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eNot really, factory factories come with the abstract factory pattern: object A needs to create instances of object B, but it does not know exactly what kind of clas B is, just that it conforms to a given interface. So you inject a factory with the BFactory interface into A, and it could be a B1Factory that creates B1 objects, or a B2Factory that creates B2 objects. We don't really do this kind of thing in MediaWiki, we almost always have concrete factories which have internal logic for deciding what kind of class to instantiate. (ContentHandler arguably doubles as an abstract factory for Content and DifferenceEngine\/SlotDiffRenderer classes, and that's the only example I can think of.) There's less need for that kind of thing in applications and more in tools that need to work together with other tools they don't know much about, or open frameworks that can be used with arbitrary tools. (That's the kind of ecosystem the PHP-FIG is trying to create, except that they have a propensity for coming up with really bad interfaces.)\u003cbr \/\u003e\nI think a lot of the recent changes in MediaWiki architecture can be framed as trying to turn MediaWiki from an application into a framework. So maybe we'll see factory factories yet! Or \u003ca href=\"https:\/\/plus.google.com\/105201233571140699617\/posts\/1QhcnQizuPc\" class=\"remarkup-link\" target=\"_blank\" rel=\"noreferrer\"\u003eabstract singleton proxy factories\u003c\/a\u003e :)\u003c\/p\u003e\n\n\u003cp\u003eThe drawback to DI is that you end up with more smaller classes - if different parts of your class are invoked in entirely different contexts and require different dependencies, then they should be separate classes. Some would argue that is actually not a drawback :) In any case, it makes the learning curve, or at least the early part of the learning curve, steeper.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eEven if the latter, we could still have an explicitly non-compliant MutableResponse that's compliant if only the ResponseInterface methods are used, with any caveats documented. Or just have easy to use methods for converting between immutable ResponseInterface and a MutableResponse that implements the interface in everything but instanceof.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eNot sure it's worth the effort. If we don't actually follow PSR-7 (and we don't want any good reason to, and immutability seems a huge pain in the ass) all we need for interop is some factory\/transformer thing that takes a MediaWiki\\Rest\\Response and returns a PSR-7 ResponseInterface, and one that takes a Response and a ResponseInterface, and updates the first with the values from the second. I think the restrictions that come from that are:\u003c\/p\u003e\n\n\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003erequest and response bodies should be available through a streaming interface\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003eparsing the body should be done by the framework, or if it's done by the handler it should happen via a separate entry point, so that ServerRequestInterface::getParsedBody in middleware can be supported\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ethere should be some concept of request attributes that middleware can use to communicate\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003cp\u003eAll of those seem like good ideas anyway, regardless of interop.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_700\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5158969\" id=\"5158969\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_699\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_254\"\u003etstarling\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003eEdited\u003cspan class=\"visual-only\" aria-hidden=\"true\"\u003e \u00b7 \u003c\/span\u003e\u003ca href=\"#5158969\" data-sigil=\"has-tooltip\" data-meta=\"0_698\"\u003e\u003cspan class=\"screen-only\"\u003eMay 6 2019, 7:27 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-06 07:27:33 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_696\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_697\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_255\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eComparison of WebRequest and PSR-7 ServerRequestInterface:\u003c\/p\u003e\n\n\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 provides getHeader() and getHeaderLine(), which are approximately equivalent to WebRequest's getHeader() with and without the GETHEADER_LIST flag. But PSR-7 stores the array internally and implode()s on demand, whereas WebRequest stores the string internally and explodes()s on demand. WebRequest has routines for parsing particular headers, such as getAcceptLang(), which PSR-7 lacks.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 provides getServerParams(), which is a wrapper for $_SERVER. WebRequest has no equivalent. WebRequest provides access to a few elements of the array, but we also have a lot of code accessing $_SERVER directly. It seems very raw, but maybe it is a useful addition.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 has getProtocolVersion(), which WebRequest lacks. There are a few direct uses of $_SERVER['SERVER_PROTOCOL'] in our code base which suggest it may be a useful addition.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 provides getQueryParams(), essentially a wrapper for $_GET, which has no equivalent in WebRequest. WebRequest mangles the $_GET superglobal on startup and provides only a merged view of $_POST and $_GET.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 provides getUri(), which returns an object representing the URL. At least this interface avoids repetitive parsing of the raw URL. WebRequest has getFullRequestURL(), which can be parsed to get something similar.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 provides getRequestTarget(), which is the second part of the first line of the HTTP message. Theoretically it can be "*" for an OPTIONS request, although maybe that's just for clients since I'm not sure how that would be routed to PHP as a server request. It is usually a path, but the correct way to get the path is from the URI object. WebRequest has no such concept.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 and WebRequest both provide getMethod().\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 provides getCookieParams(), which is a relatively raw wrapper around $_COOKIE. WebRequest has getCookie(), which you might say is fully-baked.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 provides getUploadedFiles(), which is an array of objects representing $_FILES. We have the somewhat similar WebRequestUpload, which is also a wrapper around an individual file, although WebRequest does not allow iteration through files, you have to know the name.\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 provides getBody(), which is a streaming wrapper around php:\/\/input, and getParsedBody(). WebRequest only provides the non-streaming getRawInput().\u003c\/li\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 provides getAttributes(), which passes template variables down from the path router. WebRequest provides early execution of its path router, with results written into $_GET, $_REQUEST and $this->data, overwriting whatever may have been stored there. That is, WebRequest conflates path routing template variables with query string parameters.\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003cp\u003eSo you see, there's almost nothing in common between PSR-7 and WebRequest, and in many cases PSR-7 has more features. WebRequest is, in my opinion, crufty and laden with a lot of b\/c baggage. It was introduced to replace direct access to register_global variables. That's why it runs early and owns the superglobals -- it's following the example of earlier setup code that owned the registered globals. It makes me want to replace WebRequest with ServerRequestInterface in legacy code, rather than the other way around.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_711\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/ms665yc73j6nadjitytk\/PHID-FILE-mocohumlrcbe2lcuplml\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Anomie\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-badges\"\u003e\u003cul class=\"phui-badge-flex-view grouped flex-view-collapsed \"\u003e\u003cli class=\"phui-badge-flex-item\"\u003e\u003ca class=\"phui-badge-mini phui-badge-mini-orange \" href=\"\/badges\/view\/5\/\" data-sigil=\"has-tooltip\" data-meta=\"0_709\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-rocket\" data-meta=\"0_710\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5160802\" id=\"5160802\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_708\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Anomie\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_256\"\u003eAnomie\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5160802\" data-sigil=\"has-tooltip\" data-meta=\"0_707\"\u003e\u003cspan class=\"screen-only\"\u003eMay 6 2019, 4:26 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-06 16:26:08 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_705\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_706\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_257\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5158558\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_140\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5158558\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_143\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eand a sufficiently motivated developer can always invoke code that's in a class (it's done with the action API, despite not being available as a service).\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eI'm not saying to try to stop said sufficiently motivated developer, that's likely not worth the effort. I am saying to avoid making it \u003cem\u003eeasy\u003c\/em\u003e to do the wrong thing.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003eparsing the body should be done by the framework, or if it's done by the handler it should happen via a separate entry point, so that ServerRequestInterface::getParsedBody in middleware can be supported\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eI note that seems to assume the body is always in some standard format (JSON or the like). Is that restriction already present (explicitly or in practice) in Restbase?\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5158969\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_141\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5158969\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/tstarling\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_144\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@tstarling\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 has getProtocolVersion(), which WebRequest lacks. There are a few direct uses of $_SERVER['SERVER_PROTOCOL'] in our code base which suggest it may be a useful addition.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eMost are just to generate the right version when manually calling \u003ctt class=\"remarkup-monospaced\"\u003eheader( "HTTP\/1.1 404 Not Found" )\u003c\/tt\u003e or the like. The one in OutputHandler was added in \u003ca href=\"\/rSVN19996\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_142\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003erSVN19996\u003c\/span\u003e\u003c\/a\u003e to send a Content-Length header to HTTP\/1.0 clients; it doesn't specify why it's not also including it for 1.1 clients.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 provides getQueryParams(), essentially a wrapper for $_GET, which has no equivalent in WebRequest. WebRequest mangles the $_GET superglobal on startup and provides only a merged view of $_POST and $_GET.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eWebRequest has getQueryValues(), which seems to be the same thing?\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cul class=\"remarkup-list\"\u003e\n\u003cli class=\"remarkup-list-item\"\u003ePSR-7 provides getRequestTarget(), which is the second part of the first line of the HTTP message. Theoretically it can be "*" for an OPTIONS request, although maybe that's just for clients since I'm not sure how that would be routed to PHP as a server request. It is usually a path, but the correct way to get the path is from the URI object. WebRequest has no such concept.\u003c\/li\u003e\n\u003c\/ul\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003egetGlobalRequestURL() seems similar.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_720\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5161079\" id=\"5161079\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_719\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_258\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5161079\" data-sigil=\"has-tooltip\" data-meta=\"0_718\"\u003e\u003cspan class=\"screen-only\"\u003eMay 6 2019, 5:34 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-06 17:34:32 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_716\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_717\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_259\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5160802\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_145\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5160802\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_146\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\n\n\u003cblockquote\u003e\u003cp\u003eI am saying to avoid making it \u003cem\u003eeasy\u003c\/em\u003e to do the wrong thing.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eAIUI we want to do that by discouraging people from using \u003ctt class=\"remarkup-monospaced\"\u003eMediaWikiServices::getInstance()\u003c\/tt\u003e, and eventually deprecating it (there's no reason for a DI container to be a singleton).\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cp\u003eI note that seems to assume the body is always in some standard format (JSON or the like).\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eNot really, the framework would just skip parsing unknown formats (and the parsed body will be the same as the raw body) and leave it to the module.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_731\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/ms665yc73j6nadjitytk\/PHID-FILE-mocohumlrcbe2lcuplml\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Anomie\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-badges\"\u003e\u003cul class=\"phui-badge-flex-view grouped flex-view-collapsed \"\u003e\u003cli class=\"phui-badge-flex-item\"\u003e\u003ca class=\"phui-badge-mini phui-badge-mini-orange \" href=\"\/badges\/view\/5\/\" data-sigil=\"has-tooltip\" data-meta=\"0_729\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-rocket\" data-meta=\"0_730\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5161310\" id=\"5161310\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_728\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Anomie\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_260\"\u003eAnomie\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5161310\" data-sigil=\"has-tooltip\" data-meta=\"0_727\"\u003e\u003cspan class=\"screen-only\"\u003eMay 6 2019, 6:35 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-06 18:35:11 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_725\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_726\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_261\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5161079\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_147\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5161079\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_148\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eAIUI we want to do that by discouraging people from using \u003ctt class=\"remarkup-monospaced\"\u003eMediaWikiServices::getInstance()\u003c\/tt\u003e, and eventually deprecating it (there's no reason for a DI container to be a singleton).\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eThat doesn't solve the problem of someone deciding they want to inject the entry point handler into their class from the wiring file.\u003c\/p\u003e\n\n\u003cblockquote\u003e\u003cblockquote\u003e\u003cp\u003eI note that seems to assume the body is always in some standard format (JSON or the like).\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eNot really, the framework would just skip parsing unknown formats (and the parsed body will be the same as the raw body) and leave it to the module.\u003c\/p\u003e\u003c\/blockquote\u003e\n\n\u003cp\u003eIt seems like what you really want is a method on the Request class that tries to parse the body as JSON, rather than the Router parsing it.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_740\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/woaup5gfhgyt3xpvwi4f\/PHID-FILE-xcnwlpurjz3rsuiv56td\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Tgr\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5162676\" id=\"5162676\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_739\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Tgr\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_262\"\u003eTgr\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5162676\" data-sigil=\"has-tooltip\" data-meta=\"0_738\"\u003e\u003cspan class=\"screen-only\"\u003eMay 6 2019, 9:48 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-06 21:48:34 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_736\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_737\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_263\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5161310\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_149\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5161310\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_150\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eThat doesn't solve the problem of someone deciding they want to inject the entry point handler into their class from the wiring file.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eI see what you mean. But there is another use for getting almost everything from wiring files, which is that the site owner can replace them with local customizations (instead of having to patch\/fork MediaWiki). I'd really like to support that.\u003c\/p\u003e\n\n\u003cp\u003eA bit hacky, but I guess we could 1) use defines to mark the entry point, so the router can only be used in \u003ctt class=\"remarkup-monospaced\"\u003erest.php\u003c\/tt\u003e (and tests) and 2) mark somehow that the router is processing a request, so you cannot do "nested" processing.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_750\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5162943\" id=\"5162943\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_749\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_264\"\u003etstarling\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003eEdited\u003cspan class=\"visual-only\" aria-hidden=\"true\"\u003e \u00b7 \u003c\/span\u003e\u003ca href=\"#5162943\" data-sigil=\"has-tooltip\" data-meta=\"0_748\"\u003e\u003cspan class=\"screen-only\"\u003eMay 6 2019, 11:36 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-06 23:36:44 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_746\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_747\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_265\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5160802\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_151\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5160802\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Anomie\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_153\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Anomie\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eMost are just to generate the right version when manually calling \u003ctt class=\"remarkup-monospaced\"\u003eheader( "HTTP\/1.1 404 Not Found" )\u003c\/tt\u003e or the like. The one in OutputHandler was added in \u003ca href=\"\/rSVN19996\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_152\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003erSVN19996\u003c\/span\u003e\u003c\/a\u003e to send a Content-Length header to HTTP\/1.0 clients; it doesn't specify why it's not also including it for 1.1 clients.\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eIt's because "Transfer-Encoding: chunked" was introduced in HTTP\/1.1, and Apache sends it by default. RFC 2616 section 4.4 says "The Content-Length header field MUST NOT be sent if these two lengths are different (i.e., if a Transfer-Encoding header field is present)". In HTTP\/1.0, sending a Content-Length header allows the end of the body to be detected without closing the connection, thus allowing keepalive. This HTTP\/1.0 keepalive was the only kind of keepalive supported by Squid at the time.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_753\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-minor-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/zirzntnbnbimsxdels47\/PHID-FILE-tu3ola3spvpugy5kzs6b\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/Mholloway\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003ca name=\"5164864\" id=\"5164864\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-user-plus phui-timeline-icon\" data-meta=\"0_752\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/Mholloway\/\" class=\"phui-handle handle-availability-disabled phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_266\"\u003e\u003cspan class=\"perfect-circle\"\u003e\u2022\u003c\/span\u003e Mholloway\u003c\/a\u003e subscribed.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5164864\" data-sigil=\"has-tooltip\" data-meta=\"0_751\"\u003e\u003cspan class=\"screen-only\"\u003eMay 7 2019, 3:53 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-07 15:53:13 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_756\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-minor-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/jxvdog2ip6pz4witcpjs\/PHID-FILE-btpqxcrdi3n2lgwjhxee\/profile-bernd-wiki0001-cropped_2.jpg)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/bearND\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003ca name=\"5164888\" id=\"5164888\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-user-plus phui-timeline-icon\" data-meta=\"0_755\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/bearND\/\" class=\"phui-handle handle-availability-disabled phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_267\"\u003e\u003cspan class=\"perfect-circle\"\u003e\u2022\u003c\/span\u003e bearND\u003c\/a\u003e subscribed.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5164888\" data-sigil=\"has-tooltip\" data-meta=\"0_754\"\u003e\u003cspan class=\"screen-only\"\u003eMay 7 2019, 4:00 PM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-07 16:00:48 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-spacer\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-shell\" data-sigil=\"transaction anchor-container\" data-meta=\"0_765\"\u003e\u003cdiv class=\"phui-timeline-event-view phui-timeline-major-event\"\u003e\u003cdiv class=\"phui-timeline-content\"\u003e\u003ca style=\"background-image: url(https:\/\/phab.wmfusercontent.org\/file\/data\/xgl3wy55ovinfnucvwhx\/PHID-FILE-zocnksypiqutzf5oubpi\/profile)\" class=\"visual-only phui-timeline-image\" href=\"\/p\/tstarling\/\" aria-hidden=\"true\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-wedge\" style=\"\"\u003e\u003c\/div\u003e\u003cdiv class=\"phui-timeline-group\"\u003e\u003cdiv class=\"phui-timeline-inner-content\"\u003e\u003ca name=\"5166035\" id=\"5166035\" class=\"phabricator-anchor-view\"\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-title phui-timeline-title-with-icon phui-timeline-title-with-menu\"\u003e\u003cspan class=\"phui-timeline-icon-fill\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-comment phui-timeline-icon\" data-meta=\"0_764\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/span\u003e\u003ca href=\"\/p\/tstarling\/\" class=\"phui-handle phui-link-person\" data-sigil=\"hovercard\" data-meta=\"0_268\"\u003etstarling\u003c\/a\u003e added a comment.\u003cspan class=\"phui-timeline-extra\"\u003e\u003ca href=\"#5166035\" data-sigil=\"has-tooltip\" data-meta=\"0_763\"\u003e\u003cspan class=\"screen-only\"\u003eMay 8 2019, 12:33 AM\u003c\/span\u003e\u003cspan class=\"print-only\" aria-hidden=\"true\"\u003e2019-05-08 00:33:19 (UTC+0)\u003c\/span\u003e\u003c\/a\u003e\u003c\/span\u003e\u003c\/div\u003e\u003ca href=\"#\" class=\"phui-timeline-menu\" aria-haspopup=\"true\" aria-expanded=\"false\" data-sigil=\"phui-dropdown-menu\" data-meta=\"0_761\"\u003e\u003cspan class=\"aural-only\"\u003eComment Actions\u003c\/span\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-caret-down\" data-meta=\"0_762\" aria-hidden=\"true\"\u003e\u003c\/span\u003e\u003c\/a\u003e\u003cdiv class=\"phui-timeline-core-content\"\u003e\u003cspan class=\"transaction-comment\" data-sigil=\"transaction-comment\" data-meta=\"0_269\"\u003e\u003cdiv class=\"phabricator-remarkup\"\u003e\u003cp\u003eI wrote a "path template matcher" class based on the tree building code I previously pasted. It's decoupled from the router and could go in a separate library. I wrote a benchmark script for it, which uses path templates like '\/6\/{8}\/{9}\/1\/2\/{7}'. They are random templates of length 1-6 where 30% of the path components are parameters, filtered in advance to remove conflicts. For 1000 such templates, it takes about 12ms to build the tree, and 1.3\u00b5s to match a path against it. The paths were prefiltered so that they always matched a template. This is a very rough model for intended usage. But it supports the proposition that tree based routing is an efficient approach to matching templates, but that a cache of the routing tree is required.\u003c\/p\u003e\n\n\u003cblockquote class=\"remarkup-reply-block\"\u003e\n\u003cdiv class=\"remarkup-reply-head\"\u003eIn \u003ca href=\"\/T221177#5148349\" class=\"phui-tag-view phui-tag-type-object \" data-sigil=\"hovercard\" data-meta=\"0_154\"\u003e\u003cspan class=\"phui-tag-core-closed\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-object\"\u003eT221177#5148349\u003c\/span\u003e\u003c\/span\u003e\u003c\/a\u003e, \u003ca href=\"\/p\/Tgr\/\" class=\"phui-tag-view phui-tag-type-person \" data-sigil=\"hovercard\" data-meta=\"0_155\"\u003e\u003cspan class=\"phui-tag-core phui-tag-color-person\"\u003e@Tgr\u003c\/span\u003e\u003c\/a\u003e wrote:\u003c\/div\u003e\n\u003cdiv class=\"remarkup-reply-body\"\u003e\u003cp\u003eSeems like a not very impactful performance optimization, given that the API module itself will probably be 2-3 magnitudes slower. Or are you thinking of a scenario where the router also handles caching?\u003c\/p\u003e\u003c\/div\u003e\n\u003c\/blockquote\u003e\n\n\u003cp\u003eI think 2-3 orders of magnitude, i.e. 0.1% to 1% overhead, is an appropriate performance target. If it was only 1 order of magnitude (10% overhead) then we would call it low-hanging fruit.\u003c\/p\u003e\u003c\/div\u003e\u003c\/span\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e\u003c\/div\u003e"},"javelin_metadata":[{"hovercardSpec":{"objectPHID":"PHID-USER-2e3ss3dyx27wu7wkk3qw"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-5dcfm3ohx3a3mqdooc4r"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-umgtrw4jh7vvh5zlsvue"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-fgoykosu7vzfhph3lgsz"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-ek5vvjxasnir4vnhaiuz"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-fgoykosu7vzfhph3lgsz"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-fgoykosu7vzfhph3lgsz"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-fgoykosu7vzfhph3lgsz"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-4czhayxttcp7oov5a5qd"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-2e3ss3dyx27wu7wkk3qw","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"uri":"","symbols":null},{"uri":"","symbols":null},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-2e3ss3dyx27wu7wkk3qw","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-y4t763iv5wmkr6plhuki"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-2e3ss3dyx27wu7wkk3qw","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-wrimmmr5w2zt7nk2t753","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-wrimmmr5w2zt7nk2t753","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-2e3ss3dyx27wu7wkk3qw","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-wrimmmr5w2zt7nk2t753","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-byiintbytq2lkvyeyg5f"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-vb6uwh72w33gr4yauj4d"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-2e3ss3dyx27wu7wkk3qw","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-5dqihbanu3caaj7pigif","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-wrimmmr5w2zt7nk2t753","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-5dqihbanu3caaj7pigif","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-5dqihbanu3caaj7pigif","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-5dqihbanu3caaj7pigif","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-byiintbytq2lkvyeyg5f"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-vb6uwh72w33gr4yauj4d"}},{"hovercardSpec":{"objectPHID":"PHID-USER-wrimmmr5w2zt7nk2t753","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-5dqihbanu3caaj7pigif","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-ylen4zt52ejxmn4nrbms"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-orqdmhnctttzw762g2ry"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-mkv6ycnzjoxgabykp6j7"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-CMIT-oadr677rx5hnjwodknhy"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-CMIT-oadr677rx5hnjwodknhy"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc","contextPHID":"PHID-TASK-2wb2a6dfuli5qjx35roe"}},{"hovercardSpec":{"objectPHID":"PHID-USER-2e3ss3dyx27wu7wkk3qw"}},{"hovercardSpec":{"objectPHID":"PHID-PROJ-qn32i65oyiiypsiepcgl"}},{"hovercardSpec":{"objectPHID":"PHID-USER-2e3ss3dyx27wu7wkk3qw"}},{"hovercardSpec":{"objectPHID":"PHID-USER-2e3ss3dyx27wu7wkk3qw"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-2kvxz5z27xdi7yn"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-niswkhckyrl24f5"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"hovercardSpec":{"objectPHID":"PHID-PROJ-4vbywitqcpqpz7tw5miu"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"hovercardSpec":{"objectPHID":"PHID-TASK-ohaontfl647r2enrcakv"}},{"phid":"PHID-XACT-TASK-xtbrxiqozlaiu6r"},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp"}},{"hovercardSpec":{"objectPHID":"PHID-USER-v7vgzvvcw7v2umf737ri"}},{"phid":"PHID-XACT-TASK-r5yiygq5gfwistn"},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp"}},{"phid":"PHID-XACT-TASK-h3x2wk62xifdquw"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-jw6m5gb7gb57h56"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-sfhmmro2xabe3fn"},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"hovercardSpec":{"objectPHID":"PHID-USER-hxwwywcyzpooynxuo7a2"}},{"phid":"PHID-XACT-TASK-v5ppf7ezfpicobh"},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp"}},{"phid":"PHID-XACT-TASK-rs5dtr7pt43wgga"},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp"}},{"phid":"PHID-XACT-TASK-hao47oku6gnyodc"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-qwugaxg6tsfc632"},{"hovercardSpec":{"objectPHID":"PHID-USER-2e3ss3dyx27wu7wkk3qw"}},{"phid":"PHID-XACT-TASK-tza77mckfntlsld"},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"phid":"PHID-XACT-TASK-au2vpq5nzyquf6e"},{"hovercardSpec":{"objectPHID":"PHID-USER-ydswvwhh5pm4lshahjje"}},{"hovercardSpec":{"objectPHID":"PHID-USER-v7vgzvvcw7v2umf737ri"}},{"phid":"PHID-XACT-TASK-wdurepkofucvxph"},{"hovercardSpec":{"objectPHID":"PHID-USER-2e3ss3dyx27wu7wkk3qw"}},{"phid":"PHID-XACT-TASK-ykdldj6v2fjz7wq"},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"phid":"PHID-XACT-TASK-obydmi6plu5ty62"},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"phid":"PHID-XACT-TASK-djt5r7qkvzmhrbv"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-3dzhvyba5fnlip6"},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp"}},{"phid":"PHID-XACT-TASK-fu6oxs5rmkaku3z"},{"phid":"PHID-XACT-TASK-iry7f3ypfjjjsal"},{"hovercardSpec":{"objectPHID":"PHID-USER-wrimmmr5w2zt7nk2t753"}},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp"}},{"phid":"PHID-XACT-TASK-xsrdr7gn6t62jgw"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-6ylyzlywxgiom2k"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-5yyiyycdmp6x6br"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-2geez6ihntbhc6l"},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"phid":"PHID-XACT-TASK-mbxiiljn3kdqygl"},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"phid":"PHID-XACT-TASK-g34fyrfilfngr4q"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-b7acch5y2kw5eqf"},{"hovercardSpec":{"objectPHID":"PHID-USER-wrimmmr5w2zt7nk2t753"}},{"phid":"PHID-XACT-TASK-rv2p5tdgicsdc2a"},{"hovercardSpec":{"objectPHID":"PHID-USER-wrimmmr5w2zt7nk2t753"}},{"phid":"PHID-XACT-TASK-ipqhoidt3d5vgep"},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"hovercardSpec":{"objectPHID":"PHID-USER-5dqihbanu3caaj7pigif"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"phid":"PHID-XACT-TASK-gbhudiluy4yhib2"},{"hovercardSpec":{"objectPHID":"PHID-USER-wrimmmr5w2zt7nk2t753"}},{"phid":"PHID-XACT-TASK-pwzzes5x6mxvm7o"},{"hovercardSpec":{"objectPHID":"PHID-USER-5dqihbanu3caaj7pigif"}},{"phid":"PHID-XACT-TASK-jrheqrbfxbhwfvk"},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp"}},{"phid":"PHID-XACT-TASK-n2bezzqc7kglah4"},{"hovercardSpec":{"objectPHID":"PHID-USER-5dqihbanu3caaj7pigif"}},{"phid":"PHID-XACT-TASK-o6xkphbqywwgvnb"},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"phid":"PHID-XACT-TASK-psnbsvtr4dvbbzz"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-6ekjhb2nu27mpwl"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-abnkhanm66t5nb3"},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"phid":"PHID-XACT-TASK-rsut7lpfttrgcnj"},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"phid":"PHID-XACT-TASK-5c7dz5kcfvo7zaf"},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp"}},{"phid":"PHID-XACT-TASK-vh2dv4uxcqjp3xj"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-vevbgexiw7gexvx"},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"phid":"PHID-XACT-TASK-bv5jkgz7v5t23c2"},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp"}},{"phid":"PHID-XACT-TASK-yw2peqbny5bwbf7"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-vj66v7talizhh4j"},{"hovercardSpec":{"objectPHID":"PHID-USER-uqcn2l4ng4murmyfnvyp"}},{"phid":"PHID-XACT-TASK-zcoztumjb6gwnwz"},{"hovercardSpec":{"objectPHID":"PHID-USER-a6p24cvyblhfzc7we7nc"}},{"phid":"PHID-XACT-TASK-bsdckhfy5owvykv"},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"phid":"PHID-XACT-TASK-qadfa7avwtme3w7"},{"hovercardSpec":{"objectPHID":"PHID-USER-d2tiurktk2jgxzcpd2jw"}},{"hovercardSpec":{"objectPHID":"PHID-USER-tjyfydvd4ncrfvvr6mkr"}},{"hovercardSpec":{"objectPHID":"PHID-USER-k6tmz5ylx4rzfl3bitse"}},{"phid":"PHID-XACT-TASK-chwafpjo7eh5mz3"},{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-z2jz5pw3wcumidx","anchor":"5117224"},{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-5szrztsbjacolmj","anchor":"5131075"},{"tip":"Via Web"},[],[],{"phid":"PHID-XACT-TASK-wxtzwes3lqfzzux","anchor":"5133576"},{"tip":"Via Web"},[],[],{"phid":"PHID-XACT-TASK-o3f3lt5bsfcjbid","anchor":"5136061"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-2kvxz5z27xdi7yn\/","ref":"T221177#5136073"},[],{"anchor":"5136073"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_1\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_284\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_285\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_3\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-2kvxz5z27xdi7yn\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_286\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_287\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-2kvxz5z27xdi7yn","anchor":"5136073"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-niswkhckyrl24f5\/","ref":"T221177#5136076"},[],{"anchor":"5136076"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_5\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_293\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_294\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_7\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-niswkhckyrl24f5\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_295\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_296\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-niswkhckyrl24f5","anchor":"5136076"},{"tip":"Via Bulk Update"},[],{"phid":"PHID-XACT-TASK-f4aj4ttyb43m3tj","anchor":"5139310"},{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-mfostshumldec2s","anchor":"5142713"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-xtbrxiqozlaiu6r\/","ref":"T221177#5142766"},[],{"anchor":"5142766"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_9\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_308\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_309\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_11\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-xtbrxiqozlaiu6r\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_310\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_311\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-xtbrxiqozlaiu6r","anchor":"5142766"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-r5yiygq5gfwistn\/","ref":"T221177#5142785"},[],{"anchor":"5142785"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_13\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_317\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_318\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_15\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-r5yiygq5gfwistn\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_319\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_320\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-ylfs2qhohtt3aad","anchor":"5142785"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-h3x2wk62xifdquw\/","ref":"T221177#5143725"},[],{"anchor":"5143725"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_17\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_326\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_327\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_19\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-h3x2wk62xifdquw\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_328\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_329\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"tip":"Backport Deployer","align":"E","size":300},[],{"phid":"PHID-XACT-TASK-h3x2wk62xifdquw","anchor":"5143725"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-jw6m5gb7gb57h56\/","ref":"T221177#5145418"},[],{"anchor":"5145418"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_21\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_337\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_338\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_23\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-jw6m5gb7gb57h56\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_339\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_340\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-jw6m5gb7gb57h56","anchor":"5145418"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-sfhmmro2xabe3fn\/","ref":"T221177#5145523"},[],{"anchor":"5145523"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_25\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_346\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_347\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_27\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-sfhmmro2xabe3fn\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_348\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_349\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-sfhmmro2xabe3fn","anchor":"5145523"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-v5ppf7ezfpicobh\/","ref":"T221177#5145852"},[],{"anchor":"5145852"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_29\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_355\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_356\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_31\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-v5ppf7ezfpicobh\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_357\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_358\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-kyaw4yn6hasdipc","anchor":"5145852"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-rs5dtr7pt43wgga\/","ref":"T221177#5147230"},[],{"anchor":"5147230"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_33\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_364\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_365\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_35\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-rs5dtr7pt43wgga\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_366\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_367\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"tip":"Backport Deployer","align":"E","size":300},[],{"phid":"PHID-XACT-TASK-rs5dtr7pt43wgga","anchor":"5147230"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-hao47oku6gnyodc\/","ref":"T221177#5147257"},[],{"anchor":"5147257"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_37\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_375\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_376\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_39\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-hao47oku6gnyodc\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_377\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_378\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"tip":"Backport Deployer","align":"E","size":300},[],{"phid":"PHID-XACT-TASK-hao47oku6gnyodc","anchor":"5147257"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-qwugaxg6tsfc632\/","ref":"T221177#5148349"},[],{"anchor":"5148349"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_41\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_386\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_387\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_43\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-qwugaxg6tsfc632\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_388\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_389\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-qwugaxg6tsfc632","anchor":"5148349"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-tza77mckfntlsld\/","ref":"T221177#5148500"},[],{"anchor":"5148500"},[],[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_45\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_395\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_396\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_47\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-tza77mckfntlsld\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_397\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_398\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_49\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/history\/PHID-XACT-TASK-tza77mckfntlsld\/\" class=\"phabricator-action-view-item\" data-sigil=\"workflow\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-list phabricator-action-view-icon\" data-meta=\"0_399\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Edit History\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-tza77mckfntlsld","anchor":"5148500"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-au2vpq5nzyquf6e\/","ref":"T221177#5149229"},[],{"anchor":"5149229"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_51\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_405\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_406\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_53\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-au2vpq5nzyquf6e\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_407\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_408\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-au2vpq5nzyquf6e","anchor":"5149229"},{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-nj35cynvrgkr4tc","anchor":"5149230"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-wdurepkofucvxph\/","ref":"T221177#5149280"},[],{"anchor":"5149280"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_55\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_417\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_418\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_57\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-wdurepkofucvxph\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_419\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_420\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"tip":"Web Perf Hero","align":"E","size":300},[],{"phid":"PHID-XACT-TASK-wdurepkofucvxph","anchor":"5149280"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-ykdldj6v2fjz7wq\/","ref":"T221177#5149288"},[],{"anchor":"5149288"},[],[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_59\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_428\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_429\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_61\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-ykdldj6v2fjz7wq\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_430\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_431\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_63\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/history\/PHID-XACT-TASK-ykdldj6v2fjz7wq\/\" class=\"phabricator-action-view-item\" data-sigil=\"workflow\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-list phabricator-action-view-icon\" data-meta=\"0_432\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Edit History\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-ykdldj6v2fjz7wq","anchor":"5149288"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-obydmi6plu5ty62\/","ref":"T221177#5149306"},[],{"anchor":"5149306"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_65\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_438\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_439\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_67\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-obydmi6plu5ty62\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_440\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_441\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-obydmi6plu5ty62","anchor":"5149306"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-djt5r7qkvzmhrbv\/","ref":"T221177#5149333"},[],{"anchor":"5149333"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_69\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_447\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_448\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_71\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-djt5r7qkvzmhrbv\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_449\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_450\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-djt5r7qkvzmhrbv","anchor":"5149333"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-3dzhvyba5fnlip6\/","ref":"T221177#5149393"},[],{"anchor":"5149393"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_73\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_456\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_457\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_75\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-3dzhvyba5fnlip6\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_458\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_459\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-3dzhvyba5fnlip6","anchor":"5149393"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-fu6oxs5rmkaku3z\/","ref":"T221177#5149843"},[],{"anchor":"5149843"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_77\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_465\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_466\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_79\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-fu6oxs5rmkaku3z\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_467\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_468\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"tip":"Backport Deployer","align":"E","size":300},[],{"phid":"PHID-XACT-TASK-fu6oxs5rmkaku3z","anchor":"5149843"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-iry7f3ypfjjjsal\/","ref":"T221177#5150131"},[],{"anchor":"5150131"},[],[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_81\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_476\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_477\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_83\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-iry7f3ypfjjjsal\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_478\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_479\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_85\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/history\/PHID-XACT-TASK-iry7f3ypfjjjsal\/\" class=\"phabricator-action-view-item\" data-sigil=\"workflow\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-list phabricator-action-view-icon\" data-meta=\"0_480\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Edit History\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-iry7f3ypfjjjsal","anchor":"5150131"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-xsrdr7gn6t62jgw\/","ref":"T221177#5150416"},[],{"anchor":"5150416"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_87\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_486\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_487\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_89\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-xsrdr7gn6t62jgw\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_488\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_489\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"tip":"Backport Deployer","align":"E","size":300},[],{"phid":"PHID-XACT-TASK-xsrdr7gn6t62jgw","anchor":"5150416"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-6ylyzlywxgiom2k\/","ref":"T221177#5151276"},[],{"anchor":"5151276"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_91\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_497\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_498\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_93\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-6ylyzlywxgiom2k\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_499\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_500\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-6ylyzlywxgiom2k","anchor":"5151276"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-5yyiyycdmp6x6br\/","ref":"T221177#5151430"},[],{"anchor":"5151430"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_95\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_506\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_507\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_97\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-5yyiyycdmp6x6br\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_508\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_509\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-5yyiyycdmp6x6br","anchor":"5151430"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-2geez6ihntbhc6l\/","ref":"T221177#5151446"},[],{"anchor":"5151446"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_99\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_515\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_516\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_101\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-2geez6ihntbhc6l\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_517\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_518\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-2geez6ihntbhc6l","anchor":"5151446"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-mbxiiljn3kdqygl\/","ref":"T221177#5151601"},[],{"anchor":"5151601"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_103\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_524\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_525\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_105\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-mbxiiljn3kdqygl\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_526\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_527\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-mbxiiljn3kdqygl","anchor":"5151601"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-g34fyrfilfngr4q\/","ref":"T221177#5151701"},[],{"anchor":"5151701"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_107\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_533\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_534\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_109\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-g34fyrfilfngr4q\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_535\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_536\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-g34fyrfilfngr4q","anchor":"5151701"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-b7acch5y2kw5eqf\/","ref":"T221177#5152090"},[],{"anchor":"5152090"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_111\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_542\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_543\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_113\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-b7acch5y2kw5eqf\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_544\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_545\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-b7acch5y2kw5eqf","anchor":"5152090"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-rv2p5tdgicsdc2a\/","ref":"T221177#5152457"},[],{"anchor":"5152457"},[],[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_115\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_551\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_552\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_117\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-rv2p5tdgicsdc2a\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_553\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_554\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_119\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/history\/PHID-XACT-TASK-rv2p5tdgicsdc2a\/\" class=\"phabricator-action-view-item\" data-sigil=\"workflow\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-list phabricator-action-view-icon\" data-meta=\"0_555\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Edit History\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-rv2p5tdgicsdc2a","anchor":"5152457"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-ipqhoidt3d5vgep\/","ref":"T221177#5152532"},[],{"anchor":"5152532"},[],[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_121\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_561\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_562\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_123\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-ipqhoidt3d5vgep\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_563\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_564\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_125\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/history\/PHID-XACT-TASK-ipqhoidt3d5vgep\/\" class=\"phabricator-action-view-item\" data-sigil=\"workflow\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-list phabricator-action-view-icon\" data-meta=\"0_565\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Edit History\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-ipqhoidt3d5vgep","anchor":"5152532"},{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-v5bwj6xacjmxnif","anchor":"5152610"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-gbhudiluy4yhib2\/","ref":"T221177#5152650"},[],{"anchor":"5152650"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_127\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_574\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_575\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_129\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-gbhudiluy4yhib2\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_576\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_577\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-gbhudiluy4yhib2","anchor":"5152650"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-pwzzes5x6mxvm7o\/","ref":"T221177#5152708"},[],{"anchor":"5152708"},[],[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_131\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_583\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_584\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_133\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-pwzzes5x6mxvm7o\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_585\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_586\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_135\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/history\/PHID-XACT-TASK-pwzzes5x6mxvm7o\/\" class=\"phabricator-action-view-item\" data-sigil=\"workflow\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-list phabricator-action-view-icon\" data-meta=\"0_587\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Edit History\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-pwzzes5x6mxvm7o","anchor":"5152708"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-jrheqrbfxbhwfvk\/","ref":"T221177#5152853"},[],{"anchor":"5152853"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_137\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_593\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_594\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_139\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-jrheqrbfxbhwfvk\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_595\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_596\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"tip":"Nerd Sniper","align":"E","size":300},[],{"phid":"PHID-XACT-TASK-jrheqrbfxbhwfvk","anchor":"5152853"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-n2bezzqc7kglah4\/","ref":"T221177#5153831"},[],{"anchor":"5153831"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_141\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_604\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_605\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_143\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-n2bezzqc7kglah4\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_606\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_607\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"tip":"Backport Deployer","align":"E","size":300},[],{"phid":"PHID-XACT-TASK-n2bezzqc7kglah4","anchor":"5153831"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-o6xkphbqywwgvnb\/","ref":"T221177#5154488"},[],{"anchor":"5154488"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_145\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_615\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_616\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_147\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-o6xkphbqywwgvnb\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_617\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_618\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"tip":"Nerd Sniper","align":"E","size":300},[],{"phid":"PHID-XACT-TASK-o6xkphbqywwgvnb","anchor":"5154488"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-psnbsvtr4dvbbzz\/","ref":"T221177#5154489"},[],{"anchor":"5154489"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_149\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_626\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_627\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_151\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-psnbsvtr4dvbbzz\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_628\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_629\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-psnbsvtr4dvbbzz","anchor":"5154489"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-6ekjhb2nu27mpwl\/","ref":"T221177#5154635"},[],{"anchor":"5154635"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_153\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_635\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_636\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_155\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-6ekjhb2nu27mpwl\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_637\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_638\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-6ekjhb2nu27mpwl","anchor":"5154635"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-abnkhanm66t5nb3\/","ref":"T221177#5154753"},[],{"anchor":"5154753"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_157\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_644\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_645\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_159\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-abnkhanm66t5nb3\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_646\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_647\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-abnkhanm66t5nb3","anchor":"5154753"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-rsut7lpfttrgcnj\/","ref":"T221177#5154940"},[],{"anchor":"5154940"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_161\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_653\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_654\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_163\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-rsut7lpfttrgcnj\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_655\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_656\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-rsut7lpfttrgcnj","anchor":"5154940"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-5c7dz5kcfvo7zaf\/","ref":"T221177#5154992"},[],{"anchor":"5154992"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_165\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_662\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_663\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_167\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-5c7dz5kcfvo7zaf\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_664\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_665\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-5c7dz5kcfvo7zaf","anchor":"5154992"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-vh2dv4uxcqjp3xj\/","ref":"T221177#5155921"},[],{"anchor":"5155921"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_169\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_671\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_672\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_171\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-vh2dv4uxcqjp3xj\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_673\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_674\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"tip":"Backport Deployer","align":"E","size":300},[],{"phid":"PHID-XACT-TASK-vh2dv4uxcqjp3xj","anchor":"5155921"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-vevbgexiw7gexvx\/","ref":"T221177#5158558"},[],{"anchor":"5158558"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_173\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_682\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_683\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_175\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-vevbgexiw7gexvx\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_684\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_685\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-vevbgexiw7gexvx","anchor":"5158558"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-bv5jkgz7v5t23c2\/","ref":"T221177#5158969"},[],{"anchor":"5158969"},[],[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_177\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_691\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_692\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_179\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-bv5jkgz7v5t23c2\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_693\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_694\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_181\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/history\/PHID-XACT-TASK-bv5jkgz7v5t23c2\/\" class=\"phabricator-action-view-item\" data-sigil=\"workflow\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-list phabricator-action-view-icon\" data-meta=\"0_695\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Edit History\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-bv5jkgz7v5t23c2","anchor":"5158969"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-yw2peqbny5bwbf7\/","ref":"T221177#5160802"},[],{"anchor":"5160802"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_183\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_701\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_702\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_185\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-yw2peqbny5bwbf7\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_703\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_704\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"tip":"Backport Deployer","align":"E","size":300},[],{"phid":"PHID-XACT-TASK-yw2peqbny5bwbf7","anchor":"5160802"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-vj66v7talizhh4j\/","ref":"T221177#5161079"},[],{"anchor":"5161079"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_187\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_712\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_713\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_189\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-vj66v7talizhh4j\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_714\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_715\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-vj66v7talizhh4j","anchor":"5161079"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-zcoztumjb6gwnwz\/","ref":"T221177#5161310"},[],{"anchor":"5161310"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_191\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_721\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_722\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_193\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-zcoztumjb6gwnwz\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_723\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_724\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"tip":"Backport Deployer","align":"E","size":300},[],{"phid":"PHID-XACT-TASK-zcoztumjb6gwnwz","anchor":"5161310"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-bsdckhfy5owvykv\/","ref":"T221177#5162676"},[],{"anchor":"5162676"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_195\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_732\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_733\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_197\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-bsdckhfy5owvykv\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_734\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_735\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-bsdckhfy5owvykv","anchor":"5162676"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-qadfa7avwtme3w7\/","ref":"T221177#5162943"},[],{"anchor":"5162943"},[],[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_199\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_741\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_742\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_201\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-qadfa7avwtme3w7\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_743\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_744\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_203\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/history\/PHID-XACT-TASK-qadfa7avwtme3w7\/\" class=\"phabricator-action-view-item\" data-sigil=\"workflow\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-list phabricator-action-view-icon\" data-meta=\"0_745\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Edit History\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-qadfa7avwtme3w7","anchor":"5162943"},{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-icteiivsi6kv5ij","anchor":"5164864"},{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-kcuee5ozmuzxiqp","anchor":"5164888"},{"targetID":"UQ0_1","uri":"\/transactions\/quote\/PHID-XACT-TASK-chwafpjo7eh5mz3\/","ref":"T221177#5166035"},[],{"anchor":"5166035"},[],{"items":"\u003cul class=\"phabricator-action-list-view \"\u003e\u003cli id=\"UQ0_205\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"#\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-quote\" data-meta=\"0_757\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-quote-left phabricator-action-view-icon\" data-meta=\"0_758\" aria-hidden=\"true\"\u003e\u003c\/span\u003eQuote Comment\u003c\/a\u003e\u003c\/li\u003e\u003cli id=\"UQ0_207\" class=\"phabricator-action-view phabricator-action-view-href action-has-icon\" style=\"\"\u003e\u003ca href=\"\/transactions\/raw\/PHID-XACT-TASK-chwafpjo7eh5mz3\/\" class=\"phabricator-action-view-item\" data-sigil=\"transaction-raw\" data-meta=\"0_759\"\u003e\u003cspan class=\"visual-only phui-icon-view phui-font-fa fa-code phabricator-action-view-icon\" data-meta=\"0_760\" aria-hidden=\"true\"\u003e\u003c\/span\u003eView Raw Remarkup\u003c\/a\u003e\u003c\/li\u003e\u003c\/ul\u003e"},[],{"tip":"Via Web"},[],{"phid":"PHID-XACT-TASK-chwafpjo7eh5mz3","anchor":"5166035"}],"javelin_behaviors":{"phui-hovercards":[],"repository-crossreference":[],"phabricator-watch-anchor":[],"phabricator-tooltips":[],"phui-dropdown-menu":[]},"javelin_resources":["https:\/\/phab.wmfusercontent.org\/res\/defaultX\/phabricator\/2eeda9e0\/core.pkg.js","https:\/\/phab.wmfusercontent.org\/res\/defaultX\/phabricator\/98e6504a\/rsrc\/externals\/javelin\/core\/init.js","https:\/\/phab.wmfusercontent.org\/res\/defaultX\/phabricator\/968d91ee\/core.pkg.css","https:\/\/phab.wmfusercontent.org\/res\/defaultX\/phabricator\/b37bcd38\/rsrc\/css\/application\/paste\/paste.css","https:\/\/phab.wmfusercontent.org\/res\/defaultX\/phabricator\/e382316a\/rsrc\/css\/layout\/phabricator-source-code-view.css","https:\/\/phab.wmfusercontent.org\/res\/defaultX\/phabricator\/46fcb3af\/differential.pkg.js","https:\/\/phab.wmfusercontent.org\/res\/defaultX\/phabricator\/666e25ad\/rsrc\/css\/phui\/phui-badge.css"]}