App.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939
  1. <?php
  2. /**
  3. * This file is part of webman.
  4. *
  5. * Licensed under The MIT License
  6. * For full copyright and license information, please see the MIT-LICENSE.txt
  7. * Redistributions of files must retain the above copyright notice.
  8. *
  9. * @author walkor<walkor@workerman.net>
  10. * @copyright walkor<walkor@workerman.net>
  11. * @link http://www.workerman.net/
  12. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  13. */
  14. namespace Webman;
  15. use Closure;
  16. use Exception;
  17. use FastRoute\Dispatcher;
  18. use InvalidArgumentException;
  19. use Monolog\Logger;
  20. use Psr\Container\ContainerExceptionInterface;
  21. use Psr\Container\ContainerInterface;
  22. use Psr\Container\NotFoundExceptionInterface;
  23. use ReflectionClass;
  24. use ReflectionException;
  25. use ReflectionFunction;
  26. use ReflectionFunctionAbstract;
  27. use ReflectionMethod;
  28. use Throwable;
  29. use Webman\Exception\ExceptionHandler;
  30. use Webman\Exception\ExceptionHandlerInterface;
  31. use Webman\Http\Request;
  32. use Webman\Http\Response;
  33. use Webman\Route\Route as RouteObject;
  34. use Workerman\Connection\TcpConnection;
  35. use Workerman\Protocols\Http;
  36. use Workerman\Worker;
  37. use function array_merge;
  38. use function array_pop;
  39. use function array_reduce;
  40. use function array_reverse;
  41. use function array_splice;
  42. use function array_values;
  43. use function class_exists;
  44. use function clearstatcache;
  45. use function count;
  46. use function current;
  47. use function end;
  48. use function explode;
  49. use function file_get_contents;
  50. use function get_class_methods;
  51. use function gettype;
  52. use function implode;
  53. use function in_array;
  54. use function is_a;
  55. use function is_array;
  56. use function is_dir;
  57. use function is_file;
  58. use function is_string;
  59. use function key;
  60. use function method_exists;
  61. use function next;
  62. use function ob_get_clean;
  63. use function ob_start;
  64. use function pathinfo;
  65. use function scandir;
  66. use function str_replace;
  67. use function strpos;
  68. use function strtolower;
  69. use function substr;
  70. use function trim;
  71. /**
  72. * Class App
  73. * @package Webman
  74. */
  75. class App
  76. {
  77. /**
  78. * @var callable[]
  79. */
  80. protected static $callbacks = [];
  81. /**
  82. * @var Worker
  83. */
  84. protected static $worker = null;
  85. /**
  86. * @var Logger
  87. */
  88. protected static $logger = null;
  89. /**
  90. * @var string
  91. */
  92. protected static $appPath = '';
  93. /**
  94. * @var string
  95. */
  96. protected static $publicPath = '';
  97. /**
  98. * @var string
  99. */
  100. protected static $requestClass = '';
  101. /**
  102. * App constructor.
  103. * @param string $requestClass
  104. * @param Logger $logger
  105. * @param string $appPath
  106. * @param string $publicPath
  107. */
  108. public function __construct(string $requestClass, Logger $logger, string $appPath, string $publicPath)
  109. {
  110. static::$requestClass = $requestClass;
  111. static::$logger = $logger;
  112. static::$publicPath = $publicPath;
  113. static::$appPath = $appPath;
  114. }
  115. /**
  116. * OnMessage.
  117. * @param TcpConnection|mixed $connection
  118. * @param Request|mixed $request
  119. * @return null
  120. */
  121. public function onMessage($connection, $request)
  122. {
  123. try {
  124. Context::set(Request::class, $request);
  125. $path = $request->path();
  126. $key = $request->method() . $path;
  127. if (isset(static::$callbacks[$key])) {
  128. [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];
  129. static::send($connection, $callback($request), $request);
  130. return null;
  131. }
  132. if (
  133. static::unsafeUri($connection, $path, $request) ||
  134. static::findFile($connection, $path, $key, $request) ||
  135. static::findRoute($connection, $path, $key, $request)
  136. ) {
  137. return null;
  138. }
  139. $controllerAndAction = static::parseControllerAction($path);
  140. $plugin = $controllerAndAction['plugin'] ?? static::getPluginByPath($path);
  141. if (!$controllerAndAction || Route::hasDisableDefaultRoute($plugin)) {
  142. $request->plugin = $plugin;
  143. $callback = static::getFallback($plugin);
  144. $request->app = $request->controller = $request->action = '';
  145. static::send($connection, $callback($request), $request);
  146. return null;
  147. }
  148. $app = $controllerAndAction['app'];
  149. $controller = $controllerAndAction['controller'];
  150. $action = $controllerAndAction['action'];
  151. $callback = static::getCallback($plugin, $app, [$controller, $action]);
  152. static::collectCallbacks($key, [$callback, $plugin, $app, $controller, $action, null]);
  153. [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];
  154. static::send($connection, $callback($request), $request);
  155. } catch (Throwable $e) {
  156. static::send($connection, static::exceptionResponse($e, $request), $request);
  157. }
  158. return null;
  159. }
  160. /**
  161. * OnWorkerStart.
  162. * @param $worker
  163. * @return void
  164. */
  165. public function onWorkerStart($worker)
  166. {
  167. static::$worker = $worker;
  168. Http::requestClass(static::$requestClass);
  169. }
  170. /**
  171. * CollectCallbacks.
  172. * @param string $key
  173. * @param array $data
  174. * @return void
  175. */
  176. protected static function collectCallbacks(string $key, array $data)
  177. {
  178. static::$callbacks[$key] = $data;
  179. if (count(static::$callbacks) >= 1024) {
  180. unset(static::$callbacks[key(static::$callbacks)]);
  181. }
  182. }
  183. /**
  184. * UnsafeUri.
  185. * @param TcpConnection $connection
  186. * @param string $path
  187. * @param $request
  188. * @return bool
  189. */
  190. protected static function unsafeUri(TcpConnection $connection, string $path, $request): bool
  191. {
  192. if (
  193. !$path ||
  194. strpos($path, '..') !== false ||
  195. strpos($path, "\\") !== false ||
  196. strpos($path, "\0") !== false
  197. ) {
  198. $callback = static::getFallback();
  199. $request->plugin = $request->app = $request->controller = $request->action = '';
  200. static::send($connection, $callback($request), $request);
  201. return true;
  202. }
  203. return false;
  204. }
  205. /**
  206. * GetFallback.
  207. * @param string $plugin
  208. * @return Closure
  209. */
  210. protected static function getFallback(string $plugin = ''): Closure
  211. {
  212. // when route, controller and action not found, try to use Route::fallback
  213. return Route::getFallback($plugin) ?: function () {
  214. try {
  215. $notFoundContent = file_get_contents(static::$publicPath . '/404.html');
  216. } catch (Throwable $e) {
  217. $notFoundContent = '404 Not Found';
  218. }
  219. return new Response(404, [], $notFoundContent);
  220. };
  221. }
  222. /**
  223. * ExceptionResponse.
  224. * @param Throwable $e
  225. * @param $request
  226. * @return Response
  227. */
  228. protected static function exceptionResponse(Throwable $e, $request): Response
  229. {
  230. try {
  231. $app = $request->app ?: '';
  232. $plugin = $request->plugin ?: '';
  233. $exceptionConfig = static::config($plugin, 'exception');
  234. $defaultException = $exceptionConfig[''] ?? ExceptionHandler::class;
  235. $exceptionHandlerClass = $exceptionConfig[$app] ?? $defaultException;
  236. /** @var ExceptionHandlerInterface $exceptionHandler */
  237. $exceptionHandler = static::container($plugin)->make($exceptionHandlerClass, [
  238. 'logger' => static::$logger,
  239. 'debug' => static::config($plugin, 'app.debug')
  240. ]);
  241. $exceptionHandler->report($e);
  242. $response = $exceptionHandler->render($request, $e);
  243. $response->exception($e);
  244. return $response;
  245. } catch (Throwable $e) {
  246. $response = new Response(500, [], static::config($plugin ?? '', 'app.debug') ? (string)$e : $e->getMessage());
  247. $response->exception($e);
  248. return $response;
  249. }
  250. }
  251. /**
  252. * GetCallback.
  253. * @param string $plugin
  254. * @param string $app
  255. * @param $call
  256. * @param array|null $args
  257. * @param bool $withGlobalMiddleware
  258. * @param RouteObject|null $route
  259. * @return callable
  260. * @throws ContainerExceptionInterface
  261. * @throws NotFoundExceptionInterface
  262. * @throws ReflectionException
  263. */
  264. protected static function getCallback(string $plugin, string $app, $call, array $args = null, bool $withGlobalMiddleware = true, RouteObject $route = null)
  265. {
  266. $args = $args === null ? null : array_values($args);
  267. $middlewares = [];
  268. if ($route) {
  269. $routeMiddlewares = $route->getMiddleware();
  270. foreach ($routeMiddlewares as $className) {
  271. $middlewares[] = [$className, 'process'];
  272. }
  273. }
  274. $middlewares = array_merge($middlewares, Middleware::getMiddleware($plugin, $app, $withGlobalMiddleware));
  275. foreach ($middlewares as $key => $item) {
  276. $middleware = $item[0];
  277. if (is_string($middleware)) {
  278. $middleware = static::container($plugin)->get($middleware);
  279. } elseif ($middleware instanceof Closure) {
  280. $middleware = call_user_func($middleware, static::container($plugin));
  281. }
  282. if (!$middleware instanceof MiddlewareInterface) {
  283. throw new InvalidArgumentException('Not support middleware type');
  284. }
  285. $middlewares[$key][0] = $middleware;
  286. }
  287. $needInject = static::isNeedInject($call, $args);
  288. if (is_array($call) && is_string($call[0])) {
  289. $controllerReuse = static::config($plugin, 'app.controller_reuse', true);
  290. if (!$controllerReuse) {
  291. if ($needInject) {
  292. $call = function ($request, ...$args) use ($call, $plugin) {
  293. $call[0] = static::container($plugin)->make($call[0]);
  294. $reflector = static::getReflector($call);
  295. $args = static::resolveMethodDependencies($plugin, $request, $args, $reflector);
  296. return $call(...$args);
  297. };
  298. $needInject = false;
  299. } else {
  300. $call = function ($request, ...$args) use ($call, $plugin) {
  301. $call[0] = static::container($plugin)->make($call[0]);
  302. return $call($request, ...$args);
  303. };
  304. }
  305. } else {
  306. $call[0] = static::container($plugin)->get($call[0]);
  307. }
  308. }
  309. if ($needInject) {
  310. $call = static::resolveInject($plugin, $call);
  311. }
  312. if ($middlewares) {
  313. $callback = array_reduce($middlewares, function ($carry, $pipe) {
  314. return function ($request) use ($carry, $pipe) {
  315. try {
  316. return $pipe($request, $carry);
  317. } catch (Throwable $e) {
  318. return static::exceptionResponse($e, $request);
  319. }
  320. };
  321. }, function ($request) use ($call, $args) {
  322. try {
  323. if ($args === null) {
  324. $response = $call($request);
  325. } else {
  326. $response = $call($request, ...$args);
  327. }
  328. } catch (Throwable $e) {
  329. return static::exceptionResponse($e, $request);
  330. }
  331. if (!$response instanceof Response) {
  332. if (!is_string($response)) {
  333. $response = static::stringify($response);
  334. }
  335. $response = new Response(200, [], $response);
  336. }
  337. return $response;
  338. });
  339. } else {
  340. if ($args === null) {
  341. $callback = $call;
  342. } else {
  343. $callback = function ($request) use ($call, $args) {
  344. return $call($request, ...$args);
  345. };
  346. }
  347. }
  348. return $callback;
  349. }
  350. /**
  351. * ResolveInject.
  352. * @param string $plugin
  353. * @param array|Closure $call
  354. * @return Closure
  355. * @see Dependency injection through reflection information
  356. */
  357. protected static function resolveInject(string $plugin, $call): Closure
  358. {
  359. return function (Request $request, ...$args) use ($plugin, $call) {
  360. $reflector = static::getReflector($call);
  361. $args = static::resolveMethodDependencies($plugin, $request, $args, $reflector);
  362. return $call(...$args);
  363. };
  364. }
  365. /**
  366. * Check whether inject is required.
  367. * @param $call
  368. * @param $args
  369. * @return bool
  370. * @throws ReflectionException
  371. */
  372. protected static function isNeedInject($call, $args): bool
  373. {
  374. if (is_array($call) && !method_exists($call[0], $call[1])) {
  375. return false;
  376. }
  377. $args = $args ?: [];
  378. $reflector = static::getReflector($call);
  379. $reflectionParameters = $reflector->getParameters();
  380. if (!$reflectionParameters) {
  381. return false;
  382. }
  383. $firstParameter = current($reflectionParameters);
  384. unset($reflectionParameters[key($reflectionParameters)]);
  385. $adaptersList = ['int', 'string', 'bool', 'array', 'object', 'float', 'mixed', 'resource'];
  386. foreach ($reflectionParameters as $parameter) {
  387. if ($parameter->hasType() && !in_array($parameter->getType()->getName(), $adaptersList)) {
  388. return true;
  389. }
  390. }
  391. if (!$firstParameter->hasType()) {
  392. return count($args) > count($reflectionParameters);
  393. }
  394. if (!is_a(static::$requestClass, $firstParameter->getType()->getName())) {
  395. return true;
  396. }
  397. return false;
  398. }
  399. /**
  400. * Get reflector.
  401. * @param $call
  402. * @return ReflectionFunction|ReflectionMethod
  403. * @throws ReflectionException
  404. */
  405. protected static function getReflector($call)
  406. {
  407. if ($call instanceof Closure || is_string($call)) {
  408. return new ReflectionFunction($call);
  409. }
  410. return new ReflectionMethod($call[0], $call[1]);
  411. }
  412. /**
  413. * Return dependent parameters
  414. * @param string $plugin
  415. * @param Request $request
  416. * @param array $args
  417. * @param ReflectionFunctionAbstract $reflector
  418. * @return array
  419. */
  420. protected static function resolveMethodDependencies(string $plugin, Request $request, array $args, ReflectionFunctionAbstract $reflector): array
  421. {
  422. // Specification parameter information
  423. $args = array_values($args);
  424. $parameters = [];
  425. // An array of reflection classes for loop parameters, with each $parameter representing a reflection object of parameters
  426. foreach ($reflector->getParameters() as $parameter) {
  427. // Parameter quota consumption
  428. if ($parameter->hasType()) {
  429. $name = $parameter->getType()->getName();
  430. switch ($name) {
  431. case 'int':
  432. case 'string':
  433. case 'bool':
  434. case 'array':
  435. case 'object':
  436. case 'float':
  437. case 'mixed':
  438. case 'resource':
  439. goto _else;
  440. default:
  441. if (is_a($request, $name)) {
  442. //Inject Request
  443. $parameters[] = $request;
  444. } else {
  445. $parameters[] = static::container($plugin)->make($name);
  446. }
  447. break;
  448. }
  449. } else {
  450. _else:
  451. // The variable parameter
  452. if (null !== key($args)) {
  453. $parameters[] = current($args);
  454. } else {
  455. // Indicates whether the current parameter has a default value. If yes, return true
  456. $parameters[] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
  457. }
  458. // Quota of consumption variables
  459. next($args);
  460. }
  461. }
  462. // Returns the result of parameters replacement
  463. return $parameters;
  464. }
  465. /**
  466. * Container.
  467. * @param string $plugin
  468. * @return ContainerInterface
  469. */
  470. public static function container(string $plugin = '')
  471. {
  472. return static::config($plugin, 'container');
  473. }
  474. /**
  475. * Get request.
  476. * @return Request|\support\Request
  477. */
  478. public static function request()
  479. {
  480. return Context::get(Request::class);
  481. }
  482. /**
  483. * Get worker.
  484. * @return Worker
  485. */
  486. public static function worker(): ?Worker
  487. {
  488. return static::$worker;
  489. }
  490. /**
  491. * Find Route.
  492. * @param TcpConnection $connection
  493. * @param string $path
  494. * @param string $key
  495. * @param Request|mixed $request
  496. * @return bool
  497. * @throws ContainerExceptionInterface
  498. * @throws NotFoundExceptionInterface
  499. * @throws ReflectionException
  500. */
  501. protected static function findRoute(TcpConnection $connection, string $path, string $key, $request): bool
  502. {
  503. $routeInfo = Route::dispatch($request->method(), $path);
  504. if ($routeInfo[0] === Dispatcher::FOUND) {
  505. $routeInfo[0] = 'route';
  506. $callback = $routeInfo[1]['callback'];
  507. $route = clone $routeInfo[1]['route'];
  508. $app = $controller = $action = '';
  509. $args = !empty($routeInfo[2]) ? $routeInfo[2] : null;
  510. if ($args) {
  511. $route->setParams($args);
  512. }
  513. if (is_array($callback)) {
  514. $controller = $callback[0];
  515. $plugin = static::getPluginByClass($controller);
  516. $app = static::getAppByController($controller);
  517. $action = static::getRealMethod($controller, $callback[1]) ?? '';
  518. } else {
  519. $plugin = static::getPluginByPath($path);
  520. }
  521. $callback = static::getCallback($plugin, $app, $callback, $args, true, $route);
  522. static::collectCallbacks($key, [$callback, $plugin, $app, $controller ?: '', $action, $route]);
  523. [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];
  524. static::send($connection, $callback($request), $request);
  525. return true;
  526. }
  527. return false;
  528. }
  529. /**
  530. * Find File.
  531. * @param TcpConnection $connection
  532. * @param string $path
  533. * @param string $key
  534. * @param Request|mixed $request
  535. * @return bool
  536. * @throws ContainerExceptionInterface
  537. * @throws NotFoundExceptionInterface
  538. * @throws ReflectionException
  539. */
  540. protected static function findFile(TcpConnection $connection, string $path, string $key, $request): bool
  541. {
  542. if (preg_match('/%[0-9a-f]{2}/i', $path)) {
  543. $path = urldecode($path);
  544. if (static::unsafeUri($connection, $path, $request)) {
  545. return true;
  546. }
  547. }
  548. $pathExplodes = explode('/', trim($path, '/'));
  549. $plugin = '';
  550. if (isset($pathExplodes[1]) && $pathExplodes[0] === 'app') {
  551. $plugin = $pathExplodes[1];
  552. $publicDir = static::config($plugin, 'app.public_path') ?: BASE_PATH . "/plugin/$pathExplodes[1]/public";
  553. $path = substr($path, strlen("/app/$pathExplodes[1]/"));
  554. } else {
  555. $publicDir = static::$publicPath;
  556. }
  557. $file = "$publicDir/$path";
  558. if (!is_file($file)) {
  559. return false;
  560. }
  561. if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
  562. if (!static::config($plugin, 'app.support_php_files', false)) {
  563. return false;
  564. }
  565. static::collectCallbacks($key, [function () use ($file) {
  566. return static::execPhpFile($file);
  567. }, '', '', '', '', null]);
  568. [, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];
  569. static::send($connection, static::execPhpFile($file), $request);
  570. return true;
  571. }
  572. if (!static::config($plugin, 'static.enable', false)) {
  573. return false;
  574. }
  575. static::collectCallbacks($key, [static::getCallback($plugin, '__static__', function ($request) use ($file, $plugin) {
  576. clearstatcache(true, $file);
  577. if (!is_file($file)) {
  578. $callback = static::getFallback($plugin);
  579. return $callback($request);
  580. }
  581. return (new Response())->file($file);
  582. }, null, false), '', '', '', '', null]);
  583. [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];
  584. static::send($connection, $callback($request), $request);
  585. return true;
  586. }
  587. /**
  588. * Send.
  589. * @param TcpConnection|mixed $connection
  590. * @param mixed $response
  591. * @param Request|mixed $request
  592. * @return void
  593. */
  594. protected static function send($connection, $response, $request)
  595. {
  596. $keepAlive = $request->header('connection');
  597. Context::destroy();
  598. if (($keepAlive === null && $request->protocolVersion() === '1.1')
  599. || $keepAlive === 'keep-alive' || $keepAlive === 'Keep-Alive'
  600. ) {
  601. $connection->send($response);
  602. return;
  603. }
  604. $connection->close($response);
  605. }
  606. /**
  607. * ParseControllerAction.
  608. * @param string $path
  609. * @return array|false
  610. * @throws ReflectionException
  611. */
  612. protected static function parseControllerAction(string $path)
  613. {
  614. $path = str_replace(['-', '//'], ['', '/'], $path);
  615. static $cache = [];
  616. if (isset($cache[$path])) {
  617. return $cache[$path];
  618. }
  619. $pathExplode = explode('/', trim($path, '/'));
  620. $isPlugin = isset($pathExplode[1]) && $pathExplode[0] === 'app';
  621. $configPrefix = $isPlugin ? "plugin.$pathExplode[1]." : '';
  622. $pathPrefix = $isPlugin ? "/app/$pathExplode[1]" : '';
  623. $classPrefix = $isPlugin ? "plugin\\$pathExplode[1]" : '';
  624. $suffix = Config::get("{$configPrefix}app.controller_suffix", '');
  625. $relativePath = trim(substr($path, strlen($pathPrefix)), '/');
  626. $pathExplode = $relativePath ? explode('/', $relativePath) : [];
  627. $action = 'index';
  628. if (!$controllerAction = static::guessControllerAction($pathExplode, $action, $suffix, $classPrefix)) {
  629. if (count($pathExplode) <= 1) {
  630. return false;
  631. }
  632. $action = end($pathExplode);
  633. unset($pathExplode[count($pathExplode) - 1]);
  634. $controllerAction = static::guessControllerAction($pathExplode, $action, $suffix, $classPrefix);
  635. }
  636. if ($controllerAction && !isset($path[256])) {
  637. $cache[$path] = $controllerAction;
  638. if (count($cache) > 1024) {
  639. unset($cache[key($cache)]);
  640. }
  641. }
  642. return $controllerAction;
  643. }
  644. /**
  645. * GuessControllerAction.
  646. * @param $pathExplode
  647. * @param $action
  648. * @param $suffix
  649. * @param $classPrefix
  650. * @return array|false
  651. * @throws ReflectionException
  652. */
  653. protected static function guessControllerAction($pathExplode, $action, $suffix, $classPrefix)
  654. {
  655. $map[] = trim("$classPrefix\\app\\controller\\" . implode('\\', $pathExplode), '\\');
  656. foreach ($pathExplode as $index => $section) {
  657. $tmp = $pathExplode;
  658. array_splice($tmp, $index, 1, [$section, 'controller']);
  659. $map[] = trim("$classPrefix\\" . implode('\\', array_merge(['app'], $tmp)), '\\');
  660. }
  661. foreach ($map as $item) {
  662. $map[] = $item . '\\index';
  663. }
  664. foreach ($map as $controllerClass) {
  665. // Remove xx\xx\controller
  666. if (substr($controllerClass, -11) === '\\controller') {
  667. continue;
  668. }
  669. $controllerClass .= $suffix;
  670. if ($controllerAction = static::getControllerAction($controllerClass, $action)) {
  671. return $controllerAction;
  672. }
  673. }
  674. return false;
  675. }
  676. /**
  677. * GetControllerAction.
  678. * @param string $controllerClass
  679. * @param string $action
  680. * @return array|false
  681. * @throws ReflectionException
  682. */
  683. protected static function getControllerAction(string $controllerClass, string $action)
  684. {
  685. // Disable calling magic methods
  686. if (strpos($action, '__') === 0) {
  687. return false;
  688. }
  689. if (($controllerClass = static::getController($controllerClass)) && ($action = static::getAction($controllerClass, $action))) {
  690. return [
  691. 'plugin' => static::getPluginByClass($controllerClass),
  692. 'app' => static::getAppByController($controllerClass),
  693. 'controller' => $controllerClass,
  694. 'action' => $action
  695. ];
  696. }
  697. return false;
  698. }
  699. /**
  700. * GetController.
  701. * @param string $controllerClass
  702. * @return string|false
  703. * @throws ReflectionException
  704. */
  705. protected static function getController(string $controllerClass)
  706. {
  707. if (class_exists($controllerClass)) {
  708. return (new ReflectionClass($controllerClass))->name;
  709. }
  710. $explodes = explode('\\', strtolower(ltrim($controllerClass, '\\')));
  711. $basePath = $explodes[0] === 'plugin' ? BASE_PATH . '/plugin' : static::$appPath;
  712. unset($explodes[0]);
  713. $fileName = array_pop($explodes) . '.php';
  714. $found = true;
  715. foreach ($explodes as $pathSection) {
  716. if (!$found) {
  717. break;
  718. }
  719. $dirs = Util::scanDir($basePath, false);
  720. $found = false;
  721. foreach ($dirs as $name) {
  722. $path = "$basePath/$name";
  723. if (is_dir($path) && strtolower($name) === $pathSection) {
  724. $basePath = $path;
  725. $found = true;
  726. break;
  727. }
  728. }
  729. }
  730. if (!$found) {
  731. return false;
  732. }
  733. foreach (scandir($basePath) ?: [] as $name) {
  734. if (strtolower($name) === $fileName) {
  735. require_once "$basePath/$name";
  736. if (class_exists($controllerClass, false)) {
  737. return (new ReflectionClass($controllerClass))->name;
  738. }
  739. }
  740. }
  741. return false;
  742. }
  743. /**
  744. * GetAction.
  745. * @param string $controllerClass
  746. * @param string $action
  747. * @return string|false
  748. */
  749. protected static function getAction(string $controllerClass, string $action)
  750. {
  751. $methods = get_class_methods($controllerClass);
  752. $lowerAction = strtolower($action);
  753. $found = false;
  754. foreach ($methods as $candidate) {
  755. if (strtolower($candidate) === $lowerAction) {
  756. $action = $candidate;
  757. $found = true;
  758. break;
  759. }
  760. }
  761. if ($found) {
  762. return $action;
  763. }
  764. // Action is not public method
  765. if (method_exists($controllerClass, $action)) {
  766. return false;
  767. }
  768. if (method_exists($controllerClass, '__call')) {
  769. return $action;
  770. }
  771. return false;
  772. }
  773. /**
  774. * GetPluginByClass.
  775. * @param string $controllerClass
  776. * @return mixed|string
  777. */
  778. public static function getPluginByClass(string $controllerClass)
  779. {
  780. $controllerClass = trim($controllerClass, '\\');
  781. $tmp = explode('\\', $controllerClass, 3);
  782. if ($tmp[0] !== 'plugin') {
  783. return '';
  784. }
  785. return $tmp[1] ?? '';
  786. }
  787. /**
  788. * GetPluginByPath.
  789. * @param string $path
  790. * @return mixed|string
  791. */
  792. public static function getPluginByPath(string $path)
  793. {
  794. $path = trim($path, '/');
  795. $tmp = explode('/', $path, 3);
  796. if ($tmp[0] !== 'app') {
  797. return '';
  798. }
  799. return $tmp[1] ?? '';
  800. }
  801. /**
  802. * GetAppByController.
  803. * @param string $controllerClass
  804. * @return mixed|string
  805. */
  806. protected static function getAppByController(string $controllerClass)
  807. {
  808. $controllerClass = trim($controllerClass, '\\');
  809. $tmp = explode('\\', $controllerClass, 5);
  810. $pos = $tmp[0] === 'plugin' ? 3 : 1;
  811. if (!isset($tmp[$pos])) {
  812. return '';
  813. }
  814. return strtolower($tmp[$pos]) === 'controller' ? '' : $tmp[$pos];
  815. }
  816. /**
  817. * ExecPhpFile.
  818. * @param string $file
  819. * @return false|string
  820. */
  821. public static function execPhpFile(string $file)
  822. {
  823. ob_start();
  824. // Try to include php file.
  825. try {
  826. include $file;
  827. } catch (Exception $e) {
  828. echo $e;
  829. }
  830. return ob_get_clean();
  831. }
  832. /**
  833. * GetRealMethod.
  834. * @param string $class
  835. * @param string $method
  836. * @return string
  837. */
  838. protected static function getRealMethod(string $class, string $method): string
  839. {
  840. $method = strtolower($method);
  841. $methods = get_class_methods($class);
  842. foreach ($methods as $candidate) {
  843. if (strtolower($candidate) === $method) {
  844. return $candidate;
  845. }
  846. }
  847. return $method;
  848. }
  849. /**
  850. * Config.
  851. * @param string $plugin
  852. * @param string $key
  853. * @param $default
  854. * @return array|mixed|null
  855. */
  856. protected static function config(string $plugin, string $key, $default = null)
  857. {
  858. return Config::get($plugin ? "plugin.$plugin.$key" : $key, $default);
  859. }
  860. /**
  861. * @param mixed $data
  862. * @return string
  863. */
  864. protected static function stringify($data): string
  865. {
  866. $type = gettype($data);
  867. switch ($type) {
  868. case 'boolean':
  869. return $data ? 'true' : 'false';
  870. case 'NULL':
  871. return 'NULL';
  872. case 'array':
  873. return 'Array';
  874. case 'object':
  875. if (!method_exists($data, '__toString')) {
  876. return 'Object';
  877. }
  878. default:
  879. return (string)$data;
  880. }
  881. }
  882. }