如何写一个属于自己的数据库封装(3) - 查询 - 入门篇(修订版)

上一期 如何写一个属于自己的数据库封装(2) - 数据库连接
下一期 如何写一个属于自己的数据库封装(4) - 查询 - 入门篇用法

本期要点

深入了解php函数的各种辅助函数 PHP核心语法:函数

理解什么是可变参数函数, ...$var, PHP5.6新特性介绍

compact函数的用法 PHP: compact - Manual

list函数的用法 PHP: list - Manual

PHP魔术方法

开始之前 (长篇预警!!!!!!)

本期主要解说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) - 查询 - 入门篇用法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容