深入了解php函數的各種輔助函數PHP核心語法:函數
理解什麼是可變參數函數, ...$var, PHP5.6新特性介紹
compact函數的用法PHP: compact - Manual
list函數的用法PHP: list - Manual
PHP魔術方法
本期說的是SQL中的查詢語句, 由於複雜程度和多個類別之間的關聯性不是Connector篇那麼簡單, 故此, 這一回需要一口氣講解Builder類, Grammar類, 還有Model類, 當然, 只是查詢部分
請求建構器, 所有類別之間的橋樑
<?php /** * 请求构建器 */ class Builder { // 连接数据库, Connector类 protected $connector; // 生成SQL语法,Grammar类 protected $grammar; // 连接的Model, Model类 protected $model; // SQL查询语句中条件的值 // 虽然列出了全部, 但本教程只实现了where,其余的因为懒(理直气壮),请自行实现, 逻辑和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 - 選擇想看到的column,預設為全部, 即'*'
public function select($columns = ['*']) { // $columns只能存入数组, 所以需要判定, 如果不是, 将所有参数合成一个数组再存入 // 这是一个更人性化的设定, 用户可以选择以下两种调用方式 // select(['first_name', 'last_name']), 以数组的方式 // select('first_name', 'last_name'), 以参数的方式 // 最后一点, 你可以发现所有函数最后都会存入对应的Builder属性中 // 这和你在做饭前先处理材料是同一个道理, 也就是预处理 $this->columns = is_array($columns) ? $columns : func_get_args(); return $this; }
distinct - 過濾重複值
public function distinct() { // 开启过滤 $this->distinct = true; return $this; }
public function from($table) { $this->from = $table; return $this; }
/** * @param string $table 需要连接的副表名 * 为什么主键和外键可以单个或数组呢 * 原因是join语法可以on多个键 * @param string/array $foregin 外键 * @param string/array $primary 主键 * @param string $type 连接方式, 默认inner * @return Builder 返回Builder实例 */ public function join($table, $foregin , $primary, $type = 'inner') { // 判定外键变量的数据类型 if(is_array($foregin)) { // 如果是数组, 循环加上副表名在前头 foreach($foregin as &$f) $f = $table.".".$f; }else { // 反之, 不需循环直接加 $foregin = $table.".".$foregin; } // 与$foreign的逻辑同理 if(is_array($primary)) { foreach($primary as &$p) $p = $this->from.".".$p; }else { $primary = $this->from.".".$primary; } // 将所有经过处理的参数收入$joins待用 $this->joins[] = (object)[ 'from' => $this->from, 'table' => $table, 'foregin' => $foregin, 'primary' => $primary, 'type' => $type ]; // 返回Builder实例 return $this; }
// 所有逻辑同join(), 不过这是left join public function leftJoin($table, $foregin , $primary) { return $this->join($table, $foregin , $primary, 'left'); } // 所有逻辑同join(), 不过这是right join public function rightJoin($table, $foregin , $primary) { return $this->join($table, $foregin , $primary, 'right'); }
/** * @param string/array $value 字段匹配的值 * @param string $type 条件类型, 默认为where, 具体查看$bindings */ public function addBinding($value, $type = 'where') { // 如果$type并不是$bindings的键, 跳出错误 if (!array_key_exists($type, $this->bindings)) throw new InvalidArgumentException("Invalid binding type: {$type}."); // 如果$value是数组,将其与之前存入的值整合为一维数组 if (is_array($value)) $this->bindings[$type] = array_values(array_merge($this->bindings[$type], $value)); // 反之, 直接存入最后一位即可 else $this->bindings[$type][] = $value; // 返回Builder实例 return $this; }
接下來多個函數都有兩種呼叫方式, 請從之後的程式碼邏輯中自行領悟
以參數的方式, 預設為'='
Actor::select('first_name', 'last_name') ->where('first_name', 'NICK') ->where('last_name','!=', 'WAHLBERG') ->first()
Actor::select('first_name', 'last_name') ->where(['first_name'=> 'NICK', 'last_name'=> 'WAHLBERG']) ->first()
public function where($column, $operator = null, $value = null, $boolean = 'and') { // 判定$column是否为数组 if (is_array($column)) { // 如果是数组, 循环再调用本函数,where() foreach ($column as $key => $value) $this->where($key, "=", $value, $boolean); }else { // 反之, 判定参数数量和$value是否为空, 如果为真,这意味着用户省略了'=',自动添加 if(func_num_args() == 2 || is_null($value)) list($operator, $value) = ['=', $operator]; // 最简单原始的条件查询, 所以$type值为Basic $type = "Basic"; // 将处理过的条件存入$wheres $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean'); // 将字段需要匹配的值存入$bindings中的where $this->addBinding($value, 'where'); } // 返回Builder实例 return $this; }
// 所有逻辑同where(), 不过这是or where public function orWhere($column, $operator = null, $value = null) { return $this->where($column, $operator, $value, 'or'); } /** * where in 语法, 字段匹配多个值 * @param string $column 字段 * @param array $values 一组字段需匹配的值 * @param string $boolean 默认为and * @param boolean $not 默认为假, 真为排除所有$value里的数据 * @return Builder 返回Builder实例 */ public function whereIn($column, $values, $boolean = 'and', $not = false) { // 判定条件查询的类型, false = where in ($value),true = where not in ($value) $type = $not ? 'NotIn' : 'In'; // 将条件存入$wheres $this->wheres[] = compact('type', 'column', 'values', 'boolean'); // 循环将字段需要匹配的值存入$bindings中的where foreach ($values as $value) $this->addBinding($value, 'where'); // 返回Builder实例 return $this; } // 所有逻辑同whereIn(), 不过这是or where in public function orWhereIn($column, $values) { return $this->whereIn($column, $values, 'or'); } // 所有逻辑同whereIn(), 不过这是and where not in public function whereNotIn($column, $values, $boolean = 'and') { return $this->whereIn($column, $values, $boolean, true); } // 所有逻辑同whereNotIn(), 不过这是or where not in public function orWhereNotIn($column, $values) { return $this->whereNotIn($column, $values, 'or'); } /** * where $coulmn is null 语法, 搜索字段为空的数据 * @param string $column 字段 * @param string $boolean 默认为and * @param boolean $not 默认为假, 真为排除所有字段为空的数据 * @return Builder 返回Builder实例 */ public function whereNull($column, $boolean = 'and', $not = false) { // 判定条件查询的类型, false = where $column is null,true = where $column is not null $type = $not ? 'NotNull' : 'Null'; // 将条件存入$wheres $this->wheres[] = compact('type', 'column', 'boolean'); // 返回Builder实例 return $this; } // 所有逻辑同whereNull(), 不过这是or where $column is null public function orWhereNull($column) { return $this->whereNull($column, 'or'); } // 所有逻辑同whereNull(), 不过这是and where $column is not null public function whereNotNull($column, $boolean = 'and') { return $this->whereNull($column, $boolean, true); } // 所有逻辑同whereNotNull(), 不过这是or where $column is not null public function orWhereNotNull($column) { return $this->whereNotNull($column, 'or'); }
/** * @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; }
function array_flatten(array $array) { $return = array(); array_walk_recursive($array, function($a) use (&$return) { $return[] = $a; }); return $return; }
$a = array('this', 'is', array('a', 'multidimentional', array('array') ), 'to', 'make', 'the', 'tutotal', array('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)
/** * @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; }
public function limit($value) { // 如果$value大于零这条函数才生效 if ($value >= 0) $this->limit = $value; return $this; } // limit函数的别名, 增加函数链的可读性 public function take($value) { return $this->limit($value); }
public function offset($value) { // 如果$value大于零这条函数才生效 if ($value >= 0) $this->offset = $value; return $this; } // offset函数的别名, 增加函数链的可读性 public function skip($value) { return $this->offset($value); }
重頭戲來了, 之前所有的函數都是返回Builder實例的,這意味著可再編輯
這是一個總結, 將前端的函數鏈請求一併處理了
// 返回一组数据库数据, 可以在这里设定想返回的字段, 但是select()的优先度最高 public function get($columns = ['*']) { // 如果Builder的$columns依然为空, 那么就用该函数的$columns, 反之则使用select()所声明的字段 if (is_null($this->columns)) $this->columns = $columns; // 如果Builder的$orders依然为空, 那么就默认第一个字段顺序 // 发现一个莫名的bug, 可能是我理解错了, 不加 order by 1数据返回竟然不是按照主键(第一个字段)排序 // 所以以防万一加一个默认 if (is_null($this->orders)) $this->orderBy(1); // 将Grammar类生成的语句,和处理过的字段所对应的值,都交给Connector类, 让它与数据库进行通信,返回数据 // 注意这里的三个函数 // read() 不用说Connector篇介绍过了 // 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); }
public function getBindings() { // 抚平多维数组成一维数组后再返回 return array_flatten($this->bindings); }
在基本思路篇的最終效果小節有提到過資料的可操作性, 核心程式碼就是這裡
如果看不明白這裡沒關係, 暫時跳過, 等看完Model.php就能理解了(吧?)
public function cast($results){ // 获取Model子类的名称 $class = get_class($this->model); // 新建一个Model子类 $model = new $class(); // 如果获得的数据库数据是数组 if (gettype($results)=="array") { $arr = []; // 循环数据 foreach ($results as $result) // 再调用本函数 $arr[] = $this->cast($result); // 返回经过转化的数据数组 return $arr; // 如果获得的数据库数据是对象 }elseif(gettype($results)=="object"){ // 存入数据对象 $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; }
/** * @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]; }
public function setModel(Model $model) { $this->model = $model; return $this; }
<?php /** * 数据库语法生成 */ class Grammar { // 构建查询语句所可能出现的各种SQL语法 // 注意, 它们的顺序是对应着各自在SQL语句中合法的位置 // sqlsrv略微不同 protected $selectComponents = [ 'distinct', 'columns', 'from', 'joins', 'wheres', 'groups', 'orders', 'limit', 'offset', ];
protected function concatenate($segments) { return implode(' ', array_filter($segments, function ($value) { return (string) $value !== ''; })); }
// 还记得Builder->get()中的compileSelect()吗? public function compileSelect(Builder $query) { // concatenate()排除编译后可能存在空的值,然后连接整句SQL语句 // 去掉可能存在的前后端空格再返回 return trim($this->concatenate($this->compileComponents($query))); }
protected function compileComponents(Builder $query) { $sql = []; // 循环$selectComponents foreach ($this->selectComponents as $component) { // 如果Builder实例中对应的函数曾经被调用,那意味着对应的语法非空 if (!is_null($query->$component)) { $method = 'compile'.ucfirst($component); // 编译该语法并将之收入$sql $sql[$component] = $this->$method($query, $query->$component); } } // 返回$sql数组 return $sql; }
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"; }
compileColumns - 編譯需查詢的欄位
protected function compileColumns(Builder $query, $columns) { return implode(', ', $columns); }
compileFrom - 編譯產生表名
protected function compileFrom(Builder $query, $table) { return 'from '.$table; }
compileJoins - 編譯join語句
protected function compileJoins(Builder $query, $joins) { $sql = []; foreach ($joins as $join) { // 如果存在多个副键和主键 if(is_array($join->foregin) && is_array($join->primary)) { $on = []; // 循环键的数量, 将之与对应的主键组合 for($i=0; $i<sizeof($join->foregin); $i++) $on[] = $join->foregin[$i]." = ".$join->primary[$i]; // 最后再将所有句子用and连接 $on = implode(' and ', $on); } else { //反之, 直接连接即可 $on = "$join->foregin = $join->primary"; } // 附上join的类型和副表 $sql[] = trim("{$join->type} join {$join->table} on $on"); } // 连接再返回 return implode(' ', $sql); }
protected function compileWheres(Builder $query) { $sql = []; // 类似与compileComponents(), 循环Builder实例中的$wheres foreach ($query->wheres as $where) { // 根据不同的$type来进行编译 $method = "where{$where['type']}"; // 返回的部分语句先收入数组 $sql[] = $where['boolean'].' '.$this->$method($query, $where); } // 最后将$sql数组连接起来, 删掉最前面的and或or在返回 return 'where '.preg_replace('/and |or /i', '', implode(" ", $sql), 1); }
protected function whereBasic(Builder $query, $where) { // 检测$where[column]是否存在小数点 // 是, 就意味着前缀已经附带了表名 // 否, 为之后的字段添上表名 // 因为join存在副表, 所以部分$where可能有附带表名, 这时候就不用添加了 $table = !preg_match('/\./', $where['column']) ? $query->from."." : ''; // 返回添上表名的字段,和表达式, 再一个问号 // 为何使用问号而不是:变量名? 因为:变量名存在太多局限性,不能标点符号,不能数字开头 return $table.$where['column'].' '.$where['operator'].' ?'; }
protected function whereIn(Builder $query, $where) { // 检测$where[column]是否存在小数点, 同理whereBasic() $table = !preg_match('/\./', $where['column']) ? $query->from."." : ''; // 有多少个匹配值那就连接多少个问号 $values = implode(', ', array_fill(0, sizeof($where['values']), '?')); // 返回where in 语句 return $table.$where['column'].' in ('.$values.')'; }
protected function whereNotIn(Builder $query, $where) { // 检测$where[column]是否存在小数点, 同理whereBasic() $table = !preg_match('/\./', $where['column']) ? $query->from."." : ''; // 有多少个匹配值那就连接多少个问号 $values = implode(', ', array_fill(0, sizeof($where['values']), '?')); // 返回where not in 语句 return $table.$where['column'].' not in ('.$values.')'; }
protected function whereNull(Builder $query, $where) { // 检测$where[column]是否存在小数点, 同理whereBasic() $table = !preg_match('/\./', $where['column']) ? $query->from."." : ''; // 返回where is null 语句 return $table.$where['column'].' is null'; }
protected function whereNotNull(Builder $query, $where) { // 检测$where[column]是否存在小数点, 同理whereBasic() $table = !preg_match('/\./', $where['column']) ? $query->from."." : ''; // 返回where is not null 语句 return $table.$where['column'].' is not null'; }
protected function compileGroups(Builder $query, $groups) { // 连接$groups, 返回group by语句 return 'group by '.implode(', ', $groups); }
protected function compileOrders(Builder $query, $orders) { // 连接每一个$order与其$direction, 然后返回order by语句 return 'order by '.implode(', ', array_map(function ($order) { return $order['column'].' '.$order['direction']; }, $orders)); }
<?php /** * 入口文件, 数据库表的父类 */ class Model { // SQL命令构建器, Builder类 protected $builder; // 数据库返回的数据存在这里 protected $data; // 数据库表名, 选填, 默认为类名 protected $table; // 主键, 二选一($unique) protected $identity; // unique key, 二选一($identity) protected $unique;
public function getTable() { return isset($this->table) ? $this->table : false; }
数据库表的依赖对象
作为数据的出口, 数据就在这里进行修饰
各种魔术方法用得飞起, 使用之前请先理解魔术方法是什么
<?php /** * 入口文件, 数据库表的父类 */ class Model { // SQL命令构建器, Builder类 protected $builder; // 数据库返回的数据存在这里 protected $data; // 数据库表名, 选填, 默认为类名 protected $table; // 主键, 二选一($unique) protected $identity; // unique key, 二选一($identity) protected $unique;
getTable - 获取数据库表名, 没有设置返回false
public function getTable() { return isset($this->table) ? $this->table : false; }
getIdentity - 获取主键名, 没有返回假
public function getIdentity() { return isset($this->identity) ? $this->identity : false; }
getUnique - 获取unique key名, 没有返回假
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; }
construct - 创建实例后的第一步
function construct() { // 检查设定是否正确 $this->check(); // 新建一个Builder实例 $this->setBuilder(new Builder); // 设置构建器的主表名称 $this->getBuilder()->from($this->table); // 将Model实例带入Builder $this->getBuilder()->setModel($this); }
callStatic - 如果找不到静态函数的时候自动运行下面的逻辑
static public function callStatic($method, $args = null) { // 这是一个伪静态, 创建一个实例 $instance = new static; // 在$instance->builder之中, 寻找函数$method, 并附上参数$args return call_user_func_array([$instance->builder, $method], $args); }
call - 如果找不到函数的时候自动运行下面的逻辑
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内是否存在该属性, 没有的话跳出错误 if(!isset($this->data->$field)) throw new Exception("column '$field' is not exists in table '$this->table'"); // 如果存在,由于返回的数据都是存在$data里, 所以要这样调用 return $this->data->$field; }
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; }
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(); }
还有更多, 看心情更...
1.) 缺少的Builder函数如果有人愿意提供例子就好了, 进阶复杂的语句那就更好了, 直接写然后再分享给我那就最好了
2.) 有些函数或结构可能没有效率或者白白添加服务器压力, 但我写的顺了可能没看见, 请指出
3.) 有人能告诉我laravel是怎么解决pdo 2100个最大参数(bind)的问题吗? 源代码把我看蒙了
以上是如何寫一個屬於自己的資料庫封裝(3)的詳細內容。更多資訊請關注PHP中文網其他相關文章!