<p>一直都想找個PHP 的 web 框架的代碼來讀讀,好學習一下 web 框架都是怎麼寫的。可是通常的 PHP 框架好像都比較傲複雜。以前在 Mysapce 實習的時候用過一套本身寫的很是簡單的 MVC 框架,只是很薄的包了一層,把 Model,View 和 Controller 分開,我本身平時寫 PHP 應用也是用這種方法寫的。憑着記憶,把當時用的那套東西重現了一下(<a href="https://github.com/ljie-PI/php-mini-framework">github</a>)。</p>php
<p>前幾天在 github 上發現<a href="https://github.com/codeguy/Slim">一個叫 Slim 的框架</a>,star 和 fork 數都不算少,也號稱很是輕量級,打算看一看它的源代碼。</p>git
<h2>Hello World</h2>github
<p>Slim Framework 的文檔第一個例子依然是 <a href="http://docs.slimframework.com/#Hello-World">Hello World</a>。先結合這個例子來看一看 Slim 的實現方法。</p>web
<h3>第一步:建立 Slim 對象</h3>正則表達式
<p>第一行代碼是:</p> ```php $app = new \Slim\Slim(); ``` <p>這一行代碼對應的 Slim 代碼以下:</p> <!--more--> ```php public function __construct(array $userSettings = array()) { // Setup IoC container $this->container = new \Slim\Helper\Set(); $this->container['settings'] = array_merge(static::getDefaultSettings(), $userSettings);數組
// Default environment $this->container->singleton('environment', function ($c) { return \Slim\Environment::getInstance(); }); // Default request $this->container->singleton('request', function ($c) { return new \Slim\Http\Request($c['environment']); }); // Default response $this->container->singleton('response', function ($c) { return new \Slim\Http\Response(); }); // Default router $this->container->singleton('router', function ($c) { return new \Slim\Router(); }); // Default view $this->container->singleton('view', function ($c) { $viewClass = $c['settings']['view']; return ($viewClass instanceOf \Slim\View) ? $viewClass : new $viewClass; }); // Default log writer $this->container->singleton('logWriter', function ($c) { $logWriter = $c['settings']['log.writer']; return is_object($logWriter) ? $logWriter : new \Slim\LogWriter($c['environment']['slim.errors']); }); // Default log $this->container->singleton('log', function ($c) { $log = new \Slim\Log($c['logWriter']); $log->setEnabled($c['settings']['log.enabled']); $log->setLevel($c['settings']['log.level']); $env = $c['environment']; $env['slim.log'] = $log; return $log; }); // Default mode $this->container['mode'] = function ($c) { $mode = $c['settings']['mode']; if (isset($_ENV['SLIM_MODE'])) { $mode = $_ENV['SLIM_MODE']; } else { $envMode = getenv('SLIM_MODE'); if ($envMode !== false) { $mode = $envMode; } } return $mode; }; // Define default middleware stack $this->middleware = array($this); $this->add(new \Slim\Middleware\Flash()); $this->add(new \Slim\Middleware\MethodOverride()); // Make default if first instance if (is_null(static::getInstance())) { $this->setName('default'); } }
<ul> <li>Environment 對象中包含 REQUEST_METHO,REMOTE_ADD,REQUEST_URI 等各類 $_SERVER 環境變量</li> <li>Request 對象中包含 REQUEST_METHOD,QUERY_STRING,HTTP_COOKIE 及各類 HTTP 請求頭內容</li> <li>Response 對象包含響應狀態碼,header 及 body</li> <li>Router 對象會解析請求的 URI 並關聯到對應的響應函數</li> <li>View 對象能夠從 HTML 模板渲染頁面</li> <li>LogWriter 和 Log 主要用來寫 log</li> <li>Middle 數組是一串 middle,每一個請求過來會一次執行 Middle 對象的 call 方法</li> </ul> <h3>第二步:指定 route 和 callback</h3> <p>這一步對應的代碼是:</p> ```php $app->get('/hello/:name', function ($name) { echo "Hello, $name"; });
<p>此時會執行 Slim 對象的 mapRoute 方法,並關聯請求方法:</p>cookie
public function get() { $args = func_get_args(); return $this->mapRoute($args)->via(\Slim\Http\Request::METHOD_GET, \Slim\Http\Request::METHOD_HEAD); }
<p>在 mapRoute 方法中,會建立一個 Route 對象,將 「/hello/:name」 做爲 Route 對象的 pattern,將第二個參數(一個函數)做爲 Route 對象的 callable,並將這個 Route 對象加入到 Router 對象的 routes 數組中。</p> ```php protected function mapRoute($args) { $pattern = array_shift($args); $callable = array_pop($args); $route = new \Slim\Route($pattern, $callable); $this->router->map($route); if (count($args) > 0) { $route->setMiddleware($args); }app
return $route; }
<p>Route 對象的 via 方法</p> ```php public function via() { $args = func_get_args(); $this->methods = array_merge($this->methods, $args); return $this; }
<h3>第三步:執行</h3>框架
<p>最後能夠執行代碼生成 reponse:</p> ```php $app->run(); ```ide
<p>這行代碼在 Slim.php 中對應:</p> ```php public function run() { set_error_handler(array('\Slim\Slim', 'handleErrors'));
//Apply final outer middleware layers if ($this->config('debug')) { //Apply pretty exceptions only in debug to avoid accidental information leakage in production $this->add(new \Slim\Middleware\PrettyExceptions()); } //Invoke middleware and application stack $this->middleware[0]->call(); //Fetch status, header, and body list($status, $headers, $body) = $this->response->finalize(); // Serialize cookies (with optional encryption) \Slim\Http\Util::serializeCookies($headers, $this->response->cookies, $this->settings); //Send headers if (headers_sent() === false) { //Send status if (strpos(PHP_SAPI, 'cgi') === 0) { header(sprintf('Status: %s', \Slim\Http\Response::getMessageForCode($status))); } else { header(sprintf('HTTP/%s %s', $this->config('http.version'), \Slim\Http\Response::getMessageForCode($status))); } //Send headers foreach ($headers as $name => $value) { $hValues = explode("\n", $value); foreach ($hValues as $hVal) { header("$name: $hVal", false); } } } //Send body, but only if it isn't a HEAD request if (!$this->request->isHead()) { echo $body; } restore_error_handler(); }
<p>因爲 Slim 對象的最後一個 Middle 是自己,因此在執行完一堆 Middle 的 call 方法後會執行本身的 call 方法:</p> ```php public function call() { try { if (isset($this->environment['slim.flash'])) { $this->view()->setData('flash', $this->environment['slim.flash']); } $this->applyHook('slim.before'); ob_start(); $this->applyHook('slim.before.router'); $dispatched = false; $matchedRoutes = $this->router->getMatchedRoutes($this->request->getMethod(), $this->request->getResourceUri()); foreach ($matchedRoutes as $route) { try { $this->applyHook('slim.before.dispatch'); $dispatched = $route->dispatch(); $this->applyHook('slim.after.dispatch'); if ($dispatched) { break; } } catch (\Slim\Exception\Pass $e) { continue; } } if (!$dispatched) { $this->notFound(); } $this->applyHook('slim.after.router'); $this->stop(); } catch (\Slim\Exception\Stop $e) { $this->response()->write(ob_get_clean()); $this->applyHook('slim.after'); } catch (\Exception $e) { if ($this->config('debug')) { throw $e; } else { try { $this->error($e); } catch (\Slim\Exception\Stop $e) { // Do nothing } } } }
<p>在 Router 的 getMatchedRoutes 方法中會針對請求的 URI,用正則表達式匹配是否有合適的 Route 對象,而且匹配出其中傳進來的參數</p> ```php public function getMatchedRoutes($httpMethod, $resourceUri, $reload = false) { if ($reload || is_null($this->matchedRoutes)) { $this->matchedRoutes = array(); foreach ($this->routes as $route) { if (!$route->supportsHttpMethod($httpMethod) && !$route->supportsHttpMethod("ANY")) { continue; }
if ($route->matches($resourceUri)) { $this->matchedRoutes[] = $route; } } } return $this->matchedRoutes; }
```php public function matches($resourceUri) { //Convert URL params into regex patterns, construct a regex for this route, init params $patternAsRegex = preg_replace_callback( '#:([\w]+)\+?#', array($this, 'matchesCallback'), str_replace(')', ')?', (string) $this->pattern) ); if (substr($this->pattern, -1) === '/') { $patternAsRegex .= '?'; } //Cache URL params' names and values if this route matches the current HTTP request if (!preg_match('#^' . $patternAsRegex . '$#', $resourceUri, $paramValues)) { return false; } foreach ($this->paramNames as $name) { if (isset($paramValues[$name])) { if (isset($this->paramNamesPath[ $name ])) { $this->params[$name] = explode('/', urldecode($paramValues[$name])); } else { $this->params[$name] = urldecode($paramValues[$name]); } } } return true; }
<p>最後執行 Route 對象的 dispatch 方法,就是將匹配出來的參數傳到 callable 而後執行。</p>
public function dispatch() { foreach ($this->middleware as $mw) { call_user_func_array($mw, array($this)); } $return = call_user_func_array($this->getCallable(), array_values($this->getParams())); return ($return === false)? false : true; }