@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * Collection of routes on a site for an application.
5 *
6 * @task info Map Information
7 * @task routing Routing
8 */
9final class AphrontRoutingMap extends Phobject {
10
11 private $site;
12 private $application;
13 private $routes = array();
14
15
16/* -( Map Info )----------------------------------------------------------- */
17
18
19 public function setSite(AphrontSite $site) {
20 $this->site = $site;
21 return $this;
22 }
23
24 /**
25 * @return AphrontSite
26 */
27 public function getSite() {
28 return $this->site;
29 }
30
31 public function setApplication(PhabricatorApplication $application) {
32 $this->application = $application;
33 return $this;
34 }
35
36 /**
37 * @return PhabricatorApplication
38 */
39 public function getApplication() {
40 return $this->application;
41 }
42
43 public function setRoutes(array $routes) {
44 $this->routes = $routes;
45 return $this;
46 }
47
48 public function getRoutes() {
49 return $this->routes;
50 }
51
52
53/* -( Routing )------------------------------------------------------------ */
54
55
56 /**
57 * Find the route matching a path, if one exists.
58 *
59 * @param string $path Path to route.
60 * @return AphrontRoutingResult|null Routing result, if path matches map.
61 * @task routing
62 */
63 public function routePath($path) {
64 $map = $this->getRoutes();
65
66 foreach ($map as $route => $value) {
67 $match = $this->tryRoute($route, $value, $path);
68 if (!$match) {
69 continue;
70 }
71
72 $result = $this->newRoutingResult();
73 $application = $result->getApplication();
74
75 $controller_class = $match['class'];
76 $controller = newv($controller_class, array());
77 $controller->setCurrentApplication($application);
78
79 $result
80 ->setController($controller)
81 ->setURIData($match['data']);
82
83 return $result;
84 }
85
86 return null;
87 }
88
89
90 /**
91 * Test a sub-map to see if any routes match a path.
92 *
93 * @param string $route Pattern from the map.
94 * @param string $value Value from the map.
95 * @param string $path Path to route.
96 * @return array<string, array<string>|string>|null Match details, if path
97 * matches sub-map.
98 * @task routing
99 */
100 private function tryRoute($route, $value, $path) {
101 $has_submap = is_array($value);
102
103 if (!$has_submap) {
104 // If the value is a controller rather than a sub-map, any matching
105 // route must completely consume the path.
106 $pattern = '(^'.$route.'\z)';
107 } else {
108 $pattern = '(^'.$route.')';
109 }
110
111 $data = null;
112 $ok = preg_match($pattern, $path, $data);
113 if ($ok === false) {
114 throw new Exception(
115 pht(
116 'Routing fragment "%s" is not a valid regular expression.',
117 $route));
118 }
119
120 if (!$ok) {
121 return null;
122 }
123
124 $path_match = $data[0];
125
126 // Clean up the data. We only want to retain named capturing groups, not
127 // the duplicated numeric captures.
128 foreach ($data as $k => $v) {
129 if (is_numeric($k)) {
130 unset($data[$k]);
131 }
132 }
133
134 if (!$has_submap) {
135 return array(
136 'class' => $value,
137 'data' => $data,
138 );
139 }
140
141 $sub_path = substr($path, strlen($path_match));
142 foreach ($value as $sub_route => $sub_value) {
143 $result = $this->tryRoute($sub_route, $sub_value, $sub_path);
144 if ($result) {
145 $result['data'] += $data;
146 return $result;
147 }
148 }
149
150 return null;
151 }
152
153
154 /**
155 * Build a new routing result for this map.
156 *
157 * @return AphrontRoutingResult New, empty routing result.
158 * @task routing
159 */
160 private function newRoutingResult() {
161 return id(new AphrontRoutingResult())
162 ->setSite($this->getSite())
163 ->setApplication($this->getApplication());
164 }
165
166}