app.class.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. <?php
  2. namespace Juju;
  3. use \Juju\{App\Controller, App\Exception, App\Router, Http\Response, Http\Request};
  4. require_once('base.abstract.class.php');
  5. require_once('App/router.class.php');
  6. require_once('App/exception.class.php');
  7. require_once('App/controller.abstract.class.php');
  8. class App extends Base {
  9. use Events;
  10. private static $apps = [];
  11. private $domains;
  12. private $routers;
  13. private $router;
  14. public $_onerror;
  15. public $request = null;
  16. public $response;
  17. public $name;
  18. public $textdomain;
  19. public $textdomain_folder;
  20. public $textdomain_codeset;
  21. public function __construct(string $name, callable $fn = null, array $events = null){
  22. $this->name = $name;
  23. $this->textdomain = $name;
  24. $this->textdomain_folder = __DIR__.'/locale/';
  25. $this->textdomain_codeset = 'UFT-8';
  26. $this->router = new Router();
  27. $this->response = new Response();
  28. $this->routers = [];
  29. $this->domains = [];
  30. $this->_onerror = function($req, $res, $error){
  31. $o = json_encode([
  32. 'message'=>$error->getMessage(),
  33. 'code'=>$error->getCode(),
  34. 'file'=>$error->getFile(),
  35. 'line'=>$error->getLine(),
  36. 'traceback'=>$error->getTrace()
  37. ]);
  38. $res->code(404);
  39. $res->header('Content-Type', 'application/json');
  40. $res->end($o);
  41. $res->shutdown();
  42. };
  43. if(!is_null($events)){
  44. foreach($events as $name => $handler){
  45. $this->on($name, $handler);
  46. }
  47. }
  48. static::$apps[] = $this;
  49. if(is_callable($fn)){
  50. $fn($this);
  51. }
  52. }
  53. public function __destruct(){
  54. $this->routers = [];
  55. $index = array_search($this, static::$apps);
  56. if($index !== false){
  57. array_splice(static::$apps, $index, 1);
  58. }
  59. if(is_callable('parent::__destruct')){
  60. parent::__destruct();
  61. }
  62. }
  63. public function __get($name){
  64. switch($name){
  65. case 'routes':
  66. $routes = $this->router->routes;
  67. foreach($this->routers as $prefix => $router){
  68. foreach($router->routes as $route => $count){
  69. if(!isset($routes[$prefix.$route])){
  70. $routes[$prefix.$route] = 0;
  71. }
  72. $routes[$prefix.$route] += $count;
  73. }
  74. }
  75. foreach($this->domains as $domain => $router){
  76. foreach($router->routes as $route => $count){
  77. if(!isset($routes[$domain.$route])){
  78. $routes[$domain.$route] = 0;
  79. }
  80. $routes[$domain.$route] += $count;
  81. }
  82. }
  83. return $routes;
  84. break;
  85. default:
  86. return parent::__get($name);
  87. }
  88. }
  89. public static function import_all(string $dirpath){
  90. foreach(scandir($dirpath) as $file){
  91. $path = "{$dirpath}/{$file}";
  92. if(is_file($path) && pathinfo($path, PATHINFO_EXTENSION) == "php"){
  93. require_once($path);
  94. }
  95. }
  96. }
  97. public static function shutdown(){
  98. $verb = Request::get_verb();
  99. $url = Request::get_url();
  100. $data = Request::get_body();
  101. if(ob_get_level() > 0){
  102. ob_clean();
  103. }
  104. foreach(static::$apps as $k => $app){
  105. if($app instanceof App){
  106. $app->handle($verb, $url, $data)->shutdown();
  107. }
  108. }
  109. while(ob_get_level() > 0){
  110. ob_end_flush();
  111. }
  112. if(is_callable('parent::__destruct')){
  113. parent::__destruct();
  114. }
  115. }
  116. public static function shutdown_error($error){
  117. while(ob_get_level() > 0){
  118. ob_end_clean();
  119. }
  120. if(count(static::$apps)){
  121. foreach(static::$apps as $k => $app){
  122. if(is_null($app->request)){
  123. $app->request = new Request(Request::get_verb(), Request::get_url(), Request::get_headers(), Request::get_body());
  124. }
  125. $app->response->open(true);
  126. $app->onerror($app->request, $app->response, $error);
  127. $app->response->shutdown();
  128. }
  129. flush();
  130. }else{
  131. $res =new Response();
  132. $res->code(500)
  133. ->header('Content-Type', 'text/plain')
  134. ->write(print_r($error, true))
  135. ->shutdown();
  136. }
  137. flush();
  138. if(is_callable('parent::__destruct')){
  139. parent::__destruct();
  140. }
  141. }
  142. public function handle(string $verb, array $url, string $data, array $headers = null) : Response{
  143. if(is_null($headers)){
  144. $headers = Request::get_headers();
  145. }
  146. $res = $this->response;
  147. $this->request = $req = new Request($verb, $url, $headers, $data);
  148. Response::locale($req->locale, $this->textdomain, $this->textdomain_folder, $this->textdomain_codeset);
  149. if($this->fire('handle', $req, $res)){
  150. $self = $this;
  151. $onerror = function($res, $error) use($self){
  152. $self->onerror($req, $res, $error);
  153. };
  154. $handled = false;
  155. // Domain routers
  156. foreach($this->domains as $host => $router){
  157. if($url['host'] == $host){
  158. while(is_string($router)){
  159. $router = $this->domains[$router];
  160. }
  161. $router->handle($url['path'], $req, $res, null, $onerror);
  162. $handled = $handled || $router->handled;
  163. }
  164. }
  165. // Prefixed path routers
  166. foreach($this->routers as $prefix => $router){
  167. if(strpos($url["path"], $prefix, 0 ) == 0){
  168. $router->handle($url["path"], $req, $res, null, $onerror);
  169. $handled = $handled || $router->handled;
  170. }
  171. }
  172. // Base router for non-prefixed paths
  173. $this->router->handle($url["path"], $req, $res, function($req, $res) use($handled, $self, $url){
  174. if(!$handled){
  175. $self->onerror($req, $res, new \Exception("{$url['scheme']}://{$url['host']}{$url["path"]} Not Found", 404));
  176. }
  177. }, $onerror);
  178. $this->fire('afterhandle', $req, $res);
  179. }
  180. return $res;
  181. }
  182. public function error(callable $fn){
  183. $this->_onerror = $fn;
  184. return $this;
  185. }
  186. public function base(string $base = null){
  187. if(is_null($base)){
  188. return $this->router->base;
  189. }else{
  190. foreach($this->domains as &$router){
  191. if($router instanceof Router){
  192. $router->base($base);
  193. }
  194. }
  195. foreach($this->routers as $prefix => &$router){
  196. $router->base("{$base}/{$prefix}");
  197. }
  198. $this->router->base($base);
  199. return $this;
  200. }
  201. }
  202. public function route(string $path, callable $fn){
  203. $this->router->path($path, $fn);
  204. return $this;
  205. }
  206. public function prefix(string $prefix, callable $fn){
  207. if(!$this->routers[$prefix]){
  208. $this->routers[$prefix] = new Router("{$this->router->base}/{$prefix}");
  209. }
  210. $fn($this->routers[$prefix]);
  211. return $this;
  212. }
  213. public function domain(string $host, callable $fn){
  214. if(!isset($this->domains[$host])){
  215. $this->domains[$host] = new Router($this->router->base);
  216. }
  217. $fn($this->domains[$host]);
  218. return $this;
  219. }
  220. public function map_domain(string $host, $handle){
  221. if(!is_array($handle)){
  222. $handle = [$handle];
  223. }
  224. foreach($handle as $host2){
  225. $this->domains[$host2] = $host;
  226. }
  227. return $this;
  228. }
  229. public function bind(string $hosts, callable $fn = null){
  230. $hosts = array_map(function($item){
  231. return trim($item);
  232. }, explode(',', $hosts));
  233. $host = array_pop($hosts);
  234. if(is_null($fn)){
  235. $fn = function(Router $router){
  236. Controller::handle_all($router);
  237. };
  238. }
  239. return $this->map_domain($host, $hosts)->domain($host, $fn);
  240. }
  241. public function onerror(Request $req, Response $res, $error){
  242. try{
  243. $this->fire('error', $error);
  244. $fn = $this->_onerror;
  245. if(is_callable($fn)){
  246. $fn($req, $res, $error);
  247. }
  248. }catch(\Exception $e){
  249. die("Error handlers failed {$e}");
  250. }
  251. }
  252. public function locale(string $domain, string $folder, string $codeset = 'UTF-8'){
  253. $this->textdomain = $domain;
  254. $this->textdomain_codeset = $codeset;
  255. $this->textdomain_folder = $folder;
  256. return $this;
  257. }
  258. }
  259. if(!defined('JUJU_DISABLE_APP')){
  260. set_exception_handler(function($error){
  261. App::shutdown_error($error);
  262. });
  263. set_error_handler(function($errno, $errstr, $errfile, $errline){
  264. // ignore warnings
  265. if($errno &~ E_WARNING){
  266. App::shutdown_error(new Exception($errstr, $errno, null, $errfile, $errline, debug_backtrace()));
  267. die();
  268. }
  269. }, E_ALL);
  270. register_shutdown_function(function(){
  271. error_reporting(E_ALL &~ E_WARNING);
  272. ini_set('display_errors', 'Off');
  273. App::shutdown();
  274. });
  275. error_reporting(E_ALL &~ E_WARNING);
  276. ini_set('display_errors', 'Off');
  277. gc_enable();
  278. }
  279. ?>