entner

entner 查看完整档案

成都编辑西南民族大学  |  信息管理与信息系统 编辑SYL/BBT/SE  |  小白 编辑 segmentfault.com/u/entner 编辑
编辑

青年 ---------------

-关注前沿、实时更新、接近生活、积淀经验、保持健康

flag:开源 MVC
to: 让小白们了解一般的mvc框架(如:laravel、thinkphp)的实现原理 我们自己也能写~

个人动态

entner 收藏了文章 · 2020-12-24

Nginx解决跨域请求,通俗解释

前言

Nginx如何处理跨域请求
(本文适用于初学者)

一、Nginx

关于Nginx的用途,听到最多的两个词,就是:

  • 端口转发
  • 负载均衡

负载均衡不属于现阶段要学习的内容,重点来看一看端口转发,本文用它来解决跨域请求的问题。

二、CROS 跨域资源共享

我们需要知道,同源的三要素:协议域名端口
如果比较两个地址,只要三者中只要有任何一个不同,就算跨域。

// 协议:http
// 域名:localhost
// 端口:8011

http://localhost:8011

出于安全原因,浏览器限制从脚本(比如JavaScript)内发起的跨源HTTP请求。
如果浏览器检测到跨域,它会尝试发起一次请求,然后查看返回的内容中,是否一个有允许跨域请求的标记(CORS响应头),如果有正确的标记,那么就不拦截;如果没有标记,浏览器就会阻止这个请求。并报错。
图片.png

三、项目中为何产生跨域

前后台分离的项目中,前台和后台分别运行在不同的端口上。

所以前台后台发起请求时,会因为跨域,而被浏览器拦截下来。

图片.png

浏览器错误信息:

这时解决方案有两个:

  • 开放跨域请求
  • 使跨域变成同源

第一种方法,显然不安全,开放跨域意味着,浏览器不再进行拦截。如果前台代码被篡改,把后台的地址指向黑客的服务器,那么会对用户造成损失。

第二种方法,想办法变成同一个域,这就轮到Nginx出场了!

四、使用Nginx转发

首先要明白,是谁把前台向后台的请求拦截下来的?不是后台,而是浏览器
如果要避免跨域,只要让浏览器认为“我正在向同一个域发起请求”,就可以了。

假设前台使用4200端口,后台使用8080端口,那么,再加入一个8011端口,作为用户访问时连接的端口。
在前台的代码中,会有拦截器,如果发现某个请求是指向后台的,就会在Url中加入一个特殊的标识(比如加一个/api/作为前缀)

// header中带有do_not_intercept,且值为true,则不添加url前缀
if (('true' !== req.headers.get(YunzhiInterceptor.DONT_INTERCEPT_HEADER_KEY))
  && !url.startsWith('https://') && !url.startsWith('http://')) {
  url = '/api/' + url;
}

现在,无论是指向前台还是后台的请求,都会发送到8011端口,只不过,指向后台的请求会有一个/api/前缀。

接下来使用Nginx监听8011端口,当接收到请求时,根据是否有前缀,来判断此请求交给前台或后台处理。

过程如图:
图片.png

上图就是转发的原理。

五、总结

使用Nginx端口转发,本质上就是:让浏览器认为前台和后台是同一个域,就不会产生跨域请求。
开发者事先约定好,根据不同的请求地址,来访问不同的服务器。
Nginx接收到数据之后,根据地址,转发给相应的服务器来处理。
如果需要另外安排其他的服务,来实现文件上传功能的话,只需要把URL处理一下,加上不同的前缀即可。

查看原文

entner 赞了文章 · 2020-12-24

Nginx解决跨域请求,通俗解释

前言

Nginx如何处理跨域请求
(本文适用于初学者)

一、Nginx

关于Nginx的用途,听到最多的两个词,就是:

  • 端口转发
  • 负载均衡

负载均衡不属于现阶段要学习的内容,重点来看一看端口转发,本文用它来解决跨域请求的问题。

二、CROS 跨域资源共享

我们需要知道,同源的三要素:协议域名端口
如果比较两个地址,只要三者中只要有任何一个不同,就算跨域。

// 协议:http
// 域名:localhost
// 端口:8011

http://localhost:8011

出于安全原因,浏览器限制从脚本(比如JavaScript)内发起的跨源HTTP请求。
如果浏览器检测到跨域,它会尝试发起一次请求,然后查看返回的内容中,是否一个有允许跨域请求的标记(CORS响应头),如果有正确的标记,那么就不拦截;如果没有标记,浏览器就会阻止这个请求。并报错。
图片.png

三、项目中为何产生跨域

前后台分离的项目中,前台和后台分别运行在不同的端口上。

所以前台后台发起请求时,会因为跨域,而被浏览器拦截下来。

图片.png

浏览器错误信息:

这时解决方案有两个:

  • 开放跨域请求
  • 使跨域变成同源

第一种方法,显然不安全,开放跨域意味着,浏览器不再进行拦截。如果前台代码被篡改,把后台的地址指向黑客的服务器,那么会对用户造成损失。

第二种方法,想办法变成同一个域,这就轮到Nginx出场了!

四、使用Nginx转发

首先要明白,是谁把前台向后台的请求拦截下来的?不是后台,而是浏览器
如果要避免跨域,只要让浏览器认为“我正在向同一个域发起请求”,就可以了。

假设前台使用4200端口,后台使用8080端口,那么,再加入一个8011端口,作为用户访问时连接的端口。
在前台的代码中,会有拦截器,如果发现某个请求是指向后台的,就会在Url中加入一个特殊的标识(比如加一个/api/作为前缀)

// header中带有do_not_intercept,且值为true,则不添加url前缀
if (('true' !== req.headers.get(YunzhiInterceptor.DONT_INTERCEPT_HEADER_KEY))
  && !url.startsWith('https://') && !url.startsWith('http://')) {
  url = '/api/' + url;
}

现在,无论是指向前台还是后台的请求,都会发送到8011端口,只不过,指向后台的请求会有一个/api/前缀。

接下来使用Nginx监听8011端口,当接收到请求时,根据是否有前缀,来判断此请求交给前台或后台处理。

过程如图:
图片.png

上图就是转发的原理。

五、总结

使用Nginx端口转发,本质上就是:让浏览器认为前台和后台是同一个域,就不会产生跨域请求。
开发者事先约定好,根据不同的请求地址,来访问不同的服务器。
Nginx接收到数据之后,根据地址,转发给相应的服务器来处理。
如果需要另外安排其他的服务,来实现文件上传功能的话,只需要把URL处理一下,加上不同的前缀即可。

查看原文

赞 7 收藏 3 评论 0

entner 收藏了文章 · 2020-12-03

深入剖析 Laravel 服务提供者实现原理

本文首发于 深入剖析 Laravel 服务提供者实现原理,转载请注明出处。

今天我们将学习 Laravel 框架另外一个核心内容「服务提供者(Service Provider)」。服务提供者的功能是完成 Laravel 应用的引导启动,或者说是将 Laravel 中的各种服务「注册」到「Laravel 服务容器」,这样才能在后续处理 HTTP 请求时使用这些服务。

服务提供者基本概念

我们知道 「服务提供者」是配置应用的中心,它的主要工作是使用「服务容器」实现服务容器绑定、事件监听器、中间件,甚至是路由的注册。

除核心服务外,几乎所有的服务提供者都定义在配置文件 config/app.php 文件中的 providers 节点中。

服务提供者的典型处理流程是,当接 Laravel 应用接收到 HTTP 请求时会去执行「服务提供者的 register(注册)」方法,将各个服务「绑定」到容器内;之后,到了实际处理请求阶段,依据使用情况按需加载所需服务。这样的优势很明显能够提升应用的性能。

细心的朋友可能发现这里用了一个词「几乎」,没错还有一些属于核心服务提供者,这些并没有定义在 providers 配置节点中而是直接由 Illuminate\Foundation\Application 服务容器直接在实例化阶段就完成了注册服务。

<?php
...

class Application extends Container implements ApplicationContract, HttpKernelInterface
{
    public function __construct($basePath = null)
    {
        ...
        $this->registerBaseServiceProviders();
        ...
    }

    /**
     * Register all of the base service providers. 注册应用基础服务提供者
     *
     * @return void
     */
    protected function registerBaseServiceProviders()
    {
        $this->register(new EventServiceProvider($this));

        $this->register(new LogServiceProvider($this));

        $this->register(new RoutingServiceProvider($this));
    }

对服务容器不是很熟悉的老铁可以阅读 深入剖析 Laravel 服务容器,并且在文中「注册基础服务提供者」一节也有详细分析服务容器是如何注册服务提供者的。

另外一个,我们还需要了解的是所有的服务提供者都继承自 Illuminate\Support\ServiceProvider 类。不过对于我们来说目前还无需研究基类,所以我们将焦点放到如何实现一个自定义的服务提供者,然后还有两个需要掌握方法。

服务提供者入门

创建自定义服务提供者

要创建自定义的「服务提供者」,可以直接使用 Laravel 内置的 artisan 命令完成。

php artisan make:provider RiskServiceProvider

这个命令会在 app/Providers 目录下创建 RiskServiceProvider.php 文件,打开文件内容如下:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class RiskServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.引导启动应用服务。
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register the application services. 注册应用服务。
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

register 方法

register 方法中,我们无需处理业务逻辑,在这个方法中你只需去处理「绑定」服务到服务容器中即可。

从文档中我们知道:

register 方法中,你只需要将类绑定到 服务容器 中。而不需要尝试在 register 方法中注册任何事件监听器、路由或者任何其他功能。否则,你可能会意外使用到尚未加载的服务提供器提供的服务。

如何理解这句话的含义呢?

如果你有了解过服务容器运行原理,就会知道在「绑定」操作仅仅是建立起接口和实现的对应关系,此时并不会创建具体的实例,即不会存在真实的依赖关系。直到某个服务真的被用到时才会从「服务容器」中解析出来,而解析的过程发生在所有服务「注册」完成之后。

一旦我们尝试在 register 注册阶段使用某些未被加载的服务依赖,即这个服务目前还没有被注册所以不可用。

这样就需要在「注册」绑定时,同时需要关注服务的注册顺序,但这一点 Laravel 并不作出任何保证。

理解了这个道理,我们就可以随便进入一个「服务提供者」来看看其中的 register 方法的逻辑,现在我们挑选的是 Illuminate\Cache\CacheServiceProvider 服务作为讲解:

<?php

namespace Illuminate\Cache;

use Illuminate\Support\ServiceProvider;

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

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('cache', function ($app) {
            return new CacheManager($app);
        });

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

        $this->app->singleton('memcached.connector', function () {
            return new MemcachedConnector;
        });
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return [
            'cache', 'cache.store', 'memcached.connector',
        ];
    }
}

没错,如你所预料的一样,它的 register 方法执行了三个单例绑定操作,仅此而已。

简单注册服务

对于处理复杂绑定逻辑,可以自定义「服务提供者」。但是如果是比较简单的注册服务,有没有比较方便的绑定方法呢?毕竟,并不是每个服务都会有复杂的依赖处理。

我们可以从 文档 中得到解答:

如果你的服务提供商注册许多简单的绑定,你可能想使用 bindingssingletons 属性而不是手动注册每个容器绑定。
<?php
class AppServiceProvider extends ServiceProvider
{
    /**
     * 设定容器绑定的对应关系
     *
     * @var array
     */
    public $bindings = [
        ServerProvider::class => DigitalOceanServerProvider::class,
    ];

    /**
     * 设定单例模式的容器绑定对应关系
     *
     * @var array
     */
    public $singletons = [
        DowntimeNotifier::class => PingdomDowntimeNotifier::class,
    ];
}

此时,通过 bingdingssingletons 成员变量来设置简单的绑定,就可以避免大量的「服务提供者」类的生成了。

boot 方法

聊完了 register 方法,接下来进入另一个主题,来研究一下服务提供者的 boot 方法。

通过前面的学习,我们知道在 register 方法中 Laravel 并不能保证所有其他服务已被加载。所以当需要处理具有依赖关系的业务逻辑时,应该将这些逻辑处理放置到 boot 方法内。在 boot 方法中我们可以去完成:注册事件监听器、引入路由文件、注册过滤器等任何你可以想象得到的业务处理。

config/app.php 配置中我们可以看到如下几个服务提供者:

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,

选择其中的 App\Providers\RouteServiceProvider::class 服务提供者它继承自 Illuminate\Foundation\Support\Providers\RouteServiceProvider 基类来看下:

// 实现类
class RouteServiceProvider extends ServiceProvider
{
    /**
     * This namespace is applied to your controller routes. In addition, it is set as the URL generator's root namespace.
     */
    protected $namespace = 'App\Http\Controllers';

    /**
     * Define your route model bindings, pattern filters, etc.
     */
    public function boot()
    {
        parent::boot();
    }

    /**
     * Define the routes for the application. 定义应用路由
     */
    public function map()
    {
        $this->mapApiRoutes();
        $this->mapWebRoutes();
    }

    /**
     * Define the "web" routes for the application. These routes all receive session state, CSRF protection, etc.
     * 定义 web 路由。web 路由支持会话状态和 CSRF 防御中间件等。
     */
    protected function mapWebRoutes()
    {
        Route::middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/web.php'));
    }

    /**
     * Define the "api" routes for the application. These routes are typically stateless.
     * 定义 api 路由。api 接口路由支持典型的  HTTP 无状态协议。
     */
    protected function mapApiRoutes()
    {
        Route::prefix('api')
             ->middleware('api')
             ->namespace($this->namespace)
             ->group(base_path('routes/api.php'));
    }
}

