· 4 years ago · Sep 06, 2021, 01:26 PM
1<?php
2
3declare(strict_types=1);
4
5namespace Yez\Core;
6
7use Yez\Core\Exceptions\{ControllerNotFoundException,
8 LayoutNotFoundException,
9 MethodNotFoundException,
10 NotIntegerException,
11 RequestMethodException,
12 InvalidActionException,
13 ViewNotFoundException};
14use JetBrains\PhpStorm\ArrayShape;
15
16class Router
17{
18 /** @var Request $request */
19 private Request $request;
20
21 /** @var array $routes */
22 private array $routes = [];
23
24 /** @var array $params */
25 private array $params = [];
26
27 /** @var string $controllerPath */
28 public string $controllerPath = 'Yez\Controllers\\';
29
30 /**
31 * Create an instance of request to
32 * get the request method and URL path
33 */
34 public function __construct()
35 {
36 $this->request = new Request();
37 }
38
39 /**
40 * First get the request method and request URL,
41 * then we check if the method and URL matches with one
42 * of the routes in the routing table if it does
43 * check if the action is a callable or a string. If it's a
44 * callable then execute the function, if it's a string split the
45 * controller from the method then return it to execute
46 *
47 * @return mixed
48 * @throws LayoutNotFoundException|ViewNotFoundException|NotIntegerException
49 */
50 public function resolve(): mixed
51 {
52 $method = $this->request->getMethod();
53 $url = $this->request->getUrl();
54
55 if ($this->match($method, $url)) {
56 if (is_callable($this->params['action'])) {
57 return $this->executeCallable($this->params['action']);
58 } elseif (is_string($this->params['action'])) {
59 return $this->executeControllerMethod();
60 }
61 }
62 Response::status(404);
63 return View::render('error/404', ['title' => 'YEZ - 404']);
64 }
65
66 /**
67 * Execute the method given by the user
68 * as action
69 *
70 * @param callable $action
71 *
72 * @return mixed
73 */
74 private function executeCallable(callable $action): mixed
75 {
76 $values = [];
77
78 foreach ($this->params as $key => $value) {
79 if ($key !== 'action') {
80 $values[$key] = htmlspecialchars($value);
81 }
82 }
83 return call_user_func($action, $values);
84 }
85
86 /**
87 * Execute the Method in the Controller
88 * given by the user
89 *
90 * @return mixed
91 */
92 private function executeControllerMethod(): mixed
93 {
94 $controllerAndMethod = $this->getControllerAndMethod($this->params['action']);
95
96 return ((new $controllerAndMethod['controller']()))->{$controllerAndMethod['method']}();
97 }
98
99 /**
100 * Loop over the routing table to find
101 * a match, if a match is found store all
102 * matches with type string in the params array
103 *
104 * @param string $method
105 * @param string $url
106 *
107 * @return bool
108 */
109 private function match(string $method, string $url): bool
110 {
111 if (key_exists($method, $this->routes)) {
112 foreach ($this->routes[$method] as $route => $params) {
113 if (preg_match($route, $url, $matches)) {
114 foreach ($matches as $key => $param) {
115 if (is_string($key)) {
116 $params[$key] = $param;
117 }
118 }
119 $this->params = $params;
120 return true;
121 }
122 }
123 }
124 return false;
125 }
126
127 /**
128 * If the request method is valid and the action method is
129 * not an empty array or empty string then convert the route
130 * to a regular expression and add it to the routing table
131 *
132 * @param string $method
133 * @param string $route
134 * @param array|string|callable $action
135 *
136 * @throws ControllerNotFoundException|InvalidActionException|MethodNotFoundException|RequestMethodException
137 */
138 public function add(string $method, string $route, array|string|callable $action)
139 {
140 if ($this->checkMethod($method)) {
141 if ($this->checkAction($action)) {
142 $route = preg_replace('/\//', '\\/', $route);
143
144 $route = preg_replace('/:([a-z0-9]+)/', '(?<\1>[a-z0-9]+)', $route);
145
146 $route = '/^' . $route . '$/i';
147
148 $this->routes[$method][$route] = ["action" => $action];
149 }
150 }
151 }
152
153 /**
154 * If the given request method is valid then return true
155 * if not throw an RequestMethodException
156 *
157 * @param string $method
158 *
159 * @return bool
160 * @throws RequestMethodException
161 */
162 private function checkMethod(string $method): bool
163 {
164 $result = match ($method) {
165 'get', 'post', 'put', 'patch', 'delete' => true,
166 default => false
167 };
168
169 if ($result === false) {
170 throw new RequestMethodException();
171 }
172 return true;
173 }
174
175 /**
176 * Check if the given argument contains a valid action,
177 * if it contains a valid action then split the Controller and Method.
178 * If the Controller exists then check if the Controller contains the
179 * Method given if so return true else throw and Exception
180 *
181 * @param string|callable $action
182 *
183 * @return bool
184 * @throws ControllerNotFoundException|InvalidActionException|MethodNotFoundException
185 */
186 private function checkAction(string|callable $action): bool
187 {
188 if (is_string($action)) {
189 if (!str_contains($action, '@')) {
190 throw new InvalidActionException(
191 "Your passed action '$action' is not valid. Please follow this syntax: Controller@methodInController"
192 );
193 }
194
195 $action = explode('@', $action);
196
197 $controller = $this->controllerPath . str_replace('/', '\\', $action[0]);
198
199 $method = $action[1];
200
201 if (!class_exists($controller)) {
202 throw new ControllerNotFoundException("Your passed controller '$controller' couldn't be found");
203 } elseif (!method_exists($controller, $method)) {
204 throw new MethodNotFoundException(
205 "Your passed method '$method' couldn't be found in controller '$controller'
206 "
207 );
208 }
209 }
210 return true;
211 }
212
213
214 /**
215 * Split the Controller from the Method and return them
216 *
217 * @param string $controllerAndMethod
218 *
219 * @return array
220 */
221 #[ArrayShape(['controller' => "string", 'method' => "string"])] private function getControllerAndMethod(string $controllerAndMethod): array
222 {
223 $controllerAndMethod = explode('@', $controllerAndMethod);
224
225 $controller = $this->controllerPath . str_replace('/', '\\', $controllerAndMethod[0]);
226
227 return ['controller' => $controller, 'method' => $controllerAndMethod[1]];
228 }
229}