上一期 如何写一个属于自己的数据库封装(2) – 数据库连接
下一期 如何写一个属于自己的数据库封装(4) – 查询 – 入门篇用法
本期要点
深入了解php函数的各种辅助函数 PHP核心语法:函数
理解什么是可变参数函数, …$var, PHP5.6新特性介绍
compact函数的用法 PHP: compact – Manual
list函数的用法 PHP: list – Manual
开始之前 (长篇预警!!!!!!)
本期主要解说SQL中的查询语句, 由于复杂程度和多个类之间的关联性不是Connector篇那么简单
故此, 这一期需要分别讲解 Builder 类, Grammar 类, 还有 Model 类
由于篇幅过大, 经过重订过后的查询篇将分为三篇,
入门篇、WHERE 篇、JOIN 篇 以及遥遥无期的进阶查询篇
Builder.php
- 请求构造器, 所有类之间的桥梁
<?php
/**
* 请求构造器
*/
class Builder {
// 连接数据库, Connector 类
protected $connector;
// 生成SQL语法,Grammar 类
protected $grammar;
// 连接的Model, Model 类
protected $model;
// SQL查询语句中条件的值
// 虽然列出了全部, 但本教程只实现了 where 和 join ,其余的因为懒(理直气壮),请自行实现, 逻辑和 where 函数大致
protected $bindings = [
'select' => [],
'join' => [],
'where' => [],
'having' => [],
'order' => [],
'union' => [],
];
// select 语法想要查看的字段
public $columns;
// 过滤重复值
public $distinct = false;
// 需要查询的表
public $from;
// 所有 join 语法
public $joins;
// 所有 where 语法
public $wheres;
// group 语法
public $groups;
// having 语法
public $havings;
// order by 语法
public $orders;
// 限制数据库返回的数据量, limit 语法
public $limit;
// 需要略过的数据量, offset 语法
public $offset;
// 数据写保护, 开启后该条数据无法删除或改写
public $writeLock = false;
- _construct – 生成实例后第一步要干嘛
function __construct() {
// 新建两个实例
// 如果已经理解Connector的原理后自然明白这个Connector实例已经联通了数据库
$this->connector = new Connector;
$this->grammar = new Grammar;
}
select – 选择想查询的表字段, 默认为全部, 即 ‘*’
为了优化体验, 开发组可以选择以下两种调用方式- 以数组的方式
select(['first_name', 'last_name'])
- 以参数的方式
select('first_name', 'last_name')
最后一点, 所有函数逻辑在最后都会存入对应的 Builder 属性中
select 函数的参数最后收入了 Builder 实例中的 $columns 属性
这和做饭前先处理食材是同一个道理, 也就是预处理public function select($columns = ['*']) { // 判定 $columns 是否数组, 如果不是, 将所有参数合成一个数组再存入 $this->columns = is_array($columns) ? $columns : func_get_args(); return $this; }
distinct – 过滤重复值
public function distinct() {
// 开启过滤
$this->distinct = true;
return $this;
}
- from – 设置表名
public function from($table) {
$this->from = $table;
return $this;
}
orderBy – order by 语句, 决定返回的数据排列
在 SQL 语法中, order by 支持多个字段排序, 因此 orderBy 函数也允许多次调用
为了优化多次调用可能带来的函数链过长、繁杂等问题, orderBy 函数支持两种调用方式- 以数组的方式 – 局限性在于必须声明排序
(new Builder())->from('actor') ->orderBy([ 'first_name' => 'asc', 'last_name' => 'desc' ]) ->get();
- 以参数的方式 – 默认为顺序, asc
(new Builder())->from('actor') ->orderBy('first_name') // ->orderBy('first_name', 'asc') ->orderBy('last_name', 'desc') ->get();
/** * @param string/array $column 字段 * @param string $direction 排序,默认为asc, 顺序 * @return Builder 返回Builder实例 */ public function orderBy($column, $direction = 'asc') { // 局限性在于必须声明顺序或逆序 if(is_array($column)) { foreach ($column as $key => $value) $this->orderBy($key, $value); }else { // 简单判定后直接存入$orders, $direction输入错误不跳错误直接选择顺序 $this->orders[] = [ 'column' => $column, 'direction' => strtolower($direction) == 'desc' ? 'desc' : 'asc', ]; } // 返回Builder实例 return $this; }
辅助函数 – array_flatten
这是我自己写的一个函数, 用于抚平多维数组
什么意思呢, 就是将多维数组整成一维数组
function array_flatten(array $array) {
$return = array();
array_walk_recursive($array, function($a) use (&$return) {
$return[] = $a;
});
return $return;
}
例子
$a = [
'this',
'is',
[
'a',
'multidimentional',
[
'array'
]
],
'to',
'make',
'the',
'tutotal',
[
'more',
'easier',
'to',
'understand'
]
];
dd(array_flatten($a));
返回结果
array (size=13)
0 => string 'this' (length=4)
1 => string 'is' (length=2)
2 => string 'a' (length=1)
3 => string 'multidimentional' (length=16)
4 => string 'array' (length=5)
5 => string 'to' (length=2)
6 => string 'make' (length=4)
7 => string 'the' (length=3)
8 => string 'tutotal' (length=7)
9 => string 'more' (length=4)
10 => string 'easier' (length=6)
11 => string 'to' (length=2)
12 => string 'understand' (length=10)
- groupBy – group by 语句, 整合数据
/**
* @param string/array $groups 字段
* @return Builder 返回Builder实例
*/
public function groupBy(...$groups) {
if(empty($this->groups)) $this->groups = [];
$this->groups = array_merge($this->groups, array_flatten($groups));
// 返回Builder实例
return $this;
}
- limit – 限制返回的数据量, sqlsrv的写法不同, 有兴趣知道的可以留言
public function limit($value) {
// 如果$value大于零这条函数才生效
if ($value >= 0) $this->limit = $value;
return $this;
}
// limit函数的别名, 增加函数链的可读性
public function take($value) {
return $this->limit($value);
}
- offset – 跳过指定的数据量, sqlsrv的写法不同, 有兴趣知道的可以留言
public function offset($value) {
// 如果$value大于零这条函数才生效
if ($value >= 0) $this->offset = $value;
return $this;
}
// offset函数的别名, 增加函数链的可读性
public function skip($value) {
return $this->offset($value);
}
- get – 读取数据库数据
重头戏来了, 之前所有的函数都是返回Builder实例的,这意味着可再编辑
而 get 函数 是一个总结, 将在它之前的函数链请求统一处理了
// 返回一组数据库数据, 可以在这里设定想返回的字段, 但是 select() 的优先度最高
public function get($columns = ['*']) {
// 如果Builder的 $columns 依然为空, 那么就用该函数的 $columns , 反之则使用 select() 所声明的字段
if (is_null($this->columns))
$this->columns = $columns;
// 将Grammar类生成的语句,和处理过的字段所对应的值,都交给Connector类, 让它与数据库进行通信,返回数据
// 注意这里的三个函数
// read() 不用说[Connector篇](http://www.jianshu.com/p/7830be449335)介绍过了
// compileSelect() 是用来编译生成查询语句
// getBindings() 用来获取收在$bindings中条件的值, 下方会有说明
$results = $this->connector->read($this->grammar->compileSelect($this), $this->getBindings());
// 返回一组数据库数据,如果查询为空,返回空数组
// cast() 转换返回数据的数据类型, 下方会有说明
return $this->cast($results);
}
// get函数的别名, 增加函数链的可读性
public function all($columns = ['*']) {
return $this->get($columns);
}
- getBindings – 返回所有$bindings中的值
public function getBindings() {
// 抚平多维数组成一维数组后再返回
return array_flatten($this->bindings);
}
- cast – 转化返回的数据类型为自身的Model子类
在基本思路篇的最终效果小节有提到过数据的可操作性, 核心代码就是这里
如果看不明白这里没关系, 暂时跳过, 等看完Model.php就能理解了(吧?)
public function cast($results){
// 获取Model子类的名称
$class = get_class($this->model);
// 如果并未设置Model类
if ($class==='Builder')
$model = null;
else
// 新建一个Model子类
$model = new $class();
// 如果获得的数据库数据是数组
if (gettype($results)=="array") {
$arr = [];
// 循环数据
foreach ($results as $result)
// 再调用本函数
$arr[] = $this->cast($result);
// 返回经过转化的数据数组
return $arr;
// 如果获得的数据库数据是对象
}elseif(gettype($results)=="object"){
// 如果并未设置Model类, 直接返回数据无需转换
if($model===null)
return $results;
// 存入数据对象
$model->setData($results);
// 加入主键或unique key以实现数据的可操作性
// 如果表存在主键和返回的数据中有主键的字段
if($model->getIdentity() && isset($results->{$model->getIdentity()})) {
$model->where($model->getIdentity(), $results->{$model->getIdentity()});
// 如果表存在unique key和返回的数据中有unique key的字段
}elseif($model->getUnique() && array_check($model->getUnique(),$results)) {
foreach ($model->getUnique() as $key)
$model->where($key, $results->$key);
// 改写和删除操作仅仅在符合以上两种条件其中之一的时候
// 反之, 开启写保护不允许改写
}else {
// 其实还可以考虑直接复制query
// 但变数太多干脆直接一棍子打死
$model->getBuilder()->writeLock = true;
}
// 返回该实例
return $model;
}
// 如果转化失败返回false
return false;
}
- first – 仅取头一条数据, 所以返回的是对象, 而 get() 返回的是数组,里头多条对象
/**
* @param array $columns 如果Builder的$columns依然为空, 那么就用该函数的$columns, 反之则使用 select() 所声明的字段
* @return boolean/Model 查询为空返回false, 反之则返回附带数据的表类
*/
public function first($columns = ['*']) {
$results = $this->take(1)->get($columns);
return empty($results) ? false : $results[0];
}
- setModel – 设置Model实例
public function setModel(Model $model) {
$this->model = $model;
return $this;
}
Grammar.php
- 根据经过处理后存在Builder实例属性中的值进行编译
- 编译是一部分一部分语法慢慢编译的, 最后在总结起来
- SQL 语法在不同的服务器有不同的写法与限制, 常见的有 MYSQL、SQLSRV等等, 在这里编译使用的是 MYSQL
<?php
/**
* 数据库语法生成
*/
class Grammar {
// 构建查询语句所可能出现的各种SQL语法
// 注意, 它们的顺序是对应着各自在SQL语句中合法的位置
// sqlsrv略微不同
protected $selectComponents = [
'distinct',
'columns',
'from',
'joins',
'wheres',
'groups',
'orders',
'limit',
'offset',
];
- concatenate – 排除编译后可能存在空的值,然后连接整句SQL语句
protected function concatenate($segments) {
return implode(' ', array_filter($segments, function ($value) {
return (string) $value !== '';
}));
}
- compileSelect – 编译SQL查询语句
// 还记得Builder->get()中的 compileSelect() 吗?
public function compileSelect(Builder $query) {
// concatenate() 排除编译后可能存在空的值,然后连接整句SQL语句
// compileComponents() 循环$selectComponents, 根据不同的语法局部编译对应的语句
// 去掉可能存在的前后端空格再返回
return trim($this->concatenate($this->compileComponents($query)));
}
为了方便理解, 加入例子单步调试
(new Builder())->from('actor')->select('first_name', 'last_name')->get();
- compileComponents – 循环$selectComponents, 根据不同的语法局部编译对应的语句
protected function compileComponents(Builder $query) {
$sql = [];
// 循环$selectComponents
foreach ($this->selectComponents as $component) {
// 如果 Builder 实例中对应的函数曾经被调用,那意味着对应的语法非空
// 例子中调用了 from() 和 select(), 分别对应了 $selectComponents 中的 from 和 select
if (!is_null($query->$component)) {
$method = 'compile'.ucfirst($component);
// $method = 'compileFrom';
// $method = 'compileCoulmns';
// 编译该语法并将之收入$sql
$sql[$component] = $this->$method($query, $query->$component);
// $sql['from'] = $this->compileFrom($query->from);
// $sql['coulmns'] = $this->compileColumns($query->columns);
}
}
// 返回$sql数组
return $sql;
}
- compileFrom – 编译生成表名
protected function compileFrom(Builder $query, $table) {
return 'from '.$table;
// 例子中返回了 'from actor'
}
- compileColumns – 编译需查询的字段
protected function compileColumns(Builder $query, $columns) {
return implode(', ', $columns);
// 例子中返回了 'first_name, last_name'
}
- compileDistinct – 编译 distinct 语句
protected function compileDistinct(Builder $query, $distinct) {
return $distinct ? 'select distinct' : 'select';
}
- compileLimit – 编译 limit 语句
protected function compileLimit(Builder $query, $limit) {
return "limit $limit";
}
- compileOffset – 编译 offset 语句
protected function compileOffset(Builder $query, $offset) {
return "offset $offset";
}
- compileGroups – 编译 group by 语句
protected function compileGroups(Builder $query, $groups) {
// 连接 $groups , 返回 group by 语句
return 'group by '.implode(', ', $groups);
}
- compileOrders – 编译 order by 语句
protected function compileOrders(Builder $query, $orders) {
// 连接每一个 $order 与 其 $direction , 然后返回 order by 语句
return 'order by '.implode(', ', array_map(function ($order) {
return $order['column'].' '.$order['direction'];
}, $orders));
}
Model.php
- 数据库表的依赖对象
- 经过定义的数据库表将形成一个门面, 调用简洁方便
- 如果用过 laravel 的话大概明白这是什么了
- 各种魔术方法用得飞起, 使用之前请先理解魔术方法是什么
<?php
/**
* 入口文件, 数据库表的父类
*/
class Model {
// SQL命令构建器, Builder类
protected $builder;
// 数据库返回的数据存在这里
protected $data;
// 数据库表名, 选填, 默认为类名
protected $table;
// 主键, 二选一($unique)
protected $identity;
// unique key, 二选一($identity)
protected $unique;
// 记录实例中调用过的函数
protected $functions = [];
- getTable – 获取数据库表名, 没有设置返回false
public function getTable() {
return isset($this->table) ? $this->table : false;
}
- getIdentity – 获取主键名, 没有返回false
public function getIdentity() {
return isset($this->identity) ? $this->identity : false;
}
- getUnique – 获取unique key名, 没有返回false
public function getUnique() {
// 检测是否存在unique key, 不存在返回假, 存在就在检查是否数组, 不是就装入数组再返回
return isset($this->unique) ? is_array($this->unique) ? $this->unique : [$this->unique] : false;
}
- check – 检查必须预设的实例属性
public function check() {
// 如果数据库表的名称和Model的子类相同,可以选择不填,默认直接取类的名称
if(!$this->getTable())
$this->table = get_class($this);
// 跳出提醒必须设置$identity或$unique其中一项
if(!$this->getIdentity() && !$this->getUnique())
throw new Exception('One of $identity or $unique should be assigned in Model "'.get_called_class().'"');
}
- set/getBuilder – 设置或读取Builder实例
// 设置Builder实例
public function setBuilder(Builder $builder) {
$this->builder = $builder;
return $this;
}
// 获取Builder实例
public function getBuilder() {
return $this->builder;
}
- setData – 设置数据库数据
public function setData($data) {
$this->data = $data;
return $this;
}
- getCalledFunctions – 获取实例调用过的函数
public function getCalledFunctions() {
return $this->functions;
}
魔术方法
- __construct – 创建实例后的第一步
function __construct() {
// 检查设定是否正确
$this->check();
// 新建一个Builder实例
$this->setBuilder(new Builder);
// 设置构建器的主表名称
$this->getBuilder()->from($this->table);
// 将Model实例带入Builder
$this->getBuilder()->setModel($this);
}
- __callStatic – 如果找不到静态函数的时候自动调用 Model 实例中 Builder 实例的函数
static public function __callStatic($method, $args = null) {
// 这是一个伪静态, 创建一个实例
$instance = new static;
// 在$instance->builder之中, 寻找函数$method, 并附上参数$args
return call_user_func_array([$instance->builder, $method], $args);
}
- __call – 如果找不到函数的时候自动调用 Model 实例中 Builder 实例的函数
public function __call($method, $args) {
// 在$this->builder之中, 寻找函数$method, 并附上参数$args
return call_user_func_array([$this->builder, $method], $args);
}
- __debugInfo – 在调试的时候隐藏多余的信息, 只留下数据库返回的数据
public function __debugInfo() {
// 也不懂算不算bug, 该方法强制要求返回的数据类型必须是array数组
// 但是就算我强行转换(casting)后返回的数据依然是对象(object)
return (array)$this->data;
}
- __get – 当调用对象的属性时, 强制调用这个魔术方法
// 为了避免局外人可以访问Model类的属性
// 为了避免对象属性和表的字段名字相同
public function __get($field) {
// 如果调用的属性是Model类内的逻辑
// 直接返回该属性的值
if(get_called_class()==="Model")
return $this->$field;
// 反之, 则检查$data内是否存在该属性
// 如果存在,由于返回的数据都是存在$data里, 所以要这样调用
if(isset($this->data->$field))
return $this->data->$field;
// 没有的话再查是否存在该函数, 这是为了实现关联关系可以再过滤条件
if(method_exists($this, $field)) {
$this->$field();
// 如果是关联关系的函数, 以变量的方式来调用意味着想直接获取结果
if(in_array('hasOne', $this->getCalledFunctions()))
return $this->first();
elseif(in_array('hasMany', $this->getCalledFunctions()))
return $this->get();
elseif(in_array('hasManyThrough', $this->getCalledFunctions()))
return $this->get();
}
// 函数或字段都不存在, 跳出错误
throw new Exception("column '$field' is not exists in table '$this->table'");
}
- __set – 当想修改的对象属性时, 强制调用这个魔术方法
public function __set($field, $value) {
// 如果调用的属性是Model类内的逻辑
// 直接赋值该属性
if(get_called_class()==="Model")
return $this->$field = $value;
// 反之, 则检查$data内是否存在该属性, 没有的话跳出错误
if(!isset($this->data->$field))
throw new Exception("column '$field' is not exists in table '$this->table'");
// 如果存在,由于返回的数据都是存在$data里, 所以要这样赋值
return $this->data->$field = $value;
}
- __clone – 纯拷贝实例, 不引用
public function __clone() {
foreach ($this as $key => $val) {
if (is_object($val) || (is_array($val))) {
if(is_object($val)) $this->{$key} = clone $val;
}
}
}
- find – 使用主键查寻数据
/**
* @param int $id 主键
* @return Model subclass 返回一个Model的子类数据
*/
public static function find($id) {
// 这是一个伪静态, 创建一个实例
$self = new static;
// 该函数只适用于设置了主键的表, 如果没有设置, 跳出错误
if(!$self->getIdentity())
throw new Exception("Table's identity key should be assgined");
return $self->where($self->identity, $id)->first();
}
完整代码
源代码放在coding.net里, 自己领
本期疑问
1.) 缺少的Builder函数如果有人愿意提供例子就好了, 进阶复杂的语句那就更好了, 直接写然后再分享给我那就最好了
2.) 有些函数或结构可能没有效率或者白白添加服务器压力, 但我写的顺了可能没看见, 请指出
上一期 如何写一个属于自己的数据库封装(2) – 数据库连接
下一期 如何写一个属于自己的数据库封装(4) – 查询 – 入门篇用法