基类 Illuminate\Foundation\Support\Providers\RouteServiceProvider

//  基类
namespace Illuminate\Foundation\Support\Providers;

/**
 * @mixin \Illuminate\Routing\Router
 */
class RouteServiceProvider extends ServiceProvider
{
    /**
     * The controller namespace for the application.
     */
    protected $namespace;

    /**
     * Bootstrap any application services. 引导启动服务
     */
    public function boot()
    {
        $this->setRootControllerNamespace();

        // 如果已缓存路由,从缓存文件中载入路由
        if ($this->app->routesAreCached()) {
            $this->loadCachedRoutes();
        } else {
            //还没有路由缓存,加载路由
            $this->loadRoutes();

            $this->app->booted(function () {
                $this->app['router']->getRoutes()->refreshNameLookups();
                $this->app['router']->getRoutes()->refreshActionLookups();
            });
        }
    }

    /**
     * Load the application routes. 加载应用路由,调用实例的 map 方法,该方法定义在 App\Providers\RouteServiceProvider::class 中。
     */
    protected function loadRoutes()
    {
        if (method_exists($this, 'map')) {
            $this->app->call([$this, 'map']);
        }
    }
}

对于 RouteServiceProvider 来讲,它的 boot 方法在处理一个路由载入的问题:

  • 判断是否已有路由缓存;
  • 有路由缓存,则直接载入路由缓存;
  • 无路由缓存,执行 map 方法载入路由。

感兴趣的朋友可以自行了解下 Application Service Providers 配置节点的相关服务提供者,这边不再赘述。

配置服务提供者

了解完「服务提供者」两个重要方法后,我们还需要知道 Laravel 是如何查找到所有的服务提供者的。这个超找的过程就是去读取 config/app.php 文件中的 providers 节点内所有的「服务提供器」。

具体的读取过程我们也会在「服务提供者启动原理」一节中讲解。

延迟绑定服务提供者

对于一个项目来说,除了要让它跑起来,往往我们还需要关注它的性能问题。

当我们打开 config/app.php 配置文件时,你会发现有配置很多服务提供者,难道所有的都需要去执行它的 registerboot 方法么?

对于不会每次使用的服务提供者很明显,无需每次注册和启动,直到需要用到它的时候。

为了解决这个问题 Laravel 内置支持 延迟服务提供者 功能,启用时延迟功能后,当它真正需要注册绑定时才会执行 register 方法,这样就可以提升我们服务的性能了。

启用「延迟服务提供者」功能,需要完成两个操作配置:

  1. 在对应服务提供者中将 defer 属性设置为 true
  2. 并定义 provides 方法,方法返回在提供者 register 方法内需要注册的服务接口名称。

我们拿 config/app.php 配置中的 BroadcastServiceProvider 作为演示说明:

<?php

class BroadcastServiceProvider extends ServiceProvider
{
    /**
     * Indicates if loading of the provider is deferred. 标识提供者是否为延迟加载类型。
     *
     * @var bool
     */
    protected $defer = true;

    public function register()
    {
        $this->app->singleton(BroadcastManager::class, function ($app) {
            return new BroadcastManager($app);
        });

        $this->app->singleton(BroadcasterContract::class, function ($app) {
            return $app->make(BroadcastManager::class)->connection();
        });

        $this->app->alias(
            BroadcastManager::class, BroadcastingFactory::class
        );
    }

    /**
     * Get the services provided by the provider. 获取提供者所提供的服务接口名称。
     */
    public function provides()
    {
        return [
            BroadcastManager::class,
            BroadcastingFactory::class,
            BroadcasterContract::class,
        ];
    }
}

小结

在「服务提供者入门」这个小节我们学习了服务提供者的基本使用和性能优化相关知识,包括:

  • 如何创建自定义的服务提供者;
  • 创建 register 方法注册服务到 Laravel 服务容器;
  • 创建 boot 方法启动服务提供者的引导程序;
  • 配置我们的服务提供者到 config/app.php 文件,这样才能在容器中加载相应服务;
  • 通过延迟绑定技术,提升 Laravel 服务性能。

下一小节,我们将焦点转移到「服务提供者」的实现原理中,深入到 Laravel 内核中去探索「服务提供者」如何被注册和启动,又是如何能够通过延迟技术提升 Laravel 应用的性能的。

服务提供者启动原理

之前我们有学习 深度挖掘 Laravel 生命周期深入剖析 Laravel 服务容器,今天我们将学习「服务提供者」。

Laravel 的所有核心服务都是通过服务提供者进行引导启动的,所以想深入了解 Laravel 那么研究「服务提供者」的原理是个绕不开的话题。

引导程序的启动流程

服务提供者 注册引导启动 直到处理 HTTP 请求阶段才开始。所以我们直接进入到 App\Console\Kernel::class 类,同时这个类继承于 Illuminate\Foundation\HttpKernel 类。

Illuminate\Foundation\Http\Kernel 类中我们可以看到如下内容:

class Kernel implements KernelContract
{
    ...

    /**
     * The bootstrap classes for the application. 应用引导类
     */
    protected $bootstrappers = [
        ...
        \Illuminate\Foundation\Bootstrap\RegisterProviders::class, // 用于注册(register)「服务提供者」的引导类
        \Illuminate\Foundation\Bootstrap\BootProviders::class, // 用于启动(boot)「服务提供者」的引导类
    ];

    /**
     * Handle an incoming HTTP request. 处理 HTTP 请求
     */
    public function handle($request)
    {
        try {
            $request->enableHttpMethodParameterOverride();

            $response = $this->sendRequestThroughRouter($request);
        } catch (Exception $e) {
            ...
        } catch (Throwable $e) {
            ...
        }

        ...
    }

    /**
     * Send the given request through the middleware / router. 对 HTTP 请求执行中间件处理后再发送到指定路由。
     */
    protected function sendRequestThroughRouter($request)
    {
        ...

        // 1. 引导类引导启动。
        $this->bootstrap();

        // 2. 中间件及请求处理,生成响应并返回响应。
        return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }

    /**
     * Bootstrap the application for HTTP requests. 接收 HTTP 请求时启动应用引导程序。
     */
    public function bootstrap()
    {
        // 引导类启动由 Application 容器引导启动。
        if (! $this->app->hasBeenBootstrapped()) {
            $this->app->bootstrapWith($this->bootstrappers());
        }
    }
}

Illuminate\Foundation\Http\Kernel 我们的内核处理 HTTP 请求时会经过一下两个主要步骤:

  1. 启动引导程序通过 $this->bootstrap() 方法完成,其中包括所有服务提供者的注册和引导处理;
  2. 处理 HTTP 请求(这个问题涉及到中间件、路由及相应处理,本文将不做深入探讨)。

进入 Illuminate\Foundation\Application 容器中的 bootstrapWith() 方法,来看看容器是如何将引导类引导启动的:

    /**
     * Run the given array of bootstrap classes. 执行给定引导程序
     */
    public function bootstrapWith(array $bootstrappers)
    {
        $this->hasBeenBootstrapped = true;

        foreach ($bootstrappers as $bootstrapper) {
            $this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);

            // 从容器中解析出实例,然后调用实例的 bootstrap() 方法引导启动。 
            $this->make($bootstrapper)->bootstrap($this);

            $this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);
        }
    }

通过服务容器的 bootstrap() 方法引导启动时,将定义的在 Illuminate\Foundation\Http\Kerne 类中的应用引导类($bootstrappers)交由 Application 服务容器引导启动。其中与「服务提供者」有关的引导类为:

Illuminate\Foundation\Http\Kerne HTTP 内核通过 bootstrap() 方法引导启动时,实际由服务容器(Application)去完成引导启动的工作,并依据定义在 HTTP 内核中的引导类属性配置顺序依次引导启动,最终「服务提供者」的启动顺序是:

  • 执行「服务提供者」register 方法的引导类:\Illuminate\Foundation\Bootstrap\RegisterProviders::class,将完成所有定义在 config/app.php 配置中的服务提供者的注册(register)处理;
  • 执行「服务提供者」boot 方法的引导类:\Illuminate\Foundation\Bootstrap\BootProviders::class,将完成所有定义在 config/app.php 配置中的服务提供者的启动(boot)处理。

Laravel 执行服务提供者注册(register)处理

前面说过「服务提供者」的注册由 \Illuminate\Foundation\Bootstrap\RegisterProviders::class 引导类启动方法(botstrap())完成。

1. RegisterProviders 引导注册
<?php
class RegisterProviders
{
    /**
     * Bootstrap the given application.
     */
    public function bootstrap(Application $app)
    {
        $app->registerConfiguredProviders();
    }
}

在其通过调用服务容器的 registerConfiguredProviders() 方法完成引导启动,所以我们需要到容器中一探究竟。

2. 由服务容器执行配置文件中的所有服务提供者服务完成注册。
    /**
     * Register all of the configured providers. 执行所有配置服务提供者完成注册处理。
     * 
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php
     */
    public function registerConfiguredProviders()
    {
        $providers = Collection::make($this->config['app.providers'])
                        ->partition(function ($provider) {
                            return Str::startsWith($provider, 'Illuminate\\');
                        });

        $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);

        // 通过服务提供者仓库(ProviderRepository)加载所有的提供者。
        (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
                    ->load($providers->collapse()->toArray());
    }
3. 最后由服务提供者仓库(ProviderRepository)执行服务提供者的注册处理。
<?php

namespace Illuminate\Foundation;

use Exception;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Contracts\Foundation\Application as ApplicationContract;

class ProviderRepository
{
    ...

    /**
     * Register the application service providers. 注册应用的服务提供者。
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/ProviderRepository.php
     */
    public function load(array $providers)
    {
        $manifest = $this->loadManifest();

        // 首先从服务提供者的缓存清单文件中载入服务提供者集合。其中包含「延迟加载」的服务提供者。
        if ($this->shouldRecompile($manifest, $providers)) {
            $manifest = $this->compileManifest($providers);
        }

        // Next, we will register events to load the providers for each of the events
        // that it has requested. This allows the service provider to defer itself
        // while still getting automatically loaded when a certain event occurs.
        foreach ($manifest['when'] as $provider => $events) {
            $this->registerLoadEvents($provider, $events);
        }

        // 到这里,先执行应用必要(贪婪)的服务提供者完成服务注册。
        foreach ($manifest['eager'] as $provider) {
            $this->app->register($provider);
        }

        // 最后将所有「延迟加载服务提供者」加入到容器中。
        $this->app->addDeferredServices($manifest['deferred']);
    }

    /**
     * Compile the application service manifest file. 将服务提供者编译到清单文件中缓存起来。
     */
    protected function compileManifest($providers)
    {
        // The service manifest should contain a list of all of the providers for
        // the application so we can compare it on each request to the service
        // and determine if the manifest should be recompiled or is current.
        $manifest = $this->freshManifest($providers);

        foreach ($providers as $provider) {
            // 解析出 $provider 对应的实例
            $instance = $this->createProvider($provider);

            // 判断当前服务提供者是否为「延迟加载」类行的,是则将其加入到缓存文件的「延迟加载(deferred)」集合中。
            if ($instance->isDeferred()) {
                foreach ($instance->provides() as $service) {
                    $manifest['deferred'][$service] = $provider;
                }

                $manifest['when'][$provider] = $instance->when();
            }

            // 如果不是「延迟加载」类型的服务提供者,则为贪婪加载必须立即去执行注册方法。
            else {
                $manifest['eager'][] = $provider;
            }
        }

        // 将归类后的服务提供者写入清单文件。
        return $this->writeManifest($manifest);
    }

服务提供者仓库(ProviderRepository) 处理程序中依次执行如下处理:

  • 如果存在服务提供者缓存清单,则直接读取「服务提供者」集合;
  • 否则,将从 config/app.php 配置中的服务提供者编译到缓存清单中;编译由 compileManifest() 方法完成; 编译缓存清单时将处理贪婪加载(eager)和延迟加载(deferred)的服务提供者;
  • 对于贪婪加载的提供者直接执行服务容器的 register 方法完成服务注册;
  • 将延迟加载提供者加入到服务容器中,按需注册和引导启动。

最后通过 Illuminate\Foundation\Application 容器完成注册处理:

    /**
     * Register a service provider with the application. 在应用服务容器中注册一个服务提供者。
     */
    public function register($provider, $options = [], $force = false)
    {
        if (($registered = $this->getProvider($provider)) && ! $force) {
            return $registered;
        }

        // 如果给定的服务提供者是接口名称,解析出它的实例。
        if (is_string($provider)) {
            $provider = $this->resolveProvider($provider);
        }

        // 服务提供者提供注册方法时,执行注册服务处理
        if (method_exists($provider, 'register')) {
            $provider->register();
        }

        $this->markAsRegistered($provider);

        // 判断 Laravel 应用是否已启动。已启动的话需要去执行启动处理。
        if ($this->booted) {
            $this->bootProvider($provider);
        }

        return $provider;
    }

为什么需要判断是否已经启动过呢?

因为对于延迟加载的服务提供者只有在使用时才会被调用,所以这里需要这样判断,然后再去启动它。

以上,便是

Laravel 执行服务提供者启动(boot)处理

「服务提供者」的启动流程和注册流程大致相同,有兴趣的朋友可以深入源码了解一下。

1. BootProviders 引导启动
<?php

namespace Illuminate\Foundation\Bootstrap;

use Illuminate\Contracts\Foundation\Application;

class BootProviders
{
    /**
     * Bootstrap the given application.
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Bootstrap/BootProviders.php
     */
    public function bootstrap(Application $app)
    {
        $app->boot();
    }
}
2. 由服务容器执行配置文件中的所有服务提供者服务完成启动。
    /**
     * Boot the application's service providers. 引导启动应用所有服务提供者
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php
     */
    public function boot()
    {
        if ($this->booted) {
            return;
        }

        // 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);

        // 遍历并执行服务提供者的 boot 方法。
        array_walk($this->serviceProviders, function ($p) {
            $this->bootProvider($p);
        });

        $this->booted = true;

        $this->fireAppCallbacks($this->bootedCallbacks);
    }

