上篇讲到了数据库Relation的实现,本篇接着讲migrations or database modification logic的功能,此处开始的git是git co aa98553
。
本文是orm系列的第三篇,也是Eloquent演化的第二篇,Eloquent系列会尝试着讲清楚Eloquent是如何一步一步演化到目前功能强大的版本的,但是毕竟个人能力有限,不可能分析的非常完善,总会有不懂的地方,所以讲的错误的地方,恳请大牛们能不吝赐教;或者如果有什么地方是没看懂的,也请提出来,因为可能那地方就是我自己没看懂,所以没讲明白,你提出后我们就可以一起讨论,让我们能共同的进步的。
数据库蓝图
先开始本文的主题。数据库管理相关的代码都放在Schema目录下, 最开始的结构如下:
src/Illuminate/Database/Schema
├── Blueprint.php
└── Builder.php
就两个文件Blueprint和Builder,Schema/Builder负责提供数据库操作的面向对象似的操作,而Schema/Blueprint则负责存储具体的操作数据,包括数据库操作的命令和数据库表的定义,因此有下面的结构:
接着,我们看看是怎么使用Blueprint的,下看创建table
$grammar = new Illuminate\Database\Schema\Grammars\MySqlGrammar;
$blueprint = new Blueprint('users');
$blueprint->create();
$blueprint->increments('id');
$blueprint->string('email');
$statements = $blueprint->toSql($grammar);
此时返回的$statements
是一个array,有count($statements)==1
,并且$statements[0]
为:
create table `users` (`id` int not null auto_increment primary key, `email` varchar(255) not null)
我们现在来看下上述是怎么实现的?
首先构造函数传入表名users
,而create
则是加了一个命令
public function create()
{
return $this->addCommand('create');
}
addCommand
则是将命令加入进$this->commands
中,而下面的increments
函数如下:
public function increments($column)
{
return $this->integer($column, true);
}
public function integer($column, $autoIncrement = false)
{
return $this->addColumn('integer', $column, compact('autoIncrement'));
}
其最终是调用了addColumn
将其加入进$this->columns
中,下面看下toSql
函数,其核心是下面的部分:
foreach ($this->commands as $command)
{
$method = 'compile'.ucfirst($command->name);
if (method_exists($grammar, $method))
{
if ( ! is_null($sql = $grammar->$method($this, $command)))
{
$statements = array_merge($statements, (array) $sql);
}
}
}
对于每个命令,我们都调用grammar的compileCommand
函数,此处我们调用的是compileCreate
函数,至此我们就分析完了数据库表操作的方法,下面我们来看migrations功能。
数据库迁移
让我们先看目录结构:
src/Illuminate/Database/Console
└── Migrations
├── MigrateCommand.php
├── MigrationRepositoryInterface.php
└── Migrator.php
此处有个新的知识点,也是laravel中一大亮点Artisan,Artisan是 Laravel 自带的命令行接口名称,此处不做具体的介绍了,有机会再细说的,当我们在命令行中执行php artisan command
的时候,会去调用migrateCommand,然后最后会调用Migrator中的函数runMigrations
函数,看下面分析:
public function runMigrations(OuputInterface $output, $package, $path, $pretend = false)
{
$this->output->writeln('<info>Running migrations at path: '.$path.'</info>');
// 从文件中获取migrate files
$files = $this->getMigrationFiles($path);
// 获取已经执行的migration
$ran = $this->repository->getRanMigrations($package);
// 比较不同
$migrations = array_diff($files, $ran);
// 执行未执行的migration
$this->runMigrationList($output, $migrations, $package, $pretend);
}
大版本前夜
看完上面的最基本版本的migrator,我们跨越下直接来看tag v1.1.1版本的eloquent,
git co v1.1.1
此版本是v4.0.0之前的一个版本,从这以后laravel会以组件的形式组织各个功能,让我们分析下v1.1.1的版本,目前具有哪些功能,都是怎么实现的,先看下目录结构:
分别介绍下:
Console和Migrations:这是本篇讲migrations or database modification logic的功能
Eloquent:是前一篇讲的对于Active Record模式中Model的功能,包括了Model、Builder和Relation功能,忘记的可以去看前一篇orm 系列 之 Eloquent演化历程1的内容
Query:包含了最基本的Sql的操作和语法逻辑,类似于自定义了一个DSL语言,提供了面向对象的操作方式
Schema:这也是本篇讲migrations or database modification logic的功能,主要是对数据库表操作sql的建模
此处Connectors是之前没有介绍过的,Connectors是在f917efa中第一次加入的,我们看下到底做了什么,其目录结构是:
src/Illuminate/Database/Connectors
├── ConnectionFactory.php
├── Connector.php
├── ConnectorInterface.php
├── MySqlConnector.php
├── PostgresConnector.php
├── SQLiteConnector.php
└── SqlServerConnector.php
看名字我们就知道大致是怎么回事了,ConnectorInterface
定义了接口,Connector
是基类,ConnectionFactory
是工厂模式,负责创建具体的Connectors,再看下Connector文件,里面有个函数叫:
public function createConnection($dsn, array $config, array $options)
{
$username = array_get($config, 'username');
$password = array_get($config, 'password');
return new PDO($dsn, $username, $password, $options);
}
其实就是对创建PDO的封装,随着代码复杂度的提高,我们根据SOLID原则(SOLID原则可以看The Clean Architecture in PHP 读书笔记(三)),将创建PDO这部分功能单独抽离出来,变为了Connectors,然后根据SOLID原则,我们再继续看下ConnectionInterface
和ConnectionResolverInterface
,分别负责的功能可以看下图:
我们可以看到ConnectionInterface
负责数据库的操作,ConnectionResolverInterface
负责connection的管理,原先这些功能在稍早的版本中都是揉在一起的,还是那个观点:
随着项目复杂度的提升,我们遵循关注点分离的原则,不断去对系统做解耦工作
新增功能
我们接着本篇开头介绍的migrate功能,来看下v1.1.0版本中有的功能,
src/Illuminate/Database/Console
├── Migrations
│ ├── BaseCommand.php
│ ├── InstallCommand.php
│ ├── MakeCommand.php
│ ├── MigrateCommand.php
│ ├── RefreshCommand.php
│ ├── ResetCommand.php
│ └── RollbackCommand.php
└── SeedCommand.php
此时Migrations目录下都是支持的命令,而Migrations下面是具体的实现了。
src/Illuminate/Database/Migrations
├── DatabaseMigrationRepository.php
├── Migration.php
├── MigrationCreator.php
├── MigrationRepositoryInterface.php
├── Migrator.php
└── stubs
├── blank.php
├── create.php
└── update.php
我们可以看到Eloquent中到处都是接口,接口定义了要满足的契约,彼此之间都过接口交流,最大的进行了解耦。
我们通过一个比较有意思的命令Make来看下migration的实现,make的作用是新建一个migration文件,其会根据命令函数参数,去读取src/Illuminate/Database/Migrations/stubs下面的3个文件中的一个,然后调用下面的函数
protected function populateStub($name, $stub, $table)
{
$stub = str_replace('{{class}}', camel_case($name), $stub);
// Here we will replace the table place-holders with the table specified by
// the developer. This is useful for quickly creaeting a tables creation
// or update migration from the console instead of typing it manually
if ( ! is_null($table))
{
$stub = str_replace('{{table}}', $table, $stub);
}
return $stub;
}
做一个简单的字符串替换,然后产生文件,然后还有一个比较有意思的是DatabaseMigrationRepository
类,其作用是:我们需要记录migration哪些已经做了,哪些还没有做,这些记录方式我们通过DatabaseMigrationRepository
来实现,最终是通过将执行记录以log的形式插入到数据库中。
本文最后讲下Eloquent中新增的对象之间的关系:多态关系,以下内容摘自[ Laravel 5.3 文档 ] Eloquent ORM —— 关联关系
表结构
多态关联允许一个模型在单个关联下属于多个不同模型。例如,假设应用用户既可以对文章进行评论也可以对视频进行评论,使用多态关联,你可以在这两种场景下使用单个comments
表,首先,让我们看看构建这种关联关系需要的表结构:
posts
id - integer
title - string
body - text
videos
id - integer
title - string
url - string
comments
id - integer
body - text
commentable_id - integer
commentable_type - string
两个重要的需要注意的列是 comments
表上的 commentable_id
和 commentable_type
。commentable_id
列对应 Post
或Video
的 ID 值,而 commentable_type
列对应所属模型的类名。当访问 commentable
关联时,ORM 根据commentable_type
字段来判断所属模型的类型并返回相应模型实例。
模型结构
接下来,让我们看看构建这种关联关系需要在模型中定义什么:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* Get all of the owning commentable models.
*/
public function commentable()
{
return $this->morphTo();
}
}
class Post extends Model
{
/**
* Get all of the post's comments.
*/
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
}
class Video extends Model
{
/**
* Get all of the video's comments.
*/
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
}
以上内容来自[ Laravel 5.3 文档 ] Eloquent ORM —— 关联关系,接下去让我们看下这是怎么实现的?
首先是morphMany
的构造函数:
public function __construct(Builder $query, Model $parent, $morphName)
{
$this->morphType = "{$morphName}_type";
$this->morphClass = get_class($parent);
parent::__construct($query, $parent, "{$morphName}_id");
}
然后morphMany
其实是扩展是HasOneOrMany
,然后再加限制的时候,新增了一个$this->query->where($this->morphType, $this->morphClass);
,通过这个限制就可以解决多态关联了,那上面的例子来说,就是多个条件commentable_type=Video
,至于HasOneOrMany
的分析参考上一篇文章。
以上就是v4.0.0之前的Eloquent的大致功能,目前orm功能已经完善了,数据库迁移功能,Active Record模式的实现,下一步Eloquent的方向是什么呢?让我们跟着git继续追踪吧_
新纪元
为了解决PHP组件管理及散步的问题,2009年的php|tek大会上成立了PHP-FIG组织,目的在于透过三个方式来制定PHP社群在开发组件时的规范,laravel依赖PHP_FIG的建议,将框架组件独立开发并命名为Illuminate,再提供Starter Package让框架使用者可以透过composer建立项目,因此我们从eloquent-v4.0.0开始,我们会开始看项目laravel/framework。
在4.*版本的时候,laravel/framework还采用的psr-0规范,所有其目录层级还是src/Illuminate/Database
,在这个版本中有个新的目录Capsule,其下面是一个Manager文件,其最初是在f851607中加入的,我们来看下Manager中的内容。
此处为什么会出现Manager,当项目变复杂后,我们很难简单的和Eloquent的内部的组件进行有效的功能了,这个时候需要一个项目的门面,帮助我们和外界沟通,让外界尽可能简单的使用Eloquent,于是就出现了Manager,记得网上搜索过关键字Using Eloquent outside Laravel,我们可以看到一篇Vivek Kumar Bansal写的文章,文章从2方面介绍了怎么使用Eloquent
- Building Schema
- Making a Model
在这两个之间,我们需要有一个第一步就是准备环境,我们来看下代码:
use Illuminate\Database\Capsule\Manager as Capsule;
/**
* Configure the database and boot Eloquent
*/
$capsule = new Capsule;
$capsule->addConnection([
'driver' => 'mysql',
'host' => 'host',
'database' => 'database',
'username' => 'username',
'password' => 'password',
'charset' => 'utf8',
'collation' => 'utf8_general_ci',
'prefix' => 'prefix_'
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();
建立有3个步骤
- 添加数据库连接addConnection
- 设置实例全局可访问setAsGlobal
- 启动Eloquent,bootEloquent
下面具体看下每个步骤做的事情。
第一步addConnection,添加了默认的数据库配置,通过这个配置,我们可以通过DatabaseManager.connection
来获取数据库连接connection,从而进行操作。
第二步setAsGlobal,其动作就做了static::$instance = $this
,通过将实例设置为静态变量,我们就能在全局通过静态方法来访问Capsule了。
第三步bootEloquent,通过Eloquent::setConnectionResolver($this->manager)
设置了Model的静态变量$resolver
,从而能够使用Model。
通过上面3步启动完后,我们就能开始下面的工作了,先看第一个Building Schema,即数据库本身的操作,我们给出示例的代码:
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
//Creating schema
Capsule::schema()->create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('username');
$table->string('first_name')->nullable();
$table->string('last_name')->nullable();
$table->string('email');
$table->string('password');
$table->timestamps();
});
//Droping Schema
Capsule::schema()->dropIfExists('users');
其中Capsule::schema()
返回的是Schema\Builder
实例,然后进行正常的操作操作,此处Blueprint
定义了蓝图,数据库表的定义,然后通过Blueprint.build
来执行蓝图,从而产生数据库表。
接着我们看第二个Making a Model,使用上非常简单:
//User Model
use Illuminate\Database\Eloquent\Model as Eloquent
class User extends Eloquent {
//
}
此处Eloquent已经通过初始化设置了静态变量$resolver
,我们可以方便的获取连接Connection了,也就是有了数据库操作的功能。
好了,以上就是Capsule/Manager的功能了,相当于Eloquent的门面,负责打点一切,此时我们再画下主要的类图:
上面Capsule是大管家,然后DatabaseManger则是内部统领,管理者两大集团Schema和Query,同时DatabaseManger和Eloquent还处理协作关系,Eloquent负责领域类的打理,最后ConnectionFactory则打点着所有的Connection,提供基础的功能。
总结
从v4.0.0开始,基本Eloquent就已经固定了,到目前最新master分支上,目录结构也没有什么变化了,因此下一篇开始,我们会在最新版的基础上,分析下Eloquent的一些具体的实现,尽情期待。
参考
[ Laravel 5.3 文档 ] Eloquent ORM —— 关联关系
这是orm的第三篇,你的鼓励是我继续写下去的动力,期待我们共同进步。
这个时代,每个人都是超级个体!关注我,一起成长!
网友评论