Laravel5.5源码详解 -- 数据库的启动与连接过程

来源:互联网 发布:工业机械人怎么编程 编辑:程序博客网 时间:2024/06/12 22:10

Laravel5.5源码详解 – 数据库的启动与连接过程

整个laravel的操作,一般情况下,数据库的处理会占掉很大一部分。所以对数据 库处理的理解,显得尤为重要。关于其源码解析,网上有非常多的文献,但流程一般都含糊其辞,读完来龙去脉甚为不解。所以,我自己做了一次流程分析,并记录下全过程。

Laravel对不同数据库连接的实例封装了对应连接的PDO类,为上层使用数据库连接实例提供了统一的接口。我这里源码分析,都以以mySql为实例进行讲解,过程大致如下,

DB::table('users') --> 拿到PDO --> 调用核心类Illuminate\Database\Connection

这里先局部后全局,分三部分讲解。

  1. 数据库的连接与PHP-PDO的关系:解释laravel与数据库的接洽点;
  2. 数据库查询构造流程:理解是如何最后实现调用核心类Connection::table()函数的;
  3. 数据库启动大脉络分析:理解全流程,然后重点是如何拿到PDO的。

另外,例子中用到的laravel的门面(Facades)模式,原理比较简单,可以参考其官方文档。

说在前面的话

Laravel目前支持的有四类数据库,laravel中对应的名称分别为:mysql,pgsql,sqlite,sqlsrv,即MySQL、Postgres、SQLite和SQL Server;同时,laravel还支持用户算定的数据库和驱动程序。

当操作数据库的查询构造器时,可以使用类似

DB::table('users')->get();DB::table('users')->select();DB::table('users')->insert();DB::update();

语法,其中

DB::table('users')

部分就是获取查询构造器,后面的“->get()”等调用查询构造的方法实现相应数据操作。后面我们会讲到(详见第三节),这些查询会通过DatabaseManager::connection()再调用各个$methods。

public function __call($method, $parameters){    return $this->connection()->$method(...$parameters);}

查询构造器的建立过程分为两个阶段:一个是数据库连接封装阶段,另一个是查询构造器生成阶段。

数据库连接封装又可以分为四个步骤:

一、数据库管理器阶段,在DatabaseServiceProvider类中的registerConnectionServices()函数中创建ConnectionFactory实例;

Laravel首先通过服务提供者“Illuminate\Database\DatabaseServiceProvider”注册了数据库管理服务(“DB”服务)和数据库连接工厂服务(“db.factory”服务),通过上述服务获取数据库管理DatabaseManager类和数据库连接工厂实例ConnectionFactory类的实例,其中数据库连接工厂实例作为数据库管理器实例的一个属性,在DatabaseServiceProvider类中的registerConnectionServices()函数中创建ConnectionFactory实例。

二、数据库连接工厂阶段,这一阶段主要是为连接数据库作配置准备,并生成连接器MySqlConnector;为了对上层提供统一的接口,Laravel在底层根据不同的配置调用了不同的数据库驱动扩展,框架上使用了简单工厂设计模式,用来根据配置文件获取不同的数据库连接实例。

三、数据库连接器阶段,连接器MySqlConnector会创建连接,并调用其子函数::createConnection() 和 ::createPdoConnection();Laravel针对不同的数据库有不同的实现,主要包括连接DSN名称及配置等。Laravel框架用四个类分别封装了默认支持的四个数据库连接的过程,通过connect()方法提供统一的接口。

四、数据库连接创建阶段,在这个阶段MySqlConnector的父类Connector会生成PDO实例,并完成连接。本质上,不同数据库连接的实例就是封装了对应连接的PDO类实例、请求语法类实例、和结果处理类实例,从而为上层使用数据库连接实例提供统一的接口。

第一节,数据库的连接与PHP-PDO的关系

首先,我们要知道,数据最终是在类Illuminate\Database\Connectors\Connector.php中完成链接的。我们先分析一下其源码:

class Connector{    use DetectsLostConnections;    //下面是连接时默认用到的参数,当然你可以在创建联接时更改    protected $options = [        PDO::ATTR_CASE => PDO::CASE_NATURAL, // 保留数据库驱动返回的列名,不强制列名为指定的大小写        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,  // 抛出 exceptions 异常        PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,  // 不转换 NULL 和空字符串        PDO::ATTR_STRINGIFY_FETCHES => false, // 提取的时候将数值转换为字符串?  => 不转换        PDO::ATTR_EMULATE_PREPARES => false,  // 禁用预处理语句的模拟    ];   // 尝试建立一个连接    public function createConnection($dsn, array $config, array $options)    {        // 先拿到连接数据库所需要的用户名和密码,这个一般在.env中设置,你可以看到有以下3项,        // DB_DATABASE=laraveldb   DB_USERNAME=username   DB_PASSWORD=password        list($username, $password) = [            $config['username'] ?? null, $config['password'] ?? null,        ];        // 尝试调用实际建立连接的createPdoConnection函数,注意上面的$options已经作为设置参数传入        try {            return $this->createPdoConnection(                $dsn, $username, $password, $options            );        } catch (Exception $e) {            return $this->tryAgainIfCausedByLostConnection(                $e, $dsn, $username, $password, $options            );        }    }    // 实际建立数据库连接的函数    protected function createPdoConnection($dsn, $username, $password, $options)    {        if (class_exists(PDOConnection::class) && ! $this->isPersistentConnection($options)) {            return new PDOConnection($dsn, $username, $password, $options);        }        return new PDO($dsn, $username, $password, $options);    }

大致上,createPdoConnection会检查有没有PDOConnection这个类,实际上在laravel提供的默认源码中这个类是不存在的,你可以检查一下你的composer.json文件。如果想安装使用doctrine,可以参考以下官网

https://www.laraveldoctrine.org/docs/1.3/orm/installation

言归正传,createPdoConnection找不到PDOConnection这个类,就会调用后面那句

return new PDO($dsn, $username, $password, $options);

其中$dsn就是我们在.env中设置的数据库地址

DB_CONNECTION=mysqlDB_HOST=127.0.0.1DB_PORT=3306

如果打印出来,就是一段字符串,如下,

'mysql:host=127.0.0.1;:port=3306;dbname=laraveldb'

这里值得一提的是PDO,PDO是个什么东西?PDO是PHP提供的数据对象扩展(PHP Data Object),它为PHP访问数据库提供了一套轻量级的接口,从PHP5.1版以后开始提供。你可以参考官方网站,

http://php.net/manual/zh/book.pdo.php

因此不难明白,所谓的laravel连接数据库,只不过是调用了PHP中的PDO(或者说该类)的API函数,并进行一系列的操作的过程。同样,Qureybuilder的相关API,也只不过是PDO的一层封装外衣!

这里需要进一步说明的是,这个PDO创建之后,是直接返回给变量$connection的。以mySql为例,

namespace Illuminate\Database\Connectors;use PDO;class MySqlConnector extends Connector implements ConnectorInterface{    public function connect(array $config)    {        $dsn = $this->getDsn($config);        $options = $this->getOptions($config);             $connection = $this->createConnection($dsn, $config, $options); // 在这里创建connection        if (! empty($config['database'])) {            $connection->exec("use `{$config['database']}`;");        }        $this->configureEncoding($connection, $config);        $this->configureTimezone($connection, $config);        $this->setModes($connection, $config);        return $connection;    }    ...}

这个类MySqlConnector是Illuminate\Database\Connectors\Connector的子类。

第二节 laravel中数据库查询构造流程

当 connection 对象构建初始化完成后,我们可以用 DB 来进行数据库的 增删改查(CRUD,即( Create、Retrieve、Update、Delete)等操作。laravel的查询构造器让我们避免使用原生的sql语句,而是用一种语法上更容易理解的方式操作(Laravel官方称这样可以避免漏洞),例如

DB::table('table')->select('*')->where('user_id', 1);

第一,查询构造的这个过程是如何实现的呢?

当然,这种看似静态调用的方法,其实是laravel里的门面模式,实际上调用的并不是静态方法。这个简单的模式只有几行代码,

<?phpnamespace Illuminate\Support\Facades;class DB extends Facade{    protected static function getFacadeAccessor()    {        return 'db';    }}

其原理只不过是通过调用其父类 Illuminate\Support\Facades\Facade中的PHP的魔术方法 __callStatic(),将请求转到了相应的方法上。这里的’db’,定义在

Illuminate\Foundation\Application.php

的 registerCoreContainerAliases()里面,如下

'db'                   => [\Illuminate\Database\DatabaseManager::class],

所以,在发出指令的时候,

DB::table('table')->select('*')->where('user_id', 1);

laravel会通过Facade,找到DatabaseManager里面的table()函数,本质上是这个魔术函数

    public function __call($method, $parameters)    {        return $this->connection()->$method(...$parameters);    }

这里的$this->connection()实质上是mySqlConnection对象,这个对象的父类正是Illuminate\Database\Connection,于是,DatabaseManager顺藤摸瓜,找到了mySqlConnection,并调用了其父类Connection中的table方法。

    public function table($table)    {        // 用其QueryBuilder进行查询        return $this->query()->from($table);    }    public function query()    {        return new QueryBuilder(            $this, $this->getQueryGrammar(), $this->getPostProcessor()        );    }

第二,Connection核心类业务

要知道,查询构造工作是在Illuminate\Database\Connection中完成的,这个类是我们要了解的核心,其构造函数如下,

    public function __construct($pdo, $database = '', $tablePrefix = '', array $config = [])    {        $this->pdo = $pdo;     // $pdo是通过MySqlConnection--MySqlConnector拿到的,参考第三节        $this->database = $database;          $this->tablePrefix = $tablePrefix;        $this->config = $config;        $this->useDefaultQueryGrammar();   // Grammar SQL语法编译器实例         $this->useDefaultPostProcessor();  // Processor SQL结果处理器实例    }

可见,除了$pdo,这里还在MySqlConnection构造函数中通过setter注入了

\Illuminate\Database\Query\Grammars\Grammar \Illuminate\Database\Query\Processors\Processor 

这里有三样东西要关注,PDO,Grammar和Processor。

不过,这些具体内容在网上已经写得比较详细,我这里不再重复,可以参考

https://segmentfault.com/a/1190000007267217

https://segmentfault.com/a/1190000007278819

https://segmentfault.com/a/1190000007315628

https://www.yanshuo.me/p/50386

https://www.yanshuo.me/p/53185

https://www.yanshuo.me/p/55168

第三节 数据库启动大脉络分析

我这里类(对象)的调用关系用===>表示,==>表示对象内部的函数调用,::表示属于该类的子函数,整个大脉络如下:

DB::table('users')->get();DB::table('users')->select();DB::table('users')->insert();DB::update();

===>

DatabaseManager::connection() ==>  ::makeConnection() 

===>

ConnectionFactory::make() ==> ::createSingleConnection()  ==> ::pdoResolver() ==> ::createPdoResolverWithHosts()  ==>  ::createConnector()

===>

MySqlConnector::Connect()

===>

Connector::createConnection() ==> ::createPdoConnection()

===> 拿到MySqlConnection,并回到DatabaseManager,

DatabaseManager::__call() ==> $method = '$table'

===>

MySqlConnection::table()

===> 实际调用MySqlConnection父类的函数

Connection::table()

下面对源码进行详细剖析。讲过的部分不再重复,重点是理解如何拿到$pdo。

在Illuminate\Database\DatabaseManager中,connection是这样定义的,

public function connection($name = null){    list($database, $type) = $this->parseConnectionName($name);    $name = $name ?: $database;    // 这里得到的$name就是$database,也就是'mysql', 上面得到的type则是空null       if (! isset($this->connections[$name])) {        $this->connections[$name] = $this->configure(            $this->makeConnection($database), $type        );    }    // 拿到数据库连接后返回 (#connections["mysql"]=MySqlConnection)    return $this->connections[$name];}

这里重点要理理解的是,在ConnectionFactory 中构造出 \Illuminate\Database\MysqlConnector ,并通过MySqlConnection的构造参数注入MysqlConnector 。结果是,通过DatabaseManager的connection()函数,我们拿到了一个链接器实例MySqlConnection,该connection中还装着一个MySqlConnector,及其相关配置 。

继续看源码,被调用的makeConnection调用了Illuminate\Database\ConnectionFactory的make函数,

    protected function makeConnection($name)    {        // 传入的参数$name="mysql", 数组的结果看后面的分析        $config = $this->configuration($name);        // 看用户有没有自定义的数据库,有的话就先用用户自定义的数据库        if (isset($this->extensions[$name])) {            return call_user_func($this->extensions[$name], $config, $name);        }        // 看有没有用户自定义的驱动,有的话先调用自定义的        if (isset($this->extensions[$driver = $config['driver']])) {            return call_user_func($this->extensions[$driver], $config, $name);        }        // 一般我们是没有自定义的数据库和驱动的,所以只有最后这一句是有效的,        return $this->factory->make($config, $name);    }

附说明:上面的函数中,得到$config的数组打印出来看一下,

array:12 [▼  "driver" => "mysql"  "host" => "127.0.0.1"  "port" => "3306"  "database" => "laraveldb"  "username" => "user01"  "password" => "secrete"  "unix_socket" => ""  "charset" => "utf8mb4"  "collation" => "utf8mb4_unicode_ci"  "prefix" => ""  "strict" => true  "engine" => "InnoDB, ROW_FORMAT=DYNAMIC"]

再来看Illuminate\Database\ConnectionFactory的make函数,

    public function make(array $config, $name = null)    {        $config = $this->parseConfig($config, $name);        if (isset($config['read'])) {            return $this->createReadWriteConnection($config);        }        // 这个函数有效的也只有最后这一行,        return $this->createSingleConnection($config);    }

附说明,上面函数的这个$config只增加了一行, “name” => “mysql”,

array:13 [▼  "driver" => "mysql"  "host" => "127.0.0.1"  "port" => "3306"  "database" => "laraveldb"  "username" => "user01"  "password" => "secrete"  "unix_socket" => ""  "charset" => "utf8mb4"  "collation" => "utf8mb4_unicode_ci"  "prefix" => ""  "strict" => true  "engine" => "InnoDB, ROW_FORMAT=DYNAMIC"  "name" => "mysql"]

再来看一下其调用的createSingleConnection函数,

    protected function createSingleConnection(array $config)    {        // 这里拿到的$pdo是一个闭包,如下第一步所述        $pdo = $this->createPdoResolver($config);        return $this->createConnection(            $config['driver'], $pdo, $config['database'], $config['prefix'], $config        );    }

第一步,先看createPdoResolver(),因为有host,就会运行createPdoResolverWithHosts(),实际上withoutHosts

相当简单,也就是创建一个 connector 对象,再利用这个connector 对象进行数据库的连接。

    protected function createPdoResolver(array $config)    {        return array_key_exists('host', $config)                            ? $this->createPdoResolverWithHosts($config)                            : $this->createPdoResolverWithoutHosts($config);    }

我们接着看WithHosts(),

protected function createPdoResolverWithHosts(array $config)    {        return function () use ($config) {            foreach (Arr::shuffle($hosts = $this->parseHosts($config)) as $key => $host) {                $config['host'] = $host;                try {                    // 这里建立数据库的连接类MySqlConnector对象,并进行连接,                    return $this->createConnector($config)->connect($config);                } catch (PDOException $e) {                    if (count($hosts) - 1 === $key && $this->container->                        bound(ExceptionHandler::class)) {                            $this->container->make(ExceptionHandler::class)->report($e);                        }                }            }            throw $e;        };    }

这个闭包调用了下面的函数,

    public function createConnector(array $config)    {        if (! isset($config['driver'])) {            throw new InvalidArgumentException('A driver must be specified.');        }        if ($this->container->bound($key = "db.connector.{$config['driver']}")) {            return $this->container->make($key);        }        switch ($config['driver']) {            case 'mysql':                return new MySqlConnector;            case 'pgsql':                return new PostgresConnector;            case 'sqlite':                return new SQLiteConnector;            case 'sqlsrv':                return new SqlServerConnector;        }        throw new InvalidArgumentException("Unsupported driver [{$config['driver']}]");    }

得到一个连接类MySqlConnector,其父类正是前面第一节所讲的Illuminate\Database\Connectors\Connector, 然后进行连接(参考createPdoResolverWithHosts),我们再次把其代码贴出来,

<?phpnamespace Illuminate\Database\Connectors;use PDO;class MySqlConnector extends Connector implements ConnectorInterface{    public function connect(array $config)    {        $dsn = $this->getDsn($config);        $options = $this->getOptions($config);         // 关键看下面这句,在这里调用其父类中定义的createConnection创建PDO,并返回连接        $connection = $this->createConnection($dsn, $config, $options);        if (! empty($config['database'])) {            $connection->exec("use `{$config['database']}`;");        }        $this->configureEncoding($connection, $config);        // Next, we will check to see if a timezone has been specified in this config        // and if it has we will issue a statement to modify the timezone with the        // database. Setting this DB timezone is an optional configuration item.        $this->configureTimezone($connection, $config);        $this->setModes($connection, $config);        return $connection;    }    ...}

这里最关键的,是看$connection = $this->createConnection($dsn, $config, $options)这句,它调用了其父类Illuminate\Database\Connectors\Connector的createConnection()函数。这个正是在前面第一节里详细描述过的。

这样,

return $this->createConnector($config)->connect($config);

运行完毕,得到一个$pdo 闭包。

第二步,运行createSingleConnection中的createConnection(),

protected function createConnection($driver, $connection, $database, $prefix = '',     array $config = []){        // 这个resolver没有去具体分析,实际运行过程中得到的是null.        if ($resolver = Connection::getResolver($driver)) {            return $resolver($connection, $database, $prefix, $config);        }        switch ($driver) {            case 'mysql':                 // 这里创建一个新的数据库连接器MySqlConnection                return new MySqlConnection($connection, $database, $prefix, $config);            case 'pgsql':                return new PostgresConnection($connection, $database, $prefix, $config);            case 'sqlite':                return new SQLiteConnection($connection, $database, $prefix, $config);            case 'sqlsrv':                return new SqlServerConnection($connection, $database, $prefix, $config);        }        throw new InvalidArgumentException("Unsupported driver [$driver]");    }}

这个要注意,Illuminate\Database\MySqlConnection继承的类是Illuminate\Database\Connection。其传入的参数中,$connection正是前面得到的$pdo

附参考,

数据库全局范围内的脉络,注意'db' ,也就是DatabaseManager在容器中的解析步骤。

#1 [internal function]: Composer\Autoload\ClassLoader->loadClass('Illuminate\\Data...')#2 Illuminate\Database\DatabaseServiceProvider.php(62): spl_autoload_call('Illuminate\\Data...')#3 Illuminate\Container\Container.php(749): Illuminate\Database\DatabaseServiceProvider->Illuminate\Database\{closure}(Object(Illuminate\Foundation\Application), Array)#4 Illuminate\Container\Container.php(631): Illuminate\Container\Container->build(Object(Closure))#5 Illuminate\Container\Container.php(586): Illuminate\Container\Container->resolve('db', Array)#6 Illuminate\Foundation\Application.php(732): Illuminate\Container\Container->make('db', Array)#7 Illuminate\Container\Container.php(1195): Illuminate\Foundation\Application->make('db')#8 Illuminate\Database\DatabaseServiceProvider.php(23): Illuminate\Container\Container->offsetGet('db')#9 [internal function]: Illuminate\Database\DatabaseServiceProvider->boot()
阅读全文
'); })();
0 0
原创粉丝点击
热门IT博客
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 气泡酒夏季 气泡酒哪个牌子好喝 气泡酒价格 气泡酒和香槟的区别 香槟气泡酒 气泡酒 香槟 香槟 气泡酒 气泡酒多少钱一瓶 起泡酒价格 起泡酒图片 香槟和起泡酒的区别 爱之湾起泡酒 起泡酒好喝吗 起泡酒怎么开 甜起泡酒 意大利起泡酒 起泡酒品牌 气泡酒是什么 起泡酒推荐 魔幻起泡酒 起泡酒是什么 起泡酒什么牌子好 无醇起泡酒 气泡酒起泡酒 起泡酒度数 进口起泡酒 汉凯起泡酒 什么起泡酒好喝 起泡酒是什么酒 马天尼起泡酒多少钱 什么叫起泡酒 干型起泡酒 怎样开起泡酒 什么是起泡酒 好喝的起泡酒 马提尼起泡酒 起泡 酒 脱醇起泡酒 起泡酒的喝法 气泡酒的喝法 起泡酒配餐