    /**
     * Boot the given service provider. 启动给定服务提供者
     */
    protected function bootProvider(ServiceProvider $provider)
    {
        if (method_exists($provider, 'boot')) {
            return $this->call([$provider, 'boot']);
        }
    }

以上便是服务提供者执行 注册绑定服务引导启动 的相关实现。

但是稍等一下,我们是不是忘记了还有「延迟加载」类型的服务提供者,它们还没有被注册和引导启动呢!

Laravel 如何完成延迟加载类型的服务提供者

对于延迟加载类型的服务提供者,我们要到使用时才会去执行它们内部的 registerboot 方法。这里我们所说的使用即使需要 解析 它,我们知道解析处理由服务容器完成。

所以我们需要进入到 Illuminate\Foundation\Application 容器中探索 make 解析的一些细节。

    /**
     * Resolve the given type from the container. 从容器中解析出给定服务
     * 
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php
     */
    public function make($abstract, array $parameters = [])
    {
        $abstract = $this->getAlias($abstract);

        // 判断这个接口是否为延迟类型的并且没有被解析过,是则去将它加载到容器中。
        if (isset($this->deferredServices[$abstract]) && ! isset($this->instances[$abstract])) {
            $this->loadDeferredProvider($abstract);
        }

        return parent::make($abstract, $parameters);
    }

    /**
     * Load the provider for a deferred service. 加载给定延迟加载服务提供者
     */
    public function loadDeferredProvider($service)
    {
        if (! isset($this->deferredServices[$service])) {
            return;
        }

        $provider = $this->deferredServices[$service];

        // 如果服务为注册则去注册并从延迟服务提供者集合中删除它。
        if (! isset($this->loadedProviders[$provider])) {
            $this->registerDeferredProvider($provider, $service);
        }
    }

    /**
     * Register a deferred provider and service. 去执行服务提供者的注册方法。
     */
    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) {
                $this->bootProvider($instance);
            });
        }
    }

总结

今天我们深入研究了 Laravel 服务提供者的注册和启动的实现原理,希望对大家有所帮助。

如果对如何自定义服务提供者不甚了解的朋友可以去阅读 Laravel 服务提供者指南 这篇文章。

查看原文

entner 赞了文章 · 2020-12-03

深入剖析 Laravel 服务提供者实现原理

本文首发于 深入剖析 Laravel 服务提供者实现原理,转载请注明出处。

今天我们将学习 Laravel 框架另外一个核心内容「服务提供者(Service Provider)」。服务提供者的功能是完成 Laravel 应用的引导启动,或者说是将 Laravel 中的各种服务「注册」到「Laravel 服务容器」,这样才能在后续处理 HTTP 请求时使用这些服务。

服务提供者基本概念

我们知道 「服务提供者」是配置应用的中心,它的主要工作是使用「服务容器」实现服务容器绑定、事件监听器、中间件,甚至是路由的注册。

除核心服务外,几乎所有的服务提供者都定义在配置文件 config/app.php 文件中的 providers 节点中。

服务提供者的典型处理流程是,当接 Laravel 应用接收到 HTTP 请求时会去执行「服务提供者的 register(注册)」方法,将各个服务「绑定」到容器内;之后,到了实际处理请求阶段,依据使用情况按需加载所需服务。这样的优势很明显能够提升应用的性能。

细心的朋友可能发现这里用了一个词「几乎」,没错还有一些属于核心服务提供者,这些并没有定义在 providers 配置节点中而是直接由 Illuminate\Foundation\Application 服务容器直接在实例化阶段就完成了注册服务。

<?php
...

class Application extends Container implements ApplicationContract, HttpKernelInterface
{
    public function __construct($basePath = null)
    {
        ...
        $this->registerBaseServiceProviders();
        ...
    }

    /**
     * Register all of the base service providers. 注册应用基础服务提供者
     *
     * @return void
     */
    protected function registerBaseServiceProviders()
    {
        $this->register(new EventServiceProvider($this));

        $this->register(new LogServiceProvider($this));

        $this->register(new RoutingServiceProvider($this));
    }

对服务容器不是很熟悉的老铁可以阅读 深入剖析 Laravel 服务容器,并且在文中「注册基础服务提供者」一节也有详细分析服务容器是如何注册服务提供者的。

另外一个,我们还需要了解的是所有的服务提供者都继承自 Illuminate\Support\ServiceProvider 类。不过对于我们来说目前还无需研究基类,所以我们将焦点放到如何实现一个自定义的服务提供者,然后还有两个需要掌握方法。

服务提供者入门

创建自定义服务提供者

要创建自定义的「服务提供者」,可以直接使用 Laravel 内置的 artisan 命令完成。

php artisan make:provider RiskServiceProvider

这个命令会在 app/Providers 目录下创建 RiskServiceProvider.php 文件,打开文件内容如下:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class RiskServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.引导启动应用服务。
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register the application services. 注册应用服务。
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

register 方法

register 方法中,我们无需处理业务逻辑,在这个方法中你只需去处理「绑定」服务到服务容器中即可。

从文档中我们知道:

register 方法中,你只需要将类绑定到 服务容器 中。而不需要尝试在 register 方法中注册任何事件监听器、路由或者任何其他功能。否则,你可能会意外使用到尚未加载的服务提供器提供的服务。

如何理解这句话的含义呢?

如果你有了解过服务容器运行原理,就会知道在「绑定」操作仅仅是建立起接口和实现的对应关系,此时并不会创建具体的实例,即不会存在真实的依赖关系。直到某个服务真的被用到时才会从「服务容器」中解析出来,而解析的过程发生在所有服务「注册」完成之后。

一旦我们尝试在 register 注册阶段使用某些未被加载的服务依赖,即这个服务目前还没有被注册所以不可用。

这样就需要在「注册」绑定时,同时需要关注服务的注册顺序,但这一点 Laravel 并不作出任何保证。

理解了这个道理,我们就可以随便进入一个「服务提供者」来看看其中的 register 方法的逻辑,现在我们挑选的是 Illuminate\Cache\CacheServiceProvider 服务作为讲解:

<?php

namespace Illuminate\Cache;

use Illuminate\Support\ServiceProvider;

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

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('cache', function ($app) {
            return new CacheManager($app);
        });

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

        $this->app->singleton('memcached.connector', function () {
            return new MemcachedConnector;
        });
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return [
            'cache', 'cache.store', 'memcached.connector',
        ];
    }
}

没错,如你所预料的一样,它的 register 方法执行了三个单例绑定操作,仅此而已。

简单注册服务

对于处理复杂绑定逻辑,可以自定义「服务提供者」。但是如果是比较简单的注册服务,有没有比较方便的绑定方法呢?毕竟,并不是每个服务都会有复杂的依赖处理。

我们可以从 文档 中得到解答:

如果你的服务提供商注册许多简单的绑定,你可能想使用 bindingssingletons 属性而不是手动注册每个容器绑定。
<?php
class AppServiceProvider extends ServiceProvider
{
    /**
     * 设定容器绑定的对应关系
     *
     * @var array
     */
    public $bindings = [
        ServerProvider::class => DigitalOceanServerProvider::class,
    ];

    /**
     * 设定单例模式的容器绑定对应关系
     *
     * @var array
     */
    public $singletons = [
        DowntimeNotifier::class => PingdomDowntimeNotifier::class,
    ];
}

此时,通过 bingdingssingletons 成员变量来设置简单的绑定,就可以避免大量的「服务提供者」类的生成了。

boot 方法

聊完了 register 方法,接下来进入另一个主题,来研究一下服务提供者的 boot 方法。

通过前面的学习,我们知道在 register 方法中 Laravel 并不能保证所有其他服务已被加载。所以当需要处理具有依赖关系的业务逻辑时,应该将这些逻辑处理放置到 boot 方法内。在 boot 方法中我们可以去完成:注册事件监听器、引入路由文件、注册过滤器等任何你可以想象得到的业务处理。

config/app.php 配置中我们可以看到如下几个服务提供者:

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,

选择其中的 App\Providers\RouteServiceProvider::class 服务提供者它继承自 Illuminate\Foundation\Support\Providers\RouteServiceProvider 基类来看下:

// 实现类
class RouteServiceProvider extends ServiceProvider
{
    /**
     * This namespace is applied to your controller routes. In addition, it is set as the URL generator's root namespace.
     */
    protected $namespace = 'App\Http\Controllers';

    /**
     * Define your route model bindings, pattern filters, etc.
     */
    public function boot()
    {
        parent::boot();
    }

    /**
     * Define the routes for the application. 定义应用路由
     */
    public function map()
    {
        $this->mapApiRoutes();
        $this->mapWebRoutes();
    }

    /**
     * Define the "web" routes for the application. These routes all receive session state, CSRF protection, etc.
     * 定义 web 路由。web 路由支持会话状态和 CSRF 防御中间件等。
     */
    protected function mapWebRoutes()
    {
        Route::middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/web.php'));
    }

    /**
     * Define the "api" routes for the application. These routes are typically stateless.
     * 定义 api 路由。api 接口路由支持典型的  HTTP 无状态协议。
     */
    protected function mapApiRoutes()
    {
        Route::prefix('api')
             ->middleware('api')
             ->namespace($this->namespace)
             ->group(base_path('routes/api.php'));
    }
}

基类 Illuminate\Foundation\Support\Providers\RouteServiceProvider

//  基类
namespace Illuminate\Foundation\Support\Providers;

/**
 * @mixin \Illuminate\Routing\Router
 */
class RouteServiceProvider extends ServiceProvider
{
    /**
     * The controller namespace for the application.
     */
    protected $namespace;

    /**
     * Bootstrap any application services. 引导启动服务
     */
    public function boot()
    {
        $this->setRootControllerNamespace();

        // 如果已缓存路由,从缓存文件中载入路由
        if ($this->app->routesAreCached()) {
            $this->loadCachedRoutes();
        } else {
            //还没有路由缓存,加载路由
            $this->loadRoutes();

            $this->app->booted(function () {
                $this->app['router']->getRoutes()->refreshNameLookups();
                $this->app['router']->getRoutes()->refreshActionLookups();
            });
        }
    }

    /**
     * Load the application routes. 加载应用路由,调用实例的 map 方法,该方法定义在 App\Providers\RouteServiceProvider::class 中。
     */
    protected function loadRoutes()
    {
        if (method_exists($this, 'map')) {
            $this->app->call([$this, 'map']);
        }
    }
}

对于 RouteServiceProvider 来讲,它的 boot 方法在处理一个路由载入的问题:

  • 判断是否已有路由缓存;
  • 有路由缓存,则直接载入路由缓存;
  • 无路由缓存,执行 map 方法载入路由。

感兴趣的朋友可以自行了解下 Application Service Providers 配置节点的相关服务提供者,这边不再赘述。

配置服务提供者

了解完「服务提供者」两个重要方法后,我们还需要知道 Laravel 是如何查找到所有的服务提供者的。这个超找的过程就是去读取 config/app.php 文件中的 providers 节点内所有的「服务提供器」。

具体的读取过程我们也会在「服务提供者启动原理」一节中讲解。

延迟绑定服务提供者

对于一个项目来说,除了要让它跑起来,往往我们还需要关注它的性能问题。

当我们打开 config/app.php 配置文件时,你会发现有配置很多服务提供者,难道所有的都需要去执行它的 registerboot 方法么?

对于不会每次使用的服务提供者很明显,无需每次注册和启动,直到需要用到它的时候。

为了解决这个问题 Laravel 内置支持 延迟服务提供者 功能,启用时延迟功能后,当它真正需要注册绑定时才会执行 register 方法,这样就可以提升我们服务的性能了。

启用「延迟服务提供者」功能,需要完成两个操作配置:

  1. 在对应服务提供者中将 defer 属性设置为 true
  2. 并定义 provides 方法,方法返回在提供者 register 方法内需要注册的服务接口名称。

我们拿 config/app.php 配置中的 BroadcastServiceProvider 作为演示说明:

<?php

class BroadcastServiceProvider extends ServiceProvider
{
    /**
     * Indicates if loading of the provider is deferred. 标识提供者是否为延迟加载类型。
     *
     * @var bool
     */
    protected $defer = true;

    public function register()
    {
        $this->app->singleton(BroadcastManager::class, function ($app) {
            return new BroadcastManager($app);
        });

        $this->app->singleton(BroadcasterContract::class, function ($app) {
            return $app->make(BroadcastManager::class)->connection();
        });

        $this->app->alias(
            BroadcastManager::class, BroadcastingFactory::class
        );
    }

    /**
     * Get the services provided by the provider. 获取提供者所提供的服务接口名称。
     */
    public function provides()
    {
        return [
            BroadcastManager::class,
            BroadcastingFactory::class,
            BroadcasterContract::class,
        ];
    }
}

