小滕的博客

小滕的技术点滴

Laravel源码分析之down命令的背后原理

1 year ago · 2 MIN READ
#Laravel 

在 Laravel 应用中,如果我们想要暂时关闭站点,可以这样:

php artisan down

待到想要恢复站点的时候,执行下:

php artisan up

就可以了。今天,就来分析下这个上线/下线站点的背后原理。

首先,我们知道,操作是通过命令行完成的,所以我们来找下 downup 背后的源码:

本次分析的Laravel版本为:5.5.39

打开 /config/app.php 文件,找到:

Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,

进入上面的类,我们看了看到:

ArtisanServiceProvider::class,

继续跟踪 ArtisanServiceProvider::class 这个类:

protected $commands = [
        'CacheClear' => 'command.cache.clear',
        'CacheForget' => 'command.cache.forget',
        'ClearCompiled' => 'command.clear-compiled',
        'ClearResets' => 'command.auth.resets.clear',
        'ConfigCache' => 'command.config.cache',
        'ConfigClear' => 'command.config.clear',
        'Down' => 'command.down',
        'Environment' => 'command.environment',
        'KeyGenerate' => 'command.key.generate',
        'Migrate' => 'command.migrate',
        'MigrateFresh' => 'command.migrate.fresh',
        'MigrateInstall' => 'command.migrate.install',
        'MigrateRefresh' => 'command.migrate.refresh',
        'MigrateReset' => 'command.migrate.reset',
        'MigrateRollback' => 'command.migrate.rollback',
        'MigrateStatus' => 'command.migrate.status',
        'Optimize' => 'command.optimize',
        'PackageDiscover' => 'command.package.discover',
        'Preset' => 'command.preset',
        'QueueFailed' => 'command.queue.failed',
        'QueueFlush' => 'command.queue.flush',
        'QueueForget' => 'command.queue.forget',
        'QueueListen' => 'command.queue.listen',
        'QueueRestart' => 'command.queue.restart',
        'QueueRetry' => 'command.queue.retry',
        'QueueWork' => 'command.queue.work',
        'RouteCache' => 'command.route.cache',
        'RouteClear' => 'command.route.clear',
        'RouteList' => 'command.route.list',
        'Seed' => 'command.seed',
        'ScheduleFinish' => ScheduleFinishCommand::class,
        'ScheduleRun' => ScheduleRunCommand::class,
        'StorageLink' => 'command.storage.link',
        'Up' => 'command.up',
        'ViewClear' => 'command.view.clear',
    ];

从这个数组中我们可以看到 Laravel 的常用命令基本上都在这里啦,于是,继续在这个文件中查找:

/**
     * Register the command.
     *
     * @return void
     */
    protected function registerDownCommand()
    {
        $this->app->singleton('command.down', function () {
            return new DownCommand;
        });
    }

这个方法,可以明显看出将 down 命令的服务注入到容器中,于是,跟踪 DownCommand 这个类:

这里聚焦它的 handle 方法:

public function handle()
    {
        file_put_contents(
            storage_path('framework/down'),
            json_encode($this->getDownFilePayload(), JSON_PRETTY_PRINT)
        );

        $this->comment('Application is now in maintenance mode.');
    }

可以明显分析出,当我们执行 php artisan down 命令的时候,他会写入一个文件,该文件的路径是 /storage/framework/down ,该文件内容为 $this->getDownFilePayload() ,该方法:

protected function getDownFilePayload()
    {
        return [
            'time' => $this->currentTime(),
            'message' => $this->option('message'),
            'retry' => $this->getRetryTime(),
        ];
    }

其中 message 是我们在执行命令的时候可以选择传入的。到这里,down 背后的原理就分析的很清楚了,就是执行维护命令的时候创建了一个 down 的 json 文件并记录一些信息,那么可以联想到的是 up 命令就是删除这个 down 文件了。HAHA

细心的小伙伴应该发现 ConsoleSupportServiceProvider::class 的结构内容与其它的 ServiceProvider 不同,例如,下面是 AuthServiceProvider 的代码:

<?php

namespace Illuminate\Auth;

use Illuminate\Auth\Access\Gate;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->registerAuthenticator();

        $this->registerUserResolver();

        $this->registerAccessGate();

        $this->registerRequestRebindHandler();
    }

    /**
     * Register the authenticator services.
     *
     * @return void
     */
    protected function registerAuthenticator()
    {
        $this->app->singleton('auth', function ($app) {
            // Once the authentication service has actually been requested by the developer
            // we will set a variable in the application indicating such. This helps us
            // know that we need to set any queued cookies in the after event later.
            $app['auth.loaded'] = true;

            return new AuthManager($app);
        });

        $this->app->singleton('auth.driver', function ($app) {
            return $app['auth']->guard();
        });
    }

    /**
     * Register a resolver for the authenticated user.
     *
     * @return void
     */
    protected function registerUserResolver()
    {
        $this->app->bind(
            AuthenticatableContract::class, function ($app) {
                return call_user_func($app['auth']->userResolver());
            }
        );
    }

    /**
     * Register the access gate service.
     *
     * @return void
     */
    protected function registerAccessGate()
    {
        $this->app->singleton(GateContract::class, function ($app) {
            return new Gate($app, function () use ($app) {
                return call_user_func($app['auth']->userResolver());
            });
        });
    }

    /**
     * Register a resolver for the authenticated user.
     *
     * @return void
     */
    protected function registerRequestRebindHandler()
    {
        $this->app->rebinding('request', function ($app, $request) {
            $request->setUserResolver(function ($guard = null) use ($app) {
                return call_user_func($app['auth']->userResolver(), $guard);
            });
        });
    }
}

但是 ConsoleSupportServiceProvider 的代码确实这样的:

<?php

namespace Illuminate\Foundation\Providers;

use Illuminate\Support\AggregateServiceProvider;
use Illuminate\Database\MigrationServiceProvider;

class ConsoleSupportServiceProvider extends AggregateServiceProvider
{
    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = true;

    /**
     * The provider class names.
     *
     * @var array
     */
    protected $providers = [
        ArtisanServiceProvider::class,
        MigrationServiceProvider::class,
        ComposerServiceProvider::class,
    ];
}

并没有 registerboot 方法,这是为什么呢?因为 ConsoleSupportServiceProvider 继承了 AggregateServiceProvider ,而 AggregateServiceProvider 是对 ServiceProvider 的一层封装而已。当然这是题外话了。

既然生成了 down 文件,那么在我们访问网站的地方可能有检测这个文件是否存在的地方,于是,锁定到中间件 Kernel.php

\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,

这个中间件是放在第一位的,所以每次请求它都是一个执行的,我们来看下代码:

<?php

namespace Illuminate\Foundation\Http\Middleware;

use Closure;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\Http\Exceptions\MaintenanceModeException;

class CheckForMaintenanceMode
{
    /**
     * The application implementation.
     *
     * @var \Illuminate\Contracts\Foundation\Application
     */
    protected $app;

    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function __construct(Application $app)
    {
        $this->app = $app;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     *
     * @throws \Symfony\Component\HttpKernel\Exception\HttpException
     */
    public function handle($request, Closure $next)
    {
        if ($this->app->isDownForMaintenance()) {
            $data = json_decode(file_get_contents($this->app->storagePath().'/framework/down'), true);

            throw new MaintenanceModeException($data['time'], $data['retry'], $data['message']);
        }

        return $next($request);
    }
}

代码很简单,就是检测 down 文件是否存在,存在的话抛出一个异常就可以了,至于怎么将响应结果返回给用户那就是 ResponseExceptionHandler 的事情啦。

到此, downup 的背后原理基本就分析到这里啦,虽然背后的原理很简单,一个文件锁,但是藏得是真的深。HAHA...

PHPUnit单元测试视频教程请戳这里

···

xiao teng



备案号:皖ICP备14012032号-5