PageRenderTime 59ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/ManaPHP/Mvc/Dispatcher.php

https://gitlab.com/szlongshu/manaphp
PHP | 446 lines | 217 code | 71 blank | 158 comment | 36 complexity | 2505c282d602b0665666deb62761c34c MD5 | raw file
  1. <?php
  2. namespace ManaPHP\Mvc {
  3. use ManaPHP\Component;
  4. use ManaPHP\Mvc\Dispatcher\Exception;
  5. use ManaPHP\Mvc\Dispatcher\NotFoundActionException;
  6. use ManaPHP\Mvc\Dispatcher\NotFoundControllerException;
  7. /**
  8. * ManaPHP\Mvc\Dispatcher
  9. *
  10. * Dispatching is the process of taking the request object, extracting the module name,
  11. * controller name, action name, and optional parameters contained in it, and then
  12. * instantiating a controller and calling an action of that controller.
  13. *
  14. *<code>
  15. *
  16. * $di = new ManaPHP\Di();
  17. *
  18. * $dispatcher = new ManaPHP\Mvc\Dispatcher();
  19. *
  20. * $dispatcher->setDI($di);
  21. *
  22. * $controller = $dispatcher->dispatch('app','posts','index');
  23. *
  24. *</code>
  25. */
  26. class Dispatcher extends Component implements DispatcherInterface
  27. {
  28. /**
  29. * @var boolean
  30. */
  31. protected $_finished = false;
  32. /**
  33. * @var boolean
  34. */
  35. protected $_forwarded = false;
  36. /**
  37. * @var string
  38. */
  39. protected $_moduleName;
  40. /**
  41. * @var string
  42. */
  43. protected $_rootNamespace;
  44. /**
  45. * @var string
  46. */
  47. protected $_controllerName;
  48. /**
  49. * @var string
  50. */
  51. protected $_actionName;
  52. /**
  53. * @var array
  54. */
  55. protected $_params = [];
  56. /**
  57. * @var mixed
  58. */
  59. protected $_returnedValue;
  60. /**
  61. * @var string
  62. */
  63. protected $_controllerSuffix = 'Controller';
  64. /**
  65. * @var string
  66. */
  67. protected $_actionSuffix = 'Action';
  68. /**
  69. * @var string
  70. */
  71. protected $_previousControllerName;
  72. /**
  73. * @var string
  74. */
  75. protected $_previousActionName;
  76. /**
  77. * @var array
  78. */
  79. protected $_initializedControllers = [];
  80. /**
  81. * Sets the namespace where the controller class is
  82. *
  83. * @param string $namespaceName
  84. *
  85. * @return static
  86. */
  87. public function setRootNamespace($namespaceName)
  88. {
  89. $this->_rootNamespace = $namespaceName;
  90. return $this;
  91. }
  92. /**
  93. * Gets a namespace to be prepended to the current handler name
  94. *
  95. * @return string
  96. */
  97. public function getRootNamespace()
  98. {
  99. return $this->_rootNamespace;
  100. }
  101. /**
  102. * Gets the module where the controller class is
  103. *
  104. * @return string
  105. */
  106. public function getModuleName()
  107. {
  108. return $this->_moduleName;
  109. }
  110. /**
  111. * Gets the latest dispatched action name
  112. *
  113. * @return string
  114. */
  115. public function getActionName()
  116. {
  117. return $this->_actionName;
  118. }
  119. /**
  120. * Gets action params
  121. *
  122. * @return array
  123. */
  124. public function getParams()
  125. {
  126. return $this->_params;
  127. }
  128. /**
  129. * Gets a param by its name or numeric index
  130. *
  131. * @param string|int $param
  132. * @param string|array $filters
  133. * @param mixed $defaultValue
  134. *
  135. * @return mixed
  136. * @throws \ManaPHP\Mvc\Dispatcher\Exception
  137. */
  138. public function getParam($param, $filters = null, $defaultValue = null)
  139. {
  140. if (!isset($this->_params[$param])) {
  141. return $defaultValue;
  142. }
  143. if ($filters === null) {
  144. return $this->_params[$param];
  145. }
  146. if (!is_object($this->_dependencyInjector)) {
  147. throw new Exception("A dependency injection object is required to access the 'filter' service");
  148. }
  149. return null;
  150. }
  151. /**
  152. * Sets the latest returned value by an action manually
  153. *
  154. * @param mixed $value
  155. *
  156. * @return static
  157. */
  158. public function setReturnedValue($value)
  159. {
  160. $this->_returnedValue = $value;
  161. return $this;
  162. }
  163. /**
  164. * Returns value returned by the latest dispatched action
  165. *
  166. * @return mixed
  167. */
  168. public function getReturnedValue()
  169. {
  170. return $this->_returnedValue;
  171. }
  172. /**
  173. * Dispatches a handle action taking into account the routing parameters
  174. *
  175. * @param string $module
  176. * @param string $controller
  177. * @param string $action
  178. * @param array $params
  179. *
  180. * @return false|\ManaPHP\Mvc\ControllerInterface
  181. * @throws \ManaPHP\Mvc\Dispatcher\Exception
  182. */
  183. public function dispatch($module, $controller, $action, $params = null)
  184. {
  185. $this->_moduleName = $this->_camelize($module);
  186. $this->_controllerName = $this->_camelize($controller);
  187. $this->_actionName = lcfirst($this->_camelize($action));
  188. $this->_params = $params === null ? [] : $params;
  189. if ($this->fireEvent('dispatcher:beforeDispatchLoop') === false) {
  190. return false;
  191. }
  192. $controllerInstance = null;
  193. $numberDispatches = 0;
  194. $this->_finished = false;
  195. while ($this->_finished === false) {
  196. // if the user made a forward in the listener,the $this->_finished will be changed to false.
  197. $this->_finished = true;
  198. if ($numberDispatches++ === 32) {
  199. throw new Exception('Dispatcher has detected a cyclic routing causing stability problems');
  200. }
  201. $this->fireEvent('dispatcher:beforeDispatch');
  202. if ($this->_finished === false) {
  203. continue;
  204. }
  205. $controllerClassName = '';
  206. if ($this->_rootNamespace !== null && $this->_rootNamespace !== '') {
  207. $controllerClassName .= $this->_rootNamespace . '\\';
  208. }
  209. if ($this->_rootNamespace !== null && $this->_moduleName !== '') {
  210. $controllerClassName .= $this->_moduleName . '\\Controllers\\';
  211. }
  212. $controllerClassName .= $this->_controllerName . $this->_controllerSuffix;
  213. if (!$this->_dependencyInjector->has($controllerClassName) && !class_exists($controllerClassName)) {
  214. if ($this->fireEvent('dispatcher:beforeNotFoundController') === false) {
  215. return false;
  216. }
  217. if ($this->_finished === false) {
  218. continue;
  219. }
  220. throw new NotFoundControllerException($controllerClassName . ' handler class cannot be loaded');
  221. }
  222. $controllerInstance = $this->_dependencyInjector->getShared($controllerClassName);
  223. if (!is_object($controllerInstance)) {
  224. throw new Exception('Invalid handler type returned from the services container: ' . gettype($controllerInstance));
  225. }
  226. $hasAction = false;
  227. $actionMethod = strtolower($this->_actionName . $this->_actionSuffix);
  228. foreach (get_class_methods($controllerInstance) as $method) {
  229. if ($actionMethod === strtolower($method)) {
  230. if (substr($method, -strlen($this->_actionSuffix)) !== $this->_actionSuffix) {
  231. throw new Exception("The action '$method' of {$this->_controllerName}{$this->_controllerSuffix} does not suffix with '{$this->_actionSuffix}' case sensitively, please amend it first.");
  232. }
  233. if (strtolower($method['0']) !== $method['0']) {
  234. throw new Exception("The action '$method' of {$this->_controllerName}{$this->_controllerSuffix} does not prefix with lowercase character, please amend it first.");
  235. }
  236. $hasAction = true;
  237. $this->_actionName = substr($method, 0, -strlen($this->_actionSuffix));
  238. $actionMethod = $method;
  239. break;
  240. }
  241. }
  242. if (!$hasAction) {
  243. if ($this->fireEvent('dispatcher:beforeNotFoundAction') === false) {
  244. continue;
  245. }
  246. if ($this->_finished === false) {
  247. continue;
  248. }
  249. throw new NotFoundActionException('Action \'' . $this->_actionName . '\' was not found on handler \'' . $controllerClassName . '\'');
  250. }
  251. // Calling beforeExecuteRoute as callback
  252. if (method_exists($controllerInstance, 'beforeExecuteRoute')) {
  253. if ($controllerInstance->beforeExecuteRoute($this) === false) {
  254. continue;
  255. }
  256. if ($this->_finished === false) {
  257. continue;
  258. }
  259. }
  260. if (!in_array($controllerClassName, $this->_initializedControllers,
  261. true) && method_exists($controllerInstance, 'initialize')
  262. ) {
  263. $controllerInstance->initialize();
  264. $this->_initializedControllers[] = $controllerClassName;
  265. }
  266. $this->_returnedValue = call_user_func_array([$controllerInstance, $actionMethod], $this->_params);
  267. $value = null;
  268. // Call afterDispatch
  269. $this->fireEvent('dispatcher:afterDispatch');
  270. if (method_exists($controllerInstance, 'afterExecuteRoute')) {
  271. if ($controllerInstance->afterExecuteRoute($this, $value) === false) {
  272. continue;
  273. }
  274. if ($this->_finished === false) {
  275. continue;
  276. }
  277. }
  278. }
  279. $this->fireEvent('dispatcher:afterDispatchLoop');
  280. return $controllerInstance;
  281. }
  282. /**
  283. * Forwards the execution flow to another controller/action
  284. * Dispatchers are unique per module. Forwarding between modules is not allowed
  285. *
  286. *<code>
  287. * $this->dispatcher->forward(array('controller' => 'posts', 'action' => 'index'));
  288. *</code>
  289. *
  290. * @param string|array $forward
  291. *
  292. * @throws \ManaPHP\Mvc\Dispatcher\Exception
  293. */
  294. public function forward($forward)
  295. {
  296. if (is_string($forward)) {
  297. if ($forward[0] === '/') {
  298. throw new Exception('Forward path starts with / character is confused, please remove it');
  299. }
  300. $_forward = [];
  301. list($_forward['module'], $_forward['controller'], $_forward['action']) = array_pad(explode('/', $forward), -3, null);
  302. $forward = $_forward;
  303. }
  304. if (isset($forward['module'])) {
  305. $this->_moduleName = $this->_camelize($forward['module']);
  306. }
  307. if (isset($forward['controller'])) {
  308. $this->_previousControllerName = $this->_controllerName;
  309. $this->_controllerName = $this->_camelize($forward['controller']);
  310. }
  311. if (isset($forward['action'])) {
  312. $this->_previousActionName = $this->_actionName;
  313. $this->_actionName = lcfirst($this->_camelize($forward['action']));
  314. }
  315. if (isset($forward['params'])) {
  316. $this->_params = $forward['params'];
  317. }
  318. $this->_finished = false;
  319. $this->_forwarded = true;
  320. }
  321. /**
  322. * Check if the current executed action was forwarded by another one
  323. *
  324. * @return boolean
  325. */
  326. public function wasForwarded()
  327. {
  328. return $this->_forwarded;
  329. }
  330. /**
  331. * @param string $str
  332. *
  333. * @return string
  334. */
  335. protected function _camelize($str)
  336. {
  337. if (strpos($str, '_') !== false) {
  338. $parts = explode('_', $str);
  339. foreach ($parts as &$v) {
  340. $v = ucfirst($v);
  341. }
  342. return implode('', $parts);
  343. } else {
  344. return ucfirst($str);
  345. }
  346. }
  347. /**
  348. * Gets last dispatched controller name
  349. *
  350. * @return string
  351. */
  352. public function getControllerName()
  353. {
  354. return $this->_controllerName;
  355. }
  356. /**
  357. * Returns the previous controller in the dispatcher
  358. *
  359. * @return string
  360. */
  361. public function getPreviousControllerName()
  362. {
  363. return $this->_previousControllerName;
  364. }
  365. /**
  366. * Returns the previous action in the dispatcher
  367. *
  368. * @return string
  369. */
  370. public function getPreviousActionName()
  371. {
  372. return $this->_previousActionName;
  373. }
  374. }
  375. }