小结

在「服务提供者入门」这个小节我们学习了服务提供者的基本使用和性能优化相关知识,包括:

  • 如何创建自定义的服务提供者;
  • 创建 register 方法注册服务到 Laravel 服务容器;
  • 创建 boot 方法启动服务提供者的引导程序;
  • 配置我们的服务提供者到 config/app.php 文件,这样才能在容器中加载相应服务;
  • 通过延迟绑定技术,提升 Laravel 服务性能。

下一小节,我们将焦点转移到「服务提供者」的实现原理中,深入到 Laravel 内核中去探索「服务提供者」如何被注册和启动,又是如何能够通过延迟技术提升 Laravel 应用的性能的。

服务提供者启动原理

之前我们有学习 深度挖掘 Laravel 生命周期深入剖析 Laravel 服务容器,今天我们将学习「服务提供者」。

Laravel 的所有核心服务都是通过服务提供者进行引导启动的,所以想深入了解 Laravel 那么研究「服务提供者」的原理是个绕不开的话题。

引导程序的启动流程

服务提供者 注册引导启动 直到处理 HTTP 请求阶段才开始。所以我们直接进入到 App\Console\Kernel::class 类,同时这个类继承于 Illuminate\Foundation\HttpKernel 类。

Illuminate\Foundation\Http\Kernel 类中我们可以看到如下内容:

class Kernel implements KernelContract
{
    ...

    /**
     * The bootstrap classes for the application. 应用引导类
     */
    protected $bootstrappers = [
        ...
        \Illuminate\Foundation\Bootstrap\RegisterProviders::class, // 用于注册(register)「服务提供者」的引导类
        \Illuminate\Foundation\Bootstrap\BootProviders::class, // 用于启动(boot)「服务提供者」的引导类
    ];

    /**
     * Handle an incoming HTTP request. 处理 HTTP 请求
     */
    public function handle($request)
    {
        try {
            $request->enableHttpMethodParameterOverride();

            $response = $this->sendRequestThroughRouter($request);
        } catch (Exception $e) {
            ...
        } catch (Throwable $e) {
            ...
        }

        ...
    }

    /**
     * Send the given request through the middleware / router. 对 HTTP 请求执行中间件处理后再发送到指定路由。
     */
    protected function sendRequestThroughRouter($request)
    {
        ...

        // 1. 引导类引导启动。
        $this->bootstrap();

        // 2. 中间件及请求处理,生成响应并返回响应。
        return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }

    /**
     * Bootstrap the application for HTTP requests. 接收 HTTP 请求时启动应用引导程序。
     */
    public function bootstrap()
    {
        // 引导类启动由 Application 容器引导启动。
        if (! $this->app->hasBeenBootstrapped()) {
            $this->app->bootstrapWith($this->bootstrappers());
        }
    }
}

Illuminate\Foundation\Http\Kernel 我们的内核处理 HTTP 请求时会经过一下两个主要步骤:

  1. 启动引导程序通过 $this->bootstrap() 方法完成,其中包括所有服务提供者的注册和引导处理;
  2. 处理 HTTP 请求(这个问题涉及到中间件、路由及相应处理,本文将不做深入探讨)。

进入 Illuminate\Foundation\Application 容器中的 bootstrapWith() 方法,来看看容器是如何将引导类引导启动的:

    /**
     * Run the given array of bootstrap classes. 执行给定引导程序
     */
    public function bootstrapWith(array $bootstrappers)
    {
        $this->hasBeenBootstrapped = true;

        foreach ($bootstrappers as $bootstrapper) {
            $this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);

            // 从容器中解析出实例,然后调用实例的 bootstrap() 方法引导启动。 
            $this->make($bootstrapper)->bootstrap($this);

            $this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);
        }
    }

通过服务容器的 bootstrap() 方法引导启动时,将定义的在 Illuminate\Foundation\Http\Kerne 类中的应用引导类($bootstrappers)交由 Application 服务容器引导启动。其中与「服务提供者」有关的引导类为:

Illuminate\Foundation\Http\Kerne HTTP 内核通过 bootstrap() 方法引导启动时,实际由服务容器(Application)去完成引导启动的工作,并依据定义在 HTTP 内核中的引导类属性配置顺序依次引导启动,最终「服务提供者」的启动顺序是:

  • 执行「服务提供者」register 方法的引导类:\Illuminate\Foundation\Bootstrap\RegisterProviders::class,将完成所有定义在 config/app.php 配置中的服务提供者的注册(register)处理;
  • 执行「服务提供者」boot 方法的引导类:\Illuminate\Foundation\Bootstrap\BootProviders::class,将完成所有定义在 config/app.php 配置中的服务提供者的启动(boot)处理。

Laravel 执行服务提供者注册(register)处理

前面说过「服务提供者」的注册由 \Illuminate\Foundation\Bootstrap\RegisterProviders::class 引导类启动方法(botstrap())完成。

1. RegisterProviders 引导注册
<?php
class RegisterProviders
{
    /**
     * Bootstrap the given application.
     */
    public function bootstrap(Application $app)
    {
        $app->registerConfiguredProviders();
    }
}

在其通过调用服务容器的 registerConfiguredProviders() 方法完成引导启动,所以我们需要到容器中一探究竟。

2. 由服务容器执行配置文件中的所有服务提供者服务完成注册。
    /**
     * Register all of the configured providers. 执行所有配置服务提供者完成注册处理。
     * 
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php
     */
    public function registerConfiguredProviders()
    {
        $providers = Collection::make($this->config['app.providers'])
                        ->partition(function ($provider) {
                            return Str::startsWith($provider, 'Illuminate\\');
                        });

        $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);

        // 通过服务提供者仓库(ProviderRepository)加载所有的提供者。
        (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
                    ->load($providers->collapse()->toArray());
    }
3. 最后由服务提供者仓库(ProviderRepository)执行服务提供者的注册处理。
<?php

namespace Illuminate\Foundation;

use Exception;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Contracts\Foundation\Application as ApplicationContract;

class ProviderRepository
{
    ...

    /**
     * Register the application service providers. 注册应用的服务提供者。
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/ProviderRepository.php
     */
    public function load(array $providers)
    {
        $manifest = $this->loadManifest();

        // 首先从服务提供者的缓存清单文件中载入服务提供者集合。其中包含「延迟加载」的服务提供者。
        if ($this->shouldRecompile($manifest, $providers)) {
            $manifest = $this->compileManifest($providers);
        }

        // Next, we will register events to load the providers for each of the events
        // that it has requested. This allows the service provider to defer itself
        // while still getting automatically loaded when a certain event occurs.
        foreach ($manifest['when'] as $provider => $events) {
            $this->registerLoadEvents($provider, $events);
        }

        // 到这里,先执行应用必要(贪婪)的服务提供者完成服务注册。
        foreach ($manifest['eager'] as $provider) {
            $this->app->register($provider);
        }

        // 最后将所有「延迟加载服务提供者」加入到容器中。
        $this->app->addDeferredServices($manifest['deferred']);
    }

    /**
     * Compile the application service manifest file. 将服务提供者编译到清单文件中缓存起来。
     */
    protected function compileManifest($providers)
    {
        // The service manifest should contain a list of all of the providers for
        // the application so we can compare it on each request to the service
        // and determine if the manifest should be recompiled or is current.
        $manifest = $this->freshManifest($providers);

        foreach ($providers as $provider) {
            // 解析出 $provider 对应的实例
            $instance = $this->createProvider($provider);

            // 判断当前服务提供者是否为「延迟加载」类行的,是则将其加入到缓存文件的「延迟加载(deferred)」集合中。
            if ($instance->isDeferred()) {
                foreach ($instance->provides() as $service) {
                    $manifest['deferred'][$service] = $provider;
                }

                $manifest['when'][$provider] = $instance->when();
            }

            // 如果不是「延迟加载」类型的服务提供者,则为贪婪加载必须立即去执行注册方法。
            else {
                $manifest['eager'][] = $provider;
            }
        }

        // 将归类后的服务提供者写入清单文件。
        return $this->writeManifest($manifest);
    }

服务提供者仓库(ProviderRepository) 处理程序中依次执行如下处理:

  • 如果存在服务提供者缓存清单,则直接读取「服务提供者」集合;
  • 否则,将从 config/app.php 配置中的服务提供者编译到缓存清单中;编译由 compileManifest() 方法完成; 编译缓存清单时将处理贪婪加载(eager)和延迟加载(deferred)的服务提供者;
  • 对于贪婪加载的提供者直接执行服务容器的 register 方法完成服务注册;
  • 将延迟加载提供者加入到服务容器中,按需注册和引导启动。

最后通过 Illuminate\Foundation\Application 容器完成注册处理:

    /**
     * Register a service provider with the application. 在应用服务容器中注册一个服务提供者。
     */
    public function register($provider, $options = [], $force = false)
    {
        if (($registered = $this->getProvider($provider)) && ! $force) {
            return $registered;
        }

        // 如果给定的服务提供者是接口名称,解析出它的实例。
        if (is_string($provider)) {
            $provider = $this->resolveProvider($provider);
        }

        // 服务提供者提供注册方法时,执行注册服务处理
        if (method_exists($provider, 'register')) {
            $provider->register();
        }

        $this->markAsRegistered($provider);

        // 判断 Laravel 应用是否已启动。已启动的话需要去执行启动处理。
        if ($this->booted) {
            $this->bootProvider($provider);
        }

        return $provider;
    }

为什么需要判断是否已经启动过呢?

因为对于延迟加载的服务提供者只有在使用时才会被调用,所以这里需要这样判断,然后再去启动它。

以上,便是

Laravel 执行服务提供者启动(boot)处理

「服务提供者」的启动流程和注册流程大致相同,有兴趣的朋友可以深入源码了解一下。

1. BootProviders 引导启动
<?php

namespace Illuminate\Foundation\Bootstrap;

use Illuminate\Contracts\Foundation\Application;

class BootProviders
{
    /**
     * Bootstrap the given application.
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Bootstrap/BootProviders.php
     */
    public function bootstrap(Application $app)
    {
        $app->boot();
    }
}
2. 由服务容器执行配置文件中的所有服务提供者服务完成启动。
    /**
     * Boot the application's service providers. 引导启动应用所有服务提供者
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php
     */
    public function boot()
    {
        if ($this->booted) {
            return;
        }

        // 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);

        // 遍历并执行服务提供者的 boot 方法。
        array_walk($this->serviceProviders, function ($p) {
            $this->bootProvider($p);
        });

        $this->booted = true;

        $this->fireAppCallbacks($this->bootedCallbacks);
    }

    /**
     * Boot the given service provider. 启动给定服务提供者
     */
    protected function bootProvider(ServiceProvider $provider)
    {
        if (method_exists($provider, 'boot')) {
            return $this->call([$provider, 'boot']);
        }
    }

以上便是服务提供者执行 注册绑定服务引导启动 的相关实现。

但是稍等一下,我们是不是忘记了还有「延迟加载」类型的服务提供者,它们还没有被注册和引导启动呢!

Laravel 如何完成延迟加载类型的服务提供者

对于延迟加载类型的服务提供者,我们要到使用时才会去执行它们内部的 registerboot 方法。这里我们所说的使用即使需要 解析 它,我们知道解析处理由服务容器完成。

所以我们需要进入到 Illuminate\Foundation\Application 容器中探索 make 解析的一些细节。

    /**
     * Resolve the given type from the container. 从容器中解析出给定服务
     * 
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php
     */
    public function make($abstract, array $parameters = [])
    {
        $abstract = $this->getAlias($abstract);

        // 判断这个接口是否为延迟类型的并且没有被解析过,是则去将它加载到容器中。
        if (isset($this->deferredServices[$abstract]) && ! isset($this->instances[$abstract])) {
            $this->loadDeferredProvider($abstract);
        }

        return parent::make($abstract, $parameters);
    }

    /**
     * Load the provider for a deferred service. 加载给定延迟加载服务提供者
     */
    public function loadDeferredProvider($service)
    {
        if (! isset($this->deferredServices[$service])) {
            return;
        }

        $provider = $this->deferredServices[$service];

        // 如果服务为注册则去注册并从延迟服务提供者集合中删除它。
        if (! isset($this->loadedProviders[$provider])) {
            $this->registerDeferredProvider($provider, $service);
        }
    }

    /**
     * Register a deferred provider and service. 去执行服务提供者的注册方法。
     */
    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) {
                $this->bootProvider($instance);
            });
        }
    }

总结

今天我们深入研究了 Laravel 服务提供者的注册和启动的实现原理,希望对大家有所帮助。

如果对如何自定义服务提供者不甚了解的朋友可以去阅读 Laravel 服务提供者指南 这篇文章。

查看原文

赞 20 收藏 17 评论 0

entner 收藏了文章 · 2020-10-14

MySQL基本操作之-DDL,DML,DQL,DCL

MySQL基本操作之DDL(数据定义语言),DML(数据操纵语言),DQL(数据查询语言),DCL(数据控制语言)微信搜索公众号:”菜鸟封神记“,定期分享一线大厂常用技术干货。

