Laravel框架中,routing、接收request、發送response幾件事是由Symphony的元件HttpKernel Component來協助完成的。
瞭解這個元件,對於瞭解Laravel原始碼有極大的幫助:The HttpKernel Component
不過,以上文件內容生硬無比。這邊推薦一個極度優質的教學文章:
Create your own framework… on top of the Symfony2 Components
示範如何用Symphony元件寫出自己的框架。讀過之後,不但可以自己寫出簡易的框架,對於瞭解Laravel原始碼也很有幫助。
繼承Container類別:使得能夠用陣列方式操作。
實作HttpKernelInterface介面:規定實作handle函式、傳入request實體並回傳response實體。
實作TerminableInterface介面:規定實作terminate函式、完成一個request/response周期。
實作ResponsePreparerInterface介面:規定實作prepareResponse、readyForResponses函式、用以協助回傳response。/** * The Laravel framework version. * * @var string */ const VERSION = '4.2.10'; /** * Indicates if the application has "booted". * * @var bool */ protected $booted = false; /** * The array of booting callbacks. * * @var array */ protected $bootingCallbacks = array(); /** * The array of booted callbacks. * * @var array */ protected $bootedCallbacks = array(); /** * The array of finish callbacks. * * @var array */ protected $finishCallbacks = array(); /** * The array of shutdown callbacks. * * @var array */ protected $shutdownCallbacks = array();4個陣列紀錄4個時間點要執行的callback:
初始化核心程序前、初始化核心程序後、執行完核心程序後、中止核心程序後。
這邊的「核心程序」是指什麼?請繼續往下看。/** * All of the developer defined middlewares. * * @var array */ protected $middlewares = array(); /** * All of the registered service providers. * * @var array */ protected $serviceProviders = array(); /** * The names of the loaded service providers. * * @var array */ protected $loadedProviders = array(); /** * The deferred services and their providers. * * @var array */ protected $deferredServices = array(); /** * The request class used by the application. * * @var string */ protected static $requestClass = 'Illuminate\Http\Request'; /** * Create a new Illuminate application instance. * * @param \Illuminate\Http\Request $request * @return void */ public function __construct(Request $request = null) { $this->registerBaseBindings($request ?: $this->createNewRequest()); $this->registerBaseServiceProviders(); $this->registerBaseMiddlewares(); }產生request實體並綁定。
綁定幾項service provider實體。
綁定中介程序(往下看,發現函式內容其實是空的)。
建構式允許傳入Request實體當參數,但實際上在bootstrap/start.php裡,並沒有在建構時傳入任何東西。
所以registerBaseBindings永遠是用createNewRequest函式的回傳值當作參數。
這邊寫成這樣是...留給進階使用者擴充空間吧?
/** * Create a new request instance from the request class. * * @return \Illuminate\Http\Request */ protected function createNewRequest() { return forward_static_call(array(static::$requestClass, 'createFromGlobals')); }開發時會用到的Input類別、呼叫它的靜態函式,背後對應到的實體就是在這裡產生的。
關於這個實體所屬的類別,參見:接受請求:Http/Request.php/** * Register the basic bindings into the container. * * @param \Illuminate\Http\Request $request * @return void */ protected function registerBaseBindings($request) { $this->instance('request', $request); $this->instance('Illuminate\Container\Container', $this); } /** * Register all of the base service providers. * * @return void */ protected function registerBaseServiceProviders() { foreach (array('Event', 'Exception', 'Routing') as $name) { $this->{"register{$name}Provider"}(); } } /** * Register the exception service provider. * * @return void */ protected function registerExceptionProvider() { $this->register(new ExceptionServiceProvider($this)); } /** * Register the routing service provider. * * @return void */ protected function registerRoutingProvider() { $this->register(new RoutingServiceProvider($this)); } /** * Register the event service provider. * * @return void */ protected function registerEventProvider() { $this->register(new EventServiceProvider($this)); }上面幾個函式內容就是實際在建構式內執行的內容。
從建構式一直到上面為止的寫法確實很OOP,可讀性也很高,但我覺得好大一串...。
/** * Bind the installation paths to the application. * * @param array $paths * @return void */ public function bindInstallPaths(array $paths) { $this->instance('path', realpath($paths['app'])); // Here we will bind the install paths into the container as strings that can be // accessed from any point in the system. Each path key is prefixed with path // so that they have the consistent naming convention inside the container. foreach (array_except($paths, array('app')) as $key => $value) { $this->instance("path.{$key}", realpath($value)); } } /** * Get the application bootstrap file. * * @return string */ public static function getBootstrapFile() { return __DIR__.'/start.php'; } /** * Start the exception handling for the request. * * @return void */ public function startExceptionHandling() { $this['exception']->register($this->environment()); $this['exception']->setDebug($this['config']['app.debug']); } /** * Get or check the current application environment. * * @param mixed * @return string */ public function environment() { if (count(func_get_args()) > 0) { return in_array($this['env'], func_get_args()); } return $this['env']; } /** * Determine if application is in local environment. * * @return bool */ public function isLocal() { return $this['env'] == 'local'; } /** * Detect the application's current environment. * * @param array|string $envs * @return string */ public function detectEnvironment($envs) { $args = isset($_SERVER['argv']) ? $_SERVER['argv'] : null; return $this['env'] = (new EnvironmentDetector())->detect($envs, $args); } /** * Determine if we are running in the console. * * @return bool */ public function runningInConsole() { return php_sapi_name() == 'cli'; } /** * Determine if we are running unit tests. * * @return bool */ public function runningUnitTests() { return $this['env'] == 'testing'; } /** * Force register a service provider with the application. * * @param \Illuminate\Support\ServiceProvider|string $provider * @param array $options * @return \Illuminate\Support\ServiceProvider */ public function forceRegister($provider, $options = array()) { return $this->register($provider, $options, true); } /** * Register a service provider with the application. * * @param \Illuminate\Support\ServiceProvider|string $provider * @param array $options * @param bool $force * @return \Illuminate\Support\ServiceProvider */ public function register($provider, $options = array(), $force = false) { if ($registered = $this->getRegistered($provider) && ! $force) return $registered; // If the given "provider" is a string, we will resolve it, passing in the // application instance automatically for the developer. This is simply // a more convenient way of specifying your service provider classes. if (is_string($provider)) { $provider = $this->resolveProviderClass($provider); } $provider->register(); // Once we have registered the service we will iterate through the options // and set each of them on the application so they will be available on // the actual loading of the service objects and for developer usage. foreach ($options as $key => $value) { $this[$key] = $value; } $this->markAsRegistered($provider); // If the application has already booted, we will call this boot method on // the provider class so it has an opportunity to do its boot logic and // will be ready for any usage by the developer's application logics. if ($this->booted) $provider->boot(); return $provider; } /** * Get the registered service provider instance if it exists. * * @param \Illuminate\Support\ServiceProvider|string $provider * @return \Illuminate\Support\ServiceProvider|null */ public function getRegistered($provider) { $name = is_string($provider) ? $provider : get_class($provider); if (array_key_exists($name, $this->loadedProviders)) { return array_first($this->serviceProviders, function($key, $value) use ($name) { return get_class($value) == $name; }); } } /** * Resolve a service provider instance from the class name. * * @param string $provider * @return \Illuminate\Support\ServiceProvider */ public function resolveProviderClass($provider) { return new $provider($this); } /** * Mark the given provider as registered. * * @param \Illuminate\Support\ServiceProvider * @return void */ protected function markAsRegistered($provider) { $this['events']->fire($class = get_class($provider), array($provider)); $this->serviceProviders[] = $provider; $this->loadedProviders[$class] = true; } /** * Load and boot all of the remaining deferred providers. * * @return void */ public function loadDeferredProviders() { // We will simply spin through each of the deferred providers and register each // one and boot them if the application has booted. This should make each of // the remaining services available to this application for immediate use. foreach ($this->deferredServices as $service => $provider) { $this->loadDeferredProvider($service); } $this->deferredServices = array(); } /** * Load the provider for a deferred service. * * @param string $service * @return void */ protected function loadDeferredProvider($service) { $provider = $this->deferredServices[$service]; // If the service provider has not already been loaded and registered we can // register it with the application and remove the service from this list // of deferred services, since it will already be loaded on subsequent. if ( ! isset($this->loadedProviders[$provider])) { $this->registerDeferredProvider($provider, $service); } } /** * Register a deferred provider and service. * * @param string $provider * @param string $service * @return void */ public function registerDeferredProvider($provider, $service = null) { // Once the provider that provides the deferred service has been registered we // will remove it from our local list of the deferred services with related // providers so that this container does not try to resolve it out again. if ($service) unset($this->deferredServices[$service]); $this->register($instance = new $provider($this)); if ( ! $this->booted) { $this->booting(function() use ($instance) { $instance->boot(); }); } } /** * Resolve the given type from the container. * * (Overriding Container::make) * * @param string $abstract * @param array $parameters * @return mixed */ public function make($abstract, $parameters = array()) { $abstract = $this->getAlias($abstract); if (isset($this->deferredServices[$abstract])) { $this->loadDeferredProvider($abstract); } return parent::make($abstract, $parameters); } /** * Determine if the given abstract type has been bound. * * (Overriding Container::bound) * * @param string $abstract * @return bool */ public function bound($abstract) { return isset($this->deferredServices[$abstract]) || parent::bound($abstract); } /** * "Extend" an abstract type in the container. * * (Overriding Container::extend) * * @param string $abstract * @param \Closure $closure * @return void * * @throws \InvalidArgumentException */ public function extend($abstract, Closure $closure) { $abstract = $this->getAlias($abstract); if (isset($this->deferredServices[$abstract])) { $this->loadDeferredProvider($abstract); } return parent::extend($abstract, $closure); } /** * Register a "before" application filter. * * @param \Closure|string $callback * @return void */ public function before($callback) { return $this['router']->before($callback); } /** * Register an "after" application filter. * * @param \Closure|string $callback * @return void */ public function after($callback) { return $this['router']->after($callback); } /** * Register a "finish" application filter. * * @param \Closure|string $callback * @return void */ public function finish($callback) { $this->finishCallbacks[] = $callback; } /** * Register a "shutdown" callback. * * @param callable $callback * @return void */ public function shutdown(callable $callback = null) { if (is_null($callback)) { $this->fireAppCallbacks($this->shutdownCallbacks); } else { $this->shutdownCallbacks[] = $callback; } } /** * Register a function for determining when to use array sessions. * * @param \Closure $callback * @return void */ public function useArraySessions(Closure $callback) { $this->bind('session.reject', function() use ($callback) { return $callback; }); } /** * Determine if the application has booted. * * @return bool */ public function isBooted() { return $this->booted; } /** * Boot the application's service providers. * * @return void */ public function boot() { if ($this->booted) return; array_walk($this->serviceProviders, function($p) { $p->boot(); }); $this->bootApplication(); } /** * Boot the application and fire app callbacks. * * @return void */ protected function bootApplication() { // Once the application has booted we will also fire some "booted" callbacks // for any listeners that need to do work after this initial booting gets // finished. This is useful when ordering the boot-up processes we run. $this->fireAppCallbacks($this->bootingCallbacks); $this->booted = true; $this->fireAppCallbacks($this->bootedCallbacks); } /** * Register a new boot listener. * * @param mixed $callback * @return void */ public function booting($callback) { $this->bootingCallbacks[] = $callback; } /** * Register a new "booted" listener. * * @param mixed $callback * @return void */ public function booted($callback) { $this->bootedCallbacks[] = $callback; if ($this->isBooted()) $this->fireAppCallbacks(array($callback)); } /** * Run the application and send the response. * * @param \Symfony\Component\HttpFoundation\Request $request * @return void */ public function run(SymfonyRequest $request = null) { $request = $request ?: $this['request']; $response = with($stack = $this->getStackedClient())->handle($request); $response->send(); $stack->terminate($request, $response); }這是入口檔案(public/index.php)初始化Laravel實體之後,唯一呼叫的函式。
「執行本次應用然後送出response給client端。」wich函數是Laravel自定義的輔助函數:
Return the given object. Useful for method chaining constructors in PHP 5.3.x.而getStackedClient則是回傳stacked HTTP kernel,將加密處理、cookie處理、session處理放進一個stack內準備給handle依序處理。關於這部份請參見Symphony框架:
The HttpKernel Component
Laravel的request與response程序就是利用Symphony的函式庫,完成一個按照步驟將request轉成response的過程。最後按照Symphony函式庫的規範,send結果之後,將stack給terminate掉。
Symphony這樣設計是讓系統能夠儘快先send結果給使用者的瀏覽器,然後再執行一些像是發送email的緩慢程序。
入口檔案並沒有傳入任何request實體當參數。原作在此處保留了擴充性給進階使用者。
/** * Get the stacked HTTP kernel for the application. * * @return \Symfony\Component\HttpKernel\HttpKernelInterface */ protected function getStackedClient() { $sessionReject = $this->bound('session.reject') ? $this['session.reject'] : null; $client = (new Builder) ->push('Illuminate\Cookie\Guard', $this['encrypter']) ->push('Illuminate\Cookie\Queue', $this['cookie']) ->push('Illuminate\Session\Middleware', $this['session'], $sessionReject); $this->mergeCustomMiddlewares($client); return $client->resolve($this); } /** * Merge the developer defined middlewares onto the stack. * * @param \Stack\Builder * @return void */ protected function mergeCustomMiddlewares(Builder $stack) { foreach ($this->middlewares as $middleware) { list($class, $parameters) = array_values($middleware); array_unshift($parameters, $class); call_user_func_array(array($stack, 'push'), $parameters); } } /** * Register the default, but optional middlewares. * * @return void */ protected function registerBaseMiddlewares() { // } /** * Add a HttpKernel middleware onto the stack. * * @param string $class * @param array $parameters * @return $this */ public function middleware($class, array $parameters = array()) { $this->middlewares[] = compact('class', 'parameters'); return $this; } /** * Remove a custom middleware from the application. * * @param string $class * @return void */ public function forgetMiddleware($class) { $this->middlewares = array_filter($this->middlewares, function($m) use ($class) { return $m['class'] != $class; }); } /** * Handle the given request and get the response. * * Provides compatibility with BrowserKit functional testing. * * @implements HttpKernelInterface::handle * * @param \Symfony\Component\HttpFoundation\Request $request * @param int $type * @param bool $catch * @return \Symfony\Component\HttpFoundation\Response * * @throws \Exception */ public function handle(SymfonyRequest $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) { try { $this->refreshRequest($request = Request::createFromBase($request)); $this->boot(); return $this->dispatch($request); } catch (\Exception $e) { if ($this->runningUnitTests()) throw $e; return $this['exception']->handleException($e); } }在run函式中被呼叫。
由HttpKernelInterface所規範、實作request轉成response的函式。
先refreshRequest將request洗乾淨(???)
然後執行boot將整個應用程序「開機」。實際的轉換工作由dispatch負責。
前面提到的The HttpKernel Component,如果沒弄懂,會很難搞懂這邊為何要一層包一層。/** * Handle the given request and get the response. * * @param \Illuminate\Http\Request $request * @return \Symfony\Component\HttpFoundation\Response */ public function dispatch(Request $request) { if ($this->isDownForMaintenance()) { $response = $this['events']->until('illuminate.app.down'); if ( ! is_null($response)) return $this->prepareResponse($response, $request); } if ($this->runningUnitTests() && ! $this['session']->isStarted()) { $this['session']->start(); } return $this['router']->dispatch($this->prepareRequest($request)); }被handle呼叫、實際進行request轉成response的函式。
先防衛性檢查主應用是否正在「下線維護」狀態,然後決定是否啟動session(???)。
最後由router決定將request丟往哪邊處理、並且回傳response。
參考:分配工作:Routing/Router.php/** * Terminate the request and send the response to the browser. * * @param \Symfony\Component\HttpFoundation\Request $request * @param \Symfony\Component\HttpFoundation\Response $response * @return void */ public function terminate(SymfonyRequest $request, SymfonyResponse $response) { $this->callFinishCallbacks($request, $response); $this->shutdown(); }terminate函式只是為了執行'finish callback'而存在(如果有的話)。
然後再呼叫shutdown去執行'shutdown callback'。至於兩種callback的使用時機...大部份情況下是不會用到的。
可以參考:
Request Lifecycle
Understanding the Request Lifecycle
得到更多資訊。/** * Refresh the bound request instance in the container. * * @param \Illuminate\Http\Request $request * @return void */ protected function refreshRequest(Request $request) { $this->instance('request', $request); Facade::clearResolvedInstance('request'); } /** * Call the "finish" callbacks assigned to the application. * * @param \Symfony\Component\HttpFoundation\Request $request * @param \Symfony\Component\HttpFoundation\Response $response * @return void */ public function callFinishCallbacks(SymfonyRequest $request, SymfonyResponse $response) { foreach ($this->finishCallbacks as $callback) { call_user_func($callback, $request, $response); } } /** * Call the booting callbacks for the application. * * @param array $callbacks * @return void */ protected function fireAppCallbacks(array $callbacks) { foreach ($callbacks as $callback) { call_user_func($callback, $this); } } /** * Prepare the request by injecting any services. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Request */ public function prepareRequest(Request $request) { if ( ! is_null($this['config']['session.driver']) && ! $request->hasSession()) { $request->setSession($this['session']->driver()); } return $request; } /** * Prepare the given value as a Response object. * * @param mixed $value * @return \Symfony\Component\HttpFoundation\Response */ public function prepareResponse($value) { if ( ! $value instanceof SymfonyResponse) $value = new Response($value); return $value->prepare($this['request']); } /** * Determine if the application is ready for responses. * * @return bool */ public function readyForResponses() { return $this->booted; } /** * Determine if the application is currently down for maintenance. * * @return bool */ public function isDownForMaintenance() { return file_exists($this['config']['app.manifest'].'/down'); } /** * Register a maintenance mode event listener. * * @param \Closure $callback * @return void */ public function down(Closure $callback) { $this['events']->listen('illuminate.app.down', $callback); } /** * Throw an HttpException with the given data. * * @param int $code * @param string $message * @param array $headers * @return void * * @throws \Symfony\Component\HttpKernel\Exception\HttpException * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ public function abort($code, $message = '', array $headers = array()) { if ($code == 404) { throw new NotFoundHttpException($message); } throw new HttpException($code, $message, null, $headers); } /** * Register a 404 error handler. * * @param \Closure $callback * @return void */ public function missing(Closure $callback) { $this->error(function(NotFoundHttpException $e) use ($callback) { return call_user_func($callback, $e); }); } /** * Register an application error handler. * * @param \Closure $callback * @return void */ public function error(Closure $callback) { $this['exception']->error($callback); } /** * Register an error handler at the bottom of the stack. * * @param \Closure $callback * @return void */ public function pushError(Closure $callback) { $this['exception']->pushError($callback); } /** * Register an error handler for fatal errors. * * @param \Closure $callback * @return void */ public function fatal(Closure $callback) { $this->error(function(FatalErrorException $e) use ($callback) { return call_user_func($callback, $e); }); } /** * Get the configuration loader instance. * * @return \Illuminate\Config\LoaderInterface */ public function getConfigLoader() { return new FileLoader(new Filesystem, $this['path'].'/config'); } /** * Get the environment variables loader instance. * * @return \Illuminate\Config\EnvironmentVariablesLoaderInterface */ public function getEnvironmentVariablesLoader() { return new FileEnvironmentVariablesLoader(new Filesystem, $this['path.base']); } /** * Get the service provider repository instance. * * @return \Illuminate\Foundation\ProviderRepository */ public function getProviderRepository() { $manifest = $this['config']['app.manifest']; return new ProviderRepository(new Filesystem, $manifest); } /** * Get the service providers that have been loaded. * * @return array */ public function getLoadedProviders() { return $this->loadedProviders; } /** * Set the application's deferred services. * * @param array $services * @return void */ public function setDeferredServices(array $services) { $this->deferredServices = $services; } /** * Determine if the given service is a deferred service. * * @param string $service * @return bool */ public function isDeferredService($service) { return isset($this->deferredServices[$service]); } /** * Get or set the request class for the application. * * @param string $class * @return string */ public static function requestClass($class = null) { if ( ! is_null($class)) static::$requestClass = $class; return static::$requestClass; } /** * Set the application request for the console environment. * * @return void */ public function setRequestForConsoleEnvironment() { $url = $this['config']->get('app.url', 'http://localhost'); $parameters = array($url, 'GET', array(), array(), array(), $_SERVER); $this->refreshRequest(static::onRequest('create', $parameters)); } /** * Call a method on the default request class. * * @param string $method * @param array $parameters * @return mixed */ public static function onRequest($method, $parameters = array()) { return forward_static_call_array(array(static::requestClass(), $method), $parameters); } /** * Get the current application locale. * * @return string */ public function getLocale() { return $this['config']->get('app.locale'); } /** * Set the current application locale. * * @param string $locale * @return void */ public function setLocale($locale) { $this['config']->set('app.locale', $locale); $this['translator']->setLocale($locale); $this['events']->fire('locale.changed', array($locale)); } /** * Register the core class aliases in the container. * * @return void */ public function registerCoreContainerAliases() { $aliases = array( 'app' => 'Illuminate\Foundation\Application', 'artisan' => 'Illuminate\Console\Application', 'auth' => 'Illuminate\Auth\AuthManager', 'auth.reminder.repository' => 'Illuminate\Auth\Reminders\ReminderRepositoryInterface', 'blade.compiler' => 'Illuminate\View\Compilers\BladeCompiler', 'cache' => 'Illuminate\Cache\CacheManager', 'cache.store' => 'Illuminate\Cache\Repository', 'config' => 'Illuminate\Config\Repository', 'cookie' => 'Illuminate\Cookie\CookieJar', 'encrypter' => 'Illuminate\Encryption\Encrypter', 'db' => 'Illuminate\Database\DatabaseManager', 'events' => 'Illuminate\Events\Dispatcher', 'files' => 'Illuminate\Filesystem\Filesystem', 'form' => 'Illuminate\Html\FormBuilder', 'hash' => 'Illuminate\Hashing\HasherInterface', 'html' => 'Illuminate\Html\HtmlBuilder', 'translator' => 'Illuminate\Translation\Translator', 'log' => 'Illuminate\Log\Writer', 'mailer' => 'Illuminate\Mail\Mailer', 'paginator' => 'Illuminate\Pagination\Factory', 'auth.reminder' => 'Illuminate\Auth\Reminders\PasswordBroker', 'queue' => 'Illuminate\Queue\QueueManager', 'redirect' => 'Illuminate\Routing\Redirector', 'redis' => 'Illuminate\Redis\Database', 'request' => 'Illuminate\Http\Request', 'router' => 'Illuminate\Routing\Router', 'session' => 'Illuminate\Session\SessionManager', 'session.store' => 'Illuminate\Session\Store', 'remote' => 'Illuminate\Remote\RemoteManager', 'url' => 'Illuminate\Routing\UrlGenerator', 'validator' => 'Illuminate\Validation\Factory', 'view' => 'Illuminate\View\Factory', ); foreach ($aliases as $key => $alias) { $this->alias($key, $alias); } } }