- 核心代码不足1000行,仅两个文件便可工作,极速加载
- 单文件入口,不依赖
PathInfo
,入口文件即是配置文件,超级简洁- 文件夹随意移动,轻松多项目共享,入口文件随意命名,CLI模式轻松使用
MYSQL/SQLITE
任意切换,注入过滤/ORM,文件缓存/HTTP缓存/数据库缓存,轻松安全- 异常捕获,
DEBUG
日志,自定义错误页,自定义异常路由一应俱全- 普通路由,正则路由,回调处理,百变URI随心所欲,插件模式,即插即用
- 文件加载自动完成,延迟按需加载、无需
Include
简单高效
index.php
入口文件即配置文件,core.php
框架核心,外加一个处理请求的控制器文件PHP8.0
及以上- 使用PDO连接数据库,支持
MySQL
和Sqlite
,需开启PDO_MYSQL
- 定义配置文件的程序路径(一般不需改变)和其他参数,例如SMTP,数据库,即可完美使用
- 需要URL REWRITE支持,否则链接中要添加
index.php
- rewrite 即为一般的index.php rewrite写法
对于nginx
try_files $uri $uri/ /index.php$is_args$args;
加入location / {}
里面
对于apache
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
Web程序可将目录结构调整为
├── app
│ ├── controller
│ │ └── home.php
│ ├── model
│ │ └── util.php
│ └── system
│ └── core.php
├── README.md
├── var
│ ├── html
│ └── log
└── www
└── index.php
入口文件index.php
即为配置文件,主要配置控制器模型等地址
配置项lib_path
为一个数组,配置控制器,模型,类库的自动查找地址
view_path
为模板查找文件夹
var_path
为缓存目录和日志目录
文件夹 controller model system 其实并无区别
系统并不是按照文件夹结构来区分 controller 和 model 而是按照类本身的特性
包含__invoke
魔术方法的类将被视为控制器
在控制器抛出异常时,此方法将被调用用于处理异常.
event
字段配置闭包函数可用于扩展app
- 所有加载过的控制器,都会记住是否已加载过,不会重复加载,也不会重复实例化
- 加载过的类库和模型全局可用
- 所有的同名文件类都可以直接通过类名调用其静态方法,或new实例化
对于低于8.1版本,需要实现array_is_list
函数
if (!function_exists('array_is_list'))
{
function array_is_list(array $a)
{
return $a === [] || (array_keys($a) === range(0, count($a) - 1));
}
}
对于低于8.0版本,需要实现str_contains
函数
if (!function_exists('str_contains'))
{
function str_contains(string $haystack, string $needle)
{
return empty($needle) || strpos($haystack, $needle) !== false;
}
}
配置文件debug
字段用于控制日志记录,其值可为0/1
或者 false/true
框架判断其布尔值,非debug
模式仅记录ERROR
级别日志,忽略INFO
,WARN
无论是否debug,只要日志目录可写,都会记录相应的错误。
框架同时包含正则路由和普通路由.
普通路由按照目录结构路由,
正则路由按照URI匹配.
使用 route::get() 添加正则路由
正则路由优先级高于普通路由.
正则路由有多种写法
字符串函数
route::get('/print','print_r');
闭包
route::get('/dump',function(...$a){
var_dump($a);
});
实例化控制器
控制器类将被自动实例化,然后执行
route::get('/hello',['home','hello'])
route::get('/hello',['home','hello','world'])
静态化控制器
控制器类方法将被静态调用
route::post('/static','home::echo');
捕获参数
route::get('/userinfo/(\d+)',['home','userinfo'])
带命名空间的静态调用,可调度到子文件夹,自己再次分发路由,admin
为namespace
,form
类无实例化,直接调用
route::get('/hi/index','admin\form::index')
带命名空间的动态调用,可调度到子文件夹,form
类校验,并实例化
route::get('/hello/hi',['admin\form','hi'])
二级,三级,文件夹同类
闭包模式
route::get('/about',function(){echo 'about';})
URL 拼接
route::u('/home/hello',['act'=>'hi'])
重定向
route::to('/login')
默认的控制器为home
,默认的action为index
控制器被实例化时,将传入触发实例化时的路由参数给构造函数
控制器实现了单例模式,一个控制器只会实例化一次
app::run(array $r)
可以内部转移控制器,交由其他控制器执行
除了控制器的__invoke
方法能处理异常外,全局异常可配置自定义处理
在配置中添加notfound
和errfound
并赋值一个闭包,开启自定义错误处理
'notfound' => function ($e, $cli) {
echo '404 page not found';
},
'errfound' => function ($e, $cli) {
echo 'user hand this error : ', $e->getMessage();
},
自定义异常处理后,框架提供的404,500等错误详情,页面将不再展现,只能通过日志查看
框架內建2种缓存,可同时工作
HTTP 缓存使用 app::cache(int $second)
开启
需要在控制器输出其他内容前调用
客户端下次请求将会200(from cache)
有客户端发起协商缓存时,框架也会自动验证,命中时返回304
可用于缓存接口,页面等
模板函数template(string $v, array $data = [], callable|int $callback = 0, string $path = '')
实现了页面缓存
设置$callback
为一个大于1的秒数,即可开启页面自动缓存
同时必须确保配置项var_path
是可写的,缓存文件存储在其html
子文件夹
页面缓存的缓存键是当前请求的URI
,不包含query部分
可用于缓存公共页面,不可缓存带有个人信息的页面
请求来临时同样鉴别对应的URI
是否已缓存
命中时释放缓存,缓存失效时删除缓存
配置$callback
为1,则返回模板渲染的数据而不是直接输出,配置为闭包可以获取渲染结果,手动处理后续逻辑
request::verify(array $rule, array|bool $post = true, bool|callable $callback = false)
$post
为要验证的数据,若非数组,则true代表$_POST,false代表$_REQUEST
$rule
例:
$r =
[
'q' => ['maxlength=50' => 'q最大50字符'],
'type' => ['set=video' => 'type不合法'],
'order' => ['set=viewCount' => 'order不合法'],
'channelId' => ['/^[\w\-]{20,40}$/' => 'channelId为20-40位字母'],
'pageToken' => ['/^[\w\-]{4,14}$/' => 'pageToken为4-14位字母'],
'relatedToVideoId' => ['/^[\w\-]{4,14}$/' => 'relatedToVideoId为4-14位字母'],
'maxResults' => ['number=1,50' => 'maxResults不合法'],
'regionCode' => ['set=HK,TW,US,KR,JP' => 'regionCode不合法'],
'part' => 'id,snippet',
];
注意 maxResults
的配置项为一个数组,元素可为闭包和其他规则,
如果直接写一个闭包而不是数组,代表使用闭包的返回值,而不是对输入值校验.
內建的验证类型有
类型限定
'array', 'bool', 'float', 'int', 'null', 'numeric', 'object', 'scalar', 'string'
ctype类型
'alnum', 'alpha', 'cntrl', 'digit', 'graph', 'lower', 'print', 'punct', 'space', 'upper', 'xdigit'
其他 require
required
default
number
json
url
ip
email
username
password
phone
动态比较的类型有 minlength
maxlength
length
eq
eqs
set
int
number
numeric
required 数字0,字符串0,空数组,空字符串等被认为校验不通过,其他true值,被认为通过校验
require 在required的基础上允许数字0和字符串0校验通过 注意:空数组,空字符串,被认为校验不通过, 与
required
的区别在于数字0和字符串0,required
更加严格
default 如果值不存在则使用此默认值并忽略规则校验,如果有值,则规则将会生效
int,float等为强类型规则, number可以既接受int型也接受字符串格式的整数
numeric可以既接受float型也接受字符串格式的小数,也可以既接受int型也接受字符串格式的整数
int,number,numeric,length 可以使用区间限定,使用
,
连接两个区间,int=1,50
表示int型1和50之间,length=10,20
表示string类型长度在10至20
无
,
时,表示需大于等于此起始值,number=1
表示int或字符串整数,值大于等于1,即除0外的正整数
minlength=8
表示string长度最小8
set 规则只能针对值是string类型的值做判断,值为int类型的,一律判断不通过
直接使用数组语法来判断更加复杂的是否在集合中, 此比较方法是强类型的比较
eqs为不区分大小写,eq为区分大小写
键可扩展为一种静态方法模式校验规则,例 r::domain 表示使用r类中的静态方法domain来校验,实现复杂校验
定义callback
值,用于检验不通过时的动作
false 抛出异常,此为默认
闭包函数, 异常将传递给此闭包函数处理
true 立即响应json并中断
某个Model要链接其他数据库,只需要重写ready方法即可,然后返回新的PDO实例
public static function ready(): PDO
{
static $_pdo;
$_pdo ??= self::init(app::get('dbdev'));
return $_pdo;
}
无过度封装,简单直接,轻松完成大部分数据库操作.
PDO参数
$options = [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_TIMEOUT => 3, PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_STRINGIFY_FETCHES => false];
ATTR_EMULATE_PREPARES
若是开启的话,数据库中的int
和float
取回来时,在PHP中被转化为string
,造成类型不一致,故需要
ATTR_EMULATE_PREPARES => false
ATTR_STRINGIFY_FETCHES => false
ATTR_EMULATE_PREPARES
配置为false
时,预编译中的同名命名参数(:name
这样的形式)只能使用一次.
见https://www.php.net/manual/en/pdo.prepare.php
命名参数与?
占位符也不可混用
此框架都已自动处理.
db::insert(array $data,string $table='',bool $ignore=false,bool $replace=false)
db::replace(array $data,string $table='')
replace
也是通过insert
方法,只是参数不同.
$ignore
设置为true
可以使用INSERT IGNORE
模式
insert
方法没有完成ON DUPLICATE KEY UPDATE
,若想使用,见下面说明
insert
方法没有完成INSERT DELAYED INTO
,若想使用,见下面说明
对于批量插入,参见下面说明
db::delete(array $where=[],string $table='')
将$where
设置为空数组即可删除全表数据
详细的$where
使用见WHERE 构造器
db::find(array $where=[],string $table='',string $col='*',array $orderLimit=[],$fetch='fetchAll')
db::findOne(array $where=[],string $table='',string $col='*',array $orderLimit=[1],$fetch='fetch')
db::findVar(array $where=[],string $table='',string $col='COUNT(1)',array $orderLimit=[1])
db::findPage(array $where=[],string $table='',string $col='*',int $page=1,int $limit=20,array $order=[])
findOne
,findVar
,findPage
均是借助于find
方法,只不过传递参数不同.
findOne
默认LIMIT 1
,只返回一行数据
findVar
在findOne
的基础上仅返回一个字段,并且默认是COUNT(1)
即计算行数,可以修改参数三返回希望的字段.
findPage
为分页,返回指定页的数据和上一页,下一页等,可以添加排序规则,详细见ORDERLIMIT 构造器
详细的$where
使用见WHERE 构造器
如果你的一条 SQL 要查询大量数据,结果集往往超过几十万条,一次读取结果集会使得内存溢出,脚本终止.
其实find
的第五个参数可以帮助你.
该参数为获取结果集的方法,find
方法默认是一次性全部获取为数组,你可以传入参数true
交由自己主动获取.
使用参数true
后将返回一个PDOStatement
,你将可以自由进行后续操作.
$stm=User::find(['id >'=>1],'userTable','*',['id'=>'ASC'],true);
while($row=$stm->fetch())
{
}
注意: pdo->exec() 返回的是一个int值,代表被影响的行数, 例如runSql()函数
stm->execute() 返回的是一个布尔值,代表是否操作成功, 使用 stm->rowCount() 才能取到执行DELETE、 INSERT、或 UPDATE 语句受影响的行数。
你可以修改方法fetch
为fetchObject
他们二者的不同是以数组还是对象的方式返回.
即使循环获取,数据也是从 MySQL 服务器发送到了 PHP 进程中保存,若数据实在太大,可以设置数据任然保存在 MySQL 服务器,循环的过程中现场取
在查询之前,给 PDO 实例设置
self::setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY,false);
然后再循环获取,内存使用会显著下降
因 PDO 使用长连接,该设置会影响一定时段内的所有 SQL 查询,你也可以查询完设置回
true
避免影响其他查询 自 PHP5.5 起,可以使用 yield,大数据量下可以显著帮你节省内存
使用原始值可以实现子查询
$where=['!id IN'=>'(SELECT `id` FROM `user` WHERE fid=1)','age >'=>18];
db::update(array $where,array $data,string $table='')
$where
的具体形式见WHERE 构造器
$data
的具体形式见SET 构造器
db::condition(array &$where,string $prefix='WHERE')
在查询和删除,更新等场景下,传入一个数组作为条件
$where
是一个数组变量,一般为一维数组,某些需要使用IN
操作时为二维数组
$where
为一个引用,执行过后会清理$where
中的数据,因此必须传入一个变量名,执行后的$where
变量将有后续用途
例如 $where=['username'=>'name1','age'=>18];
这样会筛选username
为name1
并且age
为 18 的用户
默认的结合条件是
AND
,你可以在数组的最后面添加一个指定的结合符
改用
OR
连接$where=['username'=>'name1','age'=>18,'OR'];
同时对于键(例如username
)还可以添加一些修饰符和操作符.
键可以由三部分组成,以空格隔开
第一段对应于数据库中的字段,第二段是修饰符,往往是NOT
或空,第三段是操作符,可以是>
, <
, >=
, <=
, !=
, <>
, IN
, LIKE
, REGEXP
例如$where=['age >'=>18,'name LIKE'=>'user%'];
同理,也可以使用NOT LIKE
如果你需要使用IN
操作符,也是可以的,给它的键值为一个数组
$where=['id'=>[1,3,5,7]];
数组作为参数默认就置为IN
操作符
将会等到 where id in (1,3,5,7)
,如果要使用NOT IN
需要显式指明
$where=['id NOT IN'=>[1,3,5,7]]
使用字段的引用和内置函数
$where
数组中的键值都会进行预处理操作,因此不能使用字段的引用和内置函数.
若要使用,可以在键的第一段,即数据库字段前加!
定义符,代表要使用原始值.
$where=['!time < '=>'UNIX_TIMESTAMP()']
使用!
定义符后对应的键值须为定值,对于用户发送来的数据,使用!
定义符前需要仔细过滤,仅能信任使用intval
过滤后的值.
!time
或者 ! time
! time <
!time <
等都是合法的,
但!time<
! time<
是非法的,字段和操作符之间必须使用空格隔开
NULL处理
当条件的值为null
时,自动构造为IS NULL
语句.
例如 ['name'=>null]
转化为
`name` IS NULL
如果需要IS NOT NULL
需要显示申明
['name IS NOT'=>null]
此时,使用原始值前缀!
对此构造无任何影响
['!name'=>null]
['!name IS NOT'=>null]
都是合法的.
EXISTS 与 FIND_IN_SET
封装一个函数使用原始值模式对where
规则添加额外的条件
final public static function extra(array $cond, array $where = [], string $filed = null): array
{
$j = 'AND';
foreach ($cond as $k => &$v) {
if (is_array($v)) {
$v = sprintf('%s IN (%s)', (str_contains($k, '.') || str_contains($k, '`')) ? $k : "`{$k}`", implode(',', $v));
} else if (is_null($v)) {
$v = sprintf('%s IS NULL', (str_contains($k, '.') || str_contains($k, '`')) ? $k : "`{$k}`");
} else if (is_string($k)) {
$v = sprintf("FIND_IN_SET('%s',%s)", trim($v, " \n\r\t\v\0\"'"), (str_contains($k, '.') || str_contains($k, '`')) ? $k : "`{$k}`");
if (ctype_alnum(str_replace('_', '', $k))) {
$filed ??= $k;
}
} else if (is_bool($v) || is_int($v) || is_float($v)) {
$j = $v ? 'AND' : 'OR';
unset($cond[$k]);
}
}
if ($cond) {
$where["! $filed IS NOT NULL AND"] = '(' . implode(" $j ", $cond) . ')';
}
return $where;
}
对
$cond
添加键值对,键代表字段,值代表此字段对应FIND_IN_SET
需要包含的值当没有键(键是数字),此时值为普通SQL语句,可以为
EXISTS
或其他子查询等如果
$cond
没有合适的字段key,则需要指定$filed
的值
self::find(self::extra(['type' => 3], ['id >' => 1]), 'messages')
构造出
SELECT * FROM `messages` WHERE (`id` > :id_1 AND `type` IS NOT NULL AND (FIND_IN_SET('3',`type`)))
如果你需要FIND_IN_SET
使用常量参数,或参数一为字段,需要用拼接模式
self::find(self::extra(["FIND_IN_SET(`id`,'1,2,3')"], ['id >' => 1], 'id'), 'messages');
此时FIND_IN_SET就相当于查询ID是IN(1,2,3),构造出
SELECT * FROM `messages` WHERE (`id` > :id_1 AND `id` IS NOT NULL AND (FIND_IN_SET(`id`,'1,2,3')))
性能比较:当子查询的表比较大时,使用EXISTS
可能比使用IN
性能更好,
IN
首先完成内层查询,然后在外层查询的过程中一一过滤;EXISTS
需要先完成外层查询,然后对所有记录一一执行EXISTS
子句进行过滤
self::find(self::extra(["EXISTS(SELECT 1 FROM `contacts` WHERE contacts.`id`=messages.`cid` AND `name` <> 'group1' )"], ['id >' => 1], 'id'), 'messages');
self::find(self::extra(["messages.`cid` IN (SELECT `id` FROM `contacts` WHERE `name` <> 'group1')"], ['id >' => 1], 'id'), 'messages')
SELECT * FROM `messages` WHERE (`id` > :id_1 AND `id` IS NOT NULL AND (EXISTS(SELECT 1 FROM `contacts` WHERE contacts.`id`=messages.`cid` AND `name` <> 'group1' )))
SELECT * FROM `messages` WHERE (`id` > :id_1 AND `id` IS NOT NULL AND (messages.`cid` IN (SELECT `id` FROM `contacts` WHERE `name` <> 'group1')))
NOT EXISTS
配合INSERT INTO ... SELECT ...
还可以实现满足条件时插入
HAVING
简单的HAVING
语句可以同上,修改condition后的参数,包含GROUP BY
等复杂语句需要自己拼接
final public static function findHaving(array $where = [], string $table = '', string $col = '*', array $orderLimit = [], $fetch = 'fetchAll')
{
$sql = sprintf('SELECT %s FROM %s%s%s', $col, static::table($table), self::condition($where, "HAVING"), $orderLimit ? self::orderLimit($orderLimit) : '');
return self::exec($sql, $where, $fetch);
}
db::values(array &$data,bool $set=false,string $table='')
$data
使用关联数组表示,默认生成VALUES()
语句用于INSERT
,将$set
设置为true
生成用于update
的语句
['name'=>'name1','pass'=>123]
数组的键也有一个前置定义符!
,表示原始值,使用此定义符可以调用函数,引用字段等,插入原始值等.
如 ['v'=>time(),'!t'=>'UNIX_TIMESTAMP()']
添加了!则存储的是时间戳,不加!则是存储此字符串
['!count'=>'count+1']
使count
的值加一
['!count'=>'count+age']
引用其他字段,count
设置为count+age
的和
除非你要调用函数或引用字段,否则不建议你使用原始值,
原始值没有引号包裹,也不是预处理字段,随意使用将会带来安全隐患.
注意
同一个变量不可传入values()
或condition()
两次,因为这些方法会修改传入值,第二次执行时,用到的值已经被第一次修改了
可以使用$update=$insert
,两个变量各自修改不会影响另一个
db::orderLimit(array $orderLimit)
$orderLimit
使用关联数组,键为数据库字段,键值为排序规则,ASC
或DESC
,也可以使用布尔值代替,true
为ASC
,false
为DESC
如$orderLimit=['id'=>'DESC','name'=>'ASC']
还可以使用LIMIT
,添加一个整形的键值对
$orderLimit=['id'=>'DESC','name'=>true,35=>20]
代表LIMIT 35,20
直接使用$orderLimit=['id'=>'DESC','name'=>'ASC',5]
代表LIMIT 0,5
如果要使用ORDER BY RAND()
可使用$orderLimit=[''=>'RAND()']
构造
db::insertOrUpdate(string $table, array $insert, array $update): bool
实现了插入或者更新
自己拼接的话
$sql = sprintf('INSERT INTO %s ON DUPLICATE KEY UPDATE id=:id,name=:name', self::values($insert, false, static::table));
return self::exec($sql, $insert + $update);
需要$update=['id'=>1,'name'=>2];
与自己写的SQL对应
DELAYED 仅适用于 MyISAM, MEMORY 和 ARCHIVE 表
可采用如下方式构造
$sql=sprintf('REPLACE DELAYED INTO %s',self::values($data,false,static::table));
$sql=sprintf('INSERT DELAYED INTO %s',self::values($data,false,static::table));
$sql=sprintf('INSERT DELAYED IGNORE INTO %s',self::values($data,false,static::table));
CASE WHEN
可以实现单条SQL语句将多个记录更新为不同的值
CASE WHEN
可以实现对查询的数据对值分组转换等
可以使用prepare
绑定数据循环.
如果数据表是InnoDB
而不是MyISAM
,还可以开启事务,进一步提升速度.
因为InnoDB
默认是auto-commit mode
,每条 SQL 都会当做一个事务自动提交,会带来额外开销.
INSERT INTO
也可以使用IGNORE
,REPLACE
,ON DUPLICATE KEY UPDATE
数据源
$column = ['title', 'text', 'ids', 'enable'];
$data = [
['title1', 'text1', 'a,b', 1],
['title2', 'text2', 'a,b', 1],
['title3', 'text3', 'b,c', 0],
['title4', 'text4', 'b,c', 0]
];
db::insertMany(string $table, array $column, array $data): bool
实现了事务批量插入
使用单条 SQL 代替循环插入速度将会更快
数据源和参数同上
db::insertOnceMany(string $table, array $column, array $data, array $duplicateKeyUpdate = []): int
实现了单条SQL批量插入,并且可以指定重复键的更新策略
批量插入中使用ON DUPLICATE KEY UPDATE
仅需配置第四个参数 , $duplicateKeyUpdate
格式同$column
,需要是其子集
如果数据量巨大,可能造成SQL语句太长,可以使用array_chunk
切割$data
分批调用
对WHERE 构造器传入二维数组,可以构造嵌套的AND
或OR
$where1=['age >'=>18,'sex'=>1];
$where2=['id >'=>20,'id <'=>40];
db::find([$where1, $where2, 'OR'], 'users');
构造出如下SQL
SELECT * FROM users WHERE ((`age` > :age_1 AND `sex` = :sex_2) OR (`id` > :id_3 AND `id` < :id_4))
如果你需要非常复杂的 SQL 查询,可能不能一次就利用方法完成,需要多次操作
或者自己进行prepare
并绑定.
使用db::query
可以一次完成多个 SQL 操作,它是db::exec
的批处理.
$sql1="SELECT 1";
$sql2="SELECT 2";
$sql3="SELECT 3";
$data1=$data2=$data3=[];
[$res1,$res2,$res3]=self::query([$sql1,$data1,'fetchAll'],[$sql2,$data2,'fetch'],[$sql3,$data3,true]);
每个参数都是数组
数组内部,第一个元素要批处理的$sql 语句,第二个参数绑定的参数,第三个参数获取方式.
所有的 SQL 执行最终都会指向db::exec($sql,array $params=[],$fetch='')
第三个参数fetch
如果为空,代表非查询语句,返回的是影响的行数(无预处理exec()
)或者是否成功(有预处理stm->execute()
)
如果是个string
,返回结果集(例如fetchAll
,无论是否走了预处理)
如果是true
(仅当是查询语句,或者是预处理操作,才能置为true),返回一个PDOStatement
(无论是否走了预处理),后续按何种方式获取结果集,自己任意操作.
注意: params 为空则代表没有预处理参数,底层会直接调用 pdo->exec() 或 pdo->query() 如果不为空,则会先预处理,然后stm->execute()
pdo->exec() 返回的是一个int值,代表被影响的行数, 例如runSql()函数
stm->execute() 返回的是一个布尔值,代表是否操作成功, 使用 stm->rowCount() 才能取到执行DELETE、 INSERT、或 UPDATE 语句受影响的行数。
如果你关心受影响的行数,在调用
db::exec
时需注意.
insert ignore 和 replace 与MySQL的语法存在差异
insert or replace:如果不存在就插入,存在就更新 insert or ignore:如果不存在就插入,存在就忽略
insert函数可以新增如下对SQLITE的改写版
final public static function sqliteInsert(array $data, string $table = '', bool $replace_or_ignore = null)
{
$sql = sprintf('%s %sINTO %s', $replace_or_ignore ? 'INSERT OR REPLACE' : 'INSERT', $replace_or_ignore === false ? 'OR IGNORE ' : '', self::values($data, false, $table));
return self::exec($sql, $data);
}