一、DDL--数据定义语言
作用:数据定义语言主要用来定义数据库中的各类对象,包括用户、库、表、视图、索引、触发器、事件、存储过程和函数等。
常见的DDL操作的基本用法如下:

   CREATE USER           #创建用户
   CREATE DATABASE       #创建数据库
   CREATE TABLE          #创建表
   CREATE VIEW           #创建视图
   CREATE INDEX          #创建索引
   CREATE TRIGGER        #创建触发器
   CREATE EVENT          #创建事件
   CREATE PROCEDURE      #创建存储过程
   CREATE FUNCTION       #创建自定义函数
   ...其他不常用的DDL(如:TABLESPACE)操作可自行查阅资料...

1、创建用户:
详细用法:CREATE USER 'username'@'[ip/domain/netmask]'

参数解释:
username:表示登陆MySQL实例的用户名
[ip/domain/ip range]:表示数据库实例允许的登陆ip,域名或者ip段

示例:创建一个名称为bingwang,登陆ip为192.168.0.10的用户:

mysql> CREATE USER 'bingwang'@'192.168.0.10';

2、创建数据库,示例如下:

详细用法:

CREATE DATABASE db_name;

示例如下:

#创建一个名称为test_db,字符集为utf8的数据库
mysql> CREATE DATABASE test_db DEFAULT CHARSET UTF8;

3、创建表:

详细用法:CREATE TABLE table_name;
示例如下:

#创建一个名称为t_test,字符集为utf8,存储引擎为InnoDB,字符校验集为utf8_general_ci的表:
mysql> CREATE TABLE t_test (
           id INT NOT NULL AUTO_INCREMENT,
           name VARCHAR(50),
           PRIMARY KEY(id)
       ) ENGINE = InnoDB DEFAUL CHARSET = UTF8 COLLATE = utf8_general_ci;

4、创建视图:

详细用法:

CREATE VIEW view_name as <SELECT phrase>;
参数解释: <SELECT phrase>:查询语句

示例如下:

#创建一个视图t_view,用来查询t_test中的ID为1或者2的数据:
mysql> CREATE VIEW test_view AS SELECT * FROM t_test WHERE id IN (1,2);
#查看创建视图的过程:
mysql> SHOW CREATE VIEW test_view;

5、创建索引,有两种方法,CREATE和ALTER,下面先介绍一下CREATE:

详细用法:

CREATE [UNIQUE] INDEX index_name ON table_name(field[num]) <OPTIONS>;

参数解释: 
UNIQUE:表示创建的索引类型为唯一索引,如果创建的为一般索引可以忽略该选项
table_name:表名称
field:表中的某个字段。num为可选参数,如果field为字符创类型,表示给该字段的前num个字符创建索引
OPTIONS:表示可选选项,可以指定索引使用的算法,比如:USING BTREE。不指定默认为BTREE;

示例如下:
(1)给t_test表中的name字段添加一个唯一索引,使用BTREE作为其索引算法:

mysql> CREATE UNIQUE INDEX name_ind ON t_test(name) USING BTREE;
mysql> SHOW [INDEX/INDEXES] FROM t_test;  #查看t_test表中的索引,[INDEX/INDEXES]两个关键字都可以

(2)给t_test表中的name字段的前5个字符创建一般索引,使用BTREE作为其索引算法:

mysql> CREATE INDEX name_index ON t_test(name(5));

关于索引的更多用法及优化在后面的文章中会详细讲解。

6、创建触发器:
详细用法:

CREATE TRIGGER trigger_name trigger_time trigger_event FOR EACH ROW 
BEGIN 
  trigger_stmt 
END;

示例:创建触发器内容稍多,此处先稍微提一下,后面专门章节介绍;

7、创建存储过程:
详细用法:

CREATE PROCEDURE procedure_name([proc_parameter[,...]])
BEGIN
  ...存储过程体
END

示例:创建存储过程内容稍多,此处先稍微提一下,后面专门章节介绍;
8、创建自定义函数:
详细用法:

CREATE FUNCTION function_name([func_parameter[,...]])
RETURNS type
BEGIN
...函数体
END

示例:创建自定义函数内容稍多,此处先稍微提一下,后面专门章节介绍;
至此,简单的DDL操作介绍完成。


二、DML--数据操纵语言:
作用:用来操作数据库中的表对象,主要包括的操作有:INSERT,UPDATE,DELETE
常见的DML的基本操作方法如下:

#给表中添加数据
INSERT INTO ...
#修改表中的数据
UPDATE table_name SET ...
#删除表中的数据
DELETE FROM table_name WHERE <condition>;
注:<condition>:表示DML操作时的条件

1、向表中插入数据:
详细用法:

mysql> INSERT INTO table_name(field1,field2,[,...]) values(value1,value2),(value3,value4),...;

示例:向学生表中插入一条数据,name:'xiaohong', age:24, gender:'M' ,如下:
(1)创建表:

 mysql> CREATE TABLE student(
        id INT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(50) NOT NULL DEFAULT '',
        age TINYINT,
        gender ENUM('F','M')
     ) ENGINE = InnoDB DEFAULT CHARSET = UTF8;

(2)插入数据:

mysql> INSERT INTO student(name,age,gender) VALUES('xiaohong',24,'M');
Query OK, 1 row affected (0.09 sec)
mysql> SELECT * FROM student;
+----+----------+------+--------+
| id | name     | age  | gender |
+----+----------+------+--------+
|  1 | xiaohong |   24 | M      |
+----+----------+------+--------+
1 row in set (0.37 sec)
注:主键如果自动递增,插入时可不用指定;

2、修改表中的数据:
详细用法:

UPDATE table_name SET field1 = value1, field2 = value2, ... ,  WHERE <condition>;

示例:将student表中id为1的记录中的name值修改为:"xiaohua",如下:

