一直都想找个PHP 的 web 框架的代码来读读,好学习一下 web 框架都是怎么写的。但是一般的 PHP 框架好像都比较傲复杂。之前在 Mysapce 实习的时候用过一套自己写的非常简单的 MVC 框架,只是很薄的包了一层,把 Model,View 和 Controller 分开,我自己平时写 PHP 应用也是用这种方法写的。凭着记忆,把当时用的那套东西重现了一下(github)。
前几天在 github 上发现一个叫 Slim 的框架,star 和 fork 数都不算少,也号称非常轻量级,打算看一看它的源代码。
Slim Framework 的文档第一个例子依然是 Hello World。先结合这个例子来看一看 Slim 的实现方法。
第一行代码是:
$app = new \Slim\Slim();
这一行代码对应的 Slim 代码如下:
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');
}
}
这一步对应的代码是:
$app->get('/hello/:name', function ($name) {
echo "Hello, $name";
});
此时会执行 Slim 对象的 mapRoute 方法,并关联请求方法:
public function get()
{
$args = func_get_args();
return $this->mapRoute($args)->via(\Slim\Http\Request::METHOD_GET, \Slim\Http\Request::METHOD_HEAD);
}
在 mapRoute 方法中,会创建一个 Route 对象,将 “/hello/:name” 作为 Route 对象的 pattern,将第二个参数(一个函数)作为 Route 对象的 callable,并将这个 Route 对象加入到 Router 对象的 routes 数组中。
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);
}
return $route;
}
Route 对象的 via 方法
public function via()
{
$args = func_get_args();
$this->methods = array_merge($this->methods, $args);
return $this;
}
最后可以执行代码生成 reponse:
$app->run();
这行代码在 Slim.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();
}
由于 Slim 对象的最后一个 Middle 是本身,所以在执行完一堆 Middle 的 call 方法后会执行自己的 call 方法:
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
}
}
}
}
在 Router 的 getMatchedRoutes 方法中会针对请求的 URI,用正则表达式匹配是否有合适的 Route 对象,并且匹配出其中传进来的参数
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;
}
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;
}
最后执行 Route 对象的 dispatch 方法,就是将匹配出来的参数传到 callable 然后执行。
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;
}