mysql> UPDATE STUDENT SET name = 'xiaohua' WHERE id = 1;
Query OK, 1 row affected (0.67 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> SELECT * FROM student;
+----+---------+------+--------+
| id | name    | age  | gender |
+----+---------+------+--------+
|  1 | xiaohua |   24 | M      |
+----+---------+------+--------+
1 row in set (0.00 sec)

注意:此处修改的时候,一定得注意加条件,否则整个表都会被改。讲个技巧:在MySQL的命令中执行操作的时候,可以在登录MySQL时添加-U选项,如果忘加条件,会被阻止执行sql语句。登录命令如下:

    [root@WB-BLOG ~]# mysql -uroot -proot -U -h127.0.0.1 -P3306
    mysql> UPDATE STUDENT SET name = 'hong';
    ERROR 1175 (HY000): You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column

可见:登录之后如果不加条件执行UPDATE语句,会被阻止;

3、删除表中的数据:
详细用法:

mysql> DELETE FROM table_name WHERE <condition>;

示例:删除student表中id为1的记录,如下:

mysql> DELETE FROM student WHERE id = 1;
Query OK, 1 row affected (0.37 sec)
mysql> SELECT * FROM student;
Empty set (0.00 sec)

注意:注意!注意!!再注意!!!,该操作非常危险,命令行中操作时,需要万分注意。可以使用登录时加-U参数的方式,防止忘加条件而删除所有数据,加了-U参数之后,如果不加条件,会被阻止,执行结果如下:

mysql> DELETE FROM student;
ERROR 1175 (HY000): You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column

至此,DML操作介绍完毕。


三、DQL--数据查询语言
作用:主要用来查看表中的数据,也是平时使用最多的操作,主要命令为:SELECT
基本用法如下:

mysql> SELECT fields FROM table_name WHERE <condition>;

注意事项:
fields:表示要查询的字段列表,可以使用代替,但是在程序中最好别写,因为使用*一方面会降低SQL的查询效率,查询到一些用不到的字段;另一方面,使用一些ORM框架时,如果数据库中字段有变动,可能会立刻导致程序报错。

1、简单不加条件的单表查询:
用法:

mysql> SELECT * FROM table;

示例略。

2、单表中的条件查询:
常见的条件:>,>=,<,<= ,=,<>,!=,IN,NOT IN,LIKE,NOT LIKE,REGEXP
示例:

#查询年龄大于23的记录
mysql> SELECT * FROM student WHERE age > 23;
#查询年龄大于等于24的记录,和上面age>23结果相同
mysql> SELECT * FROM student WHERE age >= 24;
#查询年龄小于24的记录
mysql> SELECT * FROM student WHERE age < 24;
#查询年龄小于等于24的记录
mysql> SELECT * FROM student WHERE age <= 24;
#查询姓名等于xiaohong的记录
mysql> SELECT * FROM student WHERE name = 'xiaohong';
#查询姓名不等于xiaohong的记录
mysql> SELECT * FROM student WHERE name <> 'xiaohong'; 
#查询姓名不等于xiaohong的记录
mysql> SELECT * FROM student WHERE name != 'xiaohong';
#查询姓名为xiaohong或者xiaohui的记录
mysql> SELECT * FROM student WHERE name in ('xiaohong','xiaohui');
#查询姓名不是xiaohong和xiaohui的记录等价于:where name != xiaohong and name != xiaohui
mysql> SELECT * FROM student WHERE name not in ('xiaohong','xiaohui');
#查询姓名以xiao开头的记录
mysql> SELECT * FROM student WHERE name like 'xiao%';
#查询姓名以xiaohon开头的记录,后面模糊匹配一位,如:xiaohong,xiaohoni
mysql> SELECT * FROM student WHERE name like 'xiaohon_';
#查询姓名中包含ao字符创的记录
mysql> SELECT * FROM student WHERE name like '%ao%';
#查询以hong结尾的记录
mysql> SELECT * FROM student WHERE name not like '%hong';
#使用正则表达式查询姓名以xiao开头的记录
mysql> SELECT * FROM student WHERE name REGEXP('^xiao');
#使用正则表达式查询姓名以hong结尾的记录
mysql> SELECT * FROM student WHERE name REGEXP('hong$');
#支持的其他复杂的正则表达式,请参阅资料:

正则表达式教程:http://www.runoob.com/regexp/...
注意:
(1)当某个字段上有索引时,使用上述的反向查询或者前模糊查询,如:<>,!=,NOT LIKE,NOT IN,LIKE "%test",将会不走索引;
(2)查询中的潜在问题:如果某个字段在创建表结构的时候未设置非空,则使用WHERE name!="BING"的时候,将不会包含name为NULL的记录;

示例:查询student表中年龄大于"xiaohong"年龄的记录的数量:

mysql> SELECT COUNT(*) FROM student WHERE age > (SELECT age FROM student WHERE name = 'xiaohong');
+----------+
| COUNT(*) |
+----------+
|        2 |
+----------+
1 row in set (0.46 sec)

3、分页查询:
用法:

mysql> SELECT * FROM table_name LIMIT start,num;

参数解释:
start:开始位置,默认从0开始;
num:偏移量,即:从开始位置向后查询的数据条数;
示例:查询test表中,第二页的数据,每页显示10条,如下:

mysql> SELECT * FROM student LIMIT 1,10;

4、使用ORDER BY对查询结果进行排序:
用法:

SELECT * FROM table_name <where condition> ORDER BY <field> ASC/DESC;

示例:从student表中查询出所有年龄大于20的学生记录,并且按照年龄age倒序排列,如下:

SELECT * FROM student WHERE age > 20 ORDER BY age DESC;

注意:如果在排序时ORDER BY <field>之后没有添加DESC和ASC关键字,默认按照ASC升序排列;

5、使用GROUP BY对查询结果集进行分组
基本用法:

mysql> SELECT res FROM table_name <where condition> GROUP BY <field>;

示例:查询student表中男生和女生的数量:

mysql> SELECT gender,COUNT(*) FROM student GROUP BY gender;

6、使用GROUP BY之后,在使用HAVING完成分组之后的条件查询
基本用法:

SELECT res FROM table_name <where condition> GROUP BY <field> <having condition>;

示例:查询student_course表中有3门成绩大于等于80的学生学号
(1)创建测试表结构:

mysql> CREATE TABLE student_course(
    sno INT(11) NOT NULL,
    cno INT(11) NOT NULL,
    grade SMALLINT NOT NULL DEFAULT 0
)ENGINE = InnoDB DEFAULT CHARSET = UTF8;

(2)插入测试数据:

INSERT INTO student_course(sno,cno,grade) VALUES(1,100,79);
INSERT INTO student_course(sno,cno,grade) VALUES(1,101,89);
INSERT INTO student_course(sno,cno,grade) VALUES(1,102,87);
INSERT INTO student_course(sno,cno,grade) VALUES(1,103,99);
INSERT INTO student_course(sno,cno,grade) VALUES(2,100,90);
INSERT INTO student_course(sno,cno,grade) VALUES(2,101,80);
INSERT INTO student_course(sno,cno,grade) VALUES(2,102,77);
INSERT INTO student_course(sno,cno,grade) VALUES(2,103,79);
INSERT INTO student_course(sno,cno,grade) VALUES(3,100,89);
INSERT INTO student_course(sno,cno,grade) VALUES(3,101,90);
INSERT INTO student_course(sno,cno,grade) VALUES(3,102,83);
INSERT INTO student_course(sno,cno,grade) VALUES(3,103,91);

(3)查询:

mysql> SELECT sno,SUM(CASE WHEN grade > 80 THEN 1 ELSE 0 END) num FROM student_course GROUP BY sno HAVING num >= 3;     
+-----+------+
| sno | num  |
+-----+------+
|   1 |    3 |
|   3 |    4 |
+-----+------+
2 rows in set (0.45 sec)


四、DCL--数据控制语言
作用:用来授予或回收访问数据库的某种特权,并控制数据库操纵事务发生的时间及效果。
1、GRANT授予用户权限:
基本用法:

mysql> GRANT priv_type ON <object_type> TO user <WITH {GRANT OPTION | resource_option} ...>;

示例:给用户jerry授予对test_db数据库的增删改查权限,允许该用户从IP为'192.168.0.10'的网络登录
(1)方法一:

mysql> GRANT INSERT,SELECT,UPDATE,DELETE ON test_db.* TO 'jerry'@'192.168.0.10' IDENTIFIED BY 'password' WITH GRANT OPTION;

(2)方法二:

mysql> CREATE USER 'jerry'@'192.168.0.10' IDENTIFIED BY 'password';
mysql> GRANT INSERT,SELECT,UPDATE,DELETE ON test_db.* TO 'jerry'@'192.168.0.10';

2、REVOKE收回用户权限:
基本用法:

mysql> REVOKE priv_type ON <object_type> FROM 'jerry'@'192.168.0.10';

示例:收回用户对test_db库的删除权限:

mysql> REVOKE DELETE ON test_db.* FROM 'jerry'@'192.168.0.10';

3、查看给某个用户所授予的权限:
基本用法:

mysql> SHOW GRANTS FOR user;

示例:查询给'jerry'@'192.168.0.10'所授予的所有权限:

mysql> SHOW GRANTS FOR 'jerry'@'192.168.0.10';

4、查询可授予的所有权限,使用技巧:

(1)首先将某个库(如:test_db)的所有权限授予给用户'jerry'@'localhost'

mysql> GRANT ALL ON test_db.* TO 'jerry'@'localhost' IDENTIFIED BY 'jerry';

(2)收回某个权限,如:查询权限

mysql> REVOKE SELECT ON test_db.* FROM 'jerry'@'localhost';

(3)查看剩余权限,就可以查到除了查询权限之外的权限,再加上查询权限即可授予的所有权限

 mysql> SHOW GRANTS FOR 'jerry'@'localhost';

至此,MySQL的基本操作DDL,DML,DQL,DCL介绍完毕。
下面几次次章节专门介绍触发器、存储过程和函数,大家有什么想法或者希望重点介绍MySQL的哪个模块可以在下方留言,一块学习数据库,欢迎转发~
后续更多文章将更新在个人小站上,欢迎查看。

另外提供一些优秀的IT视频资料,可免费下载!如需要请查看https://www.592xuexi.com

查看原文

entner 赞了文章 · 2020-10-14

MySQL基本操作之-DDL,DML,DQL,DCL

MySQL基本操作之DDL(数据定义语言),DML(数据操纵语言),DQL(数据查询语言),DCL(数据控制语言)微信搜索公众号:”菜鸟封神记“,定期分享一线大厂常用技术干货。

一、DDL--数据定义语言
作用:数据定义语言主要用来定义数据库中的各类对象,包括用户、库、表、视图、索引、触发器、事件、存储过程和函数等。
常见的DDL操作的基本用法如下:

   CREATE USER           #创建用户
   CREATE DATABASE       #创建数据库
   CREATE TABLE          #创建表
   CREATE VIEW           #创建视图
   CREATE INDEX          #创建索引
   CREATE TRIGGER        #创建触发器
   CREATE EVENT          #创建事件
   CREATE PROCEDURE      #创建存储过程
   CREATE FUNCTION       #创建自定义函数
   ...其他不常用的DDL(如:TABLESPACE)操作可自行查阅资料...

1、创建用户:
详细用法:CREATE USER 'username'@'[ip/domain/netmask]'

参数解释:
username:表示登陆MySQL实例的用户名
[ip/domain/ip range]:表示数据库实例允许的登陆ip,域名或者ip段

示例:创建一个名称为bingwang,登陆ip为192.168.0.10的用户:

mysql> CREATE USER 'bingwang'@'192.168.0.10';

2、创建数据库,示例如下:

详细用法:

CREATE DATABASE db_name;

示例如下:

#创建一个名称为test_db,字符集为utf8的数据库
mysql> CREATE DATABASE test_db DEFAULT CHARSET UTF8;

3、创建表:

详细用法:CREATE TABLE table_name;
示例如下:

#创建一个名称为t_test,字符集为utf8,存储引擎为InnoDB,字符校验集为utf8_general_ci的表:
mysql> CREATE TABLE t_test (
           id INT NOT NULL AUTO_INCREMENT,
           name VARCHAR(50),
           PRIMARY KEY(id)
       ) ENGINE = InnoDB DEFAUL CHARSET = UTF8 COLLATE = utf8_general_ci;

4、创建视图:

详细用法:

CREATE VIEW view_name as <SELECT phrase>;
参数解释: <SELECT phrase>:查询语句

示例如下:

#创建一个视图t_view,用来查询t_test中的ID为1或者2的数据:
mysql> CREATE VIEW test_view AS SELECT * FROM t_test WHERE id IN (1,2);
#查看创建视图的过程:
mysql> SHOW CREATE VIEW test_view;

5、创建索引,有两种方法,CREATE和ALTER,下面先介绍一下CREATE:

详细用法:

CREATE [UNIQUE] INDEX index_name ON table_name(field[num]) <OPTIONS>;

参数解释: 
UNIQUE:表示创建的索引类型为唯一索引,如果创建的为一般索引可以忽略该选项
table_name:表名称
field:表中的某个字段。num为可选参数,如果field为字符创类型,表示给该字段的前num个字符创建索引
OPTIONS:表示可选选项,可以指定索引使用的算法,比如:USING BTREE。不指定默认为BTREE;

示例如下:
(1)给t_test表中的name字段添加一个唯一索引,使用BTREE作为其索引算法:

mysql> CREATE UNIQUE INDEX name_ind ON t_test(name) USING BTREE;
mysql> SHOW [INDEX/INDEXES] FROM t_test;  #查看t_test表中的索引,[INDEX/INDEXES]两个关键字都可以

(2)给t_test表中的name字段的前5个字符创建一般索引,使用BTREE作为其索引算法:

mysql> CREATE INDEX name_index ON t_test(name(5));

关于索引的更多用法及优化在后面的文章中会详细讲解。

6、创建触发器:
详细用法:

CREATE TRIGGER trigger_name trigger_time trigger_event FOR EACH ROW 
BEGIN 
  trigger_stmt 
END;

示例:创建触发器内容稍多,此处先稍微提一下,后面专门章节介绍;

7、创建存储过程:
详细用法:

CREATE PROCEDURE procedure_name([proc_parameter[,...]])
BEGIN
  ...存储过程体
END

示例:创建存储过程内容稍多,此处先稍微提一下,后面专门章节介绍;
8、创建自定义函数:
详细用法:

CREATE FUNCTION function_name([func_parameter[,...]])
RETURNS type
BEGIN
...函数体
END

示例:创建自定义函数内容稍多,此处先稍微提一下,后面专门章节介绍;
至此,简单的DDL操作介绍完成。


二、DML--数据操纵语言:
作用:用来操作数据库中的表对象,主要包括的操作有:INSERT,UPDATE,DELETE
常见的DML的基本操作方法如下:

#给表中添加数据
INSERT INTO ...
#修改表中的数据
UPDATE table_name SET ...
#删除表中的数据
DELETE FROM table_name WHERE <condition>;
注:<condition>:表示DML操作时的条件

1、向表中插入数据:
详细用法:

mysql> INSERT INTO table_name(field1,field2,[,...]) values(value1,value2),(value3,value4),...;

示例:向学生表中插入一条数据,name:'xiaohong', age:24, gender:'M' ,如下:
(1)创建表:

 mysql> CREATE TABLE student(
        id INT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(50) NOT NULL DEFAULT '',
        age TINYINT,
        gender ENUM('F','M')
     ) ENGINE = InnoDB DEFAULT CHARSET = UTF8;

(2)插入数据:

mysql> INSERT INTO student(name,age,gender) VALUES('xiaohong',24,'M');
Query OK, 1 row affected (0.09 sec)
mysql> SELECT * FROM student;
+----+----------+------+--------+
| id | name     | age  | gender |
+----+----------+------+--------+
|  1 | xiaohong |   24 | M      |
+----+----------+------+--------+
1 row in set (0.37 sec)
注:主键如果自动递增,插入时可不用指定;

2、修改表中的数据:
详细用法:

UPDATE table_name SET field1 = value1, field2 = value2, ... ,  WHERE <condition>;

示例:将student表中id为1的记录中的name值修改为:"xiaohua",如下:

mysql> UPDATE STUDENT SET name = 'xiaohua' WHERE id = 1;
Query OK, 1 row affected (0.67 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> SELECT * FROM student;
+----+---------+------+--------+
| id | name    | age  | gender |
+----+---------+------+--------+
|  1 | xiaohua |   24 | M      |
+----+---------+------+--------+
1 row in set (0.00 sec)

注意:此处修改的时候,一定得注意加条件,否则整个表都会被改。讲个技巧:在MySQL的命令中执行操作的时候,可以在登录MySQL时添加-U选项,如果忘加条件,会被阻止执行sql语句。登录命令如下:

    [root@WB-BLOG ~]# mysql -uroot -proot -U -h127.0.0.1 -P3306
    mysql> UPDATE STUDENT SET name = 'hong';
    ERROR 1175 (HY000): You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column

可见:登录之后如果不加条件执行UPDATE语句,会被阻止;

3、删除表中的数据:
详细用法:

mysql> DELETE FROM table_name WHERE <condition>;

示例:删除student表中id为1的记录,如下:

mysql> DELETE FROM student WHERE id = 1;
Query OK, 1 row affected (0.37 sec)
mysql> SELECT * FROM student;
Empty set (0.00 sec)

注意:注意!注意!!再注意!!!,该操作非常危险,命令行中操作时,需要万分注意。可以使用登录时加-U参数的方式,防止忘加条件而删除所有数据,加了-U参数之后,如果不加条件,会被阻止,执行结果如下:

mysql> DELETE FROM student;
ERROR 1175 (HY000): You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column

至此,DML操作介绍完毕。


三、DQL--数据查询语言
作用:主要用来查看表中的数据,也是平时使用最多的操作,主要命令为:SELECT
基本用法如下:

mysql> SELECT fields FROM table_name WHERE <condition>;

注意事项:
fields:表示要查询的字段列表,可以使用代替,但是在程序中最好别写,因为使用*一方面会降低SQL的查询效率,查询到一些用不到的字段;另一方面,使用一些ORM框架时,如果数据库中字段有变动,可能会立刻导致程序报错。

1、简单不加条件的单表查询:
用法:

mysql> SELECT * FROM table;

示例略。

2、单表中的条件查询:
常见的条件:>,>=,<,<= ,=,<>,!=,IN,NOT IN,LIKE,NOT LIKE,REGEXP
示例:

#查询年龄大于23的记录
mysql> SELECT * FROM student WHERE age > 23;
#查询年龄大于等于24的记录,和上面age>23结果相同
mysql> SELECT * FROM student WHERE age >= 24;
#查询年龄小于24的记录
mysql> SELECT * FROM student WHERE age < 24;
#查询年龄小于等于24的记录
mysql> SELECT * FROM student WHERE age <= 24;
#查询姓名等于xiaohong的记录
mysql> SELECT * FROM student WHERE name = 'xiaohong';
#查询姓名不等于xiaohong的记录
mysql> SELECT * FROM student WHERE name <> 'xiaohong'; 
#查询姓名不等于xiaohong的记录
mysql> SELECT * FROM student WHERE name != 'xiaohong';
#查询姓名为xiaohong或者xiaohui的记录
mysql> SELECT * FROM student WHERE name in ('xiaohong','xiaohui');
#查询姓名不是xiaohong和xiaohui的记录等价于:where name != xiaohong and name != xiaohui
mysql> SELECT * FROM student WHERE name not in ('xiaohong','xiaohui');
#查询姓名以xiao开头的记录
mysql> SELECT * FROM student WHERE name like 'xiao%';
#查询姓名以xiaohon开头的记录,后面模糊匹配一位,如:xiaohong,xiaohoni
mysql> SELECT * FROM student WHERE name like 'xiaohon_';
#查询姓名中包含ao字符创的记录
mysql> SELECT * FROM student WHERE name like '%ao%';
#查询以hong结尾的记录
mysql> SELECT * FROM student WHERE name not like '%hong';
#使用正则表达式查询姓名以xiao开头的记录
mysql> SELECT * FROM student WHERE name REGEXP('^xiao');
#使用正则表达式查询姓名以hong结尾的记录
mysql> SELECT * FROM student WHERE name REGEXP('hong$');
#支持的其他复杂的正则表达式,请参阅资料:

正则表达式教程:http://www.runoob.com/regexp/...
注意:
(1)当某个字段上有索引时,使用上述的反向查询或者前模糊查询,如:<>,!=,NOT LIKE,NOT IN,LIKE "%test",将会不走索引;
(2)查询中的潜在问题:如果某个字段在创建表结构的时候未设置非空,则使用WHERE name!="BING"的时候,将不会包含name为NULL的记录;

示例:查询student表中年龄大于"xiaohong"年龄的记录的数量:

mysql> SELECT COUNT(*) FROM student WHERE age > (SELECT age FROM student WHERE name = 'xiaohong');
+----------+
| COUNT(*) |
+----------+
|        2 |
+----------+
1 row in set (0.46 sec)

3、分页查询:
用法:

mysql> SELECT * FROM table_name LIMIT start,num;

参数解释:
start:开始位置,默认从0开始;
num:偏移量,即:从开始位置向后查询的数据条数;
示例:查询test表中,第二页的数据,每页显示10条,如下:

mysql> SELECT * FROM student LIMIT 1,10;

4、使用ORDER BY对查询结果进行排序:
用法:

SELECT * FROM table_name <where condition> ORDER BY <field> ASC/DESC;

示例:从student表中查询出所有年龄大于20的学生记录,并且按照年龄age倒序排列,如下:

SELECT * FROM student WHERE age > 20 ORDER BY age DESC;

注意:如果在排序时ORDER BY <field>之后没有添加DESC和ASC关键字,默认按照ASC升序排列;

5、使用GROUP BY对查询结果集进行分组
基本用法:

mysql> SELECT res FROM table_name <where condition> GROUP BY <field>;

示例:查询student表中男生和女生的数量:

mysql> SELECT gender,COUNT(*) FROM student GROUP BY gender;

6、使用GROUP BY之后,在使用HAVING完成分组之后的条件查询
基本用法:

SELECT res FROM table_name <where condition> GROUP BY <field> <having condition>;

示例:查询student_course表中有3门成绩大于等于80的学生学号
(1)创建测试表结构:

mysql> CREATE TABLE student_course(
    sno INT(11) NOT NULL,
    cno INT(11) NOT NULL,
    grade SMALLINT NOT NULL DEFAULT 0
)ENGINE = InnoDB DEFAULT CHARSET = UTF8;

(2)插入测试数据:

INSERT INTO student_course(sno,cno,grade) VALUES(1,100,79);
INSERT INTO student_course(sno,cno,grade) VALUES(1,101,89);
INSERT INTO student_course(sno,cno,grade) VALUES(1,102,87);
INSERT INTO student_course(sno,cno,grade) VALUES(1,103,99);
INSERT INTO student_course(sno,cno,grade) VALUES(2,100,90);
INSERT INTO student_course(sno,cno,grade) VALUES(2,101,80);
INSERT INTO student_course(sno,cno,grade) VALUES(2,102,77);
INSERT INTO student_course(sno,cno,grade) VALUES(2,103,79);
INSERT INTO student_course(sno,cno,grade) VALUES(3,100,89);
INSERT INTO student_course(sno,cno,grade) VALUES(3,101,90);
INSERT INTO student_course(sno,cno,grade) VALUES(3,102,83);
INSERT INTO student_course(sno,cno,grade) VALUES(3,103,91);

(3)查询:

mysql> SELECT sno,SUM(CASE WHEN grade > 80 THEN 1 ELSE 0 END) num FROM student_course GROUP BY sno HAVING num >= 3;     
+-----+------+
| sno | num  |
+-----+------+
|   1 |    3 |
|   3 |    4 |
+-----+------+
2 rows in set (0.45 sec)


四、DCL--数据控制语言
作用:用来授予或回收访问数据库的某种特权,并控制数据库操纵事务发生的时间及效果。
1、GRANT授予用户权限:
基本用法:

mysql> GRANT priv_type ON <object_type> TO user <WITH {GRANT OPTION | resource_option} ...>;

示例:给用户jerry授予对test_db数据库的增删改查权限,允许该用户从IP为'192.168.0.10'的网络登录
(1)方法一:

mysql> GRANT INSERT,SELECT,UPDATE,DELETE ON test_db.* TO 'jerry'@'192.168.0.10' IDENTIFIED BY 'password' WITH GRANT OPTION;

(2)方法二:

mysql> CREATE USER 'jerry'@'192.168.0.10' IDENTIFIED BY 'password';
mysql> GRANT INSERT,SELECT,UPDATE,DELETE ON test_db.* TO 'jerry'@'192.168.0.10';

2、REVOKE收回用户权限:
基本用法:

mysql> REVOKE priv_type ON <object_type> FROM 'jerry'@'192.168.0.10';

示例:收回用户对test_db库的删除权限:

mysql> REVOKE DELETE ON test_db.* FROM 'jerry'@'192.168.0.10';

3、查看给某个用户所授予的权限:
基本用法:

mysql> SHOW GRANTS FOR user;

示例:查询给'jerry'@'192.168.0.10'所授予的所有权限:

mysql> SHOW GRANTS FOR 'jerry'@'192.168.0.10';

4、查询可授予的所有权限,使用技巧:

(1)首先将某个库(如:test_db)的所有权限授予给用户'jerry'@'localhost'

mysql> GRANT ALL ON test_db.* TO 'jerry'@'localhost' IDENTIFIED BY 'jerry';

(2)收回某个权限,如:查询权限

mysql> REVOKE SELECT ON test_db.* FROM 'jerry'@'localhost';

(3)查看剩余权限,就可以查到除了查询权限之外的权限,再加上查询权限即可授予的所有权限

 mysql> SHOW GRANTS FOR 'jerry'@'localhost';

至此,MySQL的基本操作DDL,DML,DQL,DCL介绍完毕。
下面几次次章节专门介绍触发器、存储过程和函数,大家有什么想法或者希望重点介绍MySQL的哪个模块可以在下方留言,一块学习数据库,欢迎转发~
后续更多文章将更新在个人小站上,欢迎查看。

另外提供一些优秀的IT视频资料,可免费下载!如需要请查看https://www.592xuexi.com

查看原文

赞 5 收藏 4 评论 0

entner 收藏了文章 · 2020-07-11

老旧话题:PHP读取超大文件

作为一名常年深耕curd的PHPer,关注内存那是不可能的,反正apache或者fpm都帮我们做了,况且运行一次就销毁,根本就不存在什么内存问题。

然而偏偏就有些个不开眼的人把这些个东西当面试题,比如总有刁民用“php读取一个10G的超大文件”当面试题来问你。当然了,作为一个和我一样的普普通通的蠢货,你听到这个问题的第一瞬间是懵逼,第二瞬间是卧槽,第三瞬间是保持结巴状态。

“面试造火箭,入职拧螺丝”。然而,刚进来就拧螺丝的人如果能够对“PHP读取一个10G的超大文件”有所见解的话,“造火箭”也是迟早的事儿。当前为了能够来这里“拧螺丝”,还是得先搞定“读取10G文件”这个问题。

要想读取10G的文件,首先,你得有个10G的文件

... ...

其实,相对来说也是比较简单的事情,我们随便找一个nginx的日志文件,哪怕只有10KB,假设文件名是test.log,然后呢执行" cat test.log >> test.log ",听我说少年,30秒左右你就该按下ctrl + C了,比如我这里,你们感受一下:

202MB,作为实验演示,够意思了。难不成真要造10G的文件?

首先,我们尝试用php的file函数来作一把死,你们感受一下:

<?php
$begin = microtime( true );
file( './test.log' );
$end = microtime( true );
echo "cost : ".( $end - $begin ).PHP_EOL;

保存为test.php,然后命令行下执行一把,结果如下图所示:

这句英文的大概意思就是“PHP最大只给每个进程分配了128MB内存,然而你特么张口要202MB?”所以,我们修改一下php配置文件... ...

千万不要手软,把这个参数改成1024MB,然后再次执行上面的php脚本:

然后,我们再试试最爱的file_get_contents()函数,结果如下图:

文件已经一次性全部被载入到内存中并将文件的每一行保存到了一个php数组中,我的机器是10G内存+256G固态硬盘,一次性载入这个202MB的文件file函数用了0.67秒钟、file_get_contents函数用了0.25秒钟(看起来file_get_content要比file靠谱的多),不过,敲重点的我们调整了配置文件才可以读取202MB的文件,如果摆在我们面前的是一个100G的文件呢?或者说,系统提供的php配置最多之给20MB内存而你又无法修改呢?

我们重点是如何在内存有限的机器上读取体积几百倍于内存的文件。下面,我们把memory_limit调整成16M,开启困难模式。

202MB的文件,允许被分配的内存为16MB,所以,总体思路其实也很简单,就是一点儿一点儿地读,只要每次读取的内容小于16MB,那就一定不会有问题,首先我们感受一下一个字符一个字符读,出场嘉宾是fgetc函数:

<?php
$begin = microtime( true );
$fp = fopen( './test.log' );
while( false !== ( $ch = fgetc( $fp ) ) ){
  // ⚠️⚠️⚠️ 作为测试代码是否正确,你可以打开注释 ⚠️⚠️⚠️
  // 但是,打开注释后屏显字符会严重拖慢程序速度!也就是说程序运行速度可能远远超出屏幕显示速度
  //echo $char.PHP_EOL;
}
fclose( $fp );
$end = microtime( true );
echo "cost : ".( $end - $begin ).PHP_EOL;

运行结果如下图:

虽然只有给了16M内存,但我们还是成功将202M文件全部读出来了,只不过这个运行速度是差了那么点儿意思,不大行。不能一个字母一个字母地读,这次我们一行一行地读:

<?php
$begin = microtime( true );
$fp = fopen( './test.log', 'r' );
while( false !== ( $buffer = fgets( $fp, 4096 ) ) ){
  //echo $buffer.PHP_EOL;
}
if( !feof( $fp ) ){
  throw new Exception('... ...');
}
fclose( $fp );
$end = microtime( true );
echo "cost : ".( $end - $begin ).' sec'.PHP_EOL;

运行结果如下图:

一行一行果然比一个一个字符要快很多,转念一想吧,系统分配给我们的内存上限是16MB,那我们索性一次读取一定量容量数据看看,会不会更快:

<?php
$begin = microtime( true );
$fp = fopen( './test.log', 'r' );
while( !feof( $fp ) ){
  // 如果你要使用echo,那么,你会很惨烈...
  fread( $fp, 10240 );
}
fclose( $fp );
$end = microtime( true );
echo "cost : ".( $end - $begin ).' sec'.PHP_EOL;
exit;

保存代码,运行一把,屌了屌了!!!在内存有限的情况下,我们还把时间缩短到了0.1秒!

然后我们考虑将问题升级一下,依然是上述这个202M的文件,这次我们要求读取倒数后5行的内容,这个问题看起来屌了些许,用原来的fread啥的虽然奏效但总感觉比较愚蠢。所以,现在又得引入全新的函数来解决这个问题:ftell和fseek。其中,ftell用于告知当前文件读取指针所在位置,fseek可以手动设定文件读取指针的位置。我建议大家去手册上重点观摩一下fseek函数:点击这里

<?php
$fp = fopen( './test1.log', 'r' );
$line = 5;
$pos = -2;
$ch = '';
$content = '';
while( $line > 0 ){
  while( $ch != "\n" ){
    fseek( $fp, $pos, SEEK_END );
    $ch = fgetc( $fp );
    $pos--;
  }
  $ch = '';
  $content .= fgets( $fp );
  $line--;
}
echo $content;
exit;

其中test1.log文件的内容如下:

aa
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
1111111111
2222222222

保存文件并运行,结果如下图所示:

查看原文

entner 赞了文章 · 2020-07-11

老旧话题:PHP读取超大文件

作为一名常年深耕curd的PHPer,关注内存那是不可能的,反正apache或者fpm都帮我们做了,况且运行一次就销毁,根本就不存在什么内存问题。

然而偏偏就有些个不开眼的人把这些个东西当面试题,比如总有刁民用“php读取一个10G的超大文件”当面试题来问你。当然了,作为一个和我一样的普普通通的蠢货,你听到这个问题的第一瞬间是懵逼,第二瞬间是卧槽,第三瞬间是保持结巴状态。

“面试造火箭,入职拧螺丝”。然而,刚进来就拧螺丝的人如果能够对“PHP读取一个10G的超大文件”有所见解的话,“造火箭”也是迟早的事儿。当前为了能够来这里“拧螺丝”,还是得先搞定“读取10G文件”这个问题。

要想读取10G的文件,首先,你得有个10G的文件

... ...

其实,相对来说也是比较简单的事情,我们随便找一个nginx的日志文件,哪怕只有10KB,假设文件名是test.log,然后呢执行" cat test.log >> test.log ",听我说少年,30秒左右你就该按下ctrl + C了,比如我这里,你们感受一下:

202MB,作为实验演示,够意思了。难不成真要造10G的文件?

首先,我们尝试用php的file函数来作一把死,你们感受一下:

<?php
$begin = microtime( true );
file( './test.log' );
$end = microtime( true );
echo "cost : ".( $end - $begin ).PHP_EOL;

保存为test.php,然后命令行下执行一把,结果如下图所示:

这句英文的大概意思就是“PHP最大只给每个进程分配了128MB内存,然而你特么张口要202MB?”所以,我们修改一下php配置文件... ...

千万不要手软,把这个参数改成1024MB,然后再次执行上面的php脚本:

然后,我们再试试最爱的file_get_contents()函数,结果如下图:

文件已经一次性全部被载入到内存中并将文件的每一行保存到了一个php数组中,我的机器是10G内存+256G固态硬盘,一次性载入这个202MB的文件file函数用了0.67秒钟、file_get_contents函数用了0.25秒钟(看起来file_get_content要比file靠谱的多),不过,敲重点的我们调整了配置文件才可以读取202MB的文件,如果摆在我们面前的是一个100G的文件呢?或者说,系统提供的php配置最多之给20MB内存而你又无法修改呢?

我们重点是如何在内存有限的机器上读取体积几百倍于内存的文件。下面,我们把memory_limit调整成16M,开启困难模式。

202MB的文件,允许被分配的内存为16MB,所以,总体思路其实也很简单,就是一点儿一点儿地读,只要每次读取的内容小于16MB,那就一定不会有问题,首先我们感受一下一个字符一个字符读,出场嘉宾是fgetc函数:

<?php
$begin = microtime( true );
$fp = fopen( './test.log' );
while( false !== ( $ch = fgetc( $fp ) ) ){
  // ⚠️⚠️⚠️ 作为测试代码是否正确,你可以打开注释 ⚠️⚠️⚠️
  // 但是,打开注释后屏显字符会严重拖慢程序速度!也就是说程序运行速度可能远远超出屏幕显示速度
  //echo $char.PHP_EOL;
}
fclose( $fp );
$end = microtime( true );
echo "cost : ".( $end - $begin ).PHP_EOL;

运行结果如下图:

虽然只有给了16M内存,但我们还是成功将202M文件全部读出来了,只不过这个运行速度是差了那么点儿意思,不大行。不能一个字母一个字母地读,这次我们一行一行地读:

<?php
$begin = microtime( true );
$fp = fopen( './test.log', 'r' );
while( false !== ( $buffer = fgets( $fp, 4096 ) ) ){
  //echo $buffer.PHP_EOL;
}
if( !feof( $fp ) ){
  throw new Exception('... ...');
}
fclose( $fp );
$end = microtime( true );
echo "cost : ".( $end - $begin ).' sec'.PHP_EOL;

运行结果如下图:

一行一行果然比一个一个字符要快很多,转念一想吧,系统分配给我们的内存上限是16MB,那我们索性一次读取一定量容量数据看看,会不会更快:

<?php
$begin = microtime( true );
$fp = fopen( './test.log', 'r' );
while( !feof( $fp ) ){
  // 如果你要使用echo,那么,你会很惨烈...
  fread( $fp, 10240 );
}
fclose( $fp );
$end = microtime( true );
echo "cost : ".( $end - $begin ).' sec'.PHP_EOL;
exit;

保存代码,运行一把,屌了屌了!!!在内存有限的情况下,我们还把时间缩短到了0.1秒!

然后我们考虑将问题升级一下,依然是上述这个202M的文件,这次我们要求读取倒数后5行的内容,这个问题看起来屌了些许,用原来的fread啥的虽然奏效但总感觉比较愚蠢。所以,现在又得引入全新的函数来解决这个问题:ftell和fseek。其中,ftell用于告知当前文件读取指针所在位置,fseek可以手动设定文件读取指针的位置。我建议大家去手册上重点观摩一下fseek函数:点击这里

<?php
$fp = fopen( './test1.log', 'r' );
$line = 5;
$pos = -2;
$ch = '';
$content = '';
while( $line > 0 ){
  while( $ch != "\n" ){
    fseek( $fp, $pos, SEEK_END );
    $ch = fgetc( $fp );
    $pos--;
  }
  $ch = '';
  $content .= fgets( $fp );
  $line--;
}
echo $content;
exit;

其中test1.log文件的内容如下:

aa
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
1111111111
2222222222

保存文件并运行,结果如下图所示:

查看原文

赞 89 收藏 55 评论 5

entner 收藏了问题 · 2020-02-18

一个让PHP小白百思不得其解的匿名函数及array_reduce的问题

最近学习PHP,看到了一段代码,其中涉及到了匿名函数以及array_reduce,把代码敲出来用各种方法分析也没想出是怎么调用的,代码如下:

<?php
interface Middleware
{
    public static function handle(Closure $next);
}

class VerifyCsrfToken implements Middleware
{
    public static function handle(Closure $next)
    {
        echo "(5)验证Csrf-Token".'<br>';
        $next();
    }
}

class ShareErrorsFromSession implements Middleware
{
    public static function handle(Closure $next)
    {
        echo "(4)如果session中有'errors'变量,则共享它".'<br>';
        $next();
    }
}

class StartSession implements Middleware
{
    public static function handle(Closure $next)
    {
        echo "(3)开启session,获取数据".'<br>';
        $next();
        echo "(7)保存数据,关闭session".'<br>';
    }
}

class AddQueuedCookiesToResponse implements Middleware
{
    public static function handle(Closure $next)
    {
        $next();
        echo "(8)添加下一次请求需要的cookie".'<br>';
    }
}

class EncryptCookies implements Middleware
{
    public static function handle(Closure $next)
    {
        echo "(2)对输入请求的cookie进行解密".'<br>';
        $next();
        echo "(9)对输出相应的cookie进行加密".'<br>';
    }
}

class CheckForMaintenanceMode implements Middleware
{
    public static function handle(Closure $next)
    {
        echo "(1)确定当前程序是否处于维护状态".'<br>';
        $next();
    }
}

function getSlice()
{
    return function($stack, $pipe)
    {
        return function() use ($stack, $pipe)
        {
            return $pipe::handle($stack);
        };
    };
}


function then()
{
    $pipes = [
        "CheckForMaintenanceMode",
        "EncryptCookies",
        "AddQueuedCookiesToResponse",
        "StartSession",
        "ShareErrorsFromSession",
        "VerifyCsrfToken"
    ];
    
    $firstSlice = function() {
        echo "(6)请求向路由器传递,返回响应.".'<br>';
    };

    $pipes = array_reverse($pipes);
    $go = array_reduce($pipes, getSlice(),$firstSlice);
    $go();
}
then();
?>

还望有大神能帮忙详解下$go = array_reduce($pipes, getSlice(),$firstSlice);和$go();这两段代码背后的每一步的调用执行流程,以及调用时的参数传递是哪些,如果能用流程图表示就更好啦,谢谢。

entner 发布了文章 · 2020-01-22

PHP手写MVC (五)—— 路由

路由是一个框架中必不可少的组件,其作用是把 URL 按照预定规则解析到特定控制器中。

我们在这里定义了两种路由规则:

  • 查询字符串。在路径后面使用问号加参数,多个参数用 & 分隔。在配置文件使用 querystring 表示
#控制器/方法?参数1=值1&参数2=值2
http://domain/user/info?name=php&chapter=10
  • 路径,以路径的形式将参数和值添加到后面,中间用 / 分隔。配置中使用 restful
#控制器/方法/参数1/值1/参数2/值2
https://domain/user/info/name/php/chapter/100

主控制器

在目录 core 创建 Controller.php,该类继承 Container

<?php

namespace core;

class Controller extends Container
{
    
}

主控制器可以添加控制器公共方法,如页面渲染 render(),错误代码等,所有控制器必须继承主控制器。由于主控制器继承 Container,因此,控制器也是分发器的子类,可以通过 register() 获取实例。

控制器类

  • 类命名规则

控制器命名遵循大写开头的驼峰命名规则,并且默认添加后缀 Controller,控制器文件命名和类命名一样,如控制器类 UserController,其文件命名为 UserController.php

  • 方法命名规则

方法命名遵循小写开头的驼峰命名规则,并且默认添加请求方式(如,get,post,put等)前缀,如 getIndex()postUpdate()

以上例 UserController 为例

<?php

namespace controller;

use core\Controller;

class UserController extends Controller
{
    /**
     * HTTP 请求方式为 GET 时有效
     * url 为 /user/info
     *
     */
    public function getInfo()
    {
        
    }

    /**
     * HTTP 请求方式为 POST 时有效
     * url 为 /user/update
     *
     */
    public function postUpdate()
    {
        
    }
}

路由解析

core 目录下创建 Router.php

$ cd tinyphp/core
$ touch Router.php

在构造函数中定义变量


<?php

namespace core;

use dispatcher\Container;

class Router extends Container
{
    public $method;
    public $uri;
    public $path;

    public function __construct()
    {
        $this->method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
        $this->uri = $_SERVER['REQUEST_URI'];
        $this->path = $_SERVER['PATH_INFO'];
    }
}

常见 $_SERVER 字段

  1. $_SERVER['PATH_INFO'] URL的路径信息,如 /user/info
  2. $_SERVER['REQUEST_METHOD'] 请求方法,如 POST,GET
  3. $_SERVER['REQUEST_URI'] 完整 URL,如 /user/info?id=1&name=Lucy

start() 方法中解析 URL

protected function start()
{
    /**
     * 也可以写成 Config::get('default.route','querystring');
     *
     */
    $route = Config::get('default.route') ?? 'querystring';

    //解析 controller 和 action
    $path = explode('/',trim($this->path,'/'));

    if (empty($path[0])) {
        $path[0] = Config::get('default.controller','index');
    }
    $controller = ucfirst($path[0]).'Controller';

    //获取请求方法
    $method = strtolower($this->method);
    $action = $method.ucfirst($path[1] ?? Config::get('default.action','index'));
    //获取参数
    $args = [];
    if (method_exists($this,$route)) {
        $args = call_user_func_array([$this,$route],[$this->uri]);
    }
    return ['controller'=>$controller,'action'=>$action,'args'=>$args];
}

querystring() 参数解析


private function querystring($url)
{
    $urls = explode('?', $url);
    if (empty($urls[1])) {
        return [];
    }
    $param_arr = [];
    $param_tmp = explode('&', $urls[1]);
    if (empty($param_tmp)) {
        return [];
    }
    foreach ($param_tmp as $param) {
        if (strpos($param, '=')) {
            list($key,$value) = explode('=', $param);
            //变量名是否复合规则
            if (preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $key)) {
                $param_arr[$key] = $value;
            }
        }
    }
    return $param_arr;
}

querystring 的参数为 ? 后面的部分,多个参数用 & 分隔。

restful() 参数解析

private function restful($url)
{
    $path = explode('/', trim(explode('?', $url)[0], '/'));
    $params = [];
    $i = 2;
    while (1) {
        if (!isset($path[$i])) {
            break;
        }
        $params[$path[$i]] = $path[$i+1] ?? '';
        $i = $i+2;
    }
    return $params;
}

restful 的参数为方法后面的路径。

完整代码如下:

<?php

namespace core;

use dispatcher\Container;

class Router extends Container
{
    public $method;
    public $uri;
    public $path;

    public function __construct()
    {
        $this->method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
        $this->uri = $_SERVER['REQUEST_URI'];
        $this->path = $_SERVER['PATH_INFO'];
    }

    protected function start()
    {
        $route = Config::get('default.route') ?? 'querystring';
    
        //解析 controller 和 action
        $path = explode('/',trim($this->path,'/'));
    
        if (empty($path[0])) {
            $path[0] = Config::get('default.controller','index');
        }
        $controller = ucfirst($path[0]).'Controller';
    
        //获取请求方法
        $method = strtolower($this->method);
        $action = $method.ucfirst($path[1] ?? Config::get('default.action','index'));
        
        //获取参数
        $args = [];
        if (method_exists($this,$route)) {
            $args = call_user_func_array([$this,$route],[$this->uri]);
        }
        return ['controller'=>$controller,'action'=>$action,'args'=>$args];
    }
    
    /**
     * 查询字符串参数
     * ?后,参数通过&&分隔
     *
     */
    private function querystring($url)
    {
        $urls = explode('?', $url);
        if (empty($urls[1])) {
            return [];
        }
        $param_arr = [];
        $param_tmp = explode('&', $urls[1]);
        if (empty($param_tmp)) {
            return [];
        }
        foreach ($param_tmp as $param) {
            if (strpos($param, '=')) {
                list($key,$value) = explode('=', $param);
                //变量名是否复合规则
                if (preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $key)) {
                    $param_arr[$key] = $value;
                }
            }
        }
        return $param_arr;
    }
    /**
     * 路径参数
     * 控制器/方法/参数1/值1/参数2/值2
     *
     */
    http://domain/user/info/name/entner?name=php&chapter=10
    private function restful($url)
    {
        $path = explode('/', trim(explode('?', $url)[0], '/'));
        $params = [];
        $i = 2;
        while (1) {
            if (!isset($path[$i])) {
                break;
            }
            $params[$path[$i]] = $path[$i+1] ?? '';
            $i = $i+2;
        }
        return $params;
    }
}

路由调用方式为

<?php

$router = Rouer::start();

测试路由

在配置文件 app/conf/config.php 中设置默认路由为 querystring

<?php

return [
    'default' => [
        'controller' => 'index',
        'action' => 'index',
        'route' => 'querystring',//还可以设置为 restful
    ],
    'view' => [
        'dir' => 'layout',
        'file' => 'base',
    ]
];

core/Application.php 文件中 run() 方法实现路由调用

<?php
...
public function run()
{
    $router = Router::start();
    echo '<pre>';
    print_r($router);
}
...

启动 PHP 内置服务器

$ cd tinyphp/public
$ php -S localhost:8080

在浏览器中输入 http://localhost:8080/course/document?name=php&&chapter=10
输出结果为

Array
(
    [controller] => CourseController
    [action] => getDocument
    [args] => Array
        (
            [name] => php
            [chapter] => 10
        )
)

同理可以测试 restful 路由规则。

调用控制器方法

路由解析后,获得需要调用的控制器名,方法和参数。由于控制器继承分发器后,可以通过 register() 获取实例,编辑 core/Applicaiton.php

<?php

...
public function run()
{
    $router = Router::start();
    //注意使用命名空间
    $controller = "controller\\".$router['controller'];
    $action = $router['action'];
    $args = $router['args'];
    
    echo call_user_func_array([$controller::register(),$action],$args);
}
...

通过这种方式可以实现方法调用,但是无法控制方法参数,比如,有时候我们需要在方法参数中使用某个对象实例,术语称为依赖注入,即把需要使用的实例注入到方法中,那么可以通过PHP的高级特性反射来实现。

查看原文

赞 1 收藏 0 评论 0

认证与成就

  • 获得 183 次点赞
  • 获得 10 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 9 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-02-21
个人主页被 2.7k 人浏览