commit 17096657dc2afaa5f29de4ee077a0d751c5e71e8 Author: 玄尘 <122383162@qq.com> Date: Thu Aug 6 14:58:51 2020 +0800 first commit diff --git a/.env b/.env new file mode 100644 index 0000000..05a5032 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +app_debug = false +app_trace = false + +[trace] +type = Console diff --git a/.user.ini b/.user.ini new file mode 100644 index 0000000..3ce1465 --- /dev/null +++ b/.user.ini @@ -0,0 +1 @@ +open_basedir=/disk/wwwroot/helper:/tmp/:/proc/ diff --git a/application/common.php b/application/common.php new file mode 100644 index 0000000..70ea85c --- /dev/null +++ b/application/common.php @@ -0,0 +1,86 @@ + +// +---------------------------------------------------------------------- + +// 应用公共文件 +// + +function get_array_url($k = '', $value = '', $emp = '') +{ + $url = \think\Request::instance()->get(); + unset($url['s']); + if ($k) { + if (isset($url[$k])) { + if ($value) { + $url[$k] = $value; + } else { + unset($url[$k]); + } + } else { + if ($value) { + $url[$k] = $value; + } + } + } + if ($emp) { + $array = explode(',', $emp); + if (is_array($array)) { + foreach ($array as $key => $value) { + if (isset($url[$value])) { + unset($url[$value]); + } + } + } + } + return http_build_query($url); +} + +/** + * curl操作函数 + * @param string $url 请求地址 + * @param string $method 提交方式 + * @param array $postFields 提交内容 + * @param array $header 请求头 + * @return mixed 返回数据 + */ +function http($url, $method = "GET", $postFields = null, $headers = null) +{ + $ch = curl_init(); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_FAILONERROR, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); + + if (!is_null($postFields)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields); + } + //https request + if (strlen($url) > 5 && strtolower(substr($url, 0, 5)) == "https") { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + } + if (is_array($headers) && 0 < count($headers)) { + $httpHeaders = []; + foreach ($headers as $key => $value) { + array_push($httpHeaders, $key . ":" . $value); + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders); + } + $data = curl_exec($ch); + $err = curl_errno($ch); + curl_close($ch); + if ($err > 0) { + return curl_error($ch); + } else { + return $data; + } +} diff --git a/application/common/model/Advert.php b/application/common/model/Advert.php new file mode 100644 index 0000000..ea35467 --- /dev/null +++ b/application/common/model/Advert.php @@ -0,0 +1,34 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Advert extends _Init +{ + /** + * 模型初始化【事件注册】 + */ + protected static function init() + { + self::beforeUpdate(function ($data) { + $data->update_time = time(); + }); + + self::beforeInsert(function ($data) { + $data->create_time = time(); + $data->update_time = 0; + $data->status = 1; + }); + } + + public function detail() + { + return $this->hasMany('AdvertDetail')->where('status', '>=', 0); + } + +} diff --git a/application/common/model/AdvertDetail.php b/application/common/model/AdvertDetail.php new file mode 100644 index 0000000..b7da139 --- /dev/null +++ b/application/common/model/AdvertDetail.php @@ -0,0 +1,34 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class AdvertDetail extends _Init +{ + /** + * 模型初始化【事件注册】 + */ + protected static function init() + { + self::beforeUpdate(function ($data) { + $data->update_time = time(); + }); + + self::beforeInsert(function ($data) { + $data->create_time = time(); + $data->update_time = 0; + $data->click = 0; + $data->status = 1; + }); + } + + public function getCoverAttr($value, $data) + { + return Storage::where('id', $data['storage_id'])->value('path') ?: ''; + } +} diff --git a/application/common/model/Article.php b/application/common/model/Article.php new file mode 100644 index 0000000..f075082 --- /dev/null +++ b/application/common/model/Article.php @@ -0,0 +1,58 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Article extends _Init +{ + /** + * 模型初始化【事件注册】 + */ + protected static function init() + { + self::beforeInsert(function ($data) { + $data->status = 1; + }); + } + + /** + * 获取封面地址 + * @param [type] $value 封面id + * @param [type] $data 文章数据 + * @return [type] 图片地址 + */ + public function getCoverAttr($value, $data) + { + return Storage::where('id', $data['storage_id'])->find(); + } + + /** + * 获取封面地址 + * @param [type] $value 封面id + * @param [type] $data 文章数据 + * @return [type] 图片地址 + */ + public function getCreateTimeAttr($value, $data) + { + return date('Y-m-d', $value); + } + + /** + * 获取关联的分类 + * @return [type] [description] + */ + public function category() + { + return $this->hasOne('Category', 'id', 'category_id'); + } + + public function user() + { + return $this->belongsTo('MemberInfo', 'uid', 'uid'); + } +} diff --git a/application/common/model/ArticleModel.php b/application/common/model/ArticleModel.php new file mode 100644 index 0000000..82bd66c --- /dev/null +++ b/application/common/model/ArticleModel.php @@ -0,0 +1,20 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class ArticleModel extends _Init +{ + protected static function init() + { + self::beforeInsert(function ($data) { + $data->status = 1; + $data->sort = $data['sort'] ?: 0; + }); + } +} diff --git a/application/common/model/Auth.php b/application/common/model/Auth.php new file mode 100644 index 0000000..4896a18 --- /dev/null +++ b/application/common/model/Auth.php @@ -0,0 +1,20 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Auth extends _Init +{ + /** + * 获取用户数量 + */ + protected function getUserCountAttr() + { + return $this->hasMany('AuthUser')->count(); + } +} diff --git a/application/common/model/AuthUser.php b/application/common/model/AuthUser.php new file mode 100644 index 0000000..11f8818 --- /dev/null +++ b/application/common/model/AuthUser.php @@ -0,0 +1,35 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class AuthUser extends _Init +{ + /** + * 关闭更新时间写入 + */ + protected $updateTime = false; + + /** + * 关联用户模型 + * @return [type] [description] + */ + public function user() + { + return $this->hasOne('Member', 'id', 'uid'); + } + + /** + * 关联分组 + * @return [type] [description] + */ + public function auth() + { + return $this->belongsTo('Auth'); + } +} diff --git a/application/common/model/Category.php b/application/common/model/Category.php new file mode 100644 index 0000000..ba91d87 --- /dev/null +++ b/application/common/model/Category.php @@ -0,0 +1,33 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +use think\Config; + +class Category extends _Init +{ + + /** + * 获取封面地址 + * @param [type] $value 封面id + * @param [type] $data 文章数据 + * @return [type] 图片地址 + */ + public function getCoverAttr($value, $data) + { + return Storage::where('id', $data['storage_id'])->value('path'); + } + public function getModelAttr($value, $data) + { + $list = config::get('category_model'); + $name = $list[$data['model']]; + return $name ?? '没有'; + } + +} diff --git a/application/common/model/City.php b/application/common/model/City.php new file mode 100644 index 0000000..00734c9 --- /dev/null +++ b/application/common/model/City.php @@ -0,0 +1,13 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class City extends _Init +{ +} diff --git a/application/common/model/Config.php b/application/common/model/Config.php new file mode 100644 index 0000000..16131fa --- /dev/null +++ b/application/common/model/Config.php @@ -0,0 +1,96 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +use think\Cache; + +class Config extends _Init +{ + + /** + * 模型初始化【事件注册】 + */ + protected static function init() + { + self::afterWrite(function () { + Cache::clear(); + }); + } + + /** + * 获取器 + */ + protected function getTypeTextAttr($value, $data) + { + return Config::getValue($data['type'], 'config_type_list'); + } + protected function getGroupTextAttr($value, $data) + { + return Config::getValue($data['group'], 'config_group_list'); + } + + /** + * 配置列表,优化输出 + */ + protected function getValueTextAttr($value, $data) + { + return preg_replace('/\r\n/', "
", $data['value']); + } + + /** + * 格式化枚举类型的配置 + */ + protected function getExtraArrayAttr($value, $data) + { + $array = preg_split('/[\r\n]+/', trim($data['extra'], "\r\n")); + $enum = []; + if (strpos($data['extra'], ':')) { + foreach ($array as $val) { + list($k, $v) = explode(':', $val, 2); + $enum[$k] = $v; + } + } else { + $enum = $array; + } + return $enum; + } + + /** + * 获取配置内容 + * @param string $key + * @param string $type + * @return string + */ + public static function getValue($key, $type = null) + { + if ($key == -1) { + return '已删除'; + } + if (!is_null($type) && is_string($type)) { + $res = \think\Config::get($type); + $res = isset($res[$key]) ? $res[$key] : ''; + } elseif (!is_null($type) && is_array($type)) { + $res = isset($type[$key]) ? $type[$key] : ''; + } else { + switch ($key) { + case 0:$res = '禁用'; + break; + case 1:$res = '正常'; + break; + case 2:$res = '待审核'; + break; + case 3:$res = '被驳回'; + break; + default:$res = ''; + break; + } + } + return $res; + } +} diff --git a/application/common/model/Content.php b/application/common/model/Content.php new file mode 100644 index 0000000..32cd042 --- /dev/null +++ b/application/common/model/Content.php @@ -0,0 +1,42 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Content extends _Init +{ + /** + * 模型初始化【事件注册】 + */ + protected static function init() + { + self::beforeInsert(function ($data) { + $data->status = 1; + }); + } + /** + * 获取封面地址 + * @param [type] $value 封面id + * @param [type] $data 文章数据 + * @return [type] 图片地址 + */ + public function getCoverAttr($value, $data) + { + return Storage::where('id', $data['storage_id'])->value('path'); + } + + /** + * 获取关联的分类 + * @return [type] [description] + */ + public function category() + { + return $this->hasOne('Category', 'id', 'category_id'); + } + +} diff --git a/application/common/model/Direct.php b/application/common/model/Direct.php new file mode 100644 index 0000000..371293b --- /dev/null +++ b/application/common/model/Direct.php @@ -0,0 +1,44 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Direct extends _Init +{ + /** + * 模型初始化【事件注册】 + */ + protected static function init() + { + self::beforeInsert(function ($data) { + $data->status = 1; + }); + } + + /** + * 获取封面地址 + * @param [type] $value 封面id + * @param [type] $data 文章数据 + * @return [type] 图片地址 + */ + public function getCoverAttr($value, $data) + { + return Storage::where('id', $data['storage_id'])->value('path'); + } + + /** + * 获取音/视频地址 + * @param [type] $value 封面id + * @param [type] $data 文章数据 + * @return [type] 图片地址 + */ + public function getExtPathAttr($value, $data) + { + return Storage::where('id', $data['ext_id'])->value('path'); + } +} diff --git a/application/common/model/Experience.php b/application/common/model/Experience.php new file mode 100644 index 0000000..d13d150 --- /dev/null +++ b/application/common/model/Experience.php @@ -0,0 +1,38 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Experience extends _Init +{ + /** + * 模型初始化【事件注册】 + */ + protected static function init() + { + self::beforeInsert(function ($data) { + $data->status = 1; + }); + } + + public function user() + { + return $this->belongsTo('MemberInfo', 'uid', 'uid'); + } + + public function comment() + { + return $this->hasMany('Experience', 'pid', 'id')->order('id asc'); + } + + public function getCountAttr($value, $data) + { + $res = ExperienceTags::where(['experience_id' => $data['id'], 'uid' => UID])->count(); + return $res; + } +} diff --git a/application/common/model/ExperienceTags.php b/application/common/model/ExperienceTags.php new file mode 100644 index 0000000..bc1a846 --- /dev/null +++ b/application/common/model/ExperienceTags.php @@ -0,0 +1,15 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class ExperienceTags extends _Init +{ + protected $updateTime = false; + +} diff --git a/application/common/model/Help.php b/application/common/model/Help.php new file mode 100644 index 0000000..693d6de --- /dev/null +++ b/application/common/model/Help.php @@ -0,0 +1,36 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +use think\Config; + +class Help extends _Init +{ + /** + * 模型初始化【事件注册】 + */ + protected static function init() + { + self::beforeInsert(function ($data) { + $data->status = 1; + }); + } + + public function getCategoryAttr($value, $data) + { + $list = Config::get('help_type'); + return empty($list[$data['category']]) ? '没这个分类' : $list[$data['category']]; + } + + public function getCreateTimeAttr($value, $data) + { + return date('Y-m-d', $value); + } + +} diff --git a/application/common/model/Logs.php b/application/common/model/Logs.php new file mode 100644 index 0000000..a7121a7 --- /dev/null +++ b/application/common/model/Logs.php @@ -0,0 +1,59 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Logs extends _Init +{ + /** + * 关闭更新时间写入 + */ + protected $updateTime = false; + + /** + * 转换datas数据 + * @param [type] $value 数据内容 + */ + protected function setDatasAttr($value) + { + return !empty($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : ''; + } + + /** + * 获取昵称 + */ + protected function getUidTextAttr($value, $data) + { + return MemberInfo::where('uid', $data['uid'])->value('nickname'); + } + + /** + * 格式化datas数据 + * @param [type] $value datas数据 + * @param [type] $data 数据集合 + */ + protected function getDatasTextAttr($value, $data) + { + $content = $data['datas']; + + if (!empty($content)) { + $content = json_decode($content); + $string = ''; + if (is_object($content) && !empty($content)) { + foreach ($content as $key => $value) { + $string .= $key . ' : ' . json_encode($value, JSON_UNESCAPED_UNICODE) . '
'; + } + } else { + $string = $content; + } + return $string; + } else { + return ''; + } + } +} diff --git a/application/common/model/Member.php b/application/common/model/Member.php new file mode 100644 index 0000000..adea410 --- /dev/null +++ b/application/common/model/Member.php @@ -0,0 +1,68 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +use think\Config as Conf; +use think\Request; +use tools\Crypt; + +class Member extends _Init +{ + protected $readonly = ['']; + + /** + * 模型初始化【事件注册】 + */ + protected static function init() + { + self::beforeInsert(function ($data) { + $data->reg_ip = Request::instance()->ip(); + }); + } + + /** + * 格式化 最后登录时间 + * @param [type] $value 最后登录时间 时间戳 + */ + protected function getLastTimeAttr($value) + { + return date(Conf::get('database.datetime_format'), $value); + } + + /** + * 加密密码 + * @param [type] $value [description] + */ + protected function setPasswordAttr($value) + { + return Crypt::uMd5($value); + } + + /** + * 用户资料 + */ + protected function info() + { + return $this->hasOne('MemberInfo', 'uid', 'id'); + } + + /** + * 关联 AuthUser 表 + * @return [type] [description] + */ + public function authUser() + { + return $this->hasOne('AuthUser', 'uid', 'id'); + } + + public function getJuniorAttr($value, $data) + { + return MemberList::where('uid', $data['id'])->count(); + } +} diff --git a/application/common/model/MemberInfo.php b/application/common/model/MemberInfo.php new file mode 100644 index 0000000..f148a6a --- /dev/null +++ b/application/common/model/MemberInfo.php @@ -0,0 +1,96 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class MemberInfo extends _Init +{ + protected $createTime = false; + protected $readonly = ['image']; + /** + * 获取头像地址 + * @param [type] $value [description] + * @param [type] $data [description] + * @return [type] [description] + */ + protected function getAvatarAttr($value, $data) + { + $avatar = $data['headimgurl']; + if (!empty($avatar) && strpos($avatar, 'wx.qlogo.cn')) { + return rtrim($avatar, '0') . '132'; + } elseif (empty($avatar)) { + return '/static/system/images/avatar.jpg'; + } else { + return $avatar; + } + } + + /** + * 获取头像地址 + * @param [type] $value [description] + * @param [type] $data [description] + * @return [type] [description] + */ + protected function getQrcodeAttr($value, $data) + { + $path = Storage::where('id', $data['qrcode'])->value('path'); + return $path ?? ''; + } + + /** + * 判断用户是否为 未过期的VIP会员 + * @param [type] $value [description] + * @param [type] $data [description] + * @return [type] [description] + */ + protected function getIsVipAttr($value, $data) + { + if ($data['vip_time'] > time() && $data["is_vip"] == 1) { + return 1; + } else { + return 0; + } + } + + protected function getVipTextAttr($value, $data) + { + if ($data['vip_time'] > time() && $data["is_vip"] == 1) { + return 'VIP会员'; + } else { + return '普通会员'; + } + } + + protected function getVipTimeAttr($value, $data) + { + if ($value < time()) { + return '永久'; + } else { + return date('Y-m-d', $value); + } + } + + protected function getVipEndTimeAttr($value, $data) + { + return $data['vip_time']; + } + + protected function getIndexTplAttr($value, $data) + { + if ($this->is_vip) { + return $value; + } else { + return 1; + } + } + + protected function getMessageCountAttr($value, $data) + { + return Message::where('to_uid', $data['uid'])->count(); + } +} diff --git a/application/common/model/MemberList.php b/application/common/model/MemberList.php new file mode 100644 index 0000000..358488c --- /dev/null +++ b/application/common/model/MemberList.php @@ -0,0 +1,14 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class MemberList extends _Init +{ + +} diff --git a/application/common/model/Menu.php b/application/common/model/Menu.php new file mode 100644 index 0000000..5d3ca02 --- /dev/null +++ b/application/common/model/Menu.php @@ -0,0 +1,34 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Menu extends _Init +{ + protected function getUrlTextAttr($value, $data) + { + if ($data['url']) { + return url($data['url']); + } else { + return ''; + } + } + + /** + * 修改器 + */ + protected function setAuthAttr($value, $data) + { + return parent::parseStatus($value); + } + + protected function setHideAttr($value, $data) + { + return parent::parseStatus($value); + } +} diff --git a/application/common/model/Message.php b/application/common/model/Message.php new file mode 100644 index 0000000..9dbc05f --- /dev/null +++ b/application/common/model/Message.php @@ -0,0 +1,22 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +/** + *留言 + */ +class Message extends _Init +{ + protected $updateTime = false; + + public function user() + { + return $this->belongsTo('MemberInfo', 'from_uid', 'uid'); + } +} diff --git a/application/common/model/Province.php b/application/common/model/Province.php new file mode 100644 index 0000000..5727667 --- /dev/null +++ b/application/common/model/Province.php @@ -0,0 +1,21 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Province extends _Init +{ + /** + * 模型初始化【事件注册】 + */ + protected static function init() + { + self::beforeInsert(function ($data) { + }); + } +} diff --git a/application/common/model/Score.php b/application/common/model/Score.php new file mode 100644 index 0000000..d8dc9db --- /dev/null +++ b/application/common/model/Score.php @@ -0,0 +1,39 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Score extends _Init +{ + /** + * 关闭更新时间写入 + */ + protected $updateTime = false; + + public function user() + { + return $this->belongsTo('MemberInfo', 'uid', 'uid'); + } + + public function getScoreAttr($value, $data) + { + return floatval($value); + } + + public function getRulesAttr($value, $data) + { + $map = ['model' => $data['rule_id']]; + return ScoreRules::where($map)->find(); + } + + public function getAddtimeAttr($value, $data) + { + return date('Y-m-d', $data['create_time']); + } + +} diff --git a/application/common/model/ScoreRules.php b/application/common/model/ScoreRules.php new file mode 100644 index 0000000..56d8c4f --- /dev/null +++ b/application/common/model/ScoreRules.php @@ -0,0 +1,28 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class ScoreRules extends _Init +{ + /** + * 模型初始化【事件注册】 + */ + protected static function init() + { + self::beforeInsert(function ($data) { + $data->status = 1; + }); + } + + public function getCreateTimeAttr($value, $data) + { + return date('Y-m-d', $value); + } + +} diff --git a/application/common/model/Sms.php b/application/common/model/Sms.php new file mode 100644 index 0000000..82740db --- /dev/null +++ b/application/common/model/Sms.php @@ -0,0 +1,27 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Sms extends _Init +{ + + public function getStatusAttr($value) + { + switch ($value) { + case '0': + return '未使用'; + break; + case '1': + return '已使用'; + break; + default: + break; + } + } +} diff --git a/application/common/model/Storage.php b/application/common/model/Storage.php new file mode 100644 index 0000000..868415d --- /dev/null +++ b/application/common/model/Storage.php @@ -0,0 +1,17 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Storage extends _Init +{ + /** + * 关闭更新时间写入 + */ + protected $updateTime = false; +} diff --git a/application/common/model/Suggest.php b/application/common/model/Suggest.php new file mode 100644 index 0000000..84d7ff2 --- /dev/null +++ b/application/common/model/Suggest.php @@ -0,0 +1,17 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Suggest extends _Init +{ + public function info() + { + return $this->belongsTo('MemberInfo', 'uid', 'uid'); + } +} diff --git a/application/common/model/Template.php b/application/common/model/Template.php new file mode 100644 index 0000000..dca4a02 --- /dev/null +++ b/application/common/model/Template.php @@ -0,0 +1,14 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +class Template extends _Init +{ + +} diff --git a/application/common/model/VipOrder.php b/application/common/model/VipOrder.php new file mode 100644 index 0000000..fd67e4a --- /dev/null +++ b/application/common/model/VipOrder.php @@ -0,0 +1,53 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +use tools\Str; + +class VipOrder extends _Init +{ + /** + * 模型初始化【事件注册】 + */ + protected static function init() + { + self::beforeInsert(function ($data) { + $data->orderid = Str::orderid('VIP'); + }); + } + + public function getStatusAttr($value) + { + switch ($value) { + case '0': + return '未支付'; + break; + case '20': + return '已支付'; + break; + default: + break; + } + } + public function getModelAttr($value, $data) + { + if ($data['model'] == 'weixin') { + return '微信支付'; + } else if ($data['model'] == 'score') { + return '积分兑换'; + } else { + return '未知'; + } + } + + public function user() + { + return $this->belongsTo('MemberInfo', 'uid', 'uid'); + } +} diff --git a/application/common/model/_Init.php b/application/common/model/_Init.php new file mode 100644 index 0000000..d590cd0 --- /dev/null +++ b/application/common/model/_Init.php @@ -0,0 +1,30 @@ + | +// +------------------------------------------------+ +namespace app\common\model; + +use think\Model; + +class _Init extends Model +{ + protected function setStatusAttr($value) + { + return self::parseStatus($value); + } + + protected function parseStatus($status) + { + if (is_numeric($status)) { + return $status; + } elseif ($status == 'true' || $status == 'on') { + return 1; + } elseif ($status == 'false' || $status == 'off') { + return 0; + } + } +} diff --git a/application/common/service/Advert.php b/application/common/service/Advert.php new file mode 100644 index 0000000..561f97b --- /dev/null +++ b/application/common/service/Advert.php @@ -0,0 +1,78 @@ + | +// +------------------------------------------------+ +namespace app\common\service; + +use app\common\model\Advert as AdvertModel; +use app\common\validate\Advert as AdvertValidate; + +class Advert extends _Init +{ + public static function add($data) + { + $validate = new AdvertValidate(); + + if (!$validate->check($data)) { + return $$validate->getError(); + } + + if (AdvertModel::create($data)) { + return true; + } else { + return '添加失败'; + } + } + + public static function edit($data) + { + $validate = new AdvertValidate(); + + if (!$validate->check($data)) { + return $$validate->getError(); + } + + if (AdvertModel::update($data)) { + return true; + } else { + return '编辑失败'; + } + } + + public static function del($id) + { + $info = AdvertModel::get($id); + + if (!$info) { + return $this->error('数据不存在'); + } elseif ($info->save(['status' => -1])) { + return true; + } else { + return '删除失败'; + } + } + + /** + * [status description] + * @param [type] $id [description] + * @param [type] $status [description] + * @param [type] $type [description] + * @return [type] [description] + */ + public static function status($id, $status, $type) + { + $info = AdvertModel::get($id); + if (!$info) { + return $this->error('数据不存在'); + } elseif ($info->save([$type => $status])) { + Logs::write('修改状态', [$type => $status]); + return true; + } else { + return '设置失败'; + } + } +} diff --git a/application/common/service/AdvertDetail.php b/application/common/service/AdvertDetail.php new file mode 100644 index 0000000..1c82fbc --- /dev/null +++ b/application/common/service/AdvertDetail.php @@ -0,0 +1,78 @@ + | +// +------------------------------------------------+ +namespace app\common\service; + +use app\common\model\AdvertDetail as AdvertDetailModel; +use app\common\validate\AdvertDetail as AdvertDetailValidate; + +class AdvertDetail extends _Init +{ + public static function add($data) + { + $validate = new AdvertDetailValidate(); + + if (!$validate->check($data)) { + return $validate->getError(); + } + + if (AdvertDetailModel::create($data)) { + return true; + } else { + return '添加失败'; + } + } + + public static function edit($data) + { + $validate = new AdvertDetailValidate(); + + if (!$validate->check($data)) { + return $validate->getError(); + } + + if (AdvertDetailModel::update($data)) { + return true; + } else { + return '修改失败'; + } + } + + public static function del($id) + { + $info = AdvertDetailModel::get($id); + + if (!$info) { + return $this->error('数据不存在'); + } elseif ($info->save(['status' => -1])) { + return true; + } else { + return '删除失败'; + } + } + + /** + * [status description] + * @param [type] $id [description] + * @param [type] $status [description] + * @param [type] $type [description] + * @return [type] [description] + */ + public static function status($id, $status, $type) + { + $info = AdvertDetailModel::get($id); + if (!$info) { + return $this->error('数据不存在'); + } elseif ($info->save([$type => $status])) { + Logs::write('修改状态', [$type => $status]); + return true; + } else { + return '设置失败'; + } + } +} diff --git a/application/common/service/Article.php b/application/common/service/Article.php new file mode 100644 index 0000000..c6f2634 --- /dev/null +++ b/application/common/service/Article.php @@ -0,0 +1,265 @@ + | +// +------------------------------------------------+ +namespace app\common\service; + +use app\common\model\Article as ArticleModel; +use app\common\model\Category as CategoryModel; +use app\common\service\Score as ScoreService; +use app\common\validate\Article as ArticleValidate; + +class Article extends _Init +{ + /** + * 添加信息 + * @param [type] $data 文章数据 + */ + public static function create($data) + { + $validate = new ArticleValidate(); + if (!$validate->scene('add')->check($data)) { + return $validate->getError(); + } + + $info = ArticleModel::create($data); + if ($info) { + return true; + } else { + return '创建文章失败'; + } + } + + /** + * 编辑文章 + * @param [type] $data 更新的数据 + * @return [type] 返回 修改的结果 + */ + public static function edit($data) + { + $validate = new ArticleValidate(); + if (!$validate->scene('add')->check($data)) { + return $validate->getError(); + } + $info = ArticleModel::update($data); + if ($info) { + return true; + } else { + return '编辑文章失败'; + } + } + + /** + * 前台用户添加文章 + * @param [type] $data 更新的数据 + * @return [type] 返回 修改的结果 + */ + public static function userAdd($data) + { + $data = [ + 'uid' => $data['uid'] ?? 0, + 'title' => $data['title'] ?? '', + 'content' => $data['content'] ?? '', + 'description' => $data['description'] ?? '', + 'category_id' => $data['category_id'] ?? '0', + 'storage_id' => $data['storage_id'] ?? '0', + 'thumb' => $data['thumb'] ?? '0', + 'status' => $data['status'] ?? '1', + 'url' => $data['url'] ?? '', + 'nickname' => $data['nickname'] ?? '', + 'head_img' => $data['head_img'] ?? '', + 'click' => 0, + ]; + + $validate = new ArticleValidate(); + if (!$validate->scene('add')->check($data)) { + return $validate->getError(); + } + $info = ArticleModel::create($data); + if ($info) { + ScoreService::addArticle($data['uid'], 'add'); + return true; + } else { + return '文章创建失败'; + } + } + + /** + * 修改文章状态 + * @param [type] $id 文章id + * @param [type] $status 状态 + * @return [type] 返回结果 + */ + public static function status($id, $status) + { + $info = ArticleModel::get($id); + if (!$info) { + return '文章不存在'; + } elseif ($info->save(['status' => $status])) { + Logs::write('修改状态', ['status' => $status]); + return true; + } else { + return '修改状态失败'; + } + } + + /** + * 删除文章 + * @param [type] $id 要删除的文章id + * @return [type] 返回 结果 + */ + public static function del($id) + { + $info = ArticleModel::get($id); + if (!$info) { + return '文章不存在'; + } elseif ($info->save(['status' => -1])) { + return true; + } else { + return '删除失败'; + } + + // $model = new ArticleModel(); + // if ($model->destroy($id)) { + // return true; + // } else { + // return '删除失败'; + // } + } + + /** + * 获取分类列表 + * @return [type] [description] + */ + public static function categoryList() + { + $map = [ + 'status' => '1', + 'model' => 'article', + ]; + return CategoryModel::where($map)->select(); + } + + /** + * 采集公众号文章 + * @param [type] $url [description] + * @param integer $uid [description] + * @return [type] [description] + */ + public static function collect($url, $uid = 1) + { + $map = [ + 'url' => $url, + 'uid' => $uid, + ]; + + $userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A403 Safari/8536.25'; + + $info = ArticleModel::where($map)->find(); + if ($info) { + return $info->id; + } else { + $info = new ArticleModel; + $html = file_get_contents($url); + // $html = http($url, 'GET', '', '', $userAgent); + + //获取文章标题 + preg_match_all("/id=\"activity-name\">(.*)<\/h2>/is", $html, $title); + //获取文章内容部分 + preg_match_all("/id=\"js_content\">(.*) + + + diff --git a/application/extra/cache.php b/application/extra/cache.php new file mode 100644 index 0000000..3ee0cfd --- /dev/null +++ b/application/extra/cache.php @@ -0,0 +1,16 @@ + | +// +------------------------------------------------+ + +return [ + 'type' => 'File', + 'path' => CACHE_PATH, + 'prefix' => '', + 'expire' => 0, + 'cache_subdir' => false, +]; diff --git a/application/extra/captcha.php b/application/extra/captcha.php new file mode 100644 index 0000000..c2fb922 --- /dev/null +++ b/application/extra/captcha.php @@ -0,0 +1,14 @@ + | +// +------------------------------------------------+ + +return [ + 'useCurve' => false, + 'length' => 4, + 'reset' => true, +]; diff --git a/application/extra/database.php b/application/extra/database.php new file mode 100644 index 0000000..d46b699 --- /dev/null +++ b/application/extra/database.php @@ -0,0 +1,55 @@ + +// +---------------------------------------------------------------------- + +return [ + // 数据库类型 + 'type' => 'mysql', + // 服务器地址 + 'hostname' => 'localhost', + // 数据库名 + 'database' => 'helper', + // 用户名 + 'username' => 'helper', + // 密码 + 'password' => 'wjyBRncHHY2ls4Cd', + // 端口 + 'hostport' => 3306, + // 连接dsn + 'dsn' => '', + // 数据库连接参数 + // 'params' => [\PDO::ATTR_PERSISTENT => true], + // 数据库编码默认采用utf8 + 'charset' => 'utf8mb4', + // 数据库表前缀 + 'prefix' => '', + // 数据库调试模式 + 'debug' => false, + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 数据集返回类型 + 'resultset_type' => 'array', + // 自动写入时间戳字段 + 'auto_timestamp' => true, + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i', + // 是否需要进行SQL性能分析 + 'sql_explain' => false, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否需要断线重连 + 'break_reconnect' => true, +]; diff --git a/application/extra/paginate.php b/application/extra/paginate.php new file mode 100644 index 0000000..515ef00 --- /dev/null +++ b/application/extra/paginate.php @@ -0,0 +1,16 @@ + | +// +------------------------------------------------+ +use think\Request; + +return [ + 'type' => '\tools\Pager', + 'list_rows' => Request::instance()->get('pageSize', 15), + 'var_page' => 'page', + 'query' => Request::instance()->get(), +]; diff --git a/application/extra/session.php b/application/extra/session.php new file mode 100644 index 0000000..8782773 --- /dev/null +++ b/application/extra/session.php @@ -0,0 +1,20 @@ + | +// +------------------------------------------------+ +// +// +---------------------------------------------------------------------- +// | 会话设置 +// +---------------------------------------------------------------------- +return [ + 'id' => '', + 'name' => 'CJANGO_SID', + 'var_session_id' => 'session_id', + 'prefix' => '', + 'type' => '', + 'auto_start' => true, +]; diff --git a/application/extra/storage.php b/application/extra/storage.php new file mode 100644 index 0000000..338a903 --- /dev/null +++ b/application/extra/storage.php @@ -0,0 +1,14 @@ + | +// +------------------------------------------------+ + +return [ + 'size' => 20 * 1024 * 1024, + 'path' => './uploads/', // 保存路径 + 'replace' => true, // 存在同名是否覆盖 +]; diff --git a/application/extra/wechat.php b/application/extra/wechat.php new file mode 100644 index 0000000..9effb26 --- /dev/null +++ b/application/extra/wechat.php @@ -0,0 +1,17 @@ + | +// +------------------------------------------------+ + +return [ + 'token' => 'A2aj8BtAv', + 'appid' => 'wxa655be02706ddeea', + 'secret' => '06196cb22490c323a5edcbf1d9e67403', + 'AESKey' => 'u7xi2oB8FcFJ4dn2MiP3wFhOaM65WQQUgHIpnbTnEiv', + 'mch_id' => '1486263952', + 'paykey' => 'CA72BE33311570FE25AFD91DE880376D', +]; diff --git a/application/index/config.php b/application/index/config.php new file mode 100644 index 0000000..213fd3e --- /dev/null +++ b/application/index/config.php @@ -0,0 +1,35 @@ + | +// +------------------------------------------------+ + +return [ + // +---------------------------------------------------------------------- + // | 日志设置 + // +---------------------------------------------------------------------- + 'log' => [ + 'type' => 'file', + 'path' => LOG_PATH . 'index/', + 'time_format' => ' c ', + 'file_size' => 2097152, + ], + // +---------------------------------------------------------------------- + // | 模板设置 + // +---------------------------------------------------------------------- + 'template' => [ + 'tpl_cache' => true, + 'strip_space' => false, + 'taglib_pre_load' => '', + 'cache_path' => TEMP_PATH . 'index/', + ], + 'view_replace_str' => [ + '__STATIC__' => '/static/index', + '__JS__' => '/static/index/js', + '__CSS__' => '/static/index/css', + '__IMG__' => '/static/index/images', + ], +]; diff --git a/application/index/controller/Index.php b/application/index/controller/Index.php new file mode 100644 index 0000000..2c8dc7a --- /dev/null +++ b/application/index/controller/Index.php @@ -0,0 +1,20 @@ + | +// +------------------------------------------------+ +namespace app\index\controller; + +use tools\Initialize; + +class Index extends Initialize +{ + public function index() + { + dump(strtoupper(md5(uniqid()))); + return $this->fetch(); + } +} diff --git a/application/index/view/index/index.html b/application/index/view/index/index.html new file mode 100644 index 0000000..98556f8 --- /dev/null +++ b/application/index/view/index/index.html @@ -0,0 +1,11 @@ + + + + + + {$Think.config.web_site_title} + + +

{$Think.config.web_site_title}

+ + diff --git a/application/mobile/common.php b/application/mobile/common.php new file mode 100644 index 0000000..8d6ba8d --- /dev/null +++ b/application/mobile/common.php @@ -0,0 +1,19 @@ + | +// +------------------------------------------------+ +use think\Loader; +//幻灯 +function advert($id) +{ + $info = Loader::model('Advert')->find($id); + if ($info && $info->status == 1) { + return $info->detail()->where('status', 1)->limit($info->limit)->order('sort asc')->select(); + } else { + return null; + } +} diff --git a/application/mobile/config.php b/application/mobile/config.php new file mode 100644 index 0000000..c623b2a --- /dev/null +++ b/application/mobile/config.php @@ -0,0 +1,53 @@ + | +// +------------------------------------------------+ + +return [ + // +---------------------------------------------------------------------- + // | 日志设置 + // +---------------------------------------------------------------------- + 'log' => [ + 'type' => 'file', + 'path' => LOG_PATH . 'mobile/', + 'time_format' => ' c ', + 'file_size' => 2097152, + ], + // +---------------------------------------------------------------------- + // | 模板设置 + // +---------------------------------------------------------------------- + 'template' => [ + 'tpl_cache' => false, + 'strip_space' => false, + 'taglib_pre_load' => '', + 'cache_path' => TEMP_PATH . 'mobile/', + ], + 'view_replace_str' => [ + '__SELF__' => __SELF__, + '__STATIC__' => '/static/mobile', + '__JS__' => '/static/mobile/js', + '__CSS__' => '/static/mobile/css', + '__IMG__' => '/static/mobile/img', + '__EDIT__' => '/static/mobile/edit', + ], + 'upload' => [ + 'mimes' => [], // 允许上传的文件MiMe类型 + 'maxSize' => 0, // 上传的文件大小限制 (0-不做限制) + 'exts' => [], // 允许上传的文件后缀 + 'autoSub' => true, // 自动子目录保存文件 + 'subName' => ['date', 'Y/m/d'], // 子目录创建方式,[0]-函数名,[1]-参数,多个参数使用数组 + 'rootPath' => './uploads/', // 保存根路径 + 'savePath' => '', // 保存路径 + 'saveName' => ['uniqid', ''], // 上传文件命名规则,[0]-函数名,[1]-参数,多个参数使用数组 + 'saveExt' => '', // 文件保存后缀,空则使用原后缀 + 'replace' => false, // 存在同名是否覆盖 + 'hash' => true, // 是否生成hash编码 + 'callback' => true, // 检测文件是否存在回调,如果存在返回文件信息数组 + 'driver' => 'local', // 文件上传驱动 + 'driverConfig' => [], // 上传驱动配置 + ], +]; diff --git a/application/mobile/controller/Ajax.php b/application/mobile/controller/Ajax.php new file mode 100644 index 0000000..a3d68b9 --- /dev/null +++ b/application/mobile/controller/Ajax.php @@ -0,0 +1,28 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\ArticleModel as ArticleModelModel; + +class Ajax extends _Init +{ + public function _initialize() + { + if (!IS_AJAX) { + return $this->error('请不要非法访问'); + } + } + + public function modelstyle() + { + $list = ArticleModelModel::where(['status' => 1])->select(); + $this->assign('list', $list); + return $this->success($this->fetch('ajax/modelstyle')); + } +} diff --git a/application/mobile/controller/Article.php b/application/mobile/controller/Article.php new file mode 100644 index 0000000..f01b4a1 --- /dev/null +++ b/application/mobile/controller/Article.php @@ -0,0 +1,146 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\Article as ArticleModel; +use app\common\model\MemberInfo as MemberInfoModel; +use app\common\service\Article as ArticleService; + +class Article extends _Init +{ + public function _initialize() + { + parent::_initialize(); + $this->nav = 1; + } + + public function index() + { + $map = [ + 'uid' => UID, + 'status' => 1, + ]; + $list = ArticleModel::where($map)->order("id desc")->paginate(10); + $this->assign('list', $list); + if (IS_AJAX) { + if (count($list) > 0) { + return $this->success($this->fetch('article/lists')); + } else { + return $this->error('已经到最后一页'); + } + } + return $this->fetch(); + } + + public function show($id) + { + $info = ArticleModel::find($id); + if (!empty($info)) { + $info->setInc('click'); + } + $this->assign('info', $info); + return $this->fetch(); + } + + public function add() + { + if (IS_POST) { + $data = $this->request->post(); + $data['uid'] = UID; + $res = ArticleService::userAdd($data); + if ($res === true) { + return $this->success('发布成功', url('mobile/article/index')); + } else { + return $this->error('发布失败,' . $res); + } + } else { + return $this->fetch(); + } + } + + public function edit($id = '') + { + if (IS_POST) { + $data = $this->request->post(); + $info = ArticleModel::find($data['id'] ?: 0); + + if ($info) { + unset($data['id']); + if ($info->save($data)) { + return $this->success('发表成功', url('mobile/article/index')); + } else { + return $this->error('发表失败'); + } + } else { + return $this->error('您修改的文章已不存在'); + } + } else { + $info = ArticleModel::find($id); + if ($info->uid != UID) { + $this->redirect(url('center/index')); + } + $this->assign('info', $info); + return $this->fetch('add'); + } + } + + public function collect() + { + if (IS_POST) { + $info = MemberInfoModel::get(UID); + if (!$info->is_vip) { + return $this->error('采集失败,您还不是VIP。', url('vip/index')); + } + $url = $this->request->post('url') ?: ''; + if ($url) { + if (!preg_match('/(http:\/\/)|(https:\/\/)/i', $url)) { + return $this->error('您粘贴的地址不正确!'); + } + if (!preg_match('/(mp.weixin.qq.com)/i', $url)) { + return $this->error('您粘贴的地址不是微信文章地址!'); + } + $id = ArticleService::collect($url, UID); + if ($id) { + return $this->success('采集成功', url('mobile/article/edit', 'id=' . $id)); + } else { + return $this->error('采集失败'); + } + } else { + return $this->error('请输入文章地址'); + } + } else { + $this->nav = 2; + return $this->fetch(); + } + } + + public function del($id) + { + if (IS_AJAX) { + $info = ArticleModel::find($id); + if ($info->uid != UID) { + return $this->error('非法操作'); + } else { + if ($info->delete()) { + return $this->success('删除成功'); + } else { + return $this->error('删除失败'); + } + } + } else { + return $this->error('请不要非法访问'); + } + } + + public function video() + { + $this->meta_title = '发布文章视频教程'; + return $this->fetch(); + } +} diff --git a/application/mobile/controller/Cause.php b/application/mobile/controller/Cause.php new file mode 100644 index 0000000..fc84a89 --- /dev/null +++ b/application/mobile/controller/Cause.php @@ -0,0 +1,47 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\Content as ContentModel; +use app\common\service\Content as ContentService; + +class Cause extends _Init +{ + public function _initialize() + { + parent::_initialize(); + $this->nav = 2; + } + + public function index() + { + $this->list = ContentService::categoryList('cause'); + return $this->fetch(); + } + + public function lists($category) + { + $this->list = ContentModel::where('category_id', $category)->where('status', 1)->order('create_time desc')->paginate(20); + if (IS_AJAX) { + if (count($this->list) > 0) { + return $this->success($this->fetch('cause/list')); + } else { + return $this->error('已经到最后一页'); + } + } + return $this->fetch(); + } + + public function detail($id) + { + parent::shareArticle(); + $this->info = ContentModel::where('id', $id)->where('status', 1)->find(); + return $this->fetch(); + } +} diff --git a/application/mobile/controller/Center.php b/application/mobile/controller/Center.php new file mode 100644 index 0000000..0d68dd0 --- /dev/null +++ b/application/mobile/controller/Center.php @@ -0,0 +1,73 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\Member as MemberModel; + +class Center extends _Init +{ + + public function _initialize() + { + parent::_initialize(); + $this->user = MemberModel::find(UID); + $this->nav = 5; + } + + public function index() + { + return $this->fetch(); + } + + /** + * 操作成功跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function success($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + $msg = $msg ?: '操作成功'; + return parent::success($msg, $url, $data, $wait, $header); + } + + /** + * 操作错误跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function error($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + $msg = $msg ?: '未知错误'; + return parent::error($msg, $url, $data, $wait, $header); + } + + protected function back($result) + { + if ($result === true) { + return $this->success(); + } else { + return $this->error($result); + } + } + + public function tishi() + { + return $this->fetch(''); + } +} diff --git a/application/mobile/controller/Experience.php b/application/mobile/controller/Experience.php new file mode 100644 index 0000000..e54f04b --- /dev/null +++ b/application/mobile/controller/Experience.php @@ -0,0 +1,135 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\Experience as ExperienceModel; +use app\common\model\MemberInfo as MemberInfoModel; +use app\common\service\Experience as ExperienceService; + +class Experience extends _Init +{ + public function _initialize() + { + parent::_initialize(); + $this->nav = 5; + } + + public function checkVip($title) + { + $info = MemberInfoModel::get(UID); + if (!$info->is_vip) { + return $this->error('您还不是vip,不能' . $title, url('vip/index')); + } + } + + public function index() + { + $map = [ + 'pid' => 0, + 'status' => 1, + ]; + $this->list = ExperienceModel::where($map)->order('id desc')->paginate(10); + + if (IS_AJAX) { + if (count($this->list) > 0) { + return $this->success($this->fetch('experience/lists')); + } else { + return $this->error('已经到最后一页'); + } + } + return $this->fetch(); + } + + /** + * 提交评论 + * @param [type] $id [description] + * @return [type] [description] + */ + public function detail($id) + { + parent::shareArticle(); + + if (IS_POST) { + self::checkVip('评论'); + $content = $this->request->post('content'); + $res = ExperienceService::comment($id, UID, $content); + return $this->back($res); + } else { + $this->info = ExperienceModel::get($id); + $this->info->setInc('click'); + return $this->fetch(); + } + } + + /** + * 点赞 + * @param [type] $id [description] + * @return [type] [description] + */ + public function tags($id) + { + self::checkVip('点赞'); + $res = ExperienceService::tags(UID, $id); + if ($res === true) { + return $this->success(); + } else { + return $this->error($res); + } + } + + /** + * 我的分享 + * @return [type] [description] + */ + public function mine() + { + $map = [ + 'pid' => 0, + 'uid' => UID, + 'status' => 1, + ]; + $this->list = ExperienceModel::where($map)->order('id desc')->paginate(10); + + if (IS_AJAX) { + if (count($this->list) > 0) { + return $this->success($this->fetch('experience/minelists')); + } else { + return $this->error('已经到最后一页'); + } + } + return $this->fetch(); + } + + public function add() + { + if (IS_POST) { + self::checkVip('添加信息'); + $data = $this->request->post(); + $res = ExperienceService::add($data, UID); + if ($res === true) { + return $this->success('添加成功!', url('Experience/index')); + } else { + return $this->error('添加失败!'); + } + } else { + return $this->fetch(); + } + } + + /** + * 删除 + * @param [type] $id [description] + * @return [type] [description] + */ + public function del($id) + { + $res = ExperienceService::del($id, UID); + return $this->back($res); + } +} diff --git a/application/mobile/controller/Help.php b/application/mobile/controller/Help.php new file mode 100644 index 0000000..f72b914 --- /dev/null +++ b/application/mobile/controller/Help.php @@ -0,0 +1,63 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\Direct as DirectModel; +use app\common\model\Help as HelpModel; + +class Help extends _Init +{ + + public function _initialize() + { + parent::_initialize(); + $this->nav = 4; + } + + public function index() + { + return $this->fetch(); + } + + public function desc() + { + return $this->fetch(); + } + + public function lists($category) + { + $this->list = HelpModel::where(['status' => 1, 'category' => $category])->select(); + return $this->fetch(); + } + + public function detail($id) + { + parent::shareArticle(); + $this->info = HelpModel::get($id); + return $this->fetch(); + } + + /** + * 直销技巧 + */ + public function direct() + { + $this->list = DirectModel::where('status', 1)->order('sort desc,id desc')->select(); + $this->nav = 5; + return $this->fetch(); + } + + public function directDetail($id) + { + $this->info = DirectModel::get($id); + $this->info->setInc('click'); + $this->nav = 4; + return $this->fetch(); + } +} diff --git a/application/mobile/controller/Index.php b/application/mobile/controller/Index.php new file mode 100644 index 0000000..9cfe959 --- /dev/null +++ b/application/mobile/controller/Index.php @@ -0,0 +1,47 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\MemberInfo; + +class Index extends _Init +{ + + public function _initialize() + { + parent::_initialize(); + $this->nav = 1; + } + + public function index() + { + if (UID || $this->shareUser) { + if ($this->shareUser) { + $this->info = $this->shareUser; + } else { + $this->info = MemberInfo::get(UID); + } + + return $this->fetch('user_' . $this->info['index_tpl']); + } else { + return $this->fetch(); + } + } + + public function info() + { + if (UID == $this->shareUser['uid']) { + $this->info = MemberInfo::get(UID); + } else { + $this->info = $this->shareUser; + } + + return $this->fetch(); + } +} diff --git a/application/mobile/controller/Invite.php b/application/mobile/controller/Invite.php new file mode 100644 index 0000000..af76ad5 --- /dev/null +++ b/application/mobile/controller/Invite.php @@ -0,0 +1,42 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\MemberInfo as MemberInfoModel; +use Endroid\QrCode\QrCode; + +class Invite extends _Init +{ + public function _initialize() + { + parent::_initialize(); + $this->nav = 5; + } + + public function index($uid = '') + { + if (empty($uid)) { + $uid == UID; + } + $qr = new QrCode(); + $url = url('login/reg', 'invite=' . $uid); + $this->img = $qr->setText($url)->setSize(350)->writeDataUri(); + + $info = MemberInfoModel::get($uid); + if (empty($info)) { + $this->redirect('login/info', 'title=邀请人已经被删除或者禁用'); + } + $info['description'] = ''; + $info['title'] = $info['nickname']; + $info['thumb'] = $info['avatar']; + $this->assign('info', $info); + return $this->fetch(); + } + +} diff --git a/application/mobile/controller/Login.php b/application/mobile/controller/Login.php new file mode 100644 index 0000000..44e5733 --- /dev/null +++ b/application/mobile/controller/Login.php @@ -0,0 +1,157 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\Member as MemberModel; +use app\common\service\Member as MemberService; +use app\common\service\Sms as SmsService; +use app\common\validate\Member as MemberValidate; +use cjango\Wechat\Oauth; + +class Login extends _Init +{ + public function _initialize() + { + // parent::fenxiang(); + $this->showWx = 1; + } + + public function index() + { + if (IS_POST) { + $user = $this->request->post('username'); + $pass = $this->request->post('password'); + $res = MemberService::login($user, $pass); + if ($res === true) { + return $this->success('登录成功', 'center/index'); + } else { + return $this->error($res); + } + } else { + if (parent::isLogin()) { + $this->redirect('center/index'); + } + return $this->fetch(); + } + } + + /** + * 注册 + * @return [type] [description] + */ + public function reg($invite = 0) + { + if (IS_POST) { + $user = $this->request->post('username'); + $pass = $this->request->post('password'); + $repass = $this->request->post('repassword'); + $code = $this->request->post('mobile_code'); + + //校验注册数据 + $data = [ + 'username' => $user, + 'password' => $pass, + 'repass' => $repass, + ]; + $validate = new MemberValidate(); + if (!$validate->scene('mobileRegister')->check($data)) { + return $this->error($validate->getError()); + } + + //验证码校验 + $checkCode = SmsService::check($user, $code); + if ($checkCode !== true) { + return $this->error($checkCode); + } + + $res = MemberService::register($user, $pass, 1, $invite); + if ($res === true) { + return $this->success('注册成功', 'login/index'); + } else { + return $this->error($res); + } + } else { + return $this->fetch(); + } + } + + /** + * 忘记密码 + * @return [type] [description] + */ + public function forget() + { + if (IS_POST) { + $mobile = $this->request->post('mobile'); + $pass = $this->request->post('password'); + $repass = $this->request->post('repassword'); + $code = $this->request->post('mobile_code'); + //验证码校验 + $checkCode = SmsService::check($mobile, $code); + if ($checkCode !== true) { + return $this->error($checkCode); + } + $userInfo = MemberModel::where('username', $mobile)->find(); + $data = [ + 'newpass' => $pass, + 'repass' => $repass, + ]; + $validate = new MemberValidate(); + if (!$validate->scene('forget')->check(['newpass' => $pass, 'repass' => $repass])) { + return $this->error($validate->getError()); + } + if (!$userInfo) { + return '没有这个用户'; + } elseif ($userInfo->save(['password' => $pass])) { + return $this->success('', url('login/index')); + } else { + return $this->error('更新失败'); + } + + } + return $this->fetch(); + } + + // 自动登录 + public function autoLogin() + { + parent::initWechat(); + $token = Oauth::token(); + if (!$token) { + $url = Oauth::url(url('login/autoLogin')); + $this->redirect($url); + } else { + $openid = $token['openid']; + $res = MemberService::openid_login($openid); + if ($res === true) { + $this->redirect(url('center/index')); + } else { + $this->redirect(url('login/info', ['title' => $res])); + } + } + } + + /** + * 信息提示 错误提示 + * @param [type] $title [description] + * @return [type] [description] + */ + public function info($title) + { + + $this->info = $title; + return $this->fetch(); + } + + public function logout() + { + MemberService::logout(); + return $this->success('退出成功', 'login/index'); + } +} diff --git a/application/mobile/controller/Member.php b/application/mobile/controller/Member.php new file mode 100644 index 0000000..079185a --- /dev/null +++ b/application/mobile/controller/Member.php @@ -0,0 +1,77 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\MemberList as MemberListModel; +use app\common\service\MemberList as MemberListService; + +class Member extends _Init +{ + + public function _initialize() + { + parent::_initialize(); + $this->nav = 5; + } + + public function index() + { + $this->list = MemberListModel::where('uid', UID)->where('status', 'egt', 0)->select(); + return $this->fetch(''); + } + + public function add() + { + if (IS_POST) { + $data = $this->request->post(); + $res = MemberListService::create($data, UID); + if ($res === true) { + $this->success('添加成功', url('member/index')); + } else { + $this->error($res); + } + } else { + $this->showfoot = '1'; + return $this->fetch(''); + } + + } + public function edit($id) + { + if (IS_POST) { + $data = $this->request->post(); + $res = MemberListService::edit($data); + if ($res === true) { + $this->success('编辑成功', url('member/index')); + } else { + $this->error($res); + } + } else { + $this->info = MemberListModel::get($id); + return $this->fetch('add'); + } + } + + public function del($id) + { + $res = MemberListService::del($id); + $this->back($res); + // if ($res === true) { + // $this->success('删除成功', url('member/index')); + // } else { + // $this->error($res); + // } + } + + public function info($id) + { + $this->info = MemberListModel::get($id); + return $this->fetch(); + } +} diff --git a/application/mobile/controller/Pay.php b/application/mobile/controller/Pay.php new file mode 100644 index 0000000..aaf4dbd --- /dev/null +++ b/application/mobile/controller/Pay.php @@ -0,0 +1,67 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\service\Score as ScoreService; +use app\common\service\Vip as VipService; +use cjango\Wechat; + +class Pay extends _Init +{ + + public function vip($openid = '') + { + parent::initWechat(); + if (!$openid) { + $url = Wechat\Oauth::url(url('Pay/getOpenId', 'callback=' . __SELF__)); + $this->redirect($url); + } + + $fee = 298; + $res = VipService::createPayOrder(UID, $fee, $openid); + if ($res['code'] != 1) { + return $res['msg']; + exit(); + } + $this->orderid = $res['data']; + parent::initWechat(); + $this->payParams = Wechat\Pay::unified($this->orderid, '超级助手VIP(有效期一年)', $fee, 'JSAPI', url('openapi/pay/vip'), $openid); + if ($this->payParams == false) { + echo Wechat::error(); + exit(); + } + return $this->fetch(); + } + + public function getOpenId($callback) + { + parent::initWechat(); + $token = Wechat\Oauth::token(); + if ($token) { + $openid = $token['openid']; + $this->redirect($callback . '?openid=' . $openid); + } else { + echo Wechat::error(); + } + } + + /** + * 兑换会员 + * @param [type] $orderid [description] + * @return [type] [description] + */ + public function exchange($orderid) + { + $res = ScoreService::buyVip(UID, $orderid); + if ($res == 1) { + $res = true; + } + return $this->back($res); + } +} diff --git a/application/mobile/controller/Qrcode.php b/application/mobile/controller/Qrcode.php new file mode 100644 index 0000000..5db5b8e --- /dev/null +++ b/application/mobile/controller/Qrcode.php @@ -0,0 +1,76 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\MemberInfo as MemberInfoModel; +use app\common\service\Member as MemberService; +use app\common\service\Message as MessageService; + +class Qrcode extends Center +{ + + public function index() + { + if (UID == $this->shareUser['uid']) { + $this->info = $this->user->info; + } else { + $this->info = $this->shareUser; + $user = MemberInfoModel::where('uid', $this->shareUser['uid'])->find(); + $user->setInc('click'); + } + return $this->fetch(); + } + + /** + * 留言 + * @param string $uid [description] + * @return [type] [description] + */ + public function comment($uid = '') + { + if (IS_POST) { + $data = $this->request->post(); + $res = MessageService::send($data, UID); + return $this->back($res); + } else { + $this->list = MessageService::getList($uid); + return $this->fetch(); + } + } + + // 点赞 + public function tags($uid) + { + $res = MessageService::tags($uid); + if ($res === true) { + return $this->success(); + } else { + return $this->error($res); + } + } + + // 写签名 + public function signature() + { + $signature = $this->request->post('signature'); + $res = MemberService::editInfo(UID, ['signature' => $signature], 'signature'); + if ($res === true) { + return $this->success('添加成功'); + } else { + return $this->error($res); + } + } + + //删除 + public function del($id) + { + $res = MessageService::del($id); + return $this->back($res); + } +} diff --git a/application/mobile/controller/Score.php b/application/mobile/controller/Score.php new file mode 100644 index 0000000..7634c71 --- /dev/null +++ b/application/mobile/controller/Score.php @@ -0,0 +1,37 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\Score as ScoreModel; +use app\common\model\ScoreRules as ScoreRulesModel; +use tools\Time; + +class Score extends _Init +{ + public function _initialize() + { + parent::_initialize(); + $this->nav = 5; + } + + public function index() + { + $this->list = ScoreModel::where('uid', UID)->select(); + $this->dayCount = ScoreModel::where('uid', UID)->where('create_time', 'between', Time::day())->sum('score'); + $this->monthCount = ScoreModel::where('uid', UID)->where('create_time', 'between', Time::month())->sum('score'); + $this->allCount = ScoreModel::where('uid', UID)->sum('score'); + return $this->fetch(); + } + public function info() + { + $this->list = ScoreRulesModel::where('')->select(); + return $this->fetch(); + } + +} diff --git a/application/mobile/controller/Setting.php b/application/mobile/controller/Setting.php new file mode 100644 index 0000000..e200068 --- /dev/null +++ b/application/mobile/controller/Setting.php @@ -0,0 +1,337 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\Member as MemberModel; +use app\common\model\Province as ProvinceModel; +use app\common\model\Storage as StorageModel; +use app\common\service\City as CitySercice; +use app\common\service\Member as MemberService; +use cjango\Wechat; +use cjango\Wechat\Oauth; + +class Setting extends Center +{ + public function index() + { + return $this->fetch(); + } + + //修改密码 + public function password() + { + if (IS_POST) { + $newpass = $this->request->post('newpass'); + $repass = $this->request->post('repass'); + $oldpass = $this->request->post('oldpass'); + $res = MemberService::changePassword(UID, $oldpass, $newpass, $repass); + if ($res === true) { + return $this->success('修改成功', 'setting/index'); + } else { + return $this->error($res); + } + } else { + return $this->fetch(); + } + } + + //设置模版 + public function template() + { + if (IS_POST) { + $tpl = $this->request->post('tpl'); + if (MemberService::changeTpl(UID, $tpl)) { + return $this->success('', 'center/index'); + } else { + return $this->error(); + } + } else { + return $this->fetch(); + } + } + + //修改姓名 + public function nickname() + { + if (IS_POST) { + $nickname = $this->request->post('nickname'); + $res = MemberService::editInfo(UID, ['nickname' => $nickname], 'nickname'); + if ($res === true) { + return $this->success('修改成功', 'setting/index'); + } else { + return $this->error($res); + } + } else { + return $this->fetch(); + } + } + + // 修改手机 + public function mobile() + { + if (IS_POST) { + $mobile = $this->request->post('mobile'); + $res = MemberService::editInfo(UID, ['mobile' => $mobile], 'mobile'); + if ($res === true) { + return $this->success('修改成功', 'setting/index'); + } else { + return $this->error($res); + } + } else { + return $this->fetch(); + } + } + + // 添加微信 + public function wechat() + { + if (IS_POST) { + $wechat = $this->request->post('wechat'); + $res = MemberService::editInfo(UID, ['wechat' => $wechat], 'wechat'); + if ($res === true) { + return $this->success('修改成功', 'setting/index'); + } else { + return $this->error($res); + } + } else { + return $this->fetch(); + } + } + + // 修改qq + public function qq() + { + if (IS_POST) { + $qq = $this->request->post('qq'); + $res = MemberService::editInfo(UID, ['qq' => $qq], 'qq'); + if ($res === true) { + return $this->success('修改成功', 'setting/index'); + } else { + return $this->error($res); + } + } else { + return $this->fetch(); + } + } + + // 修改Email + public function email() + { + if (IS_POST) { + $email = $this->request->post('email'); + $res = MemberService::editInfo(UID, ['email' => $email], 'email'); + if ($res === true) { + return $this->success('修改成功', 'setting/index'); + } else { + return $this->error($res); + } + } else { + return $this->fetch(); + } + } + + // 修改二维码 + public function qrcode() + { + if (IS_POST) { + $qrcode = $this->request->post('qrcode'); + $res = MemberService::editInfo(UID, ['qrcode' => $qrcode], 'qrcode'); + if ($res === true) { + return $this->success('修改成功', 'setting/index'); + } else { + return $this->error($res); + } + } else { + return $this->fetch(); + } + } + + // 修改职位 + public function position() + { + if (IS_POST) { + $position = $this->request->post('position'); + $res = MemberService::editInfo(UID, ['position' => $position], 'position'); + if ($res === true) { + return $this->success('修改成功', 'setting/index'); + } else { + return $this->error($res); + } + } else { + return $this->fetch(); + } + } + + // 修改地区 + public function city() + { + if (IS_POST) { + $province = $this->request->post('province'); + $city = $this->request->post('city'); + $res = MemberService::editInfo(UID, ['province' => $province, 'city' => $city], 'city'); + if ($res === true) { + return $this->success('修改成功', 'setting/index'); + } else { + return $this->error($res); + } + } else { + $this->info = MemberService::info(UID); + $this->province = ProvinceModel::where('status', 0)->select(); + $this->city = CitySercice::getCity($this->info->province); + + return $this->fetch(); + } + } + + // 绑定/解绑微信 + public function bindWechat() + { + if (IS_POST) { + $openid = $this->request->post('openid'); + $unbind = $this->request->post('unbind'); + + if (!empty($unbind)) { + $res = MemberService::unBindWechat(UID, $openid); //解绑 + $str = '解除绑定成功'; + } else { + $res = MemberService::bindWechat(UID, ['openid' => $openid]); //绑定 + $str = '绑定成功'; + } + + if ($res === true) { + return $this->success($str, 'setting/index'); + } else { + return $this->error($res); + } + } else { + $this->wechat = self::getWechat(); + $userInfo = MemberModel::where('openid', $this->wechat['openid'])->find(); + if ($userInfo) { + $this->assign('userInfo', $userInfo); + } + return $this->fetch(); + } + } + + /** + * [uploadPicture h5图片上传(后端)] + * @return [type] [description] + */ + public function uploadPicture() + { + $input = $this->request->post(); + $data = $input['files']['base64']; + $name = $input['files']['name']; + $hash = hash('md5', base64_decode($data)); + $info = StorageModel::where('hash', $hash)->find(); + if ($info) { + $return = array('code' => 1, 'msg' => '上传成功', 'id' => $info->id, 'url' => $info->path); + if ($input['type'] == 'headimgurl') { + $res = MemberService::editInfo(UID, ['headimgurl' => $info->path], 'headimgurl'); + if ($res === true) { + return $this->success($return); + } else { + return $this->error($res); + } + } else { + return $this->success($return); + } + + } else { + // 获取图片 + $temp = $data; + list($type, $data) = explode(',', $temp); + + // 判断类型 + if (strstr($type, 'image/jpeg') !== '') { + $ext = '.jpg'; + } elseif (strstr($type, 'image/gif') !== '') { + $ext = '.gif'; + } elseif (strstr($type, 'image/png') !== '') { + $ext = '.png'; + } + + // 生成的文件名 + $path = "/uploads/" . $input['type'] . "/" . date('Y-m/d') . '/'; + if (!$this->mkdir('.' . $path)) { + exit(); + } + $url = $path . $name; + $photo = '.' . $url; + // 生成文件 + file_put_contents($photo, base64_decode($data)); + $data = [ + 'hash' => $hash, + 'type' => 'image', + 'name' => $name, + 'ext' => $ext, + 'path' => $url, + 'size' => $input['files']['size'], + ]; + if ($insert = StorageModel::create($data)) { + $return = array('code' => 1, 'msg' => '上传成功', 'url' => $url, 'id' => $insert->id); + if ($input['type'] == 'headimgurl') { + $res = MemberService::editInfo(UID, ['headimgurl' => $url], 'headimgurl'); + if ($res === true) { + return $this->success($return); + } else { + return $this->error($res); + } + } else { + return $this->success($return); + } + + } else { + return $this->error('上传失败'); + } + } + } + + /** + * [mkdir 检测路径] + * @param [type] $savepath [description] + * @return [type] [description] + */ + public function mkdir($dir) + { + if (is_dir($dir)) { + return true; + } + + if (mkdir($dir, 0777, true)) { + return true; + } else { + return false; + } + } + + //获取城市数据 + public function getCity($id) + { + $list = CitySercice::getData($id); + echo json_encode($list); + } + + // 获取微信数据 + public function getWechat() + { + parent::initWechat(); + $token = Oauth::token(); + if (!$token) { + $url = Oauth::url(url('setting/bindWechat'), 'STATE', 'snsapi_userinfo'); + $this->redirect($url); + } + $wechat = Oauth::info($token['access_token'], $token['openid']); + if ($wechat) { + return $wechat; + } else { + $this->redirect(url('setting/bindWechat')); + } + } +} diff --git a/application/mobile/controller/Share.php b/application/mobile/controller/Share.php new file mode 100644 index 0000000..b19dfda --- /dev/null +++ b/application/mobile/controller/Share.php @@ -0,0 +1,47 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\Content as ContentModel; +use app\common\service\Content as ContentService; + +class Share extends _Init +{ + public function _initialize() + { + parent::_initialize(); + $this->nav = 3; + } + + public function index() + { + $this->list = ContentService::categoryList('share'); + return $this->fetch(); + } + + public function lists($category) + { + $this->list = ContentModel::where(['status' => 1, 'category_id' => $category])->order('id desc')->paginate(20); + if (IS_AJAX) { + if (count($this->list) > 0) { + return $this->success($this->fetch('share/list')); + } else { + return $this->error('已经到最后一页'); + } + } + return $this->fetch(); + } + + public function detail($id) + { + parent::shareArticle(); + $this->info = ContentModel::get($id); + return $this->fetch(); + } +} diff --git a/application/mobile/controller/Sms.php b/application/mobile/controller/Sms.php new file mode 100644 index 0000000..559cc5f --- /dev/null +++ b/application/mobile/controller/Sms.php @@ -0,0 +1,58 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\Member as MemberModel; +use app\common\service\Sms as SmsService; + +class Sms extends _Init +{ + public function _initialize() + { + } + + /** + * [短信验证码] + * @return [type] [description] + */ + public function getsms() + { + $mobile = $this->request->post('mobile'); + $forget = $this->request->post('forget'); + $MemberInfo = MemberModel::where('username', $mobile)->find(); + + if (!empty($MemberInfo) && empty($forget)) { + return $this->error('这个手机号已经注册过了'); + } + $res = SmsService::send($mobile); + + if ($res === true) { + return $this->success('发送成功'); + } else { + return $this->error($res); + } + } + + /** + * [短信验证码] + * @return [type] [description] + */ + public function getresms() + { + $mobile = $this->request->post('mobile'); + $MemberInfo = MemberModel::where('username', $mobile)->find(); + $res = SmsService::send($mobile); + + if ($res === true) { + return $this->success('发送成功'); + } else { + return $this->error($res); + } + } +} diff --git a/application/mobile/controller/Suggest.php b/application/mobile/controller/Suggest.php new file mode 100644 index 0000000..2ff0608 --- /dev/null +++ b/application/mobile/controller/Suggest.php @@ -0,0 +1,33 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\Suggest as SuggestModel; + +class Suggest extends Center +{ + + public function index() + { + if (IS_POST) { + $content = $this->request->post('content'); + $data = [ + 'uid' => UID, + 'content' => $content, + ]; + if (SuggestModel::create($data)) { + return $this->success('提交成功', 'center/index'); + } else { + return $this->error(); + } + } else { + return $this->fetch(); + } + } +} diff --git a/application/mobile/controller/Tools.php b/application/mobile/controller/Tools.php new file mode 100644 index 0000000..dcb96f4 --- /dev/null +++ b/application/mobile/controller/Tools.php @@ -0,0 +1,40 @@ + | +// | Copyright (c) 2017, http://www.xdeepu.cn. All Rights Reserved. | +// +-----------------------------------------------------------------------------------+ + +namespace app\mobile\controller; + +use app\common\model\MemberList as MemberListModel; + +/** + * fdsaf + */ +class Tools extends _Init +{ + public function index() + { + $this->list = MemberListModel::where('')->select(); + return $this->fetch(''); + } + public function add() + { + if (IS_POST) { + # code... + } else { + $this->showfoot = '1'; + $this->list = MemberListModel::where('')->select(); + + return $this->fetch(''); + } + + } + public function info() + { + return $this->fetch(''); + } +} diff --git a/application/mobile/controller/Upload.php b/application/mobile/controller/Upload.php new file mode 100644 index 0000000..96e2a3f --- /dev/null +++ b/application/mobile/controller/Upload.php @@ -0,0 +1,75 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use think\Config; +use think\Db; + +class Upload extends _Init +{ + public function uploadPicture() + { + $data = $this->request->post('files.base64'); + $size = $this->request->post('files.size'); + list($type, $data) = explode(',', $data); + if (strstr($type, 'image/jpeg') !== '') { + $ext = '.jpg'; + } elseif (strstr($type, 'image/gif') !== '') { + $ext = '.gif'; + } elseif (strstr($type, 'image/png') !== '') { + $ext = '.png'; + } + $fileN = 'img' . time() . $ext; + $path = Config::get('upload.rootPath') . 'image/'; + if (!$this->mkdir($path)) { + return $this->error('头像保存目录不存在'); + } + $fileName = $path . $fileN; + + file_put_contents($fileName, base64_decode($data)); + $path = substr($fileName, 1); + $data = [ + 'type' => 'image', + 'name' => $fileN, + 'ext' => $ext, + 'hash' => md5($fileN), + 'path' => $path, + 'size' => $size, + 'create_time' => time(), + ]; + $last = Db::name('Storage')->insertGetId($data); + if ($last) { + $re = [ + 'filename' => $path, + 'fileid' => $last, + ]; + return $this->success('上传成功', '', $re); + } else { + return $this->error('上传失败'); + } + + } + + /** + * [mkdir 检测路径] + * @param [type] $savepath [description] + */ + private function mkdir($dir) + { + if (is_dir($dir)) { + return true; + } + + if (mkdir($dir, 0755, true)) { + return true; + } else { + return false; + } + } +} diff --git a/application/mobile/controller/User.php b/application/mobile/controller/User.php new file mode 100644 index 0000000..3f584e2 --- /dev/null +++ b/application/mobile/controller/User.php @@ -0,0 +1,31 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\model\Member as MemberModel; + +class User extends _Init +{ + + public function _initialize() + { + parent::_initialize(); + } + + public function index() + { + $this->info = MemberModel::get($uid); + return $this->fetch('user_' . $this->info->index_tpl); + } + + public function intro() + { + return $this->fetch(); + } +} diff --git a/application/mobile/controller/Vip.php b/application/mobile/controller/Vip.php new file mode 100644 index 0000000..59994f5 --- /dev/null +++ b/application/mobile/controller/Vip.php @@ -0,0 +1,18 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +class Vip extends Center +{ + + public function index() + { + return $this->fetch(); + } +} diff --git a/application/mobile/controller/_Init.php b/application/mobile/controller/_Init.php new file mode 100644 index 0000000..162742d --- /dev/null +++ b/application/mobile/controller/_Init.php @@ -0,0 +1,151 @@ + | +// +------------------------------------------------+ +namespace app\mobile\controller; + +use app\common\service\MemberInfo; +use app\common\service\Score as ScoreService; +use cjango\Wechat\Token; +use think\Cache; +use think\Config; +use tools\Initialize; +use tools\Str; + +class _Init extends Initialize +{ + + public function _initialize() + { + define('UID', self::isLogin()); + if (!UID) { + //判断跳转。文章、每日分享、事业介绍详情页,个人资料页面不跳转 + self::T(); + // $this->redirect('login/wxLogin', 'callback=' . base64_encode(__SELF__)); + } + $uid = $this->request->get('uid'); + + if ($uid) { + $this->shareUser = MemberInfo::show($uid); + } + + //分享初始化 + self::fenxiang(); + } + + /** + * 跳转 + */ + public function T() + { + $data = [ + 'Login/index', + 'Login/reg', + 'Login/autoLogin', + 'Login/info', + 'Cause/detail', + 'Share/detail', + 'Article/show', + 'Index/info', + 'Index/index', + 'Help/directdetail', + 'Help/detail', + 'Invite/index', + 'Qrcode/index', + ]; + $url = CONTROLLER_NAME . '/' . ACTION_NAME; + if (!in_array($url, $data) || (!UID && !$this->request->get('uid'))) { + if (IS_AJAX) { + return $this->error('操作失败', url('login/index')); + } else { + $this->redirect('login/index'); + } + } + } + + /** + * 分享文章获取积分 + */ + public function ShareArticle() + { + + if (!empty($this->shareUser->uid) && $this->shareUser->uid != UID) { + ScoreService::share($this->shareUser->uid); + } + } + + /** + * 操作成功跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function success($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + $msg = $msg ?: '操作成功'; + return parent::success($msg, $url, $data, $wait, $header); + } + + /** + * 操作错误跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function error($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + $msg = $msg ?: '未知错误'; + return parent::error($msg, $url, $data, $wait, $header); + } + + protected function back($result) + { + if ($result === true) { + return $this->success(); + } else { + return $this->error($result); + } + } + + /** + * 微信分享 + * @return [type] [description] + */ + public function fenxiang() + { + parent::initWechat(); + // 微信分享 + $ticket = Cache::get('Wechat_ticket'); + $config = Config::get('wechat'); + + if (!$ticket) { + $ticket = Token::ticket(); + Cache::set('Wechat_ticket', $ticket, 7000); + } + $wx['appid'] = $config['appid']; + $wx['timestamp'] = time(); + $wx['noncestr'] = $noncestr = Str::random(32); + $sign = array( + 'noncestr' => $noncestr, + 'jsapi_ticket' => $ticket, + 'timestamp' => time(), + 'url' => __SELF__, + ); + ksort($sign); + $signStr = sha1(urldecode(http_build_query($sign))); + $wx['signature'] = $signStr; + $this->assign('wx', $wx); + } +} diff --git a/application/mobile/view/ajax/modelstyle.html b/application/mobile/view/ajax/modelstyle.html new file mode 100644 index 0000000..e89059b --- /dev/null +++ b/application/mobile/view/ajax/modelstyle.html @@ -0,0 +1,7 @@ +{volist name="list" id="vo"} +
+
+
{$vo.content}
+
+
+{/volist} diff --git a/application/mobile/view/article/add.html b/application/mobile/view/article/add.html new file mode 100644 index 0000000..bf5700b --- /dev/null +++ b/application/mobile/view/article/add.html @@ -0,0 +1,119 @@ + + + + + + {$meta_title|default=$Think.config.web_site_title} + + + + + + + + + + + + + +
+
+ +
+ + + 返回 + + + 发表 + +

编辑文章

+
+ + + + +
+
+ {$info.content|default=''} +
+
+ + {include file="article/tools" /} +
+
+ + + + + + + + + + diff --git a/application/mobile/view/article/collect.html b/application/mobile/view/article/collect.html new file mode 100644 index 0000000..a2b4dfd --- /dev/null +++ b/application/mobile/view/article/collect.html @@ -0,0 +1,25 @@ +{extend name="public/base" /} + +{block name="body"} +
+
+ +

+ + +

+
+
+

 快速使用指南

+

第一步:朋友圈-微信公众账号找到好文章
第二步:复制此文章链接
第三步:粘贴到上方输入框内,点击编辑
第四步:保存,然后查看分享吧!

+
+
+{/block} + +{block name="script"} + +{/block} diff --git a/application/mobile/view/article/index.html b/application/mobile/view/article/index.html new file mode 100644 index 0000000..d697187 --- /dev/null +++ b/application/mobile/view/article/index.html @@ -0,0 +1,42 @@ +{extend name="public/base" /} + +{block name="header"} +
+ +  返回 + + + + + 发布文章 +
+{/block} + +{block name="body"} + +
+ 点击查看新手视频教学 + +
+ +{empty name="list"} + +
+ +

您还没有发布过哦!
点击右上角加号开始添加

+
+ +{else/} + + +
+ 点击更多 +
+ +{/empty} +{/block} + +{block name="script"} +{/block} diff --git a/application/mobile/view/article/lists.html b/application/mobile/view/article/lists.html new file mode 100644 index 0000000..1b50cdf --- /dev/null +++ b/application/mobile/view/article/lists.html @@ -0,0 +1,18 @@ +{volist name="list" id="vo"} +
  • +
    +
    + +
    +
    +

    {$vo.title}

    +

    {$vo.create_time}{$vo.click}人浏览

    +
    +
    +

    + 查看 + 编辑 + 删除 +

    +
  • +{/volist} diff --git a/application/mobile/view/article/show.html b/application/mobile/view/article/show.html new file mode 100644 index 0000000..76357cd --- /dev/null +++ b/application/mobile/view/article/show.html @@ -0,0 +1,14 @@ +{extend name="public/base" /} +{block name="style"} + +{/block} +{block name="body"} +

    {$info.title}

    +{include file="public/share" /} +{$info.content} + +{include file="public/user" /} +{/block} + diff --git a/application/mobile/view/article/tools.html b/application/mobile/view/article/tools.html new file mode 100644 index 0000000..df18453 --- /dev/null +++ b/application/mobile/view/article/tools.html @@ -0,0 +1,141 @@ +
    +
    +
    + 插入链接 +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    +
      +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    +
    +
    + 清空 +
    +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + 文字颜色 + 清空 +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    + 请粘贴腾讯视频网址 +
    +
    + +
    +
    + + +
    +
    + +
    +
    + + 插入图片 +
    +
    +
    + +
    +
    + +
    + +
    +
    + 文字背景颜色 + 清空 +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + 字体大小 +
    +
    + +
    +
    + diff --git a/application/mobile/view/article/video.html b/application/mobile/view/article/video.html new file mode 100644 index 0000000..93f87b3 --- /dev/null +++ b/application/mobile/view/article/video.html @@ -0,0 +1,12 @@ +{extend name="public/base" /} + +{block name="body"} + +
    +
    + +
    +
    + +

    请在WIFI下观看,若加载缓慢请稍等一会,记得全屏哦!如果看了本机教程还是没有懂,请致电我们的客服。

    +{/block} diff --git a/application/mobile/view/cause/detail.html b/application/mobile/view/cause/detail.html new file mode 100644 index 0000000..41debc0 --- /dev/null +++ b/application/mobile/view/cause/detail.html @@ -0,0 +1,34 @@ +{extend name="public/base" /} + +{block name="body"} + +

    {$info.title}

    + + +{include file="public/share" /} + + +{notempty name="$info.id"} + +
    + + +
    +{$info.content} +
    +{else/} +
    + +

    文章已被删除

    +
    +{/notempty} + +{include file="public/user" /} +{/block} + +{block name="script"} + +{/block} + diff --git a/application/mobile/view/cause/index.html b/application/mobile/view/cause/index.html new file mode 100644 index 0000000..7e83333 --- /dev/null +++ b/application/mobile/view/cause/index.html @@ -0,0 +1,40 @@ +{extend name="public/base" /} + +{block name="body"} + + + + + +{/block} + +{block name="script"} + +{/block} diff --git a/application/mobile/view/cause/list.html b/application/mobile/view/cause/list.html new file mode 100644 index 0000000..fab3ba8 --- /dev/null +++ b/application/mobile/view/cause/list.html @@ -0,0 +1,13 @@ +{volist name="list" id="vo"} +
  • + +
    + +
    +
    +

    {$vo.title}

    +

    {$vo.description}

    +
    +
    +
  • +{/volist} diff --git a/application/mobile/view/cause/lists.html b/application/mobile/view/cause/lists.html new file mode 100644 index 0000000..adc995a --- /dev/null +++ b/application/mobile/view/cause/lists.html @@ -0,0 +1,11 @@ +{extend name="public/base" /} + +{block name="body"} + + +
    + 点击更多 +
    +{/block} diff --git a/application/mobile/view/center/index.html b/application/mobile/view/center/index.html new file mode 100644 index 0000000..f87c98a --- /dev/null +++ b/application/mobile/view/center/index.html @@ -0,0 +1,117 @@ +{extend name="public/base" /} + +{block name="header"}{/block} + +{block name="body"} + +
    + 设置 +
    +

    未上传

    + +
    +
    +

    {$user->info->nickname|default='未完善资料'}

    +
    +

    + {$user->info->vip_text} + 立即开通VIP + VIP会员 +

    +  积分  {$user->info->score} +
    +
    +
    + + + + +{/block} + +{block name="script"} + +{/block} diff --git a/application/mobile/view/center/tishi.html b/application/mobile/view/center/tishi.html new file mode 100644 index 0000000..0fd65af --- /dev/null +++ b/application/mobile/view/center/tishi.html @@ -0,0 +1,41 @@ +{extend name="public/base" /} + +{block name="body"} + +
    +
    +
    + +
    +

    + 因不同手机的原因,有的人可能无法登录。当不能登录的时候请点击右上角的 + 然后点击 “在浏览器中打开”。 +

    + +
    + +
    +
    +
    +

    浏览器中打开

    +
    +
    +{/block} + +{block name="script"} + +{/block} diff --git a/application/mobile/view/experience/add.html b/application/mobile/view/experience/add.html new file mode 100644 index 0000000..1e5d46f --- /dev/null +++ b/application/mobile/view/experience/add.html @@ -0,0 +1,21 @@ +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + 发布 + + 发布经验 +
    +{/block} + +{block name="body"} + +
    + + +
    +{/block} diff --git a/application/mobile/view/experience/detail.html b/application/mobile/view/experience/detail.html new file mode 100644 index 0000000..7d05413 --- /dev/null +++ b/application/mobile/view/experience/detail.html @@ -0,0 +1,65 @@ +{extend name="public/base" /} + +{block name="footer"} +{/block} +{block name="body"} +
    +
    +

    评论

    +
    + + +
    +
    + + + + +
    +
    +
    +

    {$info.user.nickname}

    +

    {$info.create_time}

    +
    +

    {$info.content}

    +
    + + +
    +

    全部评论评论

    + + + + + {empty name="info.comment"} +

    暂无评论

    + {/empty} +
    + +{/block} + +{block name="script"} + +{/block} diff --git a/application/mobile/view/experience/index.html b/application/mobile/view/experience/index.html new file mode 100644 index 0000000..7740881 --- /dev/null +++ b/application/mobile/view/experience/index.html @@ -0,0 +1,32 @@ +{extend name="public/base" /} + +{block name="header"} + +
    + +  返回 + + + 我的分享 + + 经验分享 +
    + +{/block} + +{block name="body"} + +
    + +
    + + + + +
    + 点击更多 +
    + +{/block} diff --git a/application/mobile/view/experience/lists.html b/application/mobile/view/experience/lists.html new file mode 100644 index 0000000..fa6c7e4 --- /dev/null +++ b/application/mobile/view/experience/lists.html @@ -0,0 +1,17 @@ +{volist name="list" id="vo"} +
  • +
    +
    + +
    +

    {$vo.user.nickname}

    +

    {$vo.create_time}

    +
    +

    {$vo.content}

    +

    +  {$vo.click} +  {$vo->comment()->count()} +  {$vo.tags} +

    +
  • +{/volist} diff --git a/application/mobile/view/experience/mine.html b/application/mobile/view/experience/mine.html new file mode 100644 index 0000000..3dfdf64 --- /dev/null +++ b/application/mobile/view/experience/mine.html @@ -0,0 +1,24 @@ +{extend name="public/base" /} + +{block name="body"} + +{empty name="list"} + +
    + +

    您还没有发布过哦!

    +
    + +{else /} + + + +
    + 加载更多 +
    +{/empty} + + +{/block} diff --git a/application/mobile/view/experience/minelists.html b/application/mobile/view/experience/minelists.html new file mode 100644 index 0000000..f12ac01 --- /dev/null +++ b/application/mobile/view/experience/minelists.html @@ -0,0 +1,18 @@ +{volist name="list" id="vo"} +
  • +
    +
    + +
    +

    {$vo.user.nickname}

    +

    {$vo.create_time}

    + +
    +

    {$vo.content}

    +

    +  {$vo.click} +  {$vo->comment()->count()} +  {$vo.tags} +

    +
  • +{/volist} diff --git a/application/mobile/view/help/desc.html b/application/mobile/view/help/desc.html new file mode 100644 index 0000000..74440e9 --- /dev/null +++ b/application/mobile/view/help/desc.html @@ -0,0 +1,12 @@ +{extend name="public/base" /} +{block name="body"} + +

    系统帮您解决人脉问题

    + + +
    + + 但想创业的同时,创业的问题来了,每个人都知道创业是件很难成功的事,据统计,中国真正创业成功的几率是2%,百分之25的企业熬不过3年。以大学生为例,大学生有创业意愿,但缺乏创业知识、创业技能、创业经验,创业成功率不高,需要加强创业指导,尤其是实训。创业指导和学习是21世纪经济时代的创业者必修课程。 +
    + +{/block} diff --git a/application/mobile/view/help/detail.html b/application/mobile/view/help/detail.html new file mode 100644 index 0000000..94cc2e7 --- /dev/null +++ b/application/mobile/view/help/detail.html @@ -0,0 +1,15 @@ +{extend name="public/base" /} +{block name="body"} + +

    {$info.title}

    + + +{include file="public/share" /} + + +
    + {$info.content} +
    + +{include file="public/user" /} +{/block} diff --git a/application/mobile/view/help/direct.html b/application/mobile/view/help/direct.html new file mode 100644 index 0000000..02fdad3 --- /dev/null +++ b/application/mobile/view/help/direct.html @@ -0,0 +1,50 @@ +{extend name="public/base" /} + +{block name="body"} + +
    + + + +
    +{/block} diff --git a/application/mobile/view/help/directdetail.html b/application/mobile/view/help/directdetail.html new file mode 100644 index 0000000..0e3f454 --- /dev/null +++ b/application/mobile/view/help/directdetail.html @@ -0,0 +1,76 @@ +{extend name="public/base" /} + +{block name="body"} + +

    {$info.title}

    + +{include file="public/share" /} + + +{switch name="$info.type" } +{case value="article"} + +
    + + {$info.content} +
    + +{/case} +{case value="video"} + +{notempty name="$info.extPath"} +
    +
    + +
    +
    +{/notempty} + +
    + {empty name="$info.extPath"} + + {/empty} + {$info.content} +
    + +{/case} +{case value="audio"} + +
    + +
    +
    + + {$info.content} +
    + +{/case} +{/switch} +{include file="public/user" /} +{/block} + +{block name="footer"}{/block} + +{block name="script"} + + +{/block} diff --git a/application/mobile/view/help/index.html b/application/mobile/view/help/index.html new file mode 100644 index 0000000..d209a97 --- /dev/null +++ b/application/mobile/view/help/index.html @@ -0,0 +1,29 @@ +{extend name="public/base" /} + +{block name="body"} + +
    + +
    + + +
    + + +

    【系统介绍】

    +
    + + +

    【资料完善详解】

    +
    + + +

    【功能使用】

    +
    + + +

    【如何用系统做直销】

    +
    +
    + +{/block} diff --git a/application/mobile/view/help/lists.html b/application/mobile/view/help/lists.html new file mode 100644 index 0000000..8ec16e0 --- /dev/null +++ b/application/mobile/view/help/lists.html @@ -0,0 +1,10 @@ +{extend name="public/base" /} + +{block name="body"} + +{/block} diff --git a/application/mobile/view/index/index.html b/application/mobile/view/index/index.html new file mode 100644 index 0000000..5f994bd --- /dev/null +++ b/application/mobile/view/index/index.html @@ -0,0 +1,7 @@ +{extend name="public/base" /} + +{block name="body"} +{include file="public/user" /} +{/block} + + diff --git a/application/mobile/view/index/info.html b/application/mobile/view/index/info.html new file mode 100644 index 0000000..52ed510 --- /dev/null +++ b/application/mobile/view/index/info.html @@ -0,0 +1,18 @@ +{extend name="public/base" /} +{block name="body"} + +
    +
    + +
    +

    {$info.nickname|default='未设置'} {$info.position|default='未设置'}

    +
    +
    + 手机:  {$info.mobile|default='未设置'} + 微信:  {$info.wechat|default='未设置'} + QQ:  {$info.qq|default='未设置'} + 城市:  {$info.province}{$info.city|default='未设置'} +
    + +{include file="public/code" /} +{/block} diff --git a/application/mobile/view/index/user_1.html b/application/mobile/view/index/user_1.html new file mode 100644 index 0000000..0613bdf --- /dev/null +++ b/application/mobile/view/index/user_1.html @@ -0,0 +1,36 @@ +{extend name="public/base" /} + +{block name="body"} + +
    +
    + +
    + +

    {$info.nickname|default='未设置'} {$info.position|default='未设置'}

    +
    + + + + + +{/block} diff --git a/application/mobile/view/index/user_2.html b/application/mobile/view/index/user_2.html new file mode 100644 index 0000000..7020629 --- /dev/null +++ b/application/mobile/view/index/user_2.html @@ -0,0 +1,43 @@ +{extend name="public/base" /} + +{block name="body"} + +
    +
    +
    + +
    +

    {$info.nickname|default='未设置'}

    +
    {$info.position|default='未设置'}
    +
    +
    + + + + +{/block} + +{block name="style"} + +{/block} diff --git a/application/mobile/view/index/user_3.html b/application/mobile/view/index/user_3.html new file mode 100644 index 0000000..8d35fc3 --- /dev/null +++ b/application/mobile/view/index/user_3.html @@ -0,0 +1,43 @@ +{extend name="public/base" /} + +{block name="body"} + +
    +
    +
    + +
    +

    {$info.nickname|default='未设置'}

    +
    {$info.position|default='未设置'}
    +
    +
    + + + + +{/block} + +{block name="style"} + +{/block} diff --git a/application/mobile/view/index/user_4.html b/application/mobile/view/index/user_4.html new file mode 100644 index 0000000..c85a149 --- /dev/null +++ b/application/mobile/view/index/user_4.html @@ -0,0 +1,38 @@ +{extend name="public/base" /} + +{block name="body"} + +
    + + +
    +
    + +
    +

    {$info.nickname|default='未设置'}

    +

    {$info.position|default='未设置'}

    +
    + + + + +{/block} diff --git a/application/mobile/view/invite/index.html b/application/mobile/view/invite/index.html new file mode 100644 index 0000000..4e66adc --- /dev/null +++ b/application/mobile/view/invite/index.html @@ -0,0 +1,35 @@ +{extend name="public/base" /} + +{block name="body"} + +
    + +
    +
    +
    + +
    +

    {$info.nickname}{$info.position}

    +

    我在{$Think.config.web_site_title}
    扫描下方二维码快来一起加入吧

    +
    +
    + +
    + +
    + 邀请好友 +
    +
    +{/block} +{block name="footer"}{/block} + +{block name="script"} + +{/block} diff --git a/application/mobile/view/login/forget.html b/application/mobile/view/login/forget.html new file mode 100644 index 0000000..8a519d0 --- /dev/null +++ b/application/mobile/view/login/forget.html @@ -0,0 +1,67 @@ +{extend name="public/base" /} +{block name="footer"}{/block} +{block name="body"} +
    + +
    + +
    +
    +{/block} + +{block name="script"} + +{/block} diff --git a/application/mobile/view/login/index.html b/application/mobile/view/login/index.html new file mode 100644 index 0000000..f95be4b --- /dev/null +++ b/application/mobile/view/login/index.html @@ -0,0 +1,29 @@ +{extend name="public/base" /} + +{block name="body"} +
    + +
    + + +
    +

    忘记密码?

    +
    +
    +

    快速登陆

    +  微信登陆 +
    +{/block} +{block name="footer"} +{/block} +{block name="script"} +{/block} diff --git a/application/mobile/view/login/info.html b/application/mobile/view/login/info.html new file mode 100644 index 0000000..7c47d54 --- /dev/null +++ b/application/mobile/view/login/info.html @@ -0,0 +1,12 @@ +{extend name="public/base" /} + +{block name="body"} +
    + +

    {$info}

    + 返回 +
    +{/block} + +{block name="footer"} +{/block} diff --git a/application/mobile/view/login/reg.html b/application/mobile/view/login/reg.html new file mode 100644 index 0000000..3fc5fad --- /dev/null +++ b/application/mobile/view/login/reg.html @@ -0,0 +1,70 @@ +{extend name="public/base" /} + +{block name="body"} +
    + +
    + + +
    +
    +{/block} +{block name="footer"} +{/block} +{block name="script"} + +{/block} diff --git a/application/mobile/view/member/add.html b/application/mobile/view/member/add.html new file mode 100644 index 0000000..bc867ba --- /dev/null +++ b/application/mobile/view/member/add.html @@ -0,0 +1,72 @@ +{extend name="public/base" /} +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + 保存 + + 添加会员 +
    +{/block} + +{block name="body"} +
    + +
    + + {notempty name="info.id"} + + {/notempty} +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +{/block} + + diff --git a/application/mobile/view/member/index.html b/application/mobile/view/member/index.html new file mode 100644 index 0000000..5c0fe8a --- /dev/null +++ b/application/mobile/view/member/index.html @@ -0,0 +1,26 @@ +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + + + 报单列表 +
    +{/block} + +{block name="body"} + +{/block} + diff --git a/application/mobile/view/member/info.html b/application/mobile/view/member/info.html new file mode 100644 index 0000000..3b5fc8b --- /dev/null +++ b/application/mobile/view/member/info.html @@ -0,0 +1,71 @@ +{extend name="public/base" /} +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + 编辑 + + 会员详情 +
    +{/block} + +{block name="body"} +
    +
    +

    姓名:

    +

    {$info.username|default=''}

    +
    +
    +

    会员级别:

    +

    {$info.level|default=''}

    +
    +
    +

    电话:

    +

    {$info.mobile|default=''}

    +
    +
    +

    身份证:

    +

    {$info.id_card|default=''}

    +
    +
    +

    银行:

    +

    {$info.bank|default=''}

    +
    +
    +

    卡号:

    +

    {$info.bank_card|default=''}

    +
    +
    +

    平台密码:

    +

    {$info.password|default=''}

    +
    +
    +

    推荐人:

    +

    {$info.referee|default=''}

    +
    +
    +

    推荐人编号:

    +

    {$info.referee_number|default=''}

    +
    +
    +

    接点人:

    +

    {$info.contact|default=''}

    +
    +
    +

    接点人编号:

    +

    {$info.contact_number|default=''}

    +
    +
    +

    添加时间:

    +

    {$info.create_time|default=''}

    +
    + + +
    +{/block} + + diff --git a/application/mobile/view/pay/vip.html b/application/mobile/view/pay/vip.html new file mode 100644 index 0000000..d9a6517 --- /dev/null +++ b/application/mobile/view/pay/vip.html @@ -0,0 +1,99 @@ +{extend name="public/base" /} + +{block name="footer"} + +
    +
    +

    支付金额:298元

    +

    支付方式:微信支付

    +

    商品名称:超级助手VIP(有效期一年)

    +

    订 单  号:{$orderid}

    +
    + +
    + +
    +
    + +
    +
    +{/block} + +{block name="script"} + +{/block} + diff --git a/application/mobile/view/public/base.html b/application/mobile/view/public/base.html new file mode 100644 index 0000000..6281ea8 --- /dev/null +++ b/application/mobile/view/public/base.html @@ -0,0 +1,35 @@ + + + + + + + {$meta_title|default=$Think.config.web_site_title} + + + + + + {block name="swiper"}{/block} + {block name="style"}{/block} + + + {block name="header"}{/block} + + {block name="footer"} + {include file="public/footer" /} + {/block} +
    + {block name="body"}{/block} +
    + + + + + {block name="script"}{/block} + {empty name="showWx"} + {include file="public/wx" /} + {/empty} + + + diff --git a/application/mobile/view/public/code.html b/application/mobile/view/public/code.html new file mode 100644 index 0000000..7757554 --- /dev/null +++ b/application/mobile/view/public/code.html @@ -0,0 +1,10 @@ +{gt name=":UID" value="0"} +{else/} +
    +

    关注微信公众号[下次免登陆]

    +
    + +
    +
    +{/gt} + diff --git a/application/mobile/view/public/footer.html b/application/mobile/view/public/footer.html new file mode 100644 index 0000000..2de71e4 --- /dev/null +++ b/application/mobile/view/public/footer.html @@ -0,0 +1,36 @@ +{gt name=":UID" value="0"} +{empty name="showfoot"} + +{/empty} +{/gt} + diff --git a/application/mobile/view/public/share.html b/application/mobile/view/public/share.html new file mode 100644 index 0000000..f18cd7a --- /dev/null +++ b/application/mobile/view/public/share.html @@ -0,0 +1,8 @@ + +{notempty name="$shareUser.is_vip"} +
    + 拨打电话 + 加TA微信 +
    +{/notempty} + diff --git a/application/mobile/view/public/user.html b/application/mobile/view/public/user.html new file mode 100644 index 0000000..4fdf945 --- /dev/null +++ b/application/mobile/view/public/user.html @@ -0,0 +1,34 @@ + +{notempty name="$shareUser"} +
    +
    +
    +

    未上传

    + +
    +

    {$shareUser.nickname|default='未设置'}

    +
    {$shareUser.position|default='未设置'}
    +
    +
    +

    {$shareUser.mobile|default='未设置'}

    +

    {$shareUser.qq|default='未设置'}

    +

    {$shareUser.wechat|default='未设置'}

    +
    +
    + +
    +
    + {eq name='$shareUser.is_vip' value='1'} + 看TA首页 + {else/} + 我要注册 + {/eq} + +
    +
    + + +{include file="public/code" /} + +{/notempty} + diff --git a/application/mobile/view/public/wx.html b/application/mobile/view/public/wx.html new file mode 100644 index 0000000..aecde5b --- /dev/null +++ b/application/mobile/view/public/wx.html @@ -0,0 +1,39 @@ + + diff --git a/application/mobile/view/qrcode/comment.html b/application/mobile/view/qrcode/comment.html new file mode 100644 index 0000000..d661cc9 --- /dev/null +++ b/application/mobile/view/qrcode/comment.html @@ -0,0 +1,59 @@ +{extend name="public/base" /} + +{block name="body"} + + + + + +
    +
    +

    给TA留言

    + + +
    + 取消 + 发布 +
    +
    +
    +{/block} + +{block name="script"} + +{/block} +{block name="footer"}{/block} diff --git a/application/mobile/view/qrcode/index.html b/application/mobile/view/qrcode/index.html new file mode 100644 index 0000000..e0381b2 --- /dev/null +++ b/application/mobile/view/qrcode/index.html @@ -0,0 +1,71 @@ +{extend name="public/base" /} + +{block name="body"} + +
    +

    {$info.nickname}{$info.position}

    +

    电话:{$info.mobile|default='未设置'}

    +

    地区:{$info.province}{$info.city|default='未设置'}

    +

    Email:{$info.email|default='未设置'}

    +
    +
    + +
    +
    + +
    二维码
    +
    +
    +
    + + + + + +
    +
    + + + +
    +
    + +{eq name=":UID" value="$Think.get.uid"} +
    + +
    +{/eq} + +{/block} +{block name="footer"} +{/block} +{block name="script"} + +{/block} diff --git a/application/mobile/view/score/index.html b/application/mobile/view/score/index.html new file mode 100644 index 0000000..8176f02 --- /dev/null +++ b/application/mobile/view/score/index.html @@ -0,0 +1,34 @@ +{extend name="public/base" /} +{block name="body"} + + + + + +
    + 积分明细 积分规则 +
    + + +{/block} diff --git a/application/mobile/view/score/info.html b/application/mobile/view/score/info.html new file mode 100644 index 0000000..5aa61c0 --- /dev/null +++ b/application/mobile/view/score/info.html @@ -0,0 +1,12 @@ +{extend name="public/base" /} +{block name="body"} + + +
    +{volist name="list" id="vo"} + {$vo.title}  {$vo.remark|default=''} +{/volist} +
    + +{include file="public/code" /} +{/block} diff --git a/application/mobile/view/setting/bindwechat.html b/application/mobile/view/setting/bindwechat.html new file mode 100644 index 0000000..6776f98 --- /dev/null +++ b/application/mobile/view/setting/bindwechat.html @@ -0,0 +1,58 @@ +{extend name="public/base" /} +{block name="header"} +
    + +  返回 + + + + + 绑定微信 +
    +{/block} +{block name="body"} +
    + +
    +
    +

    当前资料

    +
    + +
    +

    {$user.info.nickname}

    + +
    +

    + +

    +
    +

    微信资料

    +
    + +
    +

    {$wechat.nickname}

    + +
    +
    + + {notempty name="userInfo"} + {neq name=":UID" value="$userInfo.id"} +
    + +

    您已绑定其他账户,如需重新绑定请先解除绑定

    + +
    + {else/} +
    + +

    您已经绑定过当前帐号

    +
    + {/neq} + {else/ } +
    + +
    + {/notempty} +
    + +{/block} diff --git a/application/mobile/view/setting/city.html b/application/mobile/view/setting/city.html new file mode 100644 index 0000000..a3fbcf6 --- /dev/null +++ b/application/mobile/view/setting/city.html @@ -0,0 +1,54 @@ +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + 保存 + + 修改地区 +
    +{/block} + +{block name="body"} +
    + +
    +{/block} + +{block name="script"} + +{/block} diff --git a/application/mobile/view/setting/email.html b/application/mobile/view/setting/email.html new file mode 100644 index 0000000..781a681 --- /dev/null +++ b/application/mobile/view/setting/email.html @@ -0,0 +1,24 @@ +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + 保存 + + 修改Email +
    +{/block} + +{block name="body"} +
    + +
    +{/block} diff --git a/application/mobile/view/setting/index.html b/application/mobile/view/setting/index.html new file mode 100644 index 0000000..34ec278 --- /dev/null +++ b/application/mobile/view/setting/index.html @@ -0,0 +1,161 @@ +{extend name="public/base" /} + +{block name="header"} +{/block} +{block name="body"} + + + + + +
    + 切换用户 +
    +{/block} + +{block name="script"} + + +{/block} diff --git a/application/mobile/view/setting/mobile.html b/application/mobile/view/setting/mobile.html new file mode 100644 index 0000000..d623ae8 --- /dev/null +++ b/application/mobile/view/setting/mobile.html @@ -0,0 +1,24 @@ +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + 保存 + + 修改手机号码 +
    +{/block} + +{block name="body"} +
    + +
    +{/block} diff --git a/application/mobile/view/setting/nickname.html b/application/mobile/view/setting/nickname.html new file mode 100644 index 0000000..9ff97a2 --- /dev/null +++ b/application/mobile/view/setting/nickname.html @@ -0,0 +1,24 @@ +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + 保存 + + 修改姓名 +
    +{/block} + +{block name="body"} +
    + +
    +{/block} diff --git a/application/mobile/view/setting/password.html b/application/mobile/view/setting/password.html new file mode 100644 index 0000000..0eb8410 --- /dev/null +++ b/application/mobile/view/setting/password.html @@ -0,0 +1,32 @@ +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + 保存 + + 修改密码 +
    +{/block} + +{block name="body"} +
    + +
    +{/block} diff --git a/application/mobile/view/setting/position.html b/application/mobile/view/setting/position.html new file mode 100644 index 0000000..f411f4a --- /dev/null +++ b/application/mobile/view/setting/position.html @@ -0,0 +1,34 @@ +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + 保存 + + 修改职位 +
    +{/block} + +{block name="body"} +
    + +
    +{/block} diff --git a/application/mobile/view/setting/qq.html b/application/mobile/view/setting/qq.html new file mode 100644 index 0000000..2aa6538 --- /dev/null +++ b/application/mobile/view/setting/qq.html @@ -0,0 +1,24 @@ +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + 保存 + + 修改密码 +
    +{/block} + +{block name="body"} +
    + +
    +{/block} diff --git a/application/mobile/view/setting/qrcode.html b/application/mobile/view/setting/qrcode.html new file mode 100644 index 0000000..9425d9e --- /dev/null +++ b/application/mobile/view/setting/qrcode.html @@ -0,0 +1,71 @@ +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + 保存 + + 修改二维码 +
    +{/block} + +{block name="body"} +
    +
    + + + +

    上传微信二维码

    +
    +
    +{/block} + +{block name="script"} + + +{/block} diff --git a/application/mobile/view/setting/template.html b/application/mobile/view/setting/template.html new file mode 100644 index 0000000..4aa945b --- /dev/null +++ b/application/mobile/view/setting/template.html @@ -0,0 +1,48 @@ +{extend name="public/base" /} + +{block name="body"} + +{/block} + +{block name="script"} +{eq name="user->info->is_vip" value="0"} + +{else/} + +{/eq} +{/block} diff --git a/application/mobile/view/setting/wechat.html b/application/mobile/view/setting/wechat.html new file mode 100644 index 0000000..916ce29 --- /dev/null +++ b/application/mobile/view/setting/wechat.html @@ -0,0 +1,24 @@ +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + 保存 + + 修改微信号 +
    +{/block} + +{block name="body"} +
    + +
    +{/block} diff --git a/application/mobile/view/share/detail.html b/application/mobile/view/share/detail.html new file mode 100644 index 0000000..ce0b43a --- /dev/null +++ b/application/mobile/view/share/detail.html @@ -0,0 +1,28 @@ +{extend name="public/base" /} + +{block name="body"} + +

    {$info.title}

    + + +{include file="public/share" /} + + +{notempty name="$info.id"} +
    + +
    +{$info.content} +
    +{else/} +
    + +

    文章已被删除

    +
    +{/notempty} + +{include file="public/user" /} +{/block} + + + diff --git a/application/mobile/view/share/index.html b/application/mobile/view/share/index.html new file mode 100644 index 0000000..8735407 --- /dev/null +++ b/application/mobile/view/share/index.html @@ -0,0 +1,40 @@ +{extend name="public/base" /} + +{block name="body"} + + + + + +{/block} + +{block name="script"} + +{/block} diff --git a/application/mobile/view/share/list.html b/application/mobile/view/share/list.html new file mode 100644 index 0000000..ef45f0b --- /dev/null +++ b/application/mobile/view/share/list.html @@ -0,0 +1,13 @@ +{volist name="list" id="vo"} +
  • + +
    + +
    +
    +

    {$vo.title}

    +

    {$vo.description}

    +
    +
    +
  • +{/volist} diff --git a/application/mobile/view/share/lists.html b/application/mobile/view/share/lists.html new file mode 100644 index 0000000..c5ed662 --- /dev/null +++ b/application/mobile/view/share/lists.html @@ -0,0 +1,11 @@ +{extend name="public/base" /} + +{block name="body"} + + +
    + 点击更多 +
    +{/block} diff --git a/application/mobile/view/suggest/index.html b/application/mobile/view/suggest/index.html new file mode 100644 index 0000000..6e37520 --- /dev/null +++ b/application/mobile/view/suggest/index.html @@ -0,0 +1,19 @@ +{extend name="public/base" /} + +{block name="header"} +
    + +  返回 + + + 提交 + + 意见反馈 +
    +{/block} + +{block name="body"} +
    + +
    +{/block} diff --git a/application/mobile/view/user/user_1.html b/application/mobile/view/user/user_1.html new file mode 100644 index 0000000..9dea5b9 --- /dev/null +++ b/application/mobile/view/user/user_1.html @@ -0,0 +1,34 @@ +{extend name="public/base" /} + +{block name="body"} + +
    +
    + +
    +

    {$info.info.nickname} 业务经理

    +
    + + + + +{/block} diff --git a/application/mobile/view/user/user_2.html b/application/mobile/view/user/user_2.html new file mode 100644 index 0000000..8f0d344 --- /dev/null +++ b/application/mobile/view/user/user_2.html @@ -0,0 +1,43 @@ +{extend name="public/base" /} + +{block name="body"} + +
    +
    +
    + +
    +

    {$info.info.nickname}

    +
    业务经理
    +
    +
    + + + + +{/block} + +{block name="style"} + +{/block} diff --git a/application/mobile/view/user/user_3.html b/application/mobile/view/user/user_3.html new file mode 100644 index 0000000..7390c06 --- /dev/null +++ b/application/mobile/view/user/user_3.html @@ -0,0 +1,43 @@ +{extend name="public/base" /} + +{block name="body"} + +
    +
    +
    + +
    +

    {$info.info.nickname}

    +
    业务经理
    +
    +
    + + + + +{/block} + +{block name="style"} + +{/block} diff --git a/application/mobile/view/user/user_4.html b/application/mobile/view/user/user_4.html new file mode 100644 index 0000000..c4b7e13 --- /dev/null +++ b/application/mobile/view/user/user_4.html @@ -0,0 +1,38 @@ +{extend name="public/base" /} + +{block name="body"} + +
    + + +
    +
    + +
    +

    {$info.info.nickname}

    +

    业务经理

    +
    + + + + +{/block} diff --git a/application/mobile/view/vip/index.html b/application/mobile/view/vip/index.html new file mode 100644 index 0000000..61fdd6f --- /dev/null +++ b/application/mobile/view/vip/index.html @@ -0,0 +1,88 @@ +{extend name="public/base" /} + +{block name="body"} +
    +

    您当前是:{$user.info.vip_text}

    +

    到期时间:{$user.info.vip_time}

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    权限普通用户VIP用户
    使用本系统
    发布文章
    分享经验
    直播技巧
    文章作者是自己
    文章采集
    显示个人资料
    模版切换
    文章下方联系方式展示
    +
    +{eq name='$user->info->is_vip' value='1'} +
    + +
    +{else /} +
    + +
    +{/eq} +{/block} + +{block name="footer"}{/block} + +{block name="script"} + +{/block} diff --git a/application/openapi/config.php b/application/openapi/config.php new file mode 100644 index 0000000..78c9241 --- /dev/null +++ b/application/openapi/config.php @@ -0,0 +1,20 @@ + | +// +------------------------------------------------+ + +return [ + // +---------------------------------------------------------------------- + // | 日志设置 + // +---------------------------------------------------------------------- + 'log' => [ + 'type' => 'file', + 'path' => LOG_PATH . 'openapi/', + 'time_format' => ' c ', + 'file_size' => 2097152, + ], +]; diff --git a/application/openapi/controller/Pay.php b/application/openapi/controller/Pay.php new file mode 100644 index 0000000..2fe4fcf --- /dev/null +++ b/application/openapi/controller/Pay.php @@ -0,0 +1,37 @@ + | +// +------------------------------------------------+ +namespace app\openapi\controller; + +use app\common\model\MemberInfo; +use app\common\model\VipOrder; +use app\common\service\Score as ScoreService; +use cjango\Wechat; +use think\Config; + +class Pay +{ + + public function vip() + { + $config = Config::get('wechat'); + Wechat::instance($config); + $res = Wechat\Pay::parsePayRequest(); + if ($res) { + $order = VipOrder::where('orderid', $res['out_trade_no'])->find(); + $order->save(['status' => 20, 'model' => 'weixin']); + $MemberInfo = MemberInfo::get($order->uid); + $time = ($MemberInfo->is_vip == 1) ? $MemberInfo->vip_end_time : time(); + MemberInfo::update(['is_vip' => 1, 'vip_time' => $time + 31536000], ['uid' => $order->uid]); + ScoreService::vip($order->uid); + Wechat\Pay::returnNotify(); + } else { + Wechat\Pay::returnNotify('处理失败'); + } + } +} diff --git a/application/openapi/controller/Wechat.php b/application/openapi/controller/Wechat.php new file mode 100644 index 0000000..77fbb13 --- /dev/null +++ b/application/openapi/controller/Wechat.php @@ -0,0 +1,32 @@ + | +// +------------------------------------------------+ +namespace app\openapi\controller; + +use cjango\Wechat as Wx; +use think\Config; +use think\Request; + +class Wechat +{ + + private $request; + + public function __construct() + { + $config = Config::get('wechat'); + Wx::instance($config); + Wx::valid(); + $this->request = Wx\Reply::request(); + } + + public function index() + { + Wx\Reply::response($this->request['content']); + } +} diff --git a/application/route.php b/application/route.php new file mode 100644 index 0000000..6460601 --- /dev/null +++ b/application/route.php @@ -0,0 +1,29 @@ + +// +---------------------------------------------------------------------- + +return [ + + '__pattern__' => [ + 'name' => '\w+', + 'id' => '\d+', + 'uid' => '\d+', + ], + + '__domain__' => [ + 'www.helper.skl2010.com' => 'index', + 'm.helper.skl2010.com' => 'mobile', + 'console.helper.skl2010.com' => 'system', + 'api.helper.skl2010.com' => 'openapi', + '*' => 'index', + ], + + // 'u/:uid' => ['user/index', ['domain' => 'm.helper.cnskl.com']], +]; // 'new/:id','News/read',); diff --git a/application/system/common.php b/application/system/common.php new file mode 100644 index 0000000..a7f99d2 --- /dev/null +++ b/application/system/common.php @@ -0,0 +1,8 @@ + | +// +------------------------------------------------+ diff --git a/application/system/config.php b/application/system/config.php new file mode 100644 index 0000000..24f1360 --- /dev/null +++ b/application/system/config.php @@ -0,0 +1,43 @@ + | +// +------------------------------------------------+ + +return [ + // +---------------------------------------------------------------------- + // | 日志设置 + // +---------------------------------------------------------------------- + 'log' => [ + 'type' => 'file', + 'path' => LOG_PATH . 'system/', + 'time_format' => ' c ', + 'file_size' => 2097152, + ], + // +---------------------------------------------------------------------- + // | 模板设置 + // +---------------------------------------------------------------------- + 'template' => [ + 'tpl_cache' => false, + 'strip_space' => false, + 'taglib_pre_load' => '', + 'cache_path' => TEMP_PATH . 'system/', + ], + 'view_replace_str' => [ + '__SELF__' => __SELF__, + '__STATIC__' => '/static/system', + '__JS__' => '/static/system/js', + '__CSS__' => '/static/system/css', + '__IMG__' => '/static/system/images', + '__LIB__' => '/static/system/lib', + ], + // +---------------------------------------------------------------------- + // | 数据备份设置 + // +---------------------------------------------------------------------- + 'backdata' => [ + 'path' => realpath('../backup/') . DS, + ], +]; diff --git a/application/system/controller/Article.php b/application/system/controller/Article.php new file mode 100644 index 0000000..bebaa1f --- /dev/null +++ b/application/system/controller/Article.php @@ -0,0 +1,86 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\Article as ArticleModel; +use app\common\service\Article as ArticleService; +use think\Request; + +class Article extends _Init +{ + /** + * 文章列表 + * @param string $title 要搜索的文章标题 + * @return [type] [description] + */ + public function index($title = '') + { + $map['status'] = ['egt', 0]; + if (!empty($title)) { + $map['title'] = ['like', "%$title%"]; + } + $this->list = ArticleModel::where($map)->paginate(); + return $this->fetch(); + } + + /** + * 添加文章 + * @param Request $Request 数据集 + */ + public function add(Request $Request) + { + if (IS_POST) { + $data = $Request->post(); + $result = ArticleService::create($data); + return $this->back($result); + } else { + $this->list = ArticleService::categoryList(); + return $this->fetch(); + } + } + /** + * 编辑文章 + * @param Request $Request 数据集 + * @param [type] $id 文章id + * @return [type] 返回 编辑的结果 + */ + public function edit(Request $Request, $id) + { + if (IS_POST) { + $data = $Request->post(); + $result = ArticleService::edit($data); + return $this->back($result); + } else { + $this->list = ArticleService::categoryList(); + $this->info = ArticleModel::get($id); + return $this->fetch('add'); + } + } + /** + * 删除文章 + * @param [type] $id 文章id + * @return [type] [description] + */ + public function del($id) + { + $result = ArticleService::del($id); + return $this->back($result); + } + /** + * 修改文章状态 + * @param [type] $id 文章id + * @param [type] $status 状态 + * @return [type] 修改文章结果 + */ + public function status($id, $status) + { + $result = ArticleService::status($id, $status); + return $this->back($result); + } +} diff --git a/application/system/controller/Articlemodel.php b/application/system/controller/Articlemodel.php new file mode 100644 index 0000000..15ed436 --- /dev/null +++ b/application/system/controller/Articlemodel.php @@ -0,0 +1,84 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\ArticleModel as ArticleModelModel; +use app\common\service\ArticleModel as ArticleModelService; +use think\Request; + +class Articlemodel extends _Init +{ + /** + * 文章列表 + * @param string $title 要搜索的文章标题 + * @return [type] [description] + */ + public function index($title = '') + { + $map['status'] = ['egt', 0]; + $this->list = ArticleModelModel::where($map)->paginate(); + return $this->fetch(); + } + + /** + * 添加文章 + * @param Request $Request 数据集 + */ + public function add(Request $Request) + { + if (IS_POST) { + $data = $Request->post(); + $result = ArticleModelService::create($data); + return $this->back($result); + } else { + return $this->fetch(); + } + } + + /** + * 编辑文章 + * @param Request $Request 数据集 + * @param [type] $id 文章id + * @return [type] 返回 编辑的结果 + */ + public function edit(Request $Request, $id) + { + if (IS_POST) { + $data = $Request->post(); + $result = ArticleModelService::edit($data); + return $this->back($result); + } else { + $this->info = ArticleModelModel::get($id); + return $this->fetch('add'); + } + } + + /** + * 删除文章 + * @param [type] $id 文章id + * @return [type] [description] + */ + public function del($id) + { + $result = ArticleModelService::del($id); + return $this->back($result); + } + + /** + * 修改文章状态 + * @param [type] $id 文章id + * @param [type] $status 状态 + * @return [type] 修改文章结果 + */ + public function status($id, $status) + { + $result = ArticleModelService::status($id, $status); + return $this->back($result); + } +} diff --git a/application/system/controller/Auth.php b/application/system/controller/Auth.php new file mode 100644 index 0000000..0fb70f1 --- /dev/null +++ b/application/system/controller/Auth.php @@ -0,0 +1,148 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\Auth as AuthModel; +use app\common\model\Member as MemberModel; +use app\common\service\Auth as AuthService; +use think\Request; + +class Auth extends _Init +{ + /** + * 授权列表 + * @return [type] [description] + */ + public function index() + { + $map['status'] = ['egt', 0]; + + $this->list = AuthModel::where($map)->paginate(); + return $this->fetch(); + } + /** + * 添加授权分组 + * @param Request $request 要添加的分组数据 + */ + public function add(Request $request) + { + if (IS_POST) { + $data = $request->post(); + $result = AuthService::add($data); + return $this->back($result); + } else { + return $this->fetch(); + } + } + /** + * 编辑授权分组 + * @param [type] $id 授权分组id + * @return [type] [description] + */ + public function edit(Request $request, $id) + { + if (IS_POST) { + $data = $request->post(); + $result = AuthService::edit($data); + return $this->back($result); + } else { + $this->info = AuthModel::get($id); + return $this->fetch('add'); + } + } + + /** + * 删除分组 + * @param [type] $id 授权分组 id + * @return [type] [description] + */ + public function del($id) + { + $result = AuthService::del($id); + return $this->back($result); + } + + /** + * 设置分组状态 正常 和 禁用 + * @param [type] $id 授权分组 id + * @param [type] $status 成功 还是 失败 失败返回失败的信息 + * @return [type] [description] + */ + public function status($id, $status) + { + $result = AuthService::status($id, $status); + return $this->back($result); + } + + /** + * 菜单授权 + * @param [type] $id 授权分组id + * @return [type] [description] + */ + public function menu(Request $request, $id) + { + if (IS_POST) { + $rules = $request->post('rules/a'); + $result = AuthService::setAuthRuels($id, $rules); + return $this->back($result); + } else { + $this->assign('node_list', AuthService::getAuthRules($id)); + return $this->fetch(); + } + } + + /** + * 用户授权页面 + * @param Request $request 提交的信息 + * @param [type] $id 分组id + * @return [type] 用户列表 + */ + public function user(Request $request, $id) + { + $authed = AuthService::getAuthedUids($id); + + if (IS_POST) { + $username = $request->post('username'); + $list = MemberModel::field('id,username')->whereOr(function ($query) use ($username) { + $query->whereOr('id', $username)->whereOr('username', 'like', "%{$username}%"); + })->where('status', 1)->where('id', 'notin', $authed)->limit(10)->select(); + foreach ($list as $key => &$value) { + $value['nickname'] = $value->info->nickname; + } + return $this->success('', '', $list); + } else { + $this->list = MemberModel::where('status', 1)->where('id', 'in', $authed)->paginate(); + return $this->fetch(); + } + } + + /** + * 把用户移除分组 + * @param [type] $id 分组id + * @param [type] $uid 用户id + * @return [type] 返回结果 + */ + public function remove($id, $uid) + { + $result = AuthService::removeAuthUser($id, $uid); + return $this->back($result); + } + + /** + * 设置用户授权 + * @param [type] $id 分组id + * @param [type] $uid 用户ids + * @return [type] [description] + */ + public function insert($id, $uid) + { + $result = AuthService::setAuthUser($id, $uid); + return $this->back($result); + } +} diff --git a/application/system/controller/Banner.php b/application/system/controller/Banner.php new file mode 100644 index 0000000..7b2b435 --- /dev/null +++ b/application/system/controller/Banner.php @@ -0,0 +1,113 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\Advert as AdvertModel; +use app\common\model\AdvertDetail as AdvertDetailModel; +use app\common\service\Advert as AdvertService; +use app\common\service\AdvertDetail as AdvertDetailService; + +class Banner extends _Init +{ + + public function index($title = '') + { + $map['status'] = ['egt', 0]; + if ($title) { + $map['title'] = ['like', "%{$title}%"]; + } + $this->list = AdvertModel::where($map)->paginate(); + return $this->fetch(); + } + + public function add() + { + if (IS_POST) { + $data = $this->request->post(); + $res = AdvertService::add($data); + return $this->back($res); + } else { + return $this->fetch(); + } + } + + public function edit($id) + { + if (IS_POST) { + $data = $this->request->post(); + $res = AdvertService::edit($data); + return $this->back($res); + } else { + $this->assign('info', AdvertModel::get($id)); + return $this->fetch('add'); + } + } + + public function del($id) + { + $res = AdvertService::del($id); + return $this->back($res); + } + + /** + * 修改状态 + * @param [type] $id [description] + * @param [type] $status [description] + * @param [type] $type [description] + */ + public function status($id, $status, $type) + { + $res = AdvertService::status($id, $status, $type); + return $this->back($res); + } + + public function detail($advert_id) + { + $this->info = AdvertModel::get($advert_id); + return $this->fetch(); + } + + public function banadd($advert_id) + { + if (IS_POST) { + $data = $this->request->post(); + $data['advert_id'] = $advert_id; + unset($data['image']); + $res = AdvertDetailService::add($data); + return $this->back($res); + } else { + return $this->fetch(); + } + } + + public function banedit($id) + { + if (IS_POST) { + $data = $this->request->post(); + unset($data['image']); + $res = AdvertDetailService::edit($data); + return $this->back($res); + } else { + $this->assign('info', AdvertDetailModel::get($id)); + return $this->fetch('banadd'); + } + } + + public function bandel($id) + { + $res = AdvertDetailService::del($id); + return $this->back($res); + } + + public function banstatus($id, $status, $type) + { + $res = AdvertDetailService::status($id, $status, $type); + return $this->back($res); + } +} diff --git a/application/system/controller/Category.php b/application/system/controller/Category.php new file mode 100644 index 0000000..135e809 --- /dev/null +++ b/application/system/controller/Category.php @@ -0,0 +1,93 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\Category as CategoryModel; +use app\common\service\Category as CategoryService; +use think\Request; + +class Category extends _Init +{ + /** + * 分类列表 + * @param string $pid 上级分类id + * @return [type] [description] + */ + public function index($pid = '') + { + if (is_numeric($pid)) { + $map['pid'] = $pid; + } else { + $map['pid'] = 0; + } + $map['status'] = ['egt', 0]; + + $this->list = CategoryModel::where($map)->order('sort desc,update_time desc')->paginate(); + $this->info = CategoryModel::get($pid); + $this->menu = CategoryService::tree(); + return $this->fetch(); + } + + /** + * 添加分类 + * @param Request $Request [description] + * @param integer $pid 上级id + */ + public function add(Request $Request, $pid = 0) + { + if (IS_POST) { + $data = $Request->post(); + $result = CategoryService::add($data); + return $this->back($result); + } else { + $info['pid'] = $pid; + $this->info = $info; + $this->up_cates = CategoryService::treeList(); + return $this->fetch(); + } + } + /** + * 编辑分类 + * @param [type] $id 分类id + */ + public function edit(Request $Request, $id) + { + if (IS_POST) { + $data = $Request->post(); + $result = CategoryService::edit($data); + return $this->back($result); + } else { + $this->up_cates = CategoryService::treeList($id); + $this->info = CategoryModel::get($id); + return $this->fetch('add'); + } + } + /** + * 删除分类 + * @param [type] $id 分类id + * @return [type] 返回删除的结果 + */ + public function del($id) + { + $result = CategoryService::del($id); + $this->back($result); + } + /** + * 设置分类状态 + * @param [type] $id 分类id + * @param [type] $status 状态 + * @param [type] $type 要设置的字段 status + * @return [type] 返回结果 + */ + public function status($id, $status, $type) + { + $result = CategoryService::status($id, $status, $type); + return $this->back($result); + } +} diff --git a/application/system/controller/Cause.php b/application/system/controller/Cause.php new file mode 100644 index 0000000..5183f15 --- /dev/null +++ b/application/system/controller/Cause.php @@ -0,0 +1,90 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\Content as ContentModel; +use app\common\service\Content as ContentService; +use think\Request; + +class Cause extends _Init +{ + + /** + * 文章列表 + * @param string $title 要搜索的文章标题 + * @return [type] [description] + */ + + public function index($title = '') + { + $CategoryIds = ContentService::getCategoryIds('cause'); + $map = [ + 'status' => ['egt', 0], + 'title' => ['like', "%$title%"], + 'category_id' => ['in', $CategoryIds], + ]; + $this->list = ContentModel::where($map)->order('id desc')->paginate(); + return $this->fetch(); + } + + /** + * 添加文章 + * @param Request $Request 数据集 + */ + public function add(Request $Request) + { + if (IS_POST) { + $data = $Request->post(); + $result = ContentService::create($data); + return $this->back($result); + } else { + $this->list = ContentService::categoryList('cause'); + return $this->fetch(); + } + } + /** + * 编辑文章 + * @param Request $Request 数据集 + * @param [type] $id 文章id + * @return [type] 返回 编辑的结果 + */ + public function edit(Request $Request, $id) + { + if (IS_POST) { + $data = $Request->post(); + $result = ContentService::edit($data); + return $this->back($result); + } else { + $this->list = ContentService::categoryList('Cause'); + $this->info = ContentModel::get($id); + return $this->fetch('add'); + } + } + /** + * 删除文章 + * @param [type] $id 文章id + * @return [type] [description] + */ + public function del($id) + { + $result = ContentService::del($id); + return $this->back($result); + } + /** + * 修改文章状态 + * @param [type] $id 文章id + * @param [type] $status 状态 + * @return [type] 修改文章结果 + */ + public function status($id, $status) + { + $result = ContentService::status($id, $status); + return $this->back($result); + } +} diff --git a/application/system/controller/Config.php b/application/system/controller/Config.php new file mode 100644 index 0000000..21832ce --- /dev/null +++ b/application/system/controller/Config.php @@ -0,0 +1,114 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\Config as ConfigModel; +use app\common\service\Config as ConfigService; +use think\Cache; +use think\Request; + +class Config extends _Init +{ + + /** + * 批量设置的页面 + * @return [type] [description] + */ + public function index() + { + if (IS_POST) { + $config = $this->request->post('config/a'); + $result = ConfigService::batchEdit($config); + $this->back($result); + } else { + $map = [ + 'status' => 1, + ]; + $this->list = ConfigModel::where($map)->order('sort asc')->select(); + return $this->fetch(); + } + } + + /** + * 所有参数页面 + * @return [type] [description] + */ + public function params() + { + $map = [ + 'status' => ['egt', 0], + ]; + $this->list = ConfigModel::where($map)->order('sort asc,create_time desc,update_time desc')->paginate(10); + return $this->fetch(); + } + + /** + * 清除缓存 + */ + public function clear() + { + if (Cache::clear()) { + return $this->success('清除缓存成功'); + } else { + return $this->error(); + } + } + + /** + * 新增配置 + */ + public function add(Request $request) + { + if (IS_POST) { + $data = $request->post(); + $result = ConfigService::add($data); + return $this->back($result); + } else { + return $this->fetch(); + } + } + + /** + * 编辑配置 + * @param [type] $id [description] + */ + public function edit(Request $request, $id) + { + if (IS_POST) { + $data = $request->post(); + $result = ConfigService::edit($data); + return $this->back($result); + } else { + $this->assign('info', ConfigModel::find($id)); + return $this->fetch('add'); + } + } + + /** + * 修改状态 + * @param [type] $id [description] + * @param [type] $status [description] + * @param [type] $type [description] + */ + public function status($id, $status, $type) + { + $result = ConfigService::status($id, $status, $type); + return $this->back($result); + } + + /** + * 删除 + * @param [type] $id [description] + */ + public function del($id) + { + $result = ConfigService::del($id); + return $this->back($result); + } +} diff --git a/application/system/controller/Database.php b/application/system/controller/Database.php new file mode 100644 index 0000000..200a959 --- /dev/null +++ b/application/system/controller/Database.php @@ -0,0 +1,67 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\service\Database as DatabaseService; + +class Database extends _Init +{ + + public function index() + { + $list = DatabaseService::getTables(); + $this->assign('list', $list); + return $this->fetch(); + } + + /** + * 备份的方法 + * @param [type] $tables [description] + * @param [type] $id [description] + * @param [type] $start [description] + * @return [type] [description] + */ + public function backup($tables = null, $id = null, $start = null) + { + $result = DatabaseService::backup($tables, $id, $start); + if (is_array($result) && !empty($result)) { + return $this->success($result['msg'], '', $result['data']); + } else { + return $this->error($result); + } + } + + /** + * 备份列表 + * @return [type] [description] + */ + public function import() + { + $list = DatabaseService::backupList(); + $this->assign('list', $list); + return $this->fetch(); + } + + /** + * 删除备份文件 + * @param [type] $time [description] + * @return [type] [description] + */ + public function del($time) + { + if (IS_POST && is_array($time)) { + $result = DatabaseService::del($time); + if ($result === true) { + return $this->success('删除成功'); + } else { + return $this->error($result); + } + } + } +} diff --git a/application/system/controller/Direct.php b/application/system/controller/Direct.php new file mode 100644 index 0000000..91eac04 --- /dev/null +++ b/application/system/controller/Direct.php @@ -0,0 +1,113 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\Direct as DirectModel; +use app\common\service\Direct as DirectService; +use think\Request; + +class Direct extends _Init +{ + /** + * 文章列表 + * @param string $title 要搜索的文章标题 + * @return [type] [description] + */ + public function index($type = '', $title = '') + { + $map['status'] = ['egt', 0]; + if (!empty($type)) { + $map['type'] = $type; + } + if (!empty($title)) { + $map['title'] = ['like', "%$title%"]; + } + $this->list = DirectModel::where($map)->order("sort asc,create_time desc")->paginate(); + + return $this->fetch(); + } + + /** + * 添加文章 + * @param Request $Request 数据集 + */ + public function add(Request $Request) + { + if (IS_POST) { + $data = $Request->post(); + $result = DirectService::create($data); + return $this->back($result); + } else { + return $this->fetch(); + } + } + /** + * 编辑文章 + * @param Request $Request 数据集 + * @param [type] $id 文章id + * @return [type] 返回 编辑的结果 + */ + public function edit(Request $Request, $id) + { + if (IS_POST) { + $data = $Request->post(); + $result = DirectService::edit($data); + return $this->back($result); + } else { + $this->info = DirectModel::get($id); + return $this->fetch('add'); + } + } + /** + * 删除文章 + * @param [type] $id 文章id + * @return [type] [description] + */ + public function del($id) + { + $result = DirectService::del($id); + return $this->back($result); + } + /** + * 修改文章状态 + * @param [type] $id 文章id + * @param [type] $status 状态 + * @return [type] 修改文章结果 + */ + public function status($id, $status) + { + $result = DirectService::status($id, $status); + return $this->back($result); + } + + public function upload($type = '') + { + if (!in_array($type, ['image', 'file', 'video', 'audio'])) { + $result = ['code' => 0, 'msg' => '不支持的上传类型']; + } else { + $file = request()->file($type); + dump($file);die(); + // $result = StorageService::upload($type); + } + + if (is_array($result) && !empty($result)) { + $ret = [ + 'code' => 1, + 'data' => $result, + ]; + } else { + $ret = [ + 'code' => 0, + 'msg' => $result, + ]; + } + + return Response::create($ret, 'json'); + } +} diff --git a/application/system/controller/Help.php b/application/system/controller/Help.php new file mode 100644 index 0000000..5b71816 --- /dev/null +++ b/application/system/controller/Help.php @@ -0,0 +1,87 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\Help as HelpModel; +use app\common\service\Help as HelpService; +use think\Request; + +class Help extends _Init +{ + /** + * 文章列表 + * @param string $title 要搜索的文章标题 + * @return [type] [description] + */ + public function index($title = '') + { + $map['status'] = ['egt', 0]; + if (!empty($title)) { + $map['title'] = ['like', "%$title%"]; + } + $this->list = HelpModel::where($map)->paginate(); + return $this->fetch(); + } + + /** + * 添加文章 + * @param Request $Request 数据集 + */ + public function add(Request $Request) + { + if (IS_POST) { + $data = $Request->post(); + $result = HelpService::create($data); + return $this->back($result); + } else { + return $this->fetch(); + } + } + + /** + * 编辑文章 + * @param Request $Request 数据集 + * @param [type] $id 文章id + * @return [type] 返回 编辑的结果 + */ + public function edit(Request $Request, $id) + { + if (IS_POST) { + $data = $Request->post(); + $result = HelpService::edit($data); + return $this->back($result); + } else { + $this->info = HelpModel::get($id); + return $this->fetch('add'); + } + } + + /** + * 删除文章 + * @param [type] $id 文章id + * @return [type] [description] + */ + public function del($id) + { + $result = HelpService::del($id); + return $this->back($result); + } + + /** + * 修改文章状态 + * @param [type] $id 文章id + * @param [type] $status 状态 + * @return [type] 修改文章结果 + */ + public function status($id, $status) + { + $result = HelpService::status($id, $status); + return $this->back($result); + } +} diff --git a/application/system/controller/Index.php b/application/system/controller/Index.php new file mode 100644 index 0000000..d60d1f3 --- /dev/null +++ b/application/system/controller/Index.php @@ -0,0 +1,69 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\service\Member as MemberService; +use app\common\service\Menu as MenuService; +use think\Request; + +class Index extends _Init +{ + /** + * 后台首页 + * @return [type] [description] + */ + public function index() + { + $this->user = MemberService::info(UID); + $this->menu = MenuService::show(UID); + return $this->fetch(); + } + + /** + * 个人信息 + * @param Request $request 提交的信息 用户名 + * @return [type] [description] + */ + public function info(Request $request) + { + if (IS_POST) { + $data = $request->post(); + if (MemberService::editInfo(UID, $data, 'nickname') === true) { + return $this->success(); + } else { + return $this->error(MemberService::getError()); + } + } else { + $this->assign('info', MemberService::info(UID)); + return $this->fetch(); + } + } + /** + * 修改密码 + * @param Request $request 数据集 + * @return [type] [description] + */ + public function password(Request $request) + { + if (IS_POST) { + $old = $request->post('oldpass'); + $new = $request->post('newpass'); + $repass = $request->post('repass'); + $result = MemberService::changePassword(UID, $old, $new, $repass); + return $this->back($result); + } else { + return $this->fetch(); + } + } + + public function main() + { + return $this->fetch(); + } +} diff --git a/application/system/controller/Login.php b/application/system/controller/Login.php new file mode 100644 index 0000000..3a2b787 --- /dev/null +++ b/application/system/controller/Login.php @@ -0,0 +1,67 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\service\Member as MemberService; +use think\Cookie; +use think\Request; +use tools\Initialize; + +class Login extends Initialize +{ + /** + * 登录页面 和 登录操作 + * @param Request $request 提交的信息 用户名 密码 验证码 是否记住帐号 + * @return [type] 返回登录结果信息 + */ + public function index(Request $request) + { + if (IS_POST) { + $data = $request->post(); + + $res = $this->validate($data, [ + 'verify|验证码' => 'require|captcha', + ]); + if ($res !== true) { + return $this->error($res); + } + + $username = $request->post('username'); + $password = $request->post('password'); + $remember = $request->post('rememberMe'); + if ($remember == true) { + Cookie::set('remember_username', $username); + } + if (MemberService::login($username, $password) === true) { + return ['code' => 1, 'msg' => '登录成功']; + } else { + return ['code' => 0, 'msg' => '登录失败']; + } + } else { + if (self::isLogin()) { + $this->redirect('/'); + } + return $this->fetch(); + } + } + + /** + * 退出登陆 + */ + public function logout() + { + $result = MemberService::logout(); + + if (MemberService::logout() === true) { + return ['code' => 1, 'msg' => '退出成功']; + } else { + return ['code' => 0, 'msg' => '退出失败']; + } + } +} diff --git a/application/system/controller/Logs.php b/application/system/controller/Logs.php new file mode 100644 index 0000000..f7e7dab --- /dev/null +++ b/application/system/controller/Logs.php @@ -0,0 +1,23 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +class Logs extends _Init +{ + + public function index() + { + #Todo... + } + + public function system() + { + #Todo.. + } +} diff --git a/application/system/controller/Member.php b/application/system/controller/Member.php new file mode 100644 index 0000000..e929969 --- /dev/null +++ b/application/system/controller/Member.php @@ -0,0 +1,79 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\Member as MemberModel; +use app\common\model\MemberList as MemberListModel; +use app\common\service\Member as MemberService; +use think\Request; + +class Member extends _Init +{ + + public function index() + { + $model = new MemberModel(); + $model->alias('Member'); + $model = $model->hasWhere('info'); + $map = [ + 'Member.status' => ['gt', 0], + ]; + $this->list = $model->where($map)->order('Member.update_time desc')->paginate(); + return $this->fetch(); + } + /** + * 添加用户 + * @param Request $request 用户的信息 username password + */ + public function add(Request $request) + { + if (IS_POST) { + $username = $request->post('username'); + $password = $request->post('password'); + $result = MemberService::register($username, $password); + return $this->back($result); + } else { + return $this->fetch(); + } + } + /** + * 设置用户状态 + * @param [type] $uid 用户id + * @param [type] $status 要设置的状态 true false 正常 禁用 + * @return [type] [description] + */ + public function status($uid, $status) + { + $result = MemberService::status($uid, $status); + return $this->back($result); + } + + /** + * 重置密码 + * @param [type] $uid 用户id + * @return [type] [description] + */ + public function password(Request $request, $uid) + { + if (IS_POST) { + $password = $request->post('password'); + $result = MemberService::updatePassword($uid, $password); + return $this->back($result); + } else { + $this->info = MemberModel::get($uid); + return $this->fetch(); + } + } + + public function junior($pid) + { + $this->list = MemberListModel::where('uid', $pid)->paginate(); + return $this->fetch(); + } +} diff --git a/application/system/controller/Menu.php b/application/system/controller/Menu.php new file mode 100644 index 0000000..70eeaf7 --- /dev/null +++ b/application/system/controller/Menu.php @@ -0,0 +1,102 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\Menu as MenuModel; +use app\common\service\Menu as MenuService; +use think\Db; +use think\Request; +use tools\Tree; + +class Menu extends _Init +{ + /** + * 菜单列表 + * @param integer $pid 上级id + * @return [type] [description] + */ + public function index($pid = 0) + { + $this->list = MenuModel::where('status', 'egt', 0)->where('pid', $pid)->order('sort asc')->paginate(); + $this->menu = MenuService::tree(); + $this->info = MenuModel::get($pid); + return $this->fetch(); + } + /** + * 添加菜单 + * @param integer $pid 上级id + */ + public function add(Request $request, $pid = 0) + { + if (IS_POST) { + $data = $request->post(); + $result = MenuService::add($data); + return $this->back($result); + } else { + $info['pid'] = $pid; + $this->info = $info; + $this->up_menus = self::_treeShow(); + return $this->fetch(); + } + } + /** + * 编辑菜单 + * @param [type] $id 菜单id + * @return [type] [description] + */ + public function edit(Request $request, $id) + { + if (IS_POST) { + $data = $request->post(); + $result = MenuService::edit($data); + return $this->back($result); + } else { + $this->info = MenuModel::get($id); + $this->up_menus = self::_treeShow($id); + return $this->fetch('add'); + } + } + + /** + * 删除菜单 + * @param [type] $id 要删除的菜单id + * @return [type] [description] + */ + public function del($id) + { + $result = MenuService::del($id); + return $this->back($result); + } + + /** + * 设置菜单状态 + * @param [type] $id 要设置的菜单id + * @param [type] $status 要设置的状态 正常 和 禁用 true false + * @param [type] $type 要设置的字段 status + * @return [type] 返回操作的结果 成功 true 失败 返回失败的信息 + */ + public function status($id, $status, $type) + { + $result = MenuService::status($id, $status, $type); + return $this->back($result); + } + + private function _treeShow($id = 0) + { + $map = []; + if ($id) { + $map['id'] = ['neq', $id]; + } + + $menus = Db::name('Menu')->where($map)->order('sort asc')->select(); + $menus = Tree::toFormatTree($menus); + $menus = array_merge([0 => ['id' => 0, 'title_show' => '顶级菜单']], $menus); + return $menus; + } +} diff --git a/application/system/controller/Score.php b/application/system/controller/Score.php new file mode 100644 index 0000000..641ecc2 --- /dev/null +++ b/application/system/controller/Score.php @@ -0,0 +1,95 @@ + | +// +------------------------------------------------+ + +namespace app\system\controller; + +use app\common\model\Score as ScoreModel; +use app\common\model\ScoreRules as ScoreRulesModel; +use app\common\service\ScoreRules as ScoreRulesService; +use think\Request; + +/** + * 积分 + */ +class Score extends _Init +{ + + /** + * 列表 + * @return [type] [description] + */ + public function index() + { + $this->list = ScoreModel::where('')->order('id desc')->paginate(); + return $this->fetch(''); + } + + public function rules() + { + $this->list = ScoreRulesModel::where('status', 1)->paginate(); + return $this->fetch(''); + } + + /** + * 添加规则 + * @param Request $Request 数据集 + */ + public function add(Request $Request) + { + if (IS_POST) { + $data = $Request->post(); + $result = ScoreRulesService::create($data); + return $this->back($result); + } else { + return $this->fetch(); + } + } + + /** + * 编辑规则 + * @param Request $Request 数据集 + * @param [type] $id 规则id + * @return [type] 返回 编辑的结果 + */ + public function edit(Request $Request, $id) + { + if (IS_POST) { + $data = $Request->post(); + $result = ScoreRulesService::edit($data); + return $this->back($result); + } else { + $this->info = ScoreRulesModel::get($id); + return $this->fetch('add'); + } + } + + /** + * 删除规则 + * @param [type] $id 规则id + * @return [type] [description] + */ + public function del($id) + { + $result = ScoreRulesService::del($id); + return $this->back($result); + } + + /** + * 修改规则状态 + * @param [type] $id 规则id + * @param [type] $status 状态 + * @return [type] 修改规则结果 + */ + public function status($id, $status) + { + $result = ScoreRulesService::status($id, $status); + return $this->back($result); + } + +} diff --git a/application/system/controller/Share.php b/application/system/controller/Share.php new file mode 100644 index 0000000..3771e77 --- /dev/null +++ b/application/system/controller/Share.php @@ -0,0 +1,90 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\Content as ContentModel; +use app\common\service\Content as ContentService; +use think\Request; + +class Share extends _Init +{ + + /** + * 文章列表 + * @param string $title 要搜索的文章标题 + * @return [type] [description] + */ + + public function index($title = '') + { + $CategoryIds = ContentService::getCategoryIds('share'); + $map = [ + 'status' => ['egt', 0], + 'title' => ['like', "%$title%"], + 'category_id' => ['in', $CategoryIds], + ]; + $this->list = ContentModel::where($map)->order('create_time desc')->paginate(); + return $this->fetch(); + } + + /** + * 添加文章 + * @param Request $Request 数据集 + */ + public function add(Request $Request) + { + if (IS_POST) { + $data = $Request->post(); + $result = ContentService::create($data); + return $this->back($result); + } else { + $this->list = ContentService::categoryList('share'); + return $this->fetch(); + } + } + /** + * 编辑文章 + * @param Request $Request 数据集 + * @param [type] $id 文章id + * @return [type] 返回 编辑的结果 + */ + public function edit(Request $Request, $id) + { + if (IS_POST) { + $data = $Request->post(); + $result = ContentService::edit($data); + return $this->back($result); + } else { + $this->list = ContentService::categoryList('share'); + $this->info = ContentModel::get($id); + return $this->fetch('add'); + } + } + /** + * 删除文章 + * @param [type] $id 文章id + * @return [type] [description] + */ + public function del($id) + { + $result = ContentService::del($id); + return $this->back($result); + } + /** + * 修改文章状态 + * @param [type] $id 文章id + * @param [type] $status 状态 + * @return [type] 修改文章结果 + */ + public function status($id, $status) + { + $result = ContentService::status($id, $status); + return $this->back($result); + } +} diff --git a/application/system/controller/Storage.php b/application/system/controller/Storage.php new file mode 100644 index 0000000..4635589 --- /dev/null +++ b/application/system/controller/Storage.php @@ -0,0 +1,106 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\JobLog as JobLogModel; +use app\common\model\Logs as LogsModel; +use app\common\model\Storage as StorageModel; +use app\common\service\Storage as StorageService; +use think\Response; + +class Storage extends _Init +{ + /** + * 资源管理 + * @param string $type [description] + */ + public function index($type = '') + { + $map = []; + if ($type) { + $map['type'] = $type; + } + + $this->assign('total', StorageService::total($map)); + $this->assign('types', StorageService::types()); + $this->assign('disk_use', StorageService::diskUse()); + + $list = StorageModel::where($map)->order('id desc')->paginate(); + $this->assign('list', $list); + return $this->fetch(); + } + + /** + * 日志管理 + */ + public function logs() + { + $list = LogsModel::order('id desc')->paginate(); + $this->assign('list', $list); + return $this->fetch(); + } + + public function queue() + { + $list = JobLogModel::order('id desc')->paginate(); + $this->assign('list', $list); + return $this->fetch(); + } + + /** + * 上传单文件 + * @param string $type [description] + * @return [type] [description] + */ + public function upload($type = '') + { + if (!in_array($type, ['image', 'file', 'video', 'audio'])) { + $result = ['code' => 0, 'msg' => '不支持的上传类型']; + } else { + $result = StorageService::upload($type); + } + + if (is_array($result) && !empty($result) && !isset($result['code'])) { + $ret = [ + 'code' => 1, + 'data' => $result, + ]; + } else { + $ret = [ + 'code' => 0, + 'msg' => $result, + ]; + } + + return Response::create($ret, 'json'); + } + + /** + * 编辑器上传文件 + */ + public function editor() + { + $result = StorageService::upload('file'); + if (is_array($result) && !empty($result)) { + $res = [ + 'code' => 0, + 'data' => [ + 'src' => $result['path'], + 'title' => $result['name'], + ], + ]; + } else { + $res = [ + 'code' => 1, + 'msg' => $result, + ]; + } + return Response::create($res, 'json'); + } +} diff --git a/application/system/controller/Suggest.php b/application/system/controller/Suggest.php new file mode 100644 index 0000000..60b99e6 --- /dev/null +++ b/application/system/controller/Suggest.php @@ -0,0 +1,28 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\Suggest as SuggestModel; + +class Suggest extends _Init +{ + + /** + * 反馈列表 + * @return [type] [description] + */ + + public function index() + { + + $this->list = SuggestModel::where('')->paginate(); + return $this->fetch(); + } + +} diff --git a/application/system/controller/Vip.php b/application/system/controller/Vip.php new file mode 100644 index 0000000..daa47b4 --- /dev/null +++ b/application/system/controller/Vip.php @@ -0,0 +1,32 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\VipOrder; +use app\common\service\Vip as VipService; + +class Vip extends _Init +{ + public function index($title = '') + { + $map = []; + if (!empty($title)) { + $ids = VipService::getIds($title); + $map['uid'] = ['in', $ids]; + } + $this->list = VipOrder::where($map)->order('create_time desc')->paginate(); + return $this->fetch(''); + } + + public function info($id) + { + $this->list = VipOrder::where('uid', $id)->paginate(); + return $this->fetch(''); + } +} diff --git a/application/system/controller/_Init.php b/application/system/controller/_Init.php new file mode 100644 index 0000000..8e7f2f4 --- /dev/null +++ b/application/system/controller/_Init.php @@ -0,0 +1,109 @@ + | +// +------------------------------------------------+ +namespace app\system\controller; + +use app\common\model\Auth as AuthModel; +use app\common\model\AuthUser as AuthUserModel; +use think\Config; +use think\Db; +use tools\Initialize; + +class _Init extends Initialize +{ + + public function _initialize() + { + define('UID', self::isLogin()); + + if (!UID) { + $this->redirect('login/index'); + } + if (!$this->checkAuth(UID, CONTROLLER_NAME . '/' . ACTION_NAME)) { + return $this->error('没有操作权限'); + } + } + + /** + * 操作成功跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function success($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + $msg = $msg ?: '操作成功'; + return parent::success($msg, $url, $data, $wait, $header); + } + + /** + * 操作错误跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function error($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + $msg = $msg ?: '未知错误'; + return parent::error($msg, $url, $data, $wait, $header); + } + + protected function back($result) + { + if ($result === true) { + return $this->success(); + } else { + return $this->error($result); + } + } + /** + * 检查授权 + * @param [type] $uid 用户id + * @param [type] $node 节点名 menu/index + * @return [type] [description] + */ + public function checkAuth($uid, $node) + { + //查询设置的超级管理的ids + $adminUsers = Config::get('administrator'); + if (!in_array($uid, $adminUsers)) { + //获取当前页的菜单id + $nodes = Db::name('Menu')->where('url', $node)->value('id'); + if ($nodes) { + //获取当前用户的授权节点 + $authId = AuthUserModel::where('uid', $uid)->column('auth_id'); + $rules = AuthModel::where('id', 'in', $authId)->column('rules'); + if ($rules) { + $rules = implode($rules, ','); + $rules = explode(',', $rules); + $rules = array_unique($rules); + if (in_array($nodes, $rules)) { + return true; + } else { + return false; + } + } else { + return false; + } + } else { + return true; + } + } else { + return true; + } + } + +} diff --git a/application/system/view/article/add.html b/application/system/view/article/add.html new file mode 100644 index 0000000..e5264b7 --- /dev/null +++ b/application/system/view/article/add.html @@ -0,0 +1,118 @@ +{extend name="public/base" /} + +{block name="body"} +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    + + +
    +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    +
    + {present name="info"} + + {/present} + +
    +
    +
    +{/block} + +{block name="layui"} + + + + + +{/block} diff --git a/application/system/view/article/index.html b/application/system/view/article/index.html new file mode 100644 index 0000000..201530f --- /dev/null +++ b/application/system/view/article/index.html @@ -0,0 +1,126 @@ +{extend name="public/base" /} +{block name="body"} +
    + + + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + + + +
    + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + {/volist} + +
    编号所属用户内容标题状态创建时间更新时间
    {$vo.id}{$vo.user.nickname}{$vo.title}{$vo.create_time}{$vo.update_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/articlemodel/add.html b/application/system/view/articlemodel/add.html new file mode 100644 index 0000000..86f117a --- /dev/null +++ b/application/system/view/articlemodel/add.html @@ -0,0 +1,52 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + {present name="info"} + + {/present} + +
    +
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/articlemodel/index.html b/application/system/view/articlemodel/index.html new file mode 100644 index 0000000..fb1f96f --- /dev/null +++ b/application/system/view/articlemodel/index.html @@ -0,0 +1,113 @@ +{extend name="public/base" /} +{block name="body"} +
    + + + +
    +
    + + + +
    + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + {/volist} + +
    编号内容样式状态创建时间更新时间
    {$vo.id}{$vo.content}{$vo.create_time}{$vo.update_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/auth/add.html b/application/system/view/auth/add.html new file mode 100644 index 0000000..7ab74ca --- /dev/null +++ b/application/system/view/auth/add.html @@ -0,0 +1,49 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + {present name="info"} + + {/present} + +
    +
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/auth/index.html b/application/system/view/auth/index.html new file mode 100644 index 0000000..4051e2a --- /dev/null +++ b/application/system/view/auth/index.html @@ -0,0 +1,136 @@ +{extend name="public/base" /} + +{block name="body"} +
    + + + + + +
    + +
    + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + {/volist} + +
    ID分组名称更新时间状态备注
    {$vo.id}{$vo.title}{$vo.update_time}{$vo.remark}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/auth/menu.html b/application/system/view/auth/menu.html new file mode 100644 index 0000000..ae9fa1e --- /dev/null +++ b/application/system/view/auth/menu.html @@ -0,0 +1,65 @@ +{extend name="public/base" /} + +{block name="body"} +
    + + + {volist name="node_list" id="vo"} + + + {eq name="vo['children']??''|count" value="0"} + + + {/eq} + {volist name="vo.children|default=''" id="c1" key="row"} + {gt name="row" value="1"} + + {/gt} + + + + {/volist} + {/volist} + +
    + +
    + + + {volist name="c1.children|default=''" id="s1"} + + {/volist} +
    + +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/auth/user.html b/application/system/view/auth/user.html new file mode 100644 index 0000000..d329485 --- /dev/null +++ b/application/system/view/auth/user.html @@ -0,0 +1,160 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    + + 已授权用户 +
    +
    + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + {/volist} + +
    UID用户名昵称创建时间
    {$vo.id}{$vo.username}{$vo.info.nickname}{$vo.auth_user.create_time}
    + {$list->render();} +
    +
    + +
    +
    + 添加用户到分组 +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    + + +{/block} + + +{block name="layui"} + +{/block} diff --git a/application/system/view/banner/add.html b/application/system/view/banner/add.html new file mode 100644 index 0000000..3a8bcc6 --- /dev/null +++ b/application/system/view/banner/add.html @@ -0,0 +1,56 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + {present name="info"} + + + {else /} + + {/present} +
    +
    +
    +{/block} +{block name="layui"} + +{/block} diff --git a/application/system/view/banner/banadd.html b/application/system/view/banner/banadd.html new file mode 100644 index 0000000..ba418b4 --- /dev/null +++ b/application/system/view/banner/banadd.html @@ -0,0 +1,90 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    + +
    + +
    +
    +
    + +
    +
    + +
    + + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + {present name="info"} + + + {else /} + + {/present} +
    +
    +
    +{/block} +{block name="layui"} + +{/block} + diff --git a/application/system/view/banner/detail.html b/application/system/view/banner/detail.html new file mode 100644 index 0000000..8c1be16 --- /dev/null +++ b/application/system/view/banner/detail.html @@ -0,0 +1,115 @@ +{extend name="public/base" /} + +{block name="body"} +
    + 返回 + + + +
    + + +
    + + + + + + + + + + + + + + + {volist name="info.detail" id="vo"} + + + + + + + + + + + {/volist} + +
    标题图片连接地址排序点击状态更新时间
    {$vo.title}{$vo.url}{$vo.sort}{$vo.click}{$vo.update_time}
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/banner/index.html b/application/system/view/banner/index.html new file mode 100644 index 0000000..b00ca1b --- /dev/null +++ b/application/system/view/banner/index.html @@ -0,0 +1,123 @@ +{extend name="public/base" /} + +{block name="body"} +
    + + + +
    +
    + +
    +
    + +
    +
    +
    + +
    + + + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + + + {/volist} + +
    IDBanner名称显示数量说明状态创建时间更新时间
    {$vo.id}{$vo.title}{$vo.limit}{$vo->detail()->count()}{$vo.remark}{$vo.create_time}{$vo.update_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/category/add.html b/application/system/view/category/add.html new file mode 100644 index 0000000..c27407a --- /dev/null +++ b/application/system/view/category/add.html @@ -0,0 +1,114 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    + + +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + + +
    +
    +
    +{/block} +{block name="layui"} + +{/block} diff --git a/application/system/view/category/index.html b/application/system/view/category/index.html new file mode 100644 index 0000000..84dd4b1 --- /dev/null +++ b/application/system/view/category/index.html @@ -0,0 +1,136 @@ +{extend name="public/base" /} + +{block name="body"} +
    + +
    +
    +
    + + {notempty name="Think.get.pid|default=0" value="0"} + 返回 + {/notempty} + + + +
    + + {notempty name="Think.get.pid|default=0" value="0"} +
    + 上级分类 +
    + {$info.title} : {$info.description} +
    +
    + {/notempty} + +
    + + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + + {/volist} + +
    分类名称所属排序状态描述添加时间更新时间
    {$vo.title}{$vo.model}{$vo.sort}{$vo.description}{$vo.create_time}{$vo.update_time}
    + {$list->render();} +
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/cause/add.html b/application/system/view/cause/add.html new file mode 100644 index 0000000..8106772 --- /dev/null +++ b/application/system/view/cause/add.html @@ -0,0 +1,128 @@ +{extend name="public/base" /} + +{block name="body"} +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    + + +
    +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + + +
    +
    + +
    +
    + {present name="info"} + + {/present} + +
    +
    +
    +{/block} + +{block name="layui"} + + + + + +{/block} diff --git a/application/system/view/cause/index.html b/application/system/view/cause/index.html new file mode 100644 index 0000000..15d32af --- /dev/null +++ b/application/system/view/cause/index.html @@ -0,0 +1,126 @@ +{extend name="public/base" /} +{block name="body"} +
    + + + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + + + +
    + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + {/volist} + +
    编号所属分类介绍标题状态创建时间更新时间
    {$vo.id}{$vo.category.title}{$vo.title}{$vo.create_time}{$vo.update_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/config/add.html b/application/system/view/config/add.html new file mode 100644 index 0000000..4754195 --- /dev/null +++ b/application/system/view/config/add.html @@ -0,0 +1,101 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    + {present name="info"} + + {/present} + +
    +
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/config/index.html b/application/system/view/config/index.html new file mode 100644 index 0000000..3e25417 --- /dev/null +++ b/application/system/view/config/index.html @@ -0,0 +1,92 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    +
    +
      + {volist name="Think.config.config_group_list" id="group"} +
    • {$group}
    • + {/volist} +
    +
    + {volist name="Think.config.config_group_list" id="group"} +
    + {foreach name="list" item="config" key="clvo"} + {eq name="config.group" value="$key"} + {switch name="config.type"} + {case value="0"} +
    + +
    + +
    +
    + {/case} + {case value="1"} +
    + +
    + +
    +
    + {/case} + {case value="2"} +
    + +
    + +
    +
    + {/case} + {case value="3"} +
    + +
    + +
    +
    + {/case} + {case value="4"} +
    + +
    + +
    +
    + {/case} + {/switch} + {/eq} + {/foreach} +
    + {/volist} +
    +
    +
    +
    +
    + +
    +
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/config/params.html b/application/system/view/config/params.html new file mode 100644 index 0000000..f600af6 --- /dev/null +++ b/application/system/view/config/params.html @@ -0,0 +1,126 @@ +{extend name="public/base" /} + +{block name="body"} +
    + + + + +
    + +
    + + + + + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + + + + + {/volist} + +
    编号参数名称配置标题类型分组排序状态配置内容创建时间更新时间
    {$vo.id}{$vo.name}{$vo.title}{$vo.type_text}{$vo.group_text}{$vo.sort}{$vo.value_text}{$vo.create_time}{$vo.update_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/database/import.html b/application/system/view/database/import.html new file mode 100644 index 0000000..8eeeaef --- /dev/null +++ b/application/system/view/database/import.html @@ -0,0 +1,78 @@ +{extend name="public/base" /} + +{block name="body"} +
    + + 备份文件下载,请到服务器站点备份目录中下载 +
    + +
    + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + {/volist} + +
    备份名称卷数压缩数据大小备份时间
    {$vo.time|date='Ymd-His',###}{$vo.part}{$vo.compress}{$vo.size}{$key}
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/database/index.html b/application/system/view/database/index.html new file mode 100644 index 0000000..3c180a5 --- /dev/null +++ b/application/system/view/database/index.html @@ -0,0 +1,114 @@ +{extend name="public/base" /} + +{block name="body"} +
    + +
    + +
    + + + + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + + + + {/volist} + +
    编号表名数据量表引擎数据大小数据索引创建时间备份状态表备注
    {$i}{$vo.name}{$vo.rows}{$vo.engine}{$vo.data_length}{$vo.index_length}{$vo.create_time}未备份{$vo.comment}
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/direct/add.html b/application/system/view/direct/add.html new file mode 100644 index 0000000..b2e28de --- /dev/null +++ b/application/system/view/direct/add.html @@ -0,0 +1,184 @@ +{extend name="public/base" /} + +{block name="body"} +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + + +
    {$info.ext_path|default=''} +
    +
    + +
    + +
    + +
    + {present name="info"} + + {/present} + + +
    +
    +
    + +
    + +
    + + +
    +
    + +
    +
    + {present name="info"} + + {/present} + +
    +
    +
    +{/block} + +{block name="layui"} + + + + + +{/block} diff --git a/application/system/view/direct/index.html b/application/system/view/direct/index.html new file mode 100644 index 0000000..83319e3 --- /dev/null +++ b/application/system/view/direct/index.html @@ -0,0 +1,131 @@ +{extend name="public/base" /} +{block name="body"} +
    + + + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + + + +
    + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + {/volist} + +
    编号类型内容标题状态创建时间更新时间
    {$vo.id}{$vo.type}{$vo.title}{$vo.create_time}{$vo.update_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/help/add.html b/application/system/view/help/add.html new file mode 100644 index 0000000..7b15980 --- /dev/null +++ b/application/system/view/help/add.html @@ -0,0 +1,109 @@ +{extend name="public/base" /} + +{block name="body"} +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    + +
    + +
    + + +
    +
    + +
    +
    + {present name="info"} + + {/present} + +
    +
    +
    +{/block} + +{block name="layui"} + + + + + +{/block} diff --git a/application/system/view/help/index.html b/application/system/view/help/index.html new file mode 100644 index 0000000..a25b69c --- /dev/null +++ b/application/system/view/help/index.html @@ -0,0 +1,123 @@ +{extend name="public/base" /} +{block name="body"} +
    + + + +
    +
    + +
    +
    + +
    +
    +
    +
    + + + +
    + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + {/volist} + +
    编号所属分类内容标题状态创建时间更新时间
    {$vo.id}{$vo.category}{$vo.title}{$vo.create_time}{$vo.update_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/index/index.html b/application/system/view/index/index.html new file mode 100644 index 0000000..61ad70b --- /dev/null +++ b/application/system/view/index/index.html @@ -0,0 +1,71 @@ + + + + + + {$Think.config.web_site_title} 管理系统 + + + + + + + + + +
    +
    + +
    +
    +
    +
    +
    +
    +
      +
    • + 控制面板 +
    • +
    +
    +
    + +
    +
    +
    +
    + + + + +
    + + + diff --git a/application/system/view/index/info.html b/application/system/view/index/info.html new file mode 100644 index 0000000..12077f9 --- /dev/null +++ b/application/system/view/index/info.html @@ -0,0 +1,61 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    + +
    + +
    +
    + +
    +
    + +
    +
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/index/main.html b/application/system/view/index/main.html new file mode 100644 index 0000000..e69de29 diff --git a/application/system/view/index/password.html b/application/system/view/index/password.html new file mode 100644 index 0000000..378d1be --- /dev/null +++ b/application/system/view/index/password.html @@ -0,0 +1,53 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/login/index.html b/application/system/view/login/index.html new file mode 100644 index 0000000..aa4482d --- /dev/null +++ b/application/system/view/login/index.html @@ -0,0 +1,101 @@ + + + + + + + 系统登录 + + + + + + +
    +
    +

    Super.helper

    +
    +

    Click  Here  To  Login

    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + + + + diff --git a/application/system/view/member/add.html b/application/system/view/member/add.html new file mode 100644 index 0000000..d59c734 --- /dev/null +++ b/application/system/view/member/add.html @@ -0,0 +1,46 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/member/index.html b/application/system/view/member/index.html new file mode 100644 index 0000000..92cd49c --- /dev/null +++ b/application/system/view/member/index.html @@ -0,0 +1,110 @@ +{extend name="public/base" /} + +{block name="body"} +
    + + + +
    +
    + + + + + + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + + + + + + {/volist} + +
    编号昵称用户名积分报单状态注册时间登录次数身份Vip到期时间
    {$vo.id}{$vo.info.nickname}{$vo.username}{$vo.info.score}{$vo.junior|default=''} 查看{$vo.create_time}{$vo.login}{$vo.info.vip_text}{$vo.info.vip_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/member/junior.html b/application/system/view/member/junior.html new file mode 100644 index 0000000..a4b729d --- /dev/null +++ b/application/system/view/member/junior.html @@ -0,0 +1,44 @@ +{extend name="public/base" /} + +{block name="body"} +
    + + + + + + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + + + + + + {/volist} + +
    姓名会员级别电话身份证银行卡号密码推荐人推荐人编号接点人接点人编号添加时间
    {$vo.username}{$vo.level}{$vo.mobile}{$vo.id_card}{$vo.bank}{$vo.bank_card}{$vo.password}{$vo.referee}{$vo.referee_number}{$vo.contact}{$vo.contact_number}{$vo.create_time}
    + {$list->render();} +
    +{/block} + diff --git a/application/system/view/member/password.html b/application/system/view/member/password.html new file mode 100644 index 0000000..02b14ee --- /dev/null +++ b/application/system/view/member/password.html @@ -0,0 +1,42 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/menu/add.html b/application/system/view/menu/add.html new file mode 100644 index 0000000..8f9d470 --- /dev/null +++ b/application/system/view/menu/add.html @@ -0,0 +1,92 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    fontawesome图标类 fa-*
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + {present name="info.id"} + + + {else /} + + {/present} +
    +
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/menu/index.html b/application/system/view/menu/index.html new file mode 100644 index 0000000..3de694d --- /dev/null +++ b/application/system/view/menu/index.html @@ -0,0 +1,143 @@ +{extend name="public/base" /} + +{block name="body"} +
    + +
    +
    +
    + + {notempty name="Think.get.pid|default=0" value="0"} + 返回 + {/notempty} + + +
    + + {notempty name="Think.get.pid|default=0" value="0"} +
    + 上级菜单 +
    + {$info.title} : {$info.url|url} +
    +
    + {/notempty} + +
    + + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + + {/volist} + +
    图标菜单名称排序状态AUTH隐藏url
    {$vo.title}{$vo.sort}{$vo.url_text}
    + {$list->render();} +
    +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/public/base.html b/application/system/view/public/base.html new file mode 100644 index 0000000..3f3515a --- /dev/null +++ b/application/system/view/public/base.html @@ -0,0 +1,40 @@ + + + + + {$Think.config.web_site_title} + + + + + + + {block name="css"}{/block} + + + + {block name="body"}{/block} + + {block name="js"}{/block} + + {block name="layui"}{/block} + + diff --git a/application/system/view/public/ueditor.html b/application/system/view/public/ueditor.html new file mode 100644 index 0000000..eef16ba --- /dev/null +++ b/application/system/view/public/ueditor.html @@ -0,0 +1,10 @@ + + + + + diff --git a/application/system/view/score/add.html b/application/system/view/score/add.html new file mode 100644 index 0000000..d7283b3 --- /dev/null +++ b/application/system/view/score/add.html @@ -0,0 +1,113 @@ +{extend name="public/base" /} + +{block name="body"} +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    + {present name="info"} + + {/present} + +
    +
    +
    +{/block} + +{block name="layui"} + + + + + +{/block} diff --git a/application/system/view/score/index.html b/application/system/view/score/index.html new file mode 100644 index 0000000..a47aa53 --- /dev/null +++ b/application/system/view/score/index.html @@ -0,0 +1,86 @@ +{extend name="public/base" /} +{block name="body"} + + + + +
    + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + {/volist} + +
    编号用户积分说明创建时间
    {$vo.id}{$vo.user.nickname|default='未设置'}{$vo.score}{$vo.remark}{$vo.create_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/score/rules.html b/application/system/view/score/rules.html new file mode 100644 index 0000000..325e15c --- /dev/null +++ b/application/system/view/score/rules.html @@ -0,0 +1,117 @@ +{extend name="public/base" /} +{block name="body"} +
    + + + +
    +
    + + + +
    + + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + + {/volist} + +
    编号规则名称英文名称积分数量状态创建时间更新时间
    {$vo.id}{$vo.title}{$vo.model}{$vo.score}{$vo.create_time}{$vo.update_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/share/add.html b/application/system/view/share/add.html new file mode 100644 index 0000000..8a81c78 --- /dev/null +++ b/application/system/view/share/add.html @@ -0,0 +1,128 @@ +{extend name="public/base" /} + +{block name="body"} +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    + + +
    +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + + +
    +
    + +
    +
    + {present name="info"} + + {/present} + +
    +
    +
    +{/block} + +{block name="layui"} + + + + + +{/block} diff --git a/application/system/view/share/index.html b/application/system/view/share/index.html new file mode 100644 index 0000000..82985fd --- /dev/null +++ b/application/system/view/share/index.html @@ -0,0 +1,126 @@ +{extend name="public/base" /} +{block name="body"} +
    + + + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + + + +
    + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + {/volist} + +
    编号所属分类分享标题状态创建时间更新时间
    {$vo.id}{$vo.category.title}{$vo.title}{$vo.create_time}{$vo.update_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/storage/index.html b/application/system/view/storage/index.html new file mode 100644 index 0000000..b0d6535 --- /dev/null +++ b/application/system/view/storage/index.html @@ -0,0 +1,55 @@ +{extend name="public/base" /} + +{block name="body"} +
    + + 文件总数: {$list->total()} 个 + 占用空间: {$total} + {volist name="types" id="vo"} + {$vo.type} + {/volist} + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + + {/volist} + +
    ID类型文件名称后缀空间使用(点)路径创建时间
    {$vo.id}{$vo.type}{$vo.name}{$vo.ext}{$vo.size}{$vo.path}{$vo.create_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/storage/logs.html b/application/system/view/storage/logs.html new file mode 100644 index 0000000..1d3a59d --- /dev/null +++ b/application/system/view/storage/logs.html @@ -0,0 +1,27 @@ +{extend name="public/base" /} + +{block name="body"} +
    + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + {/volist} + +
    用户日志数据操作时间
    {$vo.uid_text}{$vo.logs}{$vo.datas_text}{$vo.create_time}
    + {$list->render();} +
    +{/block} diff --git a/application/system/view/storage/queue.html b/application/system/view/storage/queue.html new file mode 100644 index 0000000..62b5897 --- /dev/null +++ b/application/system/view/storage/queue.html @@ -0,0 +1,95 @@ +{extend name="public/base" /} + +{block name="body"} +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + {/volist} + +
    任务ID队列名称任务名称交付数据状态创建时间备注信息
    {$vo.queue_id}{$vo.queue}{$vo.job}{$vo.payload}{$vo.status_text}{$vo.create_time}{$vo.remark}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/suggest/index.html b/application/system/view/suggest/index.html new file mode 100644 index 0000000..d17e3ea --- /dev/null +++ b/application/system/view/suggest/index.html @@ -0,0 +1,57 @@ +{extend name="public/base" /} +{block name="body"} + +
    + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + {/volist} + +
    编号用户内容创建时间更新时间
    {$vo.id}{$vo.info.nickname}{$vo.content}{$vo.create_time}{$vo.update_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/upload&editor.html b/application/system/view/upload&editor.html new file mode 100644 index 0000000..078f6cd --- /dev/null +++ b/application/system/view/upload&editor.html @@ -0,0 +1,91 @@ +{extend name="public/base" /} + +{block name="body"} +
    + +
    + +
    + +
    +
    + + + + + + +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/vip/index.html b/application/system/view/vip/index.html new file mode 100644 index 0000000..10dd80a --- /dev/null +++ b/application/system/view/vip/index.html @@ -0,0 +1,84 @@ +{extend name="public/base" /} +{block name="body"} +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + + + {/volist} + +
    编号用户单号金额支付方式状态创建时间更新时间
    {$vo.id}{$vo.user.nickname|default='未设置'}{$vo.orderid}{$vo.money}{$vo.model}{$vo.status}{$vo.create_time}{$vo.update_time}
    + {$list->render();} +
    +{/block} + +{block name="layui"} + +{/block} diff --git a/application/system/view/vip/info.html b/application/system/view/vip/info.html new file mode 100644 index 0000000..a4baa61 --- /dev/null +++ b/application/system/view/vip/info.html @@ -0,0 +1,32 @@ +{extend name="public/base" /} +{block name="body"} +
    + + + + + + + + + + + + + + {volist name="list" id="vo"} + + + + + + + + + + {/volist} + +
    编号用户单号金额支付方式状态创建时间
    {$vo.id}{$vo.user.nickname|default='未设置'}{$vo.orderid}{$vo.money}{$vo.model}{$vo.status}{$vo.create_time}
    + {$list->render();} +
    +{/block} diff --git a/application/tags.php b/application/tags.php new file mode 100644 index 0000000..d5d0f6c --- /dev/null +++ b/application/tags.php @@ -0,0 +1,38 @@ + | +// +------------------------------------------------+ + +/** + * 行为扩展定义 + */ +return [ + // 应用初始化 + 'app_init' => [], + // 应用开始 + 'app_begin' => [ + 'tools\\Behavior', + ], + // 模块初始化 + 'module_init' => [ + 'tools\\Behavior', + ], + // 操作开始执行 + 'action_begin' => [], + // 视图内容过滤 + 'view_filter' => [], + // 日志写入 + 'log_write' => [], + // 应用结束 + 'app_end' => [ + 'tools\\Behavior', + ], + // 日志write方法标签位 + 'log_write' => [], + // 输出结束标签位 + 'response_end' => [], +]; diff --git a/backup/helper_sql.rar b/backup/helper_sql.rar new file mode 100644 index 0000000..ee5fb8c Binary files /dev/null and b/backup/helper_sql.rar differ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..80ba3aa --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "topthink/think", + "description": "the new thinkphp framework", + "type": "project", + "keywords": [ + "framework", + "thinkphp", + "ORM" + ], + "homepage": "http://thinkphp.cn/", + "license": "Apache-2.0", + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "require": { + "php": ">=5.4.0", + "topthink/framework": "dev-master", + "cjango/wechat": "dev-master", + "topthink/think-captcha": "*", + "endroid/qrcode": "dev-master" + }, + "extra": { + "think-path": "thinkphp" + + }, + "config": { + "preferred-install": "dist" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..3a7c8c2 --- /dev/null +++ b/composer.lock @@ -0,0 +1,687 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "963c2df868cdb0c025652c96abf1d6d5", + "packages": [ + { + "name": "bacon/bacon-qr-code", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "031a2ce68c5794064b49d11775b2daf45c96e21c" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/Bacon/BaconQrCode/031a2ce68c5794064b49d11775b2daf45c96e21c.zip", + "reference": "031a2ce68c5794064b49d11775b2daf45c96e21c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-gd": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-0": { + "BaconQrCode": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "http://www.dasprids.de", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "time": "2016-01-09T22:55:35+00:00" + }, + { + "name": "cjango/wechat", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/cjango/wechat.git", + "reference": "085ece24569802ebc08488fdfad252c0ce12e5bc" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/cjango/wechat/085ece24569802ebc08488fdfad252c0ce12e5bc.zip", + "reference": "085ece24569802ebc08488fdfad252c0ce12e5bc", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "cjango\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jason.Chen", + "email": "chenjxlg@163.com", + "homepage": "http://www.cjango.com/" + } + ], + "description": "wechat sdk", + "homepage": "https://github.com/cjango/wechat", + "keywords": [ + "cjango", + "code", + "sdk", + "wechat" + ], + "time": "2017-06-08T03:09:34+00:00" + }, + { + "name": "endroid/qrcode", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/endroid/QrCode.git", + "reference": "2974f1c1fd020eb323c3c97179a40dd1d7795627" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/endroid/QrCode/2974f1c1fd020eb323c3c97179a40dd1d7795627.zip", + "reference": "2974f1c1fd020eb323c3c97179a40dd1d7795627", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^1.0", + "ext-gd": "*", + "khanamiryan/qrcode-detector-decoder": "^1.0", + "myclabs/php-enum": "^1.5", + "php": ">=5.6", + "symfony/options-resolver": "^2.7|^3.0", + "symfony/property-access": "^2.7|^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7|^6.0", + "sensio/framework-extra-bundle": "^3.0", + "symfony/asset": "^2.7|^3.0", + "symfony/browser-kit": "^2.7|^3.0", + "symfony/finder": "^2.7|^3.0", + "symfony/framework-bundle": "^2.7|^3.0", + "symfony/http-kernel": "^2.7|^3.0", + "symfony/templating": "^2.7|^3.0", + "symfony/twig-bundle": "^2.7|^3.0", + "symfony/yaml": "^2.7|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Endroid\\QrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeroen van den Enden", + "email": "info@endroid.nl", + "homepage": "http://endroid.nl/" + } + ], + "description": "Endroid QR Code", + "homepage": "https://github.com/endroid/QrCode", + "keywords": [ + "bundle", + "code", + "endroid", + "qr", + "qrcode", + "symfony" + ], + "time": "2017-07-23T14:07:09+00:00" + }, + { + "name": "khanamiryan/qrcode-detector-decoder", + "version": "1", + "source": { + "type": "git", + "url": "https://github.com/khanamiryan/php-qrcode-detector-decoder.git", + "reference": "96d5f80680b04803c4f1b69d6e01735e876b80c7" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/khanamiryan/php-qrcode-detector-decoder/96d5f80680b04803c4f1b69d6e01735e876b80c7.zip", + "reference": "96d5f80680b04803c4f1b69d6e01735e876b80c7", + "shasum": "" + }, + "require": { + "php": "^5.6|^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ], + "files": [ + "lib/common/customFunctions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ashot Khanamiryan", + "email": "a.khanamiryan@gmail.com", + "homepage": "https://github.com/khanamiryan", + "role": "Developer" + } + ], + "description": "QR code decoder / reader", + "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder", + "keywords": [ + "barcode", + "qr", + "zxing" + ], + "time": "2017-01-13T09:11:46+00:00" + }, + { + "name": "myclabs/php-enum", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "3ed7088cfd0a0e06534b7f8b0eee82acea574fac" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/myclabs/php-enum/3ed7088cfd0a0e06534b7f8b0eee82acea574fac.zip", + "reference": "3ed7088cfd0a0e06534b7f8b0eee82acea574fac", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|^5.7|^6.0", + "squizlabs/php_codesniffer": "1.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "http://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "time": "2017-06-28T16:24:08+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v2.0.10", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/paragonie/random_compat/634bae8e911eefa89c1abfbf1b66da679ac8f54d.zip", + "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "autoload": { + "files": [ + "lib/random.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "pseudorandom", + "random" + ], + "time": "2017-03-13T16:27:32+00:00" + }, + { + "name": "symfony/inflector", + "version": "v3.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/inflector.git", + "reference": "aed5a0874a3bcfd8d0393a2d91b4cf828f29c7fb" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/symfony/inflector/aed5a0874a3bcfd8d0393a2d91b4cf828f29c7fb.zip", + "reference": "aed5a0874a3bcfd8d0393a2d91b4cf828f29c7fb", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Inflector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Inflector Component", + "homepage": "https://symfony.com", + "keywords": [ + "inflection", + "pluralize", + "singularize", + "string", + "symfony", + "words" + ], + "time": "2017-04-12T14:14:56+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v3.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/symfony/options-resolver/ff48982d295bcac1fd861f934f041ebc73ae40f0.zip", + "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony OptionsResolver Component", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "time": "2017-04-12T14:14:56+00:00" + }, + { + "name": "symfony/polyfill-php70", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "032fd647d5c11a9ceab8ee8747e13b5448e93874" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/symfony/polyfill-php70/032fd647d5c11a9ceab8ee8747e13b5448e93874.zip", + "reference": "032fd647d5c11a9ceab8ee8747e13b5448e93874", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "~1.0|~2.0", + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php70\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2017-06-09T14:24:12+00:00" + }, + { + "name": "symfony/property-access", + "version": "v3.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "4cd2bc4afdfd914ad18cec97bb4159fc403384ea" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/symfony/property-access/4cd2bc4afdfd914ad18cec97bb4159fc403384ea.zip", + "reference": "4cd2bc4afdfd914ad18cec97bb4159fc403384ea", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/inflector": "~3.1", + "symfony/polyfill-php70": "~1.0" + }, + "require-dev": { + "symfony/cache": "~3.1" + }, + "suggest": { + "psr/cache-implementation": "To cache access methods." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony PropertyAccess Component", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property path", + "reflection" + ], + "time": "2017-07-03T08:12:02+00:00" + }, + { + "name": "topthink/framework", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/top-think/framework.git", + "reference": "d1c2044745a7465f827c733affbcfcb6e0f1bb49" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/top-think/framework/d1c2044745a7465f827c733affbcfcb6e0f1bb49.zip", + "reference": "d1c2044745a7465f827c733affbcfcb6e0f1bb49", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "topthink/think-installer": "~1.0" + }, + "require-dev": { + "johnkary/phpunit-speedtrap": "^1.0", + "mikey179/vfsstream": "~1.6", + "phpdocumentor/reflection-docblock": "^2.0", + "phploc/phploc": "2.*", + "phpunit/phpunit": "4.8.*", + "sebastian/phpcpd": "2.*" + }, + "type": "think-framework", + "autoload": { + "psr-4": { + "think\\": "library/think" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "the new thinkphp framework", + "homepage": "http://thinkphp.cn/", + "keywords": [ + "framework", + "orm", + "thinkphp" + ], + "time": "2017-07-27T10:03:26+00:00" + }, + { + "name": "topthink/think-captcha", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-captcha.git", + "reference": "0c55455df26a1626a60d0dc35d2d89002b741d44" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/top-think/think-captcha/0c55455df26a1626a60d0dc35d2d89002b741d44.zip", + "reference": "0c55455df26a1626a60d0dc35d2d89002b741d44", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "think\\captcha\\": "src/" + }, + "files": [ + "src/helper.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "captcha package for thinkphp5", + "time": "2016-07-06T01:47:11+00:00" + }, + { + "name": "topthink/think-installer", + "version": "v1.0.12", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-installer.git", + "reference": "1be326e68f63de4e95977ed50f46ae75f017556d" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/top-think/think-installer/1be326e68f63de4e95977ed50f46ae75f017556d.zip", + "reference": "1be326e68f63de4e95977ed50f46ae75f017556d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0" + }, + "require-dev": { + "composer/composer": "1.0.*@dev" + }, + "type": "composer-plugin", + "extra": { + "class": "think\\composer\\Plugin" + }, + "autoload": { + "psr-4": { + "think\\composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "time": "2017-05-27T06:58:09+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "topthink/framework": 20, + "cjango/wechat": 20, + "endroid/qrcode": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.4.0" + }, + "platform-dev": [] +} diff --git a/extend/tools/Behavior.php b/extend/tools/Behavior.php new file mode 100644 index 0000000..b4c202b --- /dev/null +++ b/extend/tools/Behavior.php @@ -0,0 +1,49 @@ + | +// +------------------------------------------------+ +namespace tools; + +use think\Request; + +class Behavior +{ + + /** + * 加载系统配置 + */ + public function appBegin() + { + Config::load(); + define('__SELF__', Request::instance()->url(true)); + } + + /** + * 定义系统常量 + */ + public function moduleInit() + { + $request = Request::instance(); + $method = $request->method(); + + define('IS_GET', $method == 'GET' ? true : false); + define('IS_POST', $method == 'POST' ? true : false); + define('IS_AJAX', $request->isAjax()); + + define('MODULE_NAME', $request->module()); + define('CONTROLLER_NAME', $request->controller()); + define('ACTION_NAME', $request->action()); + } + + /** + * 返回头修改 + */ + public function appEnd(&$response) + { + $response->header('X-Powered-By', 'cjango.com'); + } +} diff --git a/extend/tools/Config.php b/extend/tools/Config.php new file mode 100644 index 0000000..bcbdcf5 --- /dev/null +++ b/extend/tools/Config.php @@ -0,0 +1,71 @@ + | +// +------------------------------------------------+ +namespace tools; + +use think\Cache; +use think\Config as Conf; +use think\Db; + +/** + * 加载系统配置,依赖数据库和缓存 + */ +class Config +{ + /** + * 加载系统扩展配置 + */ + public static function load() + { + $config = Cache::get('db_config_cache_data'); + if (!$config) { + $data = Db::name('Config')->where('status', 1)->field('type,name,value')->select(); + $config = []; + if ($data && is_array($data)) { + foreach ($data as $value) { + $config[$value['name']] = self::parse($value['type'], $value['value']); + } + } + Cache::set('db_config_cache_data', $config); + } + Conf::set($config); + } + + /** + * 根据配置类型解析配置 + * @param integer $type 配置类型 + * @param string $value 配置值 + * @return array + */ + private static function parse($type, $value) + { + switch ($type) { + case 3: //解析数组 + $array = preg_split('/[\r\n]+/', trim($value, "\r\n")); + if (strpos($value, ':')) { + $value = []; + foreach ($array as $val) { + list($k, $v) = explode(':', $val, 2); + $value[$k] = $v; + } + } else { + $value = $array; + } + break; + } + return $value; + } + + /** + * 清除配置缓存 + */ + public static function clear() + { + Cache::rm('db_config_cache_data'); + } +} diff --git a/extend/tools/Crypt.php b/extend/tools/Crypt.php new file mode 100644 index 0000000..86c7944 --- /dev/null +++ b/extend/tools/Crypt.php @@ -0,0 +1,99 @@ + | +// +------------------------------------------------+ +namespace tools; + +use think\Config as Conf; + +class Crypt +{ + + /** + * 数据签名认证 + * @param array $data + * @return string + */ + public static function dataAuthSign($data = []) + { + ksort($data); + $code = http_build_query($data); + return sha1($code . Conf::get('data_auth_key')); + } + + /** + * 用户MD5不可逆加密 + * @param string $string + * @return string + */ + public static function uMd5($string) + { + $key = Conf::get('data_auth_key'); + return '' === $string ? '' : md5(sha1($string) . $key); + } + + /** + * 长串加密 + * @param string $string + * @return string + */ + public static function uSha1($string) + { + $key = Conf::get('data_auth_key'); + return '' === $string ? '' : sha1(md5($string) . $key); + } + + /** + * Discuz 经典双向加密/解密 + * @param string $string 明文 或 密文 + * @param string $operation DECODE表示解密,其它表示加密 + * @param string $key 密匙 + * @param string $expiry 密文有效期 + */ + public static function discuz($string, $operation = 'DECODE', $key = '', $expiry = 0) + { + $ckey_length = 4; + $key = md5($key ? $key : Conf::get('data_auth_key')); + $keya = md5(substr($key, 0, 16)); + $keyb = md5(substr($key, 16, 16)); + $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length) : substr(md5(microtime()), -$ckey_length)) : ''; + $cryptkey = $keya . md5($keya . $keyc); + $key_length = strlen($cryptkey); + $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string; + $string_length = strlen($string); + $result = ''; + $box = range(0, 255); + $rndkey = []; + for ($i = 0; $i <= 255; $i++) { + $rndkey[$i] = ord($cryptkey[$i % $key_length]); + } + for ($j = $i = 0; $i < 256; $i++) { + $j = ($j + $box[$i] + $rndkey[$i]) % 256; + $tmp = $box[$i]; + $box[$i] = $box[$j]; + $box[$j] = $tmp; + } + for ($a = $j = $i = 0; $i < $string_length; $i++) { + $a = ($a + 1) % 256; + $j = ($j + $box[$a]) % 256; + $tmp = $box[$a]; + $box[$a] = $box[$j]; + $box[$j] = $tmp; + $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); + } + if ($operation == 'DECODE') { + if ((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)) { + return substr($result, 26); + } else { + return ''; + } + } else { + return $keyc . str_replace('=', '', base64_encode($result)); + } + } + +} diff --git a/extend/tools/Database.php b/extend/tools/Database.php new file mode 100644 index 0000000..5c56925 --- /dev/null +++ b/extend/tools/Database.php @@ -0,0 +1,241 @@ + | +// +------------------------------------------------+ +namespace tools; + +/** + * 数据导出工具 + */ +class Database +{ + /** + * 文件指针 + * @var resource + */ + private $fp; + + /** + * 备份文件信息 part - 卷号,name - 文件名 + * @var array + */ + private $file; + + /** + * 当前打开文件大小 + * @var integer + */ + private $size = 0; + + /** + * 备份配置 + * @var integer + */ + private $config = [ + 'path' => '', // 数据库备份根路径 + 'part' => 2097152, // 数据库备份卷大小 2Mb + 'compress' => true, // 数据库备份文件是否启用压缩 + 'level' => 9, // 数据库备份文件压缩级别 + 'line' => 1000, + ]; + + /** + * 数据库备份构造方法 + * @param array $file 备份或还原的文件信息 + * @param array $config 备份配置信息 + */ + public function __construct($file, $config) + { + $this->file = $file; + $this->config = array_merge($this->config, $config); + } + + /** + * 打开一个卷,用于写入数据 + * @param integer $size 写入数据的大小 + */ + private function open($size) + { + if ($this->fp) { + $this->size += $size; + if ($this->size > $this->config['part']) { + $this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp); + $this->fp = null; + $this->file['part']++; + session('backup_file', $this->file); + $this->create(); + } + } else { + $backuppath = $this->config['path']; + $filename = "{$backuppath}{$this->file['name']}-{$this->file['part']}.sql"; + if ($this->config['compress']) { + $filename = "{$filename}.gz"; + $this->fp = @gzopen($filename, "a{$this->config['level']}"); + } else { + $this->fp = @fopen($filename, 'a'); + } + $this->size = filesize($filename) + $size; + } + } + + /** + * 写入初始数据 + * @return boolean true - 写入成功,false - 写入失败 + */ + public function create() + { + $sql = "-- -----------------------------\n"; + $sql .= "-- c.Jango MySQL Data Transfer\n"; + $sql .= "--\n"; + $sql .= "-- Host : " . \think\Config::get('database.hostname') . "\n"; + $sql .= "-- UserName : " . \think\Config::get('database.username') . "\n"; + $sql .= "-- Database : " . \think\Config::get('database.database') . "\n"; + $sql .= "--\n"; + $sql .= "-- Part : #{$this->file['part']}\n"; + $sql .= "-- Date : " . date("Y-m-d H:i:s") . "\n"; + $sql .= "-- -----------------------------\n\n"; + return $this->write($sql); + } + + /** + * 写入SQL语句 + * @param string $sql 要写入的SQL语句 + * @return boolean true - 写入成功,false - 写入失败! + */ + private function write($sql) + { + $size = strlen($sql); + // 由于压缩原因,无法计算出压缩后的长度,这里假设压缩率为50%, + // 一般情况压缩率都会高于50%; + $size = $this->config['compress'] ? $size / 2 : $size; + $this->open($size); + return $this->config['compress'] ? @gzwrite($this->fp, $sql) : @fwrite($this->fp, $sql); + } + + /** + * 备份表结构 + * @param string $table 表名 + * @param integer $start 起始行数 + * @return boolean false - 备份失败 + */ + public function backup($table, $start) + { + // 创建DB对象 + $db = \think\Loader::db(); + + // 备份表结构 + if (0 == $start) { + $result = $db->query("SHOW CREATE TABLE `{$table}`"); + $result = array_change_key_case($result[0]); + + $sql = "\n"; + $sql .= "-- -----------------------------\n"; + $sql .= "-- Table structure for `{$table}`\n"; + $sql .= "-- -----------------------------\n"; + $sql .= "SET FOREIGN_KEY_CHECKS = 0;\n"; + $sql .= "DROP TABLE IF EXISTS `{$table}`;\n"; + $sql .= trim($result['create table']) . ";\n\n"; + if (false === $this->write($sql)) { + return false; + } + } + + // 数据总数 + $result = $db->query("SELECT COUNT(*) AS count FROM `{$table}`"); + $count = $result[0]['count']; + $line = $this->config['line']; + + // 备份表数据 + if ($count) { + // 写入数据注释 + if (0 == $start) { + $sql = "-- -----------------------------\n"; + $sql .= "-- Records of `{$table}`\n"; + $sql .= "-- -----------------------------\n"; + $this->write($sql); + } + + // 备份数据记录 + $result = $db->query("SELECT * FROM `{$table}` LIMIT {$start}, $line"); + $columns = $db->query("SHOW COLUMNS FROM `{$table}`"); + $columns = array_column($columns, 'Field'); + $columns = implode('`, `', $columns); + + $sql = "INSERT INTO `{$table}` (`{$columns}`) VALUES\r\n"; + + foreach ($result as $row) { + $sql .= "('" . str_replace(["\r", "\n"], ['\r', '\n'], implode("', '", $row)) . "'),\r\n"; + } + + $sql = rtrim(rtrim($sql), ',') . ";\r\n"; + + if (false === $this->write($sql)) { + return false; + } + + // 还有更多数据 + if ($count > $start + $line) { + return [$start + $line, $count]; + } + } + + // 备份下一表 + return 0; + } + + /** + * 还原数据库 + * @param integer $start [description] + * @return [type] [description] + */ + public function import($start = 0) + { + //还原数据 + $db = \think\Loader::db(); + if ($this->config['compress']) { + $gz = gzopen($this->file[1], 'r'); + $size = 0; + } else { + $size = filesize($this->file[1]); + $gz = fopen($this->file[1], 'r'); + } + $sql = ''; + if ($start) { + $this->config['compress'] ? gzseek($gz, $start) : fseek($gz, $start); + } + + for ($i = 0; $i < 1000; $i++) { + $sql .= $this->config['compress'] ? gzgets($gz) : fgets($gz); + if (preg_match('/.*;$/', trim($sql))) { + if (false !== $db->execute($sql)) { + $start += strlen($sql); + } else { + return false; + } + $sql = ''; + } elseif ($this->config['compress'] ? gzeof($gz) : feof($gz)) { + return 0; + } + } + + return [$start, $size]; + } + + /** + * 析构方法,用于关闭文件资源 + */ + public function __destruct() + { + if ($this->config['compress'] && !is_null($this->fp)) { + @gzclose($this->fp); + } elseif (!is_null($this->fp)) { + @fclose($this->fp); + } else { + + } + } +} diff --git a/extend/tools/Format.php b/extend/tools/Format.php new file mode 100644 index 0000000..cb605e7 --- /dev/null +++ b/extend/tools/Format.php @@ -0,0 +1,26 @@ + | +// +------------------------------------------------+ +namespace tools; + +class Format +{ + /** + * 格式化字节数显示 + * @param integer $value + * @return string + */ + public static function byte($value) + { + $units = array('B', 'KB', 'MB', 'GB', 'TB', 'PB'); + for ($i = 0; $value >= 1024 && $i < 5; $i++) { + $value /= 1024; + } + return round($value, 2) . $units[$i]; + } +} diff --git a/extend/tools/Initialize.php b/extend/tools/Initialize.php new file mode 100644 index 0000000..e485e2a --- /dev/null +++ b/extend/tools/Initialize.php @@ -0,0 +1,104 @@ + | +// +------------------------------------------------+ +namespace tools; + +use cjango\Wechat; +use cjango\Wechat\Token; +use think\Cache; +use think\Config; +use think\Controller; +use think\Session; + +/** + * 所有模块的公共方法 + */ +class Initialize extends Controller +{ + /** + * 判断是否登录,返回当前UID + * @return integer + */ + final protected static function isLogin() + { + $user = Session::get('user_auth'); + if (empty($user)) { + return 0; + } else { + return Session::get('user_auth_sign') == Crypt::dataAuthSign($user) ? $user['uid'] : 0; + } + } + + /** + * 初始化微信 + * @return void + */ + protected function initWechat() + { + $token = Cache::get('wechat_access_token'); + $config = Config::get('wechat'); + if (!$token) { + Wechat::instance($config); + $token = Token::get(); + Cache::set('wechat_access_token', $token, 7200); + $config['access_token'] = $token; + Wechat::instance($config, true); + } else { + $config['access_token'] = $token; + Wechat::instance($config); + } + } + + /** + * 微信分享 + */ + protected function jsWechat() + { + $ticket = Cache::get('Wechat_ticket'); + $config = Config::get('wechat_config'); + self::initWechat(); + if (!$ticket) { + $ticket = Token::ticket(); + Cache::set('Wechat_ticket', $ticket, 7000); + } + $now_time = time(); + $wx['appid'] = $config['appid']; + $wx['timestamp'] = $now_time; + $wx['noncestr'] = $noncestr = \tools\Str::random(32); + $sign = array( + 'noncestr' => $noncestr, + 'jsapi_ticket' => $ticket, + 'timestamp' => $now_time, + 'url' => __SELF__, + ); + ksort($sign); + $signStr = sha1(urldecode(http_build_query($sign))); + $wx['signature'] = $signStr; + return $wx; + } + + /** + * 模板变量赋值 + * @param string $name 变量名 + * @param mixed $value 变量值 + */ + final public function __set($name, $value) + { + $this->view->$name = $value; + } + + /** + * 取得模板显示变量的值 + * @param string $name 模板变量 + * @return mixed|null + */ + final public function __get($name) + { + return $this->view->$name ?? null; + } +} diff --git a/extend/tools/Pager.php b/extend/tools/Pager.php new file mode 100644 index 0000000..a8c4f76 --- /dev/null +++ b/extend/tools/Pager.php @@ -0,0 +1,214 @@ + +// +---------------------------------------------------------------------- +namespace tools; + +use think\Paginator; + +class Pager extends Paginator +{ + + /** + * 上一页按钮 + * @param string $text + * @return string + */ + protected function getPreviousButton($text = "«") + { + + if ($this->currentPage() <= 1) { + return $this->getDisabledTextWrapper($text); + } + + $url = $this->url( + $this->currentPage() - 1 + ); + + return $this->getPageLinkWrapper($url, $text); + } + + /** + * 下一页按钮 + * @param string $text + * @return string + */ + protected function getNextButton($text = '»') + { + if (!$this->hasMore) { + return $this->getDisabledTextWrapper($text); + } + + $url = $this->url($this->currentPage() + 1); + + return $this->getPageLinkWrapper($url, $text); + } + + /** + * 页码按钮 + * @return string + */ + protected function getLinks() + { + if ($this->simple) { + return ''; + } + + $block = [ + 'first' => null, + 'slider' => null, + 'last' => null, + ]; + + $length = 3; + + if ($this->lastPage < $length * 4) { + $block['first'] = $this->getUrlRange(1, $this->lastPage); + } elseif ($this->currentPage <= $length * 2) { + $block['first'] = $this->getUrlRange(1, $length * 2 + 2); + $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage); + } elseif ($this->currentPage > ($this->lastPage - $length * 2)) { + $block['first'] = $this->getUrlRange(1, 2); + $block['last'] = $this->getUrlRange($this->lastPage - $length * 2 + 2, $this->lastPage); + } else { + $block['first'] = $this->getUrlRange(1, 2); + $block['slider'] = $this->getUrlRange($this->currentPage - $length, $this->currentPage + $length); + $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage); + } + + $html = ''; + + if (is_array($block['first'])) { + $html .= $this->getUrlLinks($block['first']); + } + + if (is_array($block['slider'])) { + $html .= $this->getDots(); + $html .= $this->getUrlLinks($block['slider']); + } + + if (is_array($block['last'])) { + $html .= $this->getDots(); + $html .= $this->getUrlLinks($block['last']); + } + + return $html; + } + + /** + * 渲染分页html + * @return mixed + */ + public function render() + { + if ($this->hasPages()) { + if ($this->simple) { + return sprintf( + '', + $this->getPreviousButton(), + $this->getNextButton() + ); + } else { + return sprintf( + '', + $this->getPreviousButton(), + $this->getLinks(), + $this->getNextButton(), + $this->getTotalCountWrapper() + ); + } + } + } + + /** + * 获取分页总数统计 + * @return [type] [description] + */ + protected function getTotalCountWrapper() + { + return '
  • 总' . $this->total() . '条/' . $this->lastPage() . '页
  • '; + } + + /** + * 生成一个可点击的按钮 + * + * @param string $url + * @param int $page + * @return string + */ + protected function getAvailablePageWrapper($url, $page) + { + return '
  • ' . $page . '
  • '; + } + + /** + * 生成一个禁用的按钮 + * + * @param string $text + * @return string + */ + protected function getDisabledTextWrapper($text) + { + return '
  • ' . $text . '
  • '; + } + + /** + * 生成一个激活的按钮 + * + * @param string $text + * @return string + */ + protected function getActivePageWrapper($text) + { + return '
  • ' . $text . '
  • '; + } + + /** + * 生成省略号按钮 + * + * @return string + */ + protected function getDots() + { + return $this->getDisabledTextWrapper('...'); + } + + /** + * 批量生成页码按钮. + * + * @param array $urls + * @return string + */ + protected function getUrlLinks(array $urls) + { + $html = ''; + + foreach ($urls as $page => $url) { + $html .= $this->getPageLinkWrapper($url, $page); + } + + return $html; + } + + /** + * 生成普通页码按钮 + * + * @param string $url + * @param int $page + * @return string + */ + protected function getPageLinkWrapper($url, $page) + { + if ($page == $this->currentPage()) { + return $this->getActivePageWrapper($page); + } + + return $this->getAvailablePageWrapper($url, $page); + } +} diff --git a/extend/tools/Str.php b/extend/tools/Str.php new file mode 100644 index 0000000..82f4a4e --- /dev/null +++ b/extend/tools/Str.php @@ -0,0 +1,235 @@ + | +// +------------------------------------------------+ +namespace tools; + +/** + * 字符串辅助函数 + */ +class Str +{ + /** + * 生成UUID 单机使用 + * @access public + * @return string + */ + public static function uuid() + { + $charid = self::guid(); + $hyphen = chr(45); + $uuid = substr($charid, 0, 8) . $hyphen + . substr($charid, 8, 4) . $hyphen + . substr($charid, 12, 4) . $hyphen + . substr($charid, 16, 4) . $hyphen + . substr($charid, 20, 12); + return $uuid; + } + + /** + * 生成GUID主键 + * @return string + */ + public static function guid() + { + return md5(uniqid(mt_rand(), true)); + } + + /** + * 检查字符串是否是UTF8编码 + * @param string $string 字符串 + * @return boolean + */ + public static function isUtf8($str) + { + $c = 0; + $b = 0; + $bits = 0; + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $c = ord($str[$i]); + if ($c > 128) { + if (($c >= 254)) { + return false; + } elseif ($c >= 252) { + $bits = 6; + } elseif ($c >= 248) { + $bits = 5; + } elseif ($c >= 240) { + $bits = 4; + } elseif ($c >= 224) { + $bits = 3; + } elseif ($c >= 192) { + $bits = 2; + } else { + return false; + } + if (($i + $bits) > $len) { + return false; + } + while ($bits > 1) { + $i++; + $b = ord($str[$i]); + if ($b < 128 || $b > 191) { + return false; + } + $bits--; + } + } + } + return true; + } + + /** + * 字符串截取,支持中文和其他编码 + * @param string $str 需要转换的字符串 + * @param string $start 开始位置 + * @param string $length 截取长度 + * @param string $suffix 截断显示字符/或拼接字符 + * @param string $charset 编码格式 + * @return string + */ + public static function msubstr($str, $start, $length, $suffix = true, $charset = "utf-8") + { + if (mb_strlen($str, $charset) < $length) { + return $str; + } + + if (function_exists("mb_substr")) { + $slice = mb_substr($str, $start, $length, $charset); + } elseif (function_exists('iconv_substr')) { + $slice = iconv_substr($str, $start, $length, $charset); + } else { + $re['utf-8'] = "/[\x01-\x7f]|[\xc2-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xff][\x80-\xbf]{3}/"; + $re['gb2312'] = "/[\x01-\x7f]|[\xb0-\xf7][\xa0-\xfe]/"; + $re['gbk'] = "/[\x01-\x7f]|[\x81-\xfe][\x40-\xfe]/"; + $re['big5'] = "/[\x01-\x7f]|[\x81-\xfe]([\x40-\x7e]|\xa1-\xfe])/"; + preg_match_all($re[$charset], $str, $match); + $slice = join("", array_slice($match[0], $start, $length)); + } + + if (is_bool($suffix)) { + return $suffix ? $slice . '..' : $slice; + } else { + return $slice . $suffix; + } + } + + /** + * 产生随机字串,可用来自动生成密码 + * 默认长度6位 字母和数字混合 + * @param string $len 长度 + * @param string $type 字串类型 + * 0 字母 1 数字 其它 混合 + * @param string $addChars 额外字符 + * @return string + */ + public static function random($len = 6, $type = '', $addChars = '') + { + $str = ''; + switch ($type) { + case 0: + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' . $addChars; + break; + case 1: + $chars = str_repeat('0123456789', 3); + break; + case 2: + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' . $addChars; + break; + case 3: + $chars = 'abcdefghijklmnopqrstuvwxyz' . $addChars; + break; + default: + $chars = 'ABCDEFGHIJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789' . $addChars; + break; + } + if ($len > 10) { + $chars = $type == 1 ? str_repeat($chars, $len) : str_repeat($chars, 5); + } + $chars = str_shuffle($chars); + $str = substr($chars, 0, $len); + return $str; + } + + /** + * 生成一定数量的随机数,并且不重复 + * @param integer $number 数量 + * @param integer $len 长度 + * @param integer $type 字串类型 + * @param integer $mode 0 字母 1 数字 其它 混合 + * @return array + */ + public static function randomArray($number, $length = 4, $mode = 1) + { + $rand = []; + for ($i = 0; $i < $number; $i++) { + $rand[] = self::random($length, $mode); + } + $unqiue = array_unique($rand); + if (count($unqiue) == count($rand)) { + return $rand; + } + $count = count($rand) - count($unqiue); + for ($i = 0; $i < $count * 3; $i++) { + $rand[] = self::random($length, $mode); + } + $rand = array_slice(array_unique($rand), 0, $number); + return $rand; + } + + /** + * 获取一定范围内的随机数字 位数不足补零 + * @param integer $min 最小值 + * @param integer $max 最大值 + * @return string + */ + public static function number($min, $max) + { + return sprintf("%0" . strlen($max) . "d", mt_rand($min, $max)); + } + + /** + * 创建一个20位的数字订单号 + * @param string $prefix 订单号前缀 + * @return string + */ + public static function orderid($prefix = '', $length = 20) + { + $code = date('ymdHis') . self::number(0, 99999999); + if (!empty($prefix)) { + $code = $prefix . substr($code, 0, $length - strlen($prefix)); + } + return $code; + } + + /** + * 短网址生成算法 + * @param string $url 要计算的URL + * @return string + */ + public static function url($url) + { + $charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + $key = "Uncle.Chen"; + $date = microtime(); + $urlhash = md5($key . $url . $date); + $len = strlen($urlhash); + for ($i = 0; $i < 4; $i++) { + $urlhash_piece = substr($urlhash, $i * $len / 4, $len / 4); + $hex = hexdec($urlhash_piece) & 0x3fffffff; + $short_url = ""; + for ($j = 0; $j < 6; $j++) { + $short_url .= $charset[$hex & 0x0000003d]; + $hex = $hex >> 5; + } + $short_url_list[] = $short_url; + } + $ret = rand(0, 3); + return $short_url_list[$ret]; + } +} diff --git a/extend/tools/Time.php b/extend/tools/Time.php new file mode 100644 index 0000000..5b91461 --- /dev/null +++ b/extend/tools/Time.php @@ -0,0 +1,107 @@ + | +// +------------------------------------------------+ +namespace tools; + +/** + * 时间戳生成类 + */ +class Time +{ + + /** + * [今日/某日 开始和结束的时间戳] + * @param date $date 标准的时间日期格式 Y-m-d + * @return boolean|array + */ + public static function day($date = '', $rule = 'Y-m-d') + { + if (!empty($date) && !Verify::isDate($date, $rule)) { + return false; + } + + $date = !empty($date) ? $date : 'today'; + $start = strtotime($date); + $end = $start + 86399; + + return [$start, $end]; + } + + /** + * 返回本周开始和结束的时间戳 + * @return array + */ + public static function week() + { + $timestamp = time(); + return [ + strtotime(date('Y-m-d', strtotime("+0 week Monday", $timestamp))), + strtotime(date('Y-m-d', strtotime("+0 week Sunday", $timestamp))) + 24 * 3600 - 1, + ]; + } + + /** + * 返回本月开始和结束的时间戳 + * @return array + */ + public static function month($month = '', $year = '') + { + if (empty($month)) { + $month = date('m'); + } elseif (!is_numeric($month) || $month < 1 || $month > 12) { + return false; + } + + if (empty($year)) { + $year = date('Y'); + } elseif (!is_numeric($year) || $year < 1970 || $year > 2038) { + return false; + } + + $start = mktime(0, 0, 0, $month, 1, $year); + $end = mktime(23, 59, 59, $month, date('t', $start), $year); + return [$start, $end]; + } + + /** + * 今年/某年 开始和结束的时间戳 + * @return boolean|array + */ + public static function year($year = '') + { + if (empty($year)) { + $year = date('Y'); + } elseif (!is_numeric($year) || $year < 1970 || $year > 2038) { + return false; + } + + return [ + mktime(0, 0, 0, 1, 1, $year), + mktime(23, 59, 59, 12, 31, $year), + ]; + } + + /** + * [日期合法性校验] + * @param [type] $data 2019-01-01 + * @return boolean + */ + public static function dateVerify($date, $rule = 'Y-m-d') + { + $unixTime = strtotime($date); + if (!$unixTime) { + return false; + } + + if (date($rule, $unixTime) == $date) { + return true; + } else { + return false; + } + } +} diff --git a/extend/tools/Tree.php b/extend/tools/Tree.php new file mode 100644 index 0000000..401793d --- /dev/null +++ b/extend/tools/Tree.php @@ -0,0 +1,90 @@ + | +// +------------------------------------------------+ +namespace tools; + +/** + * 生成多层树状下拉选框的工具 + */ +class Tree +{ + + /** + * 用于树型数组完成递归格式的全局变量 + */ + private static $formatTree; + + /** + * 生成多层树,供下拉选框使用 + */ + public static function toFormatTree($list, $title = 'title', $pk = 'id', $pid = 'pid', $root = 0) + { + $list = self::list2tree($list, $pk, $pid, '_child', $root); + + self::$formatTree = []; + self::_toFormatTree($list, 0, $title); + return self::$formatTree; + } + + /** + * 把数据集转换成Tree + * @param array $list 要转换的数据集 + * @param string $pk [description] + * @param string $pid [description] + * @param string $child [description] + * @param integer $root [description] + * @return array + */ + public static function list2tree($list, $pk = 'id', $pid = 'pid', $child = 'children', $root = 0) + { + $tree = []; + if (is_array($list)) { + $refer = []; + foreach ($list as $key => $data) { + $refer[$data[$pk]] = &$list[$key]; + } + foreach ($list as $key => $data) { + $parentId = $data[$pid]; + if ($root == $parentId) { + $tree[] = &$list[$key]; + } else { + if (isset($refer[$parentId])) { + $parent = &$refer[$parentId]; + $parent[$child][] = &$list[$key]; + } + } + } + } + return $tree; + } + + /** + * 将格式数组转换为树 + * @param array $list + * @param integer $level 进行递归时传递用的参数 + * @author 小陈叔叔 + */ + private static function _toFormatTree($list, $level = 0, $title = 'title') + { + foreach ($list as $key => $val) { + $tmp_str = str_repeat(" ", $level * 4); + $tmp_str .= "└ "; + $val['level'] = $level; + $val['title_show'] = $level == 0 ? $val[$title] . " " : $tmp_str . $val[$title]; + if (!array_key_exists('_child', $val)) { + array_push(self::$formatTree, $val); + } else { + $tmp_ary = $val['_child']; + unset($val['_child']); + array_push(self::$formatTree, $val); + self::_toFormatTree($tmp_ary, $level + 1, $title); //进行下一层递归 + } + } + return; + } +} diff --git a/extend/tools/Verify.php b/extend/tools/Verify.php new file mode 100644 index 0000000..e2fc20b --- /dev/null +++ b/extend/tools/Verify.php @@ -0,0 +1,90 @@ + | +// +------------------------------------------------+ +namespace tools; + +/** + * 校验工具类 + */ +class Verify +{ + + /** + * 身份证号码校验 + * @param string $idcard + * @return boolean + */ + public static function isIdcard($cardNo) + { + if (strlen($cardNo) != 18) { + return false; + } + + $idcard_base = substr($cardNo, 0, 17); + $verify_code = substr($cardNo, 17, 1); + + $factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; + $verify_list = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; + + $total = 0; + for ($i = 0; $i < 17; $i++) { + $total += substr($idcard_base, $i, 1) * $factor[$i]; + } + $mod = $total % 11; + + if ($verify_code == $verify_list[$mod]) { + return true; + } else { + return false; + } + } + + /** + * 检测手机号码格式 + * @param string $mobile + * @return boolean + */ + public static function isMobilePhone($mobile) + { + if (preg_match("/^1[3578]{1}[0-9]{9}$|14[57]{1}[0-9]{8}$/", $mobile)) { + return true; + } else { + return false; + } + } + + /** + * JSON校验 + * @param string $jsonStr + * @return boolean + */ + public static function isJson($jsonStr) + { + if (!is_object(json_decode($jsonStr))) { + return false; + } else { + return true; + } + } + + /** + * 校验日期格式是否合法 + * @param [type] $date 日期 + * @param string $rule 要校验的格式 + * @return boolean + */ + public static function isDate($date, $rule = 'Y-m-d') + { + $unixTime = strtotime($date); + if (!$unixTime || date($rule, $unixTime) != $date) { + return false; + } else { + return true; + } + } +} diff --git a/public/MP_verify_Z7uDTB4KvjbXaqwl.txt b/public/MP_verify_Z7uDTB4KvjbXaqwl.txt new file mode 100644 index 0000000..ff25502 --- /dev/null +++ b/public/MP_verify_Z7uDTB4KvjbXaqwl.txt @@ -0,0 +1 @@ +Z7uDTB4KvjbXaqwl \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..fe76a46 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/image/img1499654863.jpg b/public/image/img1499654863.jpg new file mode 100644 index 0000000..ca7c2ad Binary files /dev/null and b/public/image/img1499654863.jpg differ diff --git a/public/image/img1499654891.jpg b/public/image/img1499654891.jpg new file mode 100644 index 0000000..5868be5 Binary files /dev/null and b/public/image/img1499654891.jpg differ diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..68c2e85 --- /dev/null +++ b/public/index.php @@ -0,0 +1,17 @@ + | +// +------------------------------------------------+ + +// ini_set('display_errors', 'on'); +// [ 应用入口文件 ] + +define('VERSION', '1.0.0'); + +define('APP_PATH', __DIR__ . '/../application/'); + +require __DIR__ . '/../thinkphp/start.php'; diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public/static/.gitignore b/public/static/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/public/static/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/think b/think new file mode 100644 index 0000000..db2e379 --- /dev/null +++ b/think @@ -0,0 +1,18 @@ +#!/usr/bin/env php + +// +---------------------------------------------------------------------- +// 定义项目路径 +define('APP_PATH', __DIR__ . '/application/'); + +define('LOG_PATH', __DIR__ . '/runtime/log/cli/'); + +// 加载框架引导文件 +require './thinkphp/console.php'; diff --git a/thinkphp/.gitignore b/thinkphp/.gitignore new file mode 100644 index 0000000..7e31ef5 --- /dev/null +++ b/thinkphp/.gitignore @@ -0,0 +1,4 @@ +/composer.lock +/vendor +.idea +.DS_Store diff --git a/thinkphp/.htaccess b/thinkphp/.htaccess new file mode 100644 index 0000000..3418e55 --- /dev/null +++ b/thinkphp/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/thinkphp/.travis.yml b/thinkphp/.travis.yml new file mode 100644 index 0000000..f74ffca --- /dev/null +++ b/thinkphp/.travis.yml @@ -0,0 +1,47 @@ +sudo: false + +language: php + +services: + - memcached + - mongodb + - mysql + - postgresql + - redis-server + +matrix: + fast_finish: true + include: + - php: 5.4 + - php: 5.5 + - php: 5.6 + - php: 7.0 + - php: hhvm + allow_failures: + - php: hhvm + +cache: + directories: + - $HOME/.composer/cache + +before_install: + - composer self-update + - mysql -e "create database IF NOT EXISTS test;" -uroot + - psql -c 'DROP DATABASE IF EXISTS test;' -U postgres + - psql -c 'create database test;' -U postgres + +install: + - ./tests/script/install.sh + +script: + ## LINT + - find . -path ./vendor -prune -o -type f -name \*.php -exec php -l {} \; + ## PHP Copy/Paste Detector + - vendor/bin/phpcpd --verbose --exclude vendor ./ || true + ## PHPLOC + - vendor/bin/phploc --exclude vendor ./ + ## PHPUNIT + - vendor/bin/phpunit --coverage-clover=coverage.xml --configuration=phpunit.xml + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/thinkphp/CONTRIBUTING.md b/thinkphp/CONTRIBUTING.md new file mode 100644 index 0000000..6cefcb3 --- /dev/null +++ b/thinkphp/CONTRIBUTING.md @@ -0,0 +1,119 @@ +如何贡献我的源代码 +=== + +此文档介绍了 ThinkPHP 团队的组成以及运转机制,您提交的代码将给 ThinkPHP 项目带来什么好处,以及如何才能加入我们的行列。 + +## 通过 Github 贡献代码 + +ThinkPHP 目前使用 Git 来控制程序版本,如果你想为 ThinkPHP 贡献源代码,请先大致了解 Git 的使用方法。我们目前把项目托管在 GitHub 上,任何 GitHub 用户都可以向我们贡献代码。 + +参与的方式很简单,`fork`一份 ThinkPHP 的代码到你的仓库中,修改后提交,并向我们发起`pull request`申请,我们会及时对代码进行审查并处理你的申请并。审查通过后,你的代码将被`merge`进我们的仓库中,这样你就会自动出现在贡献者名单里了,非常方便。 + +我们希望你贡献的代码符合: + +* ThinkPHP 的编码规范 +* 适当的注释,能让其他人读懂 +* 遵循 Apache2 开源协议 + +**如果想要了解更多细节或有任何疑问,请继续阅读下面的内容** + +### 注意事项 + +* 本项目代码格式化标准选用 [**PSR-2**](http://www.kancloud.cn/thinkphp/php-fig-psr/3141); +* 类名和类文件名遵循 [**PSR-4**](http://www.kancloud.cn/thinkphp/php-fig-psr/3144); +* 对于 Issues 的处理,请使用诸如 `fix #xxx(Issue ID)` 的 commit title 直接关闭 issue。 +* 系统会自动在 PHP 5.4 5.5 5.6 7.0 和 HHVM 上测试修改,其中 HHVM 下的测试容许报错,请确保你的修改符合 PHP 5.4 ~ 5.6 和 PHP 7.0 的语法规范; +* 管理员不会合并造成 CI faild 的修改,若出现 CI faild 请检查自己的源代码或修改相应的[单元测试文件](tests); + +## GitHub Issue + +GitHub 提供了 Issue 功能,该功能可以用于: + +* 提出 bug +* 提出功能改进 +* 反馈使用体验 + +该功能不应该用于: + + * 提出修改意见(涉及代码署名和修订追溯问题) + * 不友善的言论 + +## 快速修改 + +**GitHub 提供了快速编辑文件的功能** + +1. 登录 GitHub 帐号; +2. 浏览项目文件,找到要进行修改的文件; +3. 点击右上角铅笔图标进行修改; +4. 填写 `Commit changes` 相关内容(Title 必填); +5. 提交修改,等待 CI 验证和管理员合并。 + +**若您需要一次提交大量修改,请继续阅读下面的内容** + +## 完整流程 + +1. `fork`本项目; +2. 克隆(`clone`)你 `fork` 的项目到本地; +3. 新建分支(`branch`)并检出(`checkout`)新分支; +4. 添加本项目到你的本地 git 仓库作为上游(`upstream`); +5. 进行修改,若你的修改包含方法或函数的增减,请记得修改[单元测试文件](tests); +6. 变基(衍合 `rebase`)你的分支到上游 master 分支; +7. `push` 你的本地仓库到 GitHub; +8. 提交 `pull request`; +9. 等待 CI 验证(若不通过则重复 5~7,GitHub 会自动更新你的 `pull request`); +10. 等待管理员处理,并及时 `rebase` 你的分支到上游 master 分支(若上游 master 分支有修改)。 + +*若有必要,可以 `git push -f` 强行推送 rebase 后的分支到自己的 `fork`* + +*绝对不可以使用 `git push -f` 强行推送修改到上游* + +### 注意事项 + +* 若对上述流程有任何不清楚的地方,请查阅 GIT 教程,如 [这个](http://backlogtool.com/git-guide/cn/); +* 对于代码**不同方面**的修改,请在自己 `fork` 的项目中**创建不同的分支**(原因参见`完整流程`第9条备注部分); +* 变基及交互式变基操作参见 [Git 交互式变基](http://pakchoi.me/2015/03/17/git-interactive-rebase/) + +## 推荐资源 + +### 开发环境 + +* XAMPP for Windows 5.5.x +* WampServer (for Windows) +* upupw Apache PHP5.4 ( for Windows) + +或自行安装 + +- Apache / Nginx +- PHP 5.4 ~ 5.6 +- MySQL / MariaDB + +*Windows 用户推荐添加 PHP bin 目录到 PATH,方便使用 composer* + +*Linux 用户自行配置环境, Mac 用户推荐使用内置 Apache 配合 Homebrew 安装 PHP 和 MariaDB* + +### 编辑器 + +Sublime Text 3 + phpfmt 插件 + +phpfmt 插件参数 + +```json +{ + "autocomplete": true, + "enable_auto_align": true, + "format_on_save": true, + "indent_with_space": true, + "psr1_naming": false, + "psr2": true, + "version": 4 +} +``` + +或其他 编辑器 / IDE 配合 PSR2 自动格式化工具 + +### Git GUI + +* SourceTree +* GitHub Desktop + +或其他 Git 图形界面客户端 diff --git a/thinkphp/LICENSE.txt b/thinkphp/LICENSE.txt new file mode 100644 index 0000000..574a39c --- /dev/null +++ b/thinkphp/LICENSE.txt @@ -0,0 +1,32 @@ + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 +版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn) +All rights reserved。 +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +Apache Licence是著名的非盈利开源组织Apache采用的协议。 +该协议和BSD类似,鼓励代码共享和尊重原作者的著作权, +允许代码修改,再作为开源或商业软件发布。需要满足 +的条件: +1. 需要给代码的用户一份Apache Licence ; +2. 如果你修改了代码,需要在被修改的文件中说明; +3. 在延伸的代码中(修改和有源代码衍生的代码中)需要 +带有原来代码中的协议,商标,专利声明和其他原来作者规 +定需要包含的说明; +4. 如果再发布的产品中包含一个Notice文件,则在Notice文 +件中需要带有本协议内容。你可以在Notice中增加自己的 +许可,但不可以表现为对Apache Licence构成更改。 +具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0 + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/thinkphp/README.md b/thinkphp/README.md new file mode 100644 index 0000000..4b0eb83 --- /dev/null +++ b/thinkphp/README.md @@ -0,0 +1,114 @@ +ThinkPHP 5.0 +=============== + +[![StyleCI](https://styleci.io/repos/48530411/shield?style=flat&branch=master)](https://styleci.io/repos/48530411) +[![Build Status](https://travis-ci.org/top-think/framework.svg?branch=master)](https://travis-ci.org/top-think/framework) +[![codecov.io](http://codecov.io/github/top-think/framework/coverage.svg?branch=master)](http://codecov.io/github/github/top-think/framework?branch=master) +[![Total Downloads](https://poser.pugx.org/topthink/framework/downloads)](https://packagist.org/packages/topthink/framework) +[![Latest Stable Version](https://poser.pugx.org/topthink/framework/v/stable)](https://packagist.org/packages/topthink/framework) +[![Latest Unstable Version](https://poser.pugx.org/topthink/framework/v/unstable)](https://packagist.org/packages/topthink/framework) +[![License](https://poser.pugx.org/topthink/framework/license)](https://packagist.org/packages/topthink/framework) + +ThinkPHP5在保持快速开发和大道至简的核心理念不变的同时,PHP版本要求提升到5.4,优化核心,减少依赖,基于全新的架构思想和命名空间实现,是ThinkPHP突破原有框架思路的颠覆之作,其主要特性包括: + + + 基于命名空间和众多PHP新特性 + + 核心功能组件化 + + 强化路由功能 + + 更灵活的控制器 + + 重构的模型和数据库类 + + 配置文件可分离 + + 重写的自动验证和完成 + + 简化扩展机制 + + API支持完善 + + 改进的Log类 + + 命令行访问支持 + + REST支持 + + 引导文件支持 + + 方便的自动生成定义 + + 真正惰性加载 + + 分布式环境支持 + + 支持Composer + + 支持MongoDb + +> ThinkPHP5的运行环境要求PHP5.4以上。 + +详细开发文档参考 [ThinkPHP5完全开发手册](http://www.kancloud.cn/manual/thinkphp5) 以及[ThinkPHP5入门系列教程](http://www.kancloud.cn/special/thinkphp5_quickstart) + +## 目录结构 + +初始的目录结构如下: + +~~~ +www WEB部署目录(或者子目录) +├─application 应用目录 +│ ├─common 公共模块目录(可以更改) +│ ├─module_name 模块目录 +│ │ ├─config.php 模块配置文件 +│ │ ├─common.php 模块函数文件 +│ │ ├─controller 控制器目录 +│ │ ├─model 模型目录 +│ │ ├─view 视图目录 +│ │ └─ ... 更多类库目录 +│ │ +│ ├─command.php 命令行工具配置文件 +│ ├─common.php 公共函数文件 +│ ├─config.php 公共配置文件 +│ ├─route.php 路由配置文件 +│ ├─tags.php 应用行为扩展定义文件 +│ └─database.php 数据库配置文件 +│ +├─public WEB目录(对外访问目录) +│ ├─index.php 入口文件 +│ ├─router.php 快速测试文件 +│ └─.htaccess 用于apache的重写 +│ +├─thinkphp 框架系统目录 +│ ├─lang 语言文件目录 +│ ├─library 框架类库目录 +│ │ ├─think Think类库包目录 +│ │ └─traits 系统Trait目录 +│ │ +│ ├─tpl 系统模板目录 +│ ├─base.php 基础定义文件 +│ ├─console.php 控制台入口文件 +│ ├─convention.php 框架惯例配置文件 +│ ├─helper.php 助手函数文件 +│ ├─phpunit.xml phpunit配置文件 +│ └─start.php 框架入口文件 +│ +├─extend 扩展类库目录 +├─runtime 应用的运行时目录(可写,可定制) +├─vendor 第三方类库目录(Composer依赖库) +├─build.php 自动生成定义文件(参考) +├─composer.json composer 定义文件 +├─LICENSE.txt 授权说明文件 +├─README.md README 文件 +├─think 命令行入口文件 +~~~ + +> router.php用于php自带webserver支持,可用于快速测试 +> 切换到public目录后,启动命令:php -S localhost:8888 router.php +> 上面的目录结构和名称是可以改变的,这取决于你的入口文件和配置参数。 + +## 命名规范 + +ThinkPHP5的命名规范遵循PSR-2规范以及PSR-4自动加载规范。 + +## 参与开发 +注册并登录 Github 帐号, fork 本项目并进行改动。 + +更多细节参阅 [CONTRIBUTING.md](CONTRIBUTING.md) + +## 版权信息 + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 + +本项目包含的第三方源码和二进制文件之版权信息另行标注。 + +版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn) + +All rights reserved。 + +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +更多细节参阅 [LICENSE.txt](LICENSE.txt) diff --git a/thinkphp/base.php b/thinkphp/base.php new file mode 100644 index 0000000..31a2333 --- /dev/null +++ b/thinkphp/base.php @@ -0,0 +1,63 @@ + +// +---------------------------------------------------------------------- + +define('THINK_VERSION', '5.0.10'); +define('THINK_START_TIME', microtime(true)); +define('THINK_START_MEM', memory_get_usage()); +define('EXT', '.php'); +define('DS', DIRECTORY_SEPARATOR); +defined('THINK_PATH') or define('THINK_PATH', __DIR__ . DS); +define('LIB_PATH', THINK_PATH . 'library' . DS); +define('CORE_PATH', LIB_PATH . 'think' . DS); +define('TRAIT_PATH', LIB_PATH . 'traits' . DS); +defined('APP_PATH') or define('APP_PATH', dirname($_SERVER['SCRIPT_FILENAME']) . DS); +defined('ROOT_PATH') or define('ROOT_PATH', dirname(realpath(APP_PATH)) . DS); +defined('EXTEND_PATH') or define('EXTEND_PATH', ROOT_PATH . 'extend' . DS); +defined('VENDOR_PATH') or define('VENDOR_PATH', ROOT_PATH . 'vendor' . DS); +defined('RUNTIME_PATH') or define('RUNTIME_PATH', ROOT_PATH . 'runtime' . DS); +defined('LOG_PATH') or define('LOG_PATH', RUNTIME_PATH . 'log' . DS); +defined('CACHE_PATH') or define('CACHE_PATH', RUNTIME_PATH . 'cache' . DS); +defined('TEMP_PATH') or define('TEMP_PATH', RUNTIME_PATH . 'temp' . DS); +defined('CONF_PATH') or define('CONF_PATH', APP_PATH); // 配置文件目录 +defined('CONF_EXT') or define('CONF_EXT', EXT); // 配置文件后缀 +defined('ENV_PREFIX') or define('ENV_PREFIX', 'PHP_'); // 环境变量的配置前缀 + +// 环境常量 +define('IS_CLI', PHP_SAPI == 'cli' ? true : false); +define('IS_WIN', strpos(PHP_OS, 'WIN') !== false); + +// 载入Loader类 +require CORE_PATH . 'Loader.php'; + +// 加载环境变量配置文件 +if (is_file(ROOT_PATH . '.env')) { + $env = parse_ini_file(ROOT_PATH . '.env', true); + foreach ($env as $key => $val) { + $name = ENV_PREFIX . strtoupper($key); + if (is_array($val)) { + foreach ($val as $k => $v) { + $item = $name . '_' . strtoupper($k); + putenv("$item=$v"); + } + } else { + putenv("$name=$val"); + } + } +} + +// 注册自动加载 +\think\Loader::register(); + +// 注册错误和异常处理机制 +\think\Error::register(); + +// 加载惯例配置文件 +\think\Config::set(include THINK_PATH . 'convention' . EXT); diff --git a/thinkphp/codecov.yml b/thinkphp/codecov.yml new file mode 100644 index 0000000..bef9d64 --- /dev/null +++ b/thinkphp/codecov.yml @@ -0,0 +1,12 @@ +comment: + layout: header, changes, diff +coverage: + ignore: + - base.php + - helper.php + - convention.php + - lang/zh-cn.php + - start.php + - console.php + status: + patch: false diff --git a/thinkphp/composer.json b/thinkphp/composer.json new file mode 100644 index 0000000..c546e11 --- /dev/null +++ b/thinkphp/composer.json @@ -0,0 +1,35 @@ +{ + "name": "topthink/framework", + "description": "the new thinkphp framework", + "type": "think-framework", + "keywords": [ + "framework", + "thinkphp", + "ORM" + ], + "homepage": "http://thinkphp.cn/", + "license": "Apache-2.0", + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "require": { + "php": ">=5.4.0", + "topthink/think-installer": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "4.8.*", + "johnkary/phpunit-speedtrap": "^1.0", + "mikey179/vfsStream": "~1.6", + "phploc/phploc": "2.*", + "sebastian/phpcpd": "2.*", + "phpdocumentor/reflection-docblock": "^2.0" + }, + "autoload": { + "psr-4": { + "think\\": "library/think" + } + } +} diff --git a/thinkphp/console.php b/thinkphp/console.php new file mode 100644 index 0000000..578e4a7 --- /dev/null +++ b/thinkphp/console.php @@ -0,0 +1,20 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +// ThinkPHP 引导文件 +// 加载基础文件 +require __DIR__ . '/base.php'; + +// 执行应用 +App::initCommon(); +Console::init(); diff --git a/thinkphp/convention.php b/thinkphp/convention.php new file mode 100644 index 0000000..4ca03c0 --- /dev/null +++ b/thinkphp/convention.php @@ -0,0 +1,289 @@ + '', + // 应用调试模式 + 'app_debug' => false, + // 应用Trace + 'app_trace' => false, + // 应用模式状态 + 'app_status' => '', + // 是否支持多模块 + 'app_multi_module' => true, + // 入口自动绑定模块 + 'auto_bind_module' => false, + // 注册的根命名空间 + 'root_namespace' => [], + // 扩展函数文件 + 'extra_file_list' => [THINK_PATH . 'helper' . EXT], + // 默认输出类型 + 'default_return_type' => 'html', + // 默认AJAX 数据返回格式,可选json xml ... + 'default_ajax_return' => 'json', + // 默认JSONP格式返回的处理方法 + 'default_jsonp_handler' => 'jsonpReturn', + // 默认JSONP处理方法 + 'var_jsonp_handler' => 'callback', + // 默认时区 + 'default_timezone' => 'PRC', + // 是否开启多语言 + 'lang_switch_on' => false, + // 默认全局过滤方法 用逗号分隔多个 + 'default_filter' => '', + // 默认语言 + 'default_lang' => 'zh-cn', + // 应用类库后缀 + 'class_suffix' => false, + // 控制器类后缀 + 'controller_suffix' => false, + + // +---------------------------------------------------------------------- + // | 模块设置 + // +---------------------------------------------------------------------- + + // 默认模块名 + 'default_module' => 'index', + // 禁止访问模块 + 'deny_module_list' => ['common'], + // 默认控制器名 + 'default_controller' => 'Index', + // 默认操作名 + 'default_action' => 'index', + // 默认验证器 + 'default_validate' => '', + // 默认的空控制器名 + 'empty_controller' => 'Error', + // 操作方法前缀 + 'use_action_prefix' => false, + // 操作方法后缀 + 'action_suffix' => '', + // 自动搜索控制器 + 'controller_auto_search' => false, + + // +---------------------------------------------------------------------- + // | URL设置 + // +---------------------------------------------------------------------- + + // PATHINFO变量名 用于兼容模式 + 'var_pathinfo' => 's', + // 兼容PATH_INFO获取 + 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], + // pathinfo分隔符 + 'pathinfo_depr' => '/', + // HTTPS代理标识 + 'https_agent_name' => '', + // URL伪静态后缀 + 'url_html_suffix' => 'html', + // URL普通方式参数 用于自动生成 + 'url_common_param' => false, + // URL参数方式 0 按名称成对解析 1 按顺序解析 + 'url_param_type' => 0, + // 是否开启路由 + 'url_route_on' => true, + // 路由配置文件(支持配置多个) + 'route_config_file' => ['route'], + // 路由使用完整匹配 + 'route_complete_match' => false, + // 是否强制使用路由 + 'url_route_must' => false, + // 域名部署 + 'url_domain_deploy' => false, + // 域名根,如thinkphp.cn + 'url_domain_root' => '', + // 是否自动转换URL中的控制器和操作名 + 'url_convert' => true, + // 默认的访问控制器层 + 'url_controller_layer' => 'controller', + // 表单请求类型伪装变量 + 'var_method' => '_method', + // 表单ajax伪装变量 + 'var_ajax' => '_ajax', + // 表单pjax伪装变量 + 'var_pjax' => '_pjax', + // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则 + 'request_cache' => false, + // 请求缓存有效期 + 'request_cache_expire' => null, + // 全局请求缓存排除规则 + 'request_cache_except' => [], + + // +---------------------------------------------------------------------- + // | 模板设置 + // +---------------------------------------------------------------------- + + 'template' => [ + // 模板引擎类型 支持 php think 支持扩展 + 'type' => 'Think', + // 视图基础目录,配置目录为所有模块的视图起始目录 + 'view_base' => '', + // 当前模板的视图目录 留空为自动获取 + 'view_path' => '', + // 模板后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DS, + // 模板引擎普通标签开始标记 + 'tpl_begin' => '{', + // 模板引擎普通标签结束标记 + 'tpl_end' => '}', + // 标签库标签开始标记 + 'taglib_begin' => '{', + // 标签库标签结束标记 + 'taglib_end' => '}', + ], + + // 视图输出字符串内容替换 + 'view_replace_str' => [], + // 默认跳转页面对应的模板文件 + 'dispatch_success_tmpl' => THINK_PATH . 'tpl' . DS . 'dispatch_jump.tpl', + 'dispatch_error_tmpl' => THINK_PATH . 'tpl' . DS . 'dispatch_jump.tpl', + + // +---------------------------------------------------------------------- + // | 异常及错误设置 + // +---------------------------------------------------------------------- + + // 异常页面的模板文件 + 'exception_tmpl' => THINK_PATH . 'tpl' . DS . 'think_exception.tpl', + + // 错误显示信息,非调试模式有效 + 'error_message' => '页面错误!请稍后再试~', + // 显示错误信息 + 'show_error_msg' => false, + // 异常处理handle类 留空使用 \think\exception\Handle + 'exception_handle' => '', + // 是否记录trace信息到日志 + 'record_trace' => false, + + // +---------------------------------------------------------------------- + // | 日志设置 + // +---------------------------------------------------------------------- + + 'log' => [ + // 日志记录方式,内置 file socket 支持扩展 + 'type' => 'File', + // 日志保存目录 + 'path' => LOG_PATH, + // 日志记录级别 + 'level' => [], + ], + + // +---------------------------------------------------------------------- + // | Trace设置 开启 app_trace 后 有效 + // +---------------------------------------------------------------------- + 'trace' => [ + // 内置Html Console 支持扩展 + 'type' => 'Html', + ], + + // +---------------------------------------------------------------------- + // | 缓存设置 + // +---------------------------------------------------------------------- + + 'cache' => [ + // 驱动方式 + 'type' => 'File', + // 缓存保存目录 + 'path' => CACHE_PATH, + // 缓存前缀 + 'prefix' => '', + // 缓存有效期 0表示永久缓存 + 'expire' => 0, + ], + + // +---------------------------------------------------------------------- + // | 会话设置 + // +---------------------------------------------------------------------- + + 'session' => [ + 'id' => '', + // SESSION_ID的提交变量,解决flash上传跨域 + 'var_session_id' => '', + // SESSION 前缀 + 'prefix' => 'think', + // 驱动方式 支持redis memcache memcached + 'type' => '', + // 是否自动开启 SESSION + 'auto_start' => true, + 'httponly' => true, + 'secure' => false, + ], + + // +---------------------------------------------------------------------- + // | Cookie设置 + // +---------------------------------------------------------------------- + 'cookie' => [ + // cookie 名称前缀 + 'prefix' => '', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => '', + // 是否使用 setcookie + 'setcookie' => true, + ], + + // +---------------------------------------------------------------------- + // | 数据库设置 + // +---------------------------------------------------------------------- + + 'database' => [ + // 数据库类型 + 'type' => 'mysql', + // 数据库连接DSN配置 + 'dsn' => '', + // 服务器地址 + 'hostname' => '127.0.0.1', + // 数据库名 + 'database' => '', + // 数据库用户名 + 'username' => 'root', + // 数据库密码 + 'password' => '', + // 数据库连接端口 + 'hostport' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => '', + // 数据库调试模式 + 'debug' => false, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 数据集返回类型 + 'resultset_type' => 'array', + // 自动写入时间戳字段 + 'auto_timestamp' => false, + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + // 是否需要进行SQL性能分析 + 'sql_explain' => false, + ], + + //分页配置 + 'paginate' => [ + 'type' => 'bootstrap', + 'var_page' => 'page', + 'list_rows' => 15, + ], + +]; diff --git a/thinkphp/helper.php b/thinkphp/helper.php new file mode 100644 index 0000000..a23b679 --- /dev/null +++ b/thinkphp/helper.php @@ -0,0 +1,589 @@ + +// +---------------------------------------------------------------------- + +//------------------------ +// ThinkPHP 助手函数 +//------------------------- + +use think\Cache; +use think\Config; +use think\Cookie; +use think\Db; +use think\Debug; +use think\exception\HttpException; +use think\exception\HttpResponseException; +use think\Lang; +use think\Loader; +use think\Log; +use think\Model; +use think\Request; +use think\Response; +use think\Session; +use think\Url; +use think\View; + +if (!function_exists('load_trait')) { + /** + * 快速导入Traits PHP5.5以上无需调用 + * @param string $class trait库 + * @param string $ext 类库后缀 + * @return boolean + */ + function load_trait($class, $ext = EXT) + { + return Loader::import($class, TRAIT_PATH, $ext); + } +} + +if (!function_exists('exception')) { + /** + * 抛出异常处理 + * + * @param string $msg 异常消息 + * @param integer $code 异常代码 默认为0 + * @param string $exception 异常类 + * + * @throws Exception + */ + function exception($msg, $code = 0, $exception = '') + { + $e = $exception ?: '\think\Exception'; + throw new $e($msg, $code); + } +} + +if (!function_exists('debug')) { + /** + * 记录时间(微秒)和内存使用情况 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 如果是m 表示统计内存占用 + * @return mixed + */ + function debug($start, $end = '', $dec = 6) + { + if ('' == $end) { + Debug::remark($start); + } else { + return 'm' == $dec ? Debug::getRangeMem($start, $end) : Debug::getRangeTime($start, $end, $dec); + } + } +} + +if (!function_exists('lang')) { + /** + * 获取语言变量值 + * @param string $name 语言变量名 + * @param array $vars 动态变量值 + * @param string $lang 语言 + * @return mixed + */ + function lang($name, $vars = [], $lang = '') + { + return Lang::get($name, $vars, $lang); + } +} + +if (!function_exists('config')) { + /** + * 获取和设置配置参数 + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @param string $range 作用域 + * @return mixed + */ + function config($name = '', $value = null, $range = '') + { + if (is_null($value) && is_string($name)) { + return 0 === strpos($name, '?') ? Config::has(substr($name, 1), $range) : Config::get($name, $range); + } else { + return Config::set($name, $value, $range); + } + } +} + +if (!function_exists('input')) { + /** + * 获取输入数据 支持默认值和过滤 + * @param string $key 获取的变量名 + * @param mixed $default 默认值 + * @param string $filter 过滤方法 + * @return mixed + */ + function input($key = '', $default = null, $filter = '') + { + if (0 === strpos($key, '?')) { + $key = substr($key, 1); + $has = true; + } + if ($pos = strpos($key, '.')) { + // 指定参数来源 + list($method, $key) = explode('.', $key, 2); + if (!in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) { + $key = $method . '.' . $key; + $method = 'param'; + } + } else { + // 默认为自动判断 + $method = 'param'; + } + if (isset($has)) { + return request()->has($key, $method, $default); + } else { + return request()->$method($key, $default, $filter); + } + } +} + +if (!function_exists('widget')) { + /** + * 渲染输出Widget + * @param string $name Widget名称 + * @param array $data 传入的参数 + * @return mixed + */ + function widget($name, $data = []) + { + return Loader::action($name, $data, 'widget'); + } +} + +if (!function_exists('model')) { + /** + * 实例化Model + * @param string $name Model名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Model + */ + function model($name = '', $layer = 'model', $appendSuffix = false) + { + return Loader::model($name, $layer, $appendSuffix); + } +} + +if (!function_exists('validate')) { + /** + * 实例化验证器 + * @param string $name 验证器名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Validate + */ + function validate($name = '', $layer = 'validate', $appendSuffix = false) + { + return Loader::validate($name, $layer, $appendSuffix); + } +} + +if (!function_exists('db')) { + /** + * 实例化数据库类 + * @param string $name 操作的数据表名称(不含前缀) + * @param array|string $config 数据库配置参数 + * @param bool $force 是否强制重新连接 + * @return \think\db\Query + */ + function db($name = '', $config = [], $force = false) + { + return Db::connect($config, $force)->name($name); + } +} + +if (!function_exists('controller')) { + /** + * 实例化控制器 格式:[模块/]控制器 + * @param string $name 资源地址 + * @param string $layer 控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Controller + */ + function controller($name, $layer = 'controller', $appendSuffix = false) + { + return Loader::controller($name, $layer, $appendSuffix); + } +} + +if (!function_exists('action')) { + /** + * 调用模块的操作方法 参数格式 [模块/控制器/]操作 + * @param string $url 调用地址 + * @param string|array $vars 调用参数 支持字符串和数组 + * @param string $layer 要调用的控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return mixed + */ + function action($url, $vars = [], $layer = 'controller', $appendSuffix = false) + { + return Loader::action($url, $vars, $layer, $appendSuffix); + } +} + +if (!function_exists('import')) { + /** + * 导入所需的类库 同java的Import 本函数有缓存功能 + * @param string $class 类库命名空间字符串 + * @param string $baseUrl 起始路径 + * @param string $ext 导入的文件扩展名 + * @return boolean + */ + function import($class, $baseUrl = '', $ext = EXT) + { + return Loader::import($class, $baseUrl, $ext); + } +} + +if (!function_exists('vendor')) { + /** + * 快速导入第三方框架类库 所有第三方框架的类库文件统一放到 系统的Vendor目录下面 + * @param string $class 类库 + * @param string $ext 类库后缀 + * @return boolean + */ + function vendor($class, $ext = EXT) + { + return Loader::import($class, VENDOR_PATH, $ext); + } +} + +if (!function_exists('dump')) { + /** + * 浏览器友好的变量输出 + * @param mixed $var 变量 + * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 + * @param string $label 标签 默认为空 + * @return void|string + */ + function dump($var, $echo = true, $label = null) + { + return Debug::dump($var, $echo, $label); + } +} + +if (!function_exists('url')) { + /** + * Url生成 + * @param string $url 路由地址 + * @param string|array $vars 变量 + * @param bool|string $suffix 生成的URL后缀 + * @param bool|string $domain 域名 + * @return string + */ + function url($url = '', $vars = '', $suffix = true, $domain = false) + { + return Url::build($url, $vars, $suffix, $domain); + } +} + +if (!function_exists('session')) { + /** + * Session管理 + * @param string|array $name session名称,如果为数组表示进行session设置 + * @param mixed $value session值 + * @param string $prefix 前缀 + * @return mixed + */ + function session($name, $value = '', $prefix = null) + { + if (is_array($name)) { + // 初始化 + Session::init($name); + } elseif (is_null($name)) { + // 清除 + Session::clear('' === $value ? null : $value); + } elseif ('' === $value) { + // 判断或获取 + return 0 === strpos($name, '?') ? Session::has(substr($name, 1), $prefix) : Session::get($name, $prefix); + } elseif (is_null($value)) { + // 删除 + return Session::delete($name, $prefix); + } else { + // 设置 + return Session::set($name, $value, $prefix); + } + } +} + +if (!function_exists('cookie')) { + /** + * Cookie管理 + * @param string|array $name cookie名称,如果为数组表示进行cookie设置 + * @param mixed $value cookie值 + * @param mixed $option 参数 + * @return mixed + */ + function cookie($name, $value = '', $option = null) + { + if (is_array($name)) { + // 初始化 + Cookie::init($name); + } elseif (is_null($name)) { + // 清除 + Cookie::clear($value); + } elseif ('' === $value) { + // 获取 + return 0 === strpos($name, '?') ? Cookie::has(substr($name, 1), $option) : Cookie::get($name, $option); + } elseif (is_null($value)) { + // 删除 + return Cookie::delete($name); + } else { + // 设置 + return Cookie::set($name, $value, $option); + } + } +} + +if (!function_exists('cache')) { + /** + * 缓存管理 + * @param mixed $name 缓存名称,如果为数组表示进行缓存设置 + * @param mixed $value 缓存值 + * @param mixed $options 缓存参数 + * @param string $tag 缓存标签 + * @return mixed + */ + function cache($name, $value = '', $options = null, $tag = null) + { + if (is_array($options)) { + // 缓存操作的同时初始化 + $cache = Cache::connect($options); + } elseif (is_array($name)) { + // 缓存初始化 + return Cache::connect($name); + } else { + $cache = Cache::init(); + } + + if (is_null($name)) { + return $cache->clear($value); + } elseif ('' === $value) { + // 获取缓存 + return 0 === strpos($name, '?') ? $cache->has(substr($name, 1)) : $cache->get($name); + } elseif (is_null($value)) { + // 删除缓存 + return $cache->rm($name); + } elseif (0 === strpos($name, '?') && '' !== $value) { + $expire = is_numeric($options) ? $options : null; + return $cache->remember(substr($name, 1), $value, $expire); + } else { + // 缓存数据 + if (is_array($options)) { + $expire = isset($options['expire']) ? $options['expire'] : null; //修复查询缓存无法设置过期时间 + } else { + $expire = is_numeric($options) ? $options : null; //默认快捷缓存设置过期时间 + } + if (is_null($tag)) { + return $cache->set($name, $value, $expire); + } else { + return $cache->tag($tag)->set($name, $value, $expire); + } + } + } +} + +if (!function_exists('trace')) { + /** + * 记录日志信息 + * @param mixed $log log信息 支持字符串和数组 + * @param string $level 日志级别 + * @return void|array + */ + function trace($log = '[think]', $level = 'log') + { + if ('[think]' === $log) { + return Log::getLog(); + } else { + Log::record($log, $level); + } + } +} + +if (!function_exists('request')) { + /** + * 获取当前Request对象实例 + * @return Request + */ + function request() + { + return Request::instance(); + } +} + +if (!function_exists('response')) { + /** + * 创建普通 Response 对象实例 + * @param mixed $data 输出数据 + * @param int|string $code 状态码 + * @param array $header 头信息 + * @param string $type + * @return Response + */ + function response($data = [], $code = 200, $header = [], $type = 'html') + { + return Response::create($data, $type, $code, $header); + } +} + +if (!function_exists('view')) { + /** + * 渲染模板输出 + * @param string $template 模板文件 + * @param array $vars 模板变量 + * @param array $replace 模板替换 + * @param integer $code 状态码 + * @return \think\response\View + */ + function view($template = '', $vars = [], $replace = [], $code = 200) + { + return Response::create($template, 'view', $code)->replace($replace)->assign($vars); + } +} + +if (!function_exists('json')) { + /** + * 获取\think\response\Json对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Json + */ + function json($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'json', $code, $header, $options); + } +} + +if (!function_exists('jsonp')) { + /** + * 获取\think\response\Jsonp对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Jsonp + */ + function jsonp($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'jsonp', $code, $header, $options); + } +} + +if (!function_exists('xml')) { + /** + * 获取\think\response\Xml对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Xml + */ + function xml($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'xml', $code, $header, $options); + } +} + +if (!function_exists('redirect')) { + /** + * 获取\think\response\Redirect对象实例 + * @param mixed $url 重定向地址 支持Url::build方法的地址 + * @param array|integer $params 额外参数 + * @param integer $code 状态码 + * @param array $with 隐式传参 + * @return \think\response\Redirect + */ + function redirect($url = [], $params = [], $code = 302, $with = []) + { + if (is_integer($params)) { + $code = $params; + $params = []; + } + return Response::create($url, 'redirect', $code)->params($params)->with($with); + } +} + +if (!function_exists('abort')) { + /** + * 抛出HTTP异常 + * @param integer|Response $code 状态码 或者 Response对象实例 + * @param string $message 错误信息 + * @param array $header 参数 + */ + function abort($code, $message = null, $header = []) + { + if ($code instanceof Response) { + throw new HttpResponseException($code); + } else { + throw new HttpException($code, $message, null, $header); + } + } +} + +if (!function_exists('halt')) { + /** + * 调试变量并且中断输出 + * @param mixed $var 调试变量或者信息 + */ + function halt($var) + { + dump($var); + throw new HttpResponseException(new Response); + } +} + +if (!function_exists('token')) { + /** + * 生成表单令牌 + * @param string $name 令牌名称 + * @param mixed $type 令牌生成方法 + * @return string + */ + function token($name = '__token__', $type = 'md5') + { + $token = Request::instance()->token($name, $type); + return ''; + } +} + +if (!function_exists('load_relation')) { + /** + * 延迟预载入关联查询 + * @param mixed $resultSet 数据集 + * @param mixed $relation 关联 + * @return array + */ + function load_relation($resultSet, $relation) + { + $item = current($resultSet); + if ($item instanceof Model) { + $item->eagerlyResultSet($resultSet, $relation); + } + return $resultSet; + } +} + +if (!function_exists('collection')) { + /** + * 数组转换为数据集对象 + * @param array $resultSet 数据集数组 + * @return \think\model\Collection|\think\Collection + */ + function collection($resultSet) + { + $item = current($resultSet); + if ($item instanceof Model) { + return \think\model\Collection::make($resultSet); + } else { + return \think\Collection::make($resultSet); + } + } +} diff --git a/thinkphp/lang/zh-cn.php b/thinkphp/lang/zh-cn.php new file mode 100644 index 0000000..6e89f01 --- /dev/null +++ b/thinkphp/lang/zh-cn.php @@ -0,0 +1,68 @@ + +// +---------------------------------------------------------------------- + +// 核心中文语言包 +return [ + // 系统错误提示 + 'Undefined variable' => '未定义变量', + 'Undefined index' => '未定义数组索引', + 'Undefined offset' => '未定义数组下标', + 'Parse error' => '语法解析错误', + 'Type error' => '类型错误', + 'Fatal error' => '致命错误', + 'syntax error' => '语法错误', + + // 框架核心错误提示 + 'dispatch type not support' => '不支持的调度类型', + 'method param miss' => '方法参数错误', + 'method not exists' => '方法不存在', + 'module not exists' => '模块不存在', + 'controller not exists' => '控制器不存在', + 'class not exists' => '类不存在', + 'property not exists' => '类的属性不存在', + 'template not exists' => '模板文件不存在', + 'illegal controller name' => '非法的控制器名称', + 'illegal action name' => '非法的操作名称', + 'url suffix deny' => '禁止的URL后缀访问', + 'Route Not Found' => '当前访问路由未定义', + 'Undefined db type' => '未定义数据库类型', + 'variable type error' => '变量类型错误', + 'PSR-4 error' => 'PSR-4 规范错误', + 'not support total' => '简洁模式下不能获取数据总数', + 'not support last' => '简洁模式下不能获取最后一页', + 'error session handler' => '错误的SESSION处理器类', + 'not allow php tag' => '模板不允许使用PHP语法', + 'not support' => '不支持', + 'redisd master' => 'Redisd 主服务器错误', + 'redisd slave' => 'Redisd 从服务器错误', + 'must run at sae' => '必须在SAE运行', + 'memcache init error' => '未开通Memcache服务,请在SAE管理平台初始化Memcache服务', + 'KVDB init error' => '没有初始化KVDB,请在SAE管理平台初始化KVDB服务', + 'fields not exists' => '数据表字段不存在', + 'where express error' => '查询表达式错误', + 'no data to update' => '没有任何数据需要更新', + 'miss data to insert' => '缺少需要写入的数据', + 'miss complex primary data' => '缺少复合主键数据', + 'miss update condition' => '缺少更新条件', + 'model data Not Found' => '模型数据不存在', + 'table data not Found' => '表数据不存在', + 'delete without condition' => '没有条件不会执行删除操作', + 'miss relation data' => '缺少关联表数据', + 'tag attr must' => '模板标签属性必须', + 'tag error' => '模板标签错误', + 'cache write error' => '缓存写入失败', + 'sae mc write error' => 'SAE mc 写入错误', + 'route name not exists' => '路由标识不存在(或参数不够)', + 'invalid request' => '非法请求', + 'bind attr has exists' => '模型的属性已经存在', + 'relation data not exists' => '关联数据不存在', + 'relation not support' => '关联不支持', +]; diff --git a/thinkphp/library/think/App.php b/thinkphp/library/think/App.php new file mode 100644 index 0000000..615be24 --- /dev/null +++ b/thinkphp/library/think/App.php @@ -0,0 +1,591 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; +use think\exception\HttpException; +use think\exception\HttpResponseException; +use think\exception\RouteNotFoundException; + +/** + * App 应用管理 + * @author liu21st + */ +class App +{ + /** + * @var bool 是否初始化过 + */ + protected static $init = false; + + /** + * @var string 当前模块路径 + */ + public static $modulePath; + + /** + * @var bool 应用调试模式 + */ + public static $debug = true; + + /** + * @var string 应用类库命名空间 + */ + public static $namespace = 'app'; + + /** + * @var bool 应用类库后缀 + */ + public static $suffix = false; + + /** + * @var bool 应用路由检测 + */ + protected static $routeCheck; + + /** + * @var bool 严格路由检测 + */ + protected static $routeMust; + + protected static $dispatch; + protected static $file = []; + + /** + * 执行应用程序 + * @access public + * @param Request $request Request对象 + * @return Response + * @throws Exception + */ + public static function run(Request $request = null) + { + is_null($request) && $request = Request::instance(); + + try { + $config = self::initCommon(); + if (defined('BIND_MODULE')) { + // 模块/控制器绑定 + BIND_MODULE && Route::bind(BIND_MODULE); + } elseif ($config['auto_bind_module']) { + // 入口自动绑定 + $name = pathinfo($request->baseFile(), PATHINFO_FILENAME); + if ($name && 'index' != $name && is_dir(APP_PATH . $name)) { + Route::bind($name); + } + } + + $request->filter($config['default_filter']); + + // 默认语言 + Lang::range($config['default_lang']); + if ($config['lang_switch_on']) { + // 开启多语言机制 检测当前语言 + Lang::detect(); + } + $request->langset(Lang::range()); + + // 加载系统语言包 + Lang::load([ + THINK_PATH . 'lang' . DS . $request->langset() . EXT, + APP_PATH . 'lang' . DS . $request->langset() . EXT, + ]); + + // 获取应用调度信息 + $dispatch = self::$dispatch; + if (empty($dispatch)) { + // 进行URL路由检测 + $dispatch = self::routeCheck($request, $config); + } + // 记录当前调度信息 + $request->dispatch($dispatch); + + // 记录路由和请求信息 + if (self::$debug) { + Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info'); + Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info'); + Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info'); + } + + // 监听app_begin + Hook::listen('app_begin', $dispatch); + // 请求缓存检查 + $request->cache($config['request_cache'], $config['request_cache_expire'], $config['request_cache_except']); + + $data = self::exec($dispatch, $config); + } catch (HttpResponseException $exception) { + $data = $exception->getResponse(); + } + + // 清空类的实例化 + Loader::clearInstance(); + + // 输出数据到客户端 + if ($data instanceof Response) { + $response = $data; + } elseif (!is_null($data)) { + // 默认自动识别响应输出类型 + $isAjax = $request->isAjax(); + $type = $isAjax ? Config::get('default_ajax_return') : Config::get('default_return_type'); + $response = Response::create($data, $type); + } else { + $response = Response::create(); + } + + // 监听app_end + Hook::listen('app_end', $response); + + return $response; + } + + /** + * 设置当前请求的调度信息 + * @access public + * @param array|string $dispatch 调度信息 + * @param string $type 调度类型 + * @return void + */ + public static function dispatch($dispatch, $type = 'module') + { + self::$dispatch = ['type' => $type, $type => $dispatch]; + } + + /** + * 执行函数或者闭包方法 支持参数调用 + * @access public + * @param string|array|\Closure $function 函数或者闭包 + * @param array $vars 变量 + * @return mixed + */ + public static function invokeFunction($function, $vars = []) + { + $reflect = new \ReflectionFunction($function); + $args = self::bindParams($reflect, $vars); + // 记录执行信息 + self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info'); + return $reflect->invokeArgs($args); + } + + /** + * 调用反射执行类的方法 支持参数绑定 + * @access public + * @param string|array $method 方法 + * @param array $vars 变量 + * @return mixed + */ + public static function invokeMethod($method, $vars = []) + { + if (is_array($method)) { + $class = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]); + $reflect = new \ReflectionMethod($class, $method[1]); + } else { + // 静态方法 + $reflect = new \ReflectionMethod($method); + } + $args = self::bindParams($reflect, $vars); + + self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info'); + return $reflect->invokeArgs(isset($class) ? $class : null, $args); + } + + /** + * 调用反射执行类的实例化 支持依赖注入 + * @access public + * @param string $class 类名 + * @param array $vars 变量 + * @return mixed + */ + public static function invokeClass($class, $vars = []) + { + $reflect = new \ReflectionClass($class); + $constructor = $reflect->getConstructor(); + if ($constructor) { + $args = self::bindParams($constructor, $vars); + } else { + $args = []; + } + return $reflect->newInstanceArgs($args); + } + + /** + * 绑定参数 + * @access private + * @param \ReflectionMethod|\ReflectionFunction $reflect 反射类 + * @param array $vars 变量 + * @return array + */ + private static function bindParams($reflect, $vars = []) + { + if (empty($vars)) { + // 自动获取请求变量 + if (Config::get('url_param_type')) { + $vars = Request::instance()->route(); + } else { + $vars = Request::instance()->param(); + } + } + $args = []; + if ($reflect->getNumberOfParameters() > 0) { + // 判断数组类型 数字数组时按顺序绑定参数 + reset($vars); + $type = key($vars) === 0 ? 1 : 0; + $params = $reflect->getParameters(); + foreach ($params as $param) { + $args[] = self::getParamValue($param, $vars, $type); + } + } + return $args; + } + + /** + * 获取参数值 + * @access private + * @param \ReflectionParameter $param + * @param array $vars 变量 + * @param string $type + * @return array + */ + private static function getParamValue($param, &$vars, $type) + { + $name = $param->getName(); + $class = $param->getClass(); + if ($class) { + $className = $class->getName(); + $bind = Request::instance()->$name; + if ($bind instanceof $className) { + $result = $bind; + } else { + if (method_exists($className, 'invoke')) { + $method = new \ReflectionMethod($className, 'invoke'); + if ($method->isPublic() && $method->isStatic()) { + return $className::invoke(Request::instance()); + } + } + $result = method_exists($className, 'instance') ? $className::instance() : new $className; + } + } elseif (1 == $type && !empty($vars)) { + $result = array_shift($vars); + } elseif (0 == $type && isset($vars[$name])) { + $result = $vars[$name]; + } elseif ($param->isDefaultValueAvailable()) { + $result = $param->getDefaultValue(); + } else { + throw new \InvalidArgumentException('method param miss:' . $name); + } + return $result; + } + + protected static function exec($dispatch, $config) + { + switch ($dispatch['type']) { + case 'redirect': + // 执行重定向跳转 + $data = Response::create($dispatch['url'], 'redirect')->code($dispatch['status']); + break; + case 'module': + // 模块/控制器/操作 + $data = self::module($dispatch['module'], $config, isset($dispatch['convert']) ? $dispatch['convert'] : null); + break; + case 'controller': + // 执行控制器操作 + $vars = array_merge(Request::instance()->param(), $dispatch['var']); + $data = Loader::action($dispatch['controller'], $vars, $config['url_controller_layer'], $config['controller_suffix']); + break; + case 'method': + // 执行回调方法 + $vars = array_merge(Request::instance()->param(), $dispatch['var']); + $data = self::invokeMethod($dispatch['method'], $vars); + break; + case 'function': + // 执行闭包 + $data = self::invokeFunction($dispatch['function']); + break; + case 'response': + $data = $dispatch['response']; + break; + default: + throw new \InvalidArgumentException('dispatch type not support'); + } + return $data; + } + + /** + * 执行模块 + * @access public + * @param array $result 模块/控制器/操作 + * @param array $config 配置参数 + * @param bool $convert 是否自动转换控制器和操作名 + * @return mixed + */ + public static function module($result, $config, $convert = null) + { + if (is_string($result)) { + $result = explode('/', $result); + } + $request = Request::instance(); + if ($config['app_multi_module']) { + // 多模块部署 + $module = strip_tags(strtolower($result[0] ?: $config['default_module'])); + $bind = Route::getBind('module'); + $available = false; + if ($bind) { + // 绑定模块 + list($bindModule) = explode('/', $bind); + if (empty($result[0])) { + $module = $bindModule; + $available = true; + } elseif ($module == $bindModule) { + $available = true; + } + } elseif (!in_array($module, $config['deny_module_list']) && is_dir(APP_PATH . $module)) { + $available = true; + } + + // 模块初始化 + if ($module && $available) { + // 初始化模块 + $request->module($module); + $config = self::init($module); + // 模块请求缓存检查 + $request->cache($config['request_cache'], $config['request_cache_expire'], $config['request_cache_except']); + } else { + throw new HttpException(404, 'module not exists:' . $module); + } + } else { + // 单一模块部署 + $module = ''; + $request->module($module); + } + // 当前模块路径 + App::$modulePath = APP_PATH . ($module ? $module . DS : ''); + + // 是否自动转换控制器和操作名 + $convert = is_bool($convert) ? $convert : $config['url_convert']; + // 获取控制器名 + $controller = strip_tags($result[1] ?: $config['default_controller']); + $controller = $convert ? strtolower($controller) : $controller; + + // 获取操作名 + $actionName = strip_tags($result[2] ?: $config['default_action']); + $actionName = $convert ? strtolower($actionName) : $actionName; + + // 设置当前请求的控制器、操作 + $request->controller(Loader::parseName($controller, 1))->action($actionName); + + // 监听module_init + Hook::listen('module_init', $request); + + try { + $instance = Loader::controller($controller, $config['url_controller_layer'], $config['controller_suffix'], $config['empty_controller']); + } catch (ClassNotFoundException $e) { + throw new HttpException(404, 'controller not exists:' . $e->getClass()); + } + + // 获取当前操作名 + $action = $actionName . $config['action_suffix']; + + $vars = []; + if (is_callable([$instance, $action])) { + // 执行操作方法 + $call = [$instance, $action]; + } elseif (is_callable([$instance, '_empty'])) { + // 空操作 + $call = [$instance, '_empty']; + $vars = [$actionName]; + } else { + // 操作不存在 + throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()'); + } + + Hook::listen('action_begin', $call); + + return self::invokeMethod($call, $vars); + } + + /** + * 初始化应用 + */ + public static function initCommon() + { + if (empty(self::$init)) { + if (defined('APP_NAMESPACE')) { + self::$namespace = APP_NAMESPACE; + } + Loader::addNamespace(self::$namespace, APP_PATH); + + // 初始化应用 + $config = self::init(); + self::$suffix = $config['class_suffix']; + + // 应用调试模式 + self::$debug = Env::get('app_debug', Config::get('app_debug')); + if (!self::$debug) { + ini_set('display_errors', 'Off'); + } elseif (!IS_CLI) { + //重新申请一块比较大的buffer + if (ob_get_level() > 0) { + $output = ob_get_clean(); + } + ob_start(); + if (!empty($output)) { + echo $output; + } + } + + if (!empty($config['root_namespace'])) { + Loader::addNamespace($config['root_namespace']); + } + + // 加载额外文件 + if (!empty($config['extra_file_list'])) { + foreach ($config['extra_file_list'] as $file) { + $file = strpos($file, '.') ? $file : APP_PATH . $file . EXT; + if (is_file($file) && !isset(self::$file[$file])) { + include $file; + self::$file[$file] = true; + } + } + } + + // 设置系统时区 + date_default_timezone_set($config['default_timezone']); + + // 监听app_init + Hook::listen('app_init'); + + self::$init = true; + } + return Config::get(); + } + + /** + * 初始化应用或模块 + * @access public + * @param string $module 模块名 + * @return array + */ + private static function init($module = '') + { + // 定位模块目录 + $module = $module ? $module . DS : ''; + + // 加载初始化文件 + if (is_file(APP_PATH . $module . 'init' . EXT)) { + include APP_PATH . $module . 'init' . EXT; + } elseif (is_file(RUNTIME_PATH . $module . 'init' . EXT)) { + include RUNTIME_PATH . $module . 'init' . EXT; + } else { + $path = APP_PATH . $module; + // 加载模块配置 + $config = Config::load(CONF_PATH . $module . 'config' . CONF_EXT); + // 读取数据库配置文件 + $filename = CONF_PATH . $module . 'database' . CONF_EXT; + Config::load($filename, 'database'); + // 读取扩展配置文件 + if (is_dir(CONF_PATH . $module . 'extra')) { + $dir = CONF_PATH . $module . 'extra'; + $files = scandir($dir); + foreach ($files as $file) { + if ('.' . pathinfo($file, PATHINFO_EXTENSION) === CONF_EXT) { + $filename = $dir . DS . $file; + Config::load($filename, pathinfo($file, PATHINFO_FILENAME)); + } + } + } + + // 加载应用状态配置 + if ($config['app_status']) { + $config = Config::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT); + } + + // 加载行为扩展文件 + if (is_file(CONF_PATH . $module . 'tags' . EXT)) { + Hook::import(include CONF_PATH . $module . 'tags' . EXT); + } + + // 加载公共文件 + if (is_file($path . 'common' . EXT)) { + include $path . 'common' . EXT; + } + + // 加载当前模块语言包 + if ($module) { + Lang::load($path . 'lang' . DS . Request::instance()->langset() . EXT); + } + } + return Config::get(); + } + + /** + * URL路由检测(根据PATH_INFO) + * @access public + * @param \think\Request $request + * @param array $config + * @return array + * @throws \think\Exception + */ + public static function routeCheck($request, array $config) + { + $path = $request->path(); + $depr = $config['pathinfo_depr']; + $result = false; + // 路由检测 + $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on']; + if ($check) { + // 开启路由 + if (is_file(RUNTIME_PATH . 'route.php')) { + // 读取路由缓存 + $rules = include RUNTIME_PATH . 'route.php'; + if (is_array($rules)) { + Route::rules($rules); + } + } else { + $files = $config['route_config_file']; + foreach ($files as $file) { + if (is_file(CONF_PATH . $file . CONF_EXT)) { + // 导入路由配置 + $rules = include CONF_PATH . $file . CONF_EXT; + if (is_array($rules)) { + Route::import($rules); + } + } + } + } + + // 路由检测(根据路由定义返回不同的URL调度) + $result = Route::check($request, $path, $depr, $config['url_domain_deploy']); + $must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must']; + if ($must && false === $result) { + // 路由无效 + throw new RouteNotFoundException(); + } + } + if (false === $result) { + // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索 + $result = Route::parseUrl($path, $depr, $config['controller_auto_search']); + } + return $result; + } + + /** + * 设置应用的路由检测机制 + * @access public + * @param bool $route 是否需要检测路由 + * @param bool $must 是否强制检测路由 + * @return void + */ + public static function route($route, $must = false) + { + self::$routeCheck = $route; + self::$routeMust = $must; + } +} diff --git a/thinkphp/library/think/Build.php b/thinkphp/library/think/Build.php new file mode 100644 index 0000000..6e055c9 --- /dev/null +++ b/thinkphp/library/think/Build.php @@ -0,0 +1,205 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Build +{ + /** + * 根据传入的build资料创建目录和文件 + * @access protected + * @param array $build build列表 + * @param string $namespace 应用类库命名空间 + * @param bool $suffix 类库后缀 + * @return void + */ + public static function run(array $build = [], $namespace = 'app', $suffix = false) + { + // 锁定 + $lockfile = APP_PATH . 'build.lock'; + if (is_writable($lockfile)) { + return; + } elseif (!touch($lockfile)) { + throw new Exception('应用目录[' . APP_PATH . ']不可写,目录无法自动生成!
    请手动生成项目目录~', 10006); + } + foreach ($build as $module => $list) { + if ('__dir__' == $module) { + // 创建目录列表 + self::buildDir($list); + } elseif ('__file__' == $module) { + // 创建文件列表 + self::buildFile($list); + } else { + // 创建模块 + self::module($module, $list, $namespace, $suffix); + } + } + // 解除锁定 + unlink($lockfile); + } + + /** + * 创建目录 + * @access protected + * @param array $list 目录列表 + * @return void + */ + protected static function buildDir($list) + { + foreach ($list as $dir) { + if (!is_dir(APP_PATH . $dir)) { + // 创建目录 + mkdir(APP_PATH . $dir, 0755, true); + } + } + } + + /** + * 创建文件 + * @access protected + * @param array $list 文件列表 + * @return void + */ + protected static function buildFile($list) + { + foreach ($list as $file) { + if (!is_dir(APP_PATH . dirname($file))) { + // 创建目录 + mkdir(APP_PATH . dirname($file), 0755, true); + } + if (!is_file(APP_PATH . $file)) { + file_put_contents(APP_PATH . $file, 'php' == pathinfo($file, PATHINFO_EXTENSION) ? " ['config.php', 'common.php'], + '__dir__' => ['controller', 'model', 'view'], + ]; + } + // 创建子目录和文件 + foreach ($list as $path => $file) { + $modulePath = APP_PATH . $module . DS; + if ('__dir__' == $path) { + // 生成子目录 + foreach ($file as $dir) { + self::checkDirBuild($modulePath . $dir); + } + } elseif ('__file__' == $path) { + // 生成(空白)文件 + foreach ($file as $name) { + if (!is_file($modulePath . $name)) { + file_put_contents($modulePath . $name, 'php' == pathinfo($name, PATHINFO_EXTENSION) ? " +// +---------------------------------------------------------------------- + +namespace think; + +use think\cache\Driver; + +class Cache +{ + protected static $instance = []; + public static $readTimes = 0; + public static $writeTimes = 0; + + /** + * 操作句柄 + * @var object + * @access protected + */ + protected static $handler; + + /** + * 连接缓存 + * @access public + * @param array $options 配置数组 + * @param bool|string $name 缓存连接标识 true 强制重新连接 + * @return Driver + */ + public static function connect(array $options = [], $name = false) + { + $type = !empty($options['type']) ? $options['type'] : 'File'; + if (false === $name) { + $name = md5(serialize($options)); + } + + if (true === $name || !isset(self::$instance[$name])) { + $class = false !== strpos($type, '\\') ? $type : '\\think\\cache\\driver\\' . ucwords($type); + + // 记录初始化信息 + App::$debug && Log::record('[ CACHE ] INIT ' . $type, 'info'); + if (true === $name) { + return new $class($options); + } else { + self::$instance[$name] = new $class($options); + } + } + return self::$instance[$name]; + } + + /** + * 自动初始化缓存 + * @access public + * @param array $options 配置数组 + * @return Driver + */ + public static function init(array $options = []) + { + if (is_null(self::$handler)) { + // 自动初始化缓存 + if (!empty($options)) { + $connect = self::connect($options); + } elseif ('complex' == Config::get('cache.type')) { + $connect = self::connect(Config::get('cache.default')); + } else { + $connect = self::connect(Config::get('cache')); + } + self::$handler = $connect; + } + return self::$handler; + } + + /** + * 切换缓存类型 需要配置 cache.type 为 complex + * @access public + * @param string $name 缓存标识 + * @return Driver + */ + public static function store($name = '') + { + if ('' !== $name && 'complex' == Config::get('cache.type')) { + return self::connect(Config::get('cache.' . $name), strtolower($name)); + } + return self::init(); + } + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public static function has($name) + { + self::$readTimes++; + return self::init()->has($name); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存标识 + * @param mixed $default 默认值 + * @return mixed + */ + public static function get($name, $default = false) + { + self::$readTimes++; + return self::init()->get($name, $default); + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存标识 + * @param mixed $value 存储数据 + * @param int|null $expire 有效时间 0为永久 + * @return boolean + */ + public static function set($name, $value, $expire = null) + { + self::$writeTimes++; + return self::init()->set($name, $value, $expire); + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public static function inc($name, $step = 1) + { + self::$writeTimes++; + return self::init()->inc($name, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public static function dec($name, $step = 1) + { + self::$writeTimes++; + return self::init()->dec($name, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存标识 + * @return boolean + */ + public static function rm($name) + { + self::$writeTimes++; + return self::init()->rm($name); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public static function clear($tag = null) + { + self::$writeTimes++; + return self::init()->clear($tag); + } + + /** + * 读取缓存并删除 + * @access public + * @param string $name 缓存变量名 + * @return mixed + */ + public static function pull($name) + { + self::$readTimes++; + self::$writeTimes++; + return self::init()->pull($name); + } + + /** + * 如果不存在则写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return mixed + */ + public static function remember($name, $value, $expire = null) + { + self::$readTimes++; + return self::init()->remember($name, $value, $expire); + } + + /** + * 缓存标签 + * @access public + * @param string $name 标签名 + * @param string|array $keys 缓存标识 + * @param bool $overlay 是否覆盖 + * @return Driver + */ + public static function tag($name, $keys = null, $overlay = false) + { + return self::init()->tag($name, $keys, $overlay); + } + +} diff --git a/thinkphp/library/think/Collection.php b/thinkphp/library/think/Collection.php new file mode 100644 index 0000000..41b4275 --- /dev/null +++ b/thinkphp/library/think/Collection.php @@ -0,0 +1,373 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use ArrayAccess; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use JsonSerializable; + +class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable +{ + protected $items = []; + + public function __construct($items = []) + { + $this->items = $this->convertToArray($items); + } + + public static function make($items = []) + { + return new static($items); + } + + /** + * 是否为空 + * @return bool + */ + public function isEmpty() + { + return empty($this->items); + } + + public function toArray() + { + return array_map(function ($value) { + return ($value instanceof Model || $value instanceof self) ? $value->toArray() : $value; + }, $this->items); + } + + public function all() + { + return $this->items; + } + + /** + * 合并数组 + * + * @param mixed $items + * @return static + */ + public function merge($items) + { + return new static(array_merge($this->items, $this->convertToArray($items))); + } + + /** + * 比较数组,返回差集 + * + * @param mixed $items + * @return static + */ + public function diff($items) + { + return new static(array_diff($this->items, $this->convertToArray($items))); + } + + /** + * 交换数组中的键和值 + * + * @return static + */ + public function flip() + { + return new static(array_flip($this->items)); + } + + /** + * 比较数组,返回交集 + * + * @param mixed $items + * @return static + */ + public function intersect($items) + { + return new static(array_intersect($this->items, $this->convertToArray($items))); + } + + /** + * 返回数组中所有的键名 + * + * @return static + */ + public function keys() + { + return new static(array_keys($this->items)); + } + + /** + * 删除数组的最后一个元素(出栈) + * + * @return mixed + */ + public function pop() + { + return array_pop($this->items); + } + + /** + * 通过使用用户自定义函数,以字符串返回数组 + * + * @param callable $callback + * @param mixed $initial + * @return mixed + */ + public function reduce(callable $callback, $initial = null) + { + return array_reduce($this->items, $callback, $initial); + } + + /** + * 以相反的顺序返回数组。 + * + * @return static + */ + public function reverse() + { + return new static(array_reverse($this->items)); + } + + /** + * 删除数组中首个元素,并返回被删除元素的值 + * + * @return mixed + */ + public function shift() + { + return array_shift($this->items); + } + + /** + * 把一个数组分割为新的数组块. + * + * @param int $size + * @param bool $preserveKeys + * @return static + */ + public function chunk($size, $preserveKeys = false) + { + $chunks = []; + + foreach (array_chunk($this->items, $size, $preserveKeys) as $chunk) { + $chunks[] = new static($chunk); + } + + return new static($chunks); + } + + /** + * 在数组开头插入一个元素 + * @param mixed $value + * @param null $key + * @return int + */ + public function unshift($value, $key = null) + { + if (is_null($key)) { + array_unshift($this->items, $value); + } else { + $this->items = [$key => $value] + $this->items; + } + } + + /** + * 给每个元素执行个回调 + * + * @param callable $callback + * @return $this + */ + public function each(callable $callback) + { + foreach ($this->items as $key => $item) { + if ($callback($item, $key) === false) { + break; + } + } + + return $this; + } + + /** + * 用回调函数过滤数组中的元素 + * @param callable|null $callback + * @return static + */ + public function filter(callable $callback = null) + { + if ($callback) { + return new static(array_filter($this->items, $callback)); + } + + return new static(array_filter($this->items)); + } + + /** + * 返回数组中指定的一列 + * @param $column_key + * @param null $index_key + * @return array + */ + public function column($column_key, $index_key = null) + { + if (function_exists('array_column')) { + return array_column($this->items, $column_key, $index_key); + } + + $result = []; + foreach ($this->items as $row) { + $key = $value = null; + $keySet = $valueSet = false; + if (null !== $index_key && array_key_exists($index_key, $row)) { + $keySet = true; + $key = (string) $row[$index_key]; + } + if (null === $column_key) { + $valueSet = true; + $value = $row; + } elseif (is_array($row) && array_key_exists($column_key, $row)) { + $valueSet = true; + $value = $row[$column_key]; + } + if ($valueSet) { + if ($keySet) { + $result[$key] = $value; + } else { + $result[] = $value; + } + } + } + return $result; + } + + /** + * 对数组排序 + * + * @param callable|null $callback + * @return static + */ + public function sort(callable $callback = null) + { + $items = $this->items; + + $callback ? uasort($items, $callback) : uasort($items, function ($a, $b) { + + if ($a == $b) { + return 0; + } + + return ($a < $b) ? -1 : 1; + }); + + return new static($items); + } + + /** + * 将数组打乱 + * + * @return static + */ + public function shuffle() + { + $items = $this->items; + + shuffle($items); + + return new static($items); + } + + /** + * 截取数组 + * + * @param int $offset + * @param int $length + * @param bool $preserveKeys + * @return static + */ + public function slice($offset, $length = null, $preserveKeys = false) + { + return new static(array_slice($this->items, $offset, $length, $preserveKeys)); + } + + // ArrayAccess + public function offsetExists($offset) + { + return array_key_exists($offset, $this->items); + } + + public function offsetGet($offset) + { + return $this->items[$offset]; + } + + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->items[] = $value; + } else { + $this->items[$offset] = $value; + } + } + + public function offsetUnset($offset) + { + unset($this->items[$offset]); + } + + //Countable + public function count() + { + return count($this->items); + } + + //IteratorAggregate + public function getIterator() + { + return new ArrayIterator($this->items); + } + + //JsonSerializable + public function jsonSerialize() + { + return $this->toArray(); + } + + /** + * 转换当前数据集为JSON字符串 + * @access public + * @param integer $options json参数 + * @return string + */ + public function toJson($options = JSON_UNESCAPED_UNICODE) + { + return json_encode($this->toArray(), $options); + } + + public function __toString() + { + return $this->toJson(); + } + + /** + * 转换成数组 + * + * @param mixed $items + * @return array + */ + protected function convertToArray($items) + { + if ($items instanceof self) { + return $items->all(); + } + return (array) $items; + } +} diff --git a/thinkphp/library/think/Config.php b/thinkphp/library/think/Config.php new file mode 100644 index 0000000..2b80843 --- /dev/null +++ b/thinkphp/library/think/Config.php @@ -0,0 +1,170 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Config +{ + // 配置参数 + private static $config = []; + // 参数作用域 + private static $range = '_sys_'; + + // 设定配置参数的作用域 + public static function range($range) + { + self::$range = $range; + if (!isset(self::$config[$range])) { + self::$config[$range] = []; + } + } + + /** + * 解析配置文件或内容 + * @param string $config 配置文件路径或内容 + * @param string $type 配置解析类型 + * @param string $name 配置名(如设置即表示二级配置) + * @param string $range 作用域 + * @return mixed + */ + public static function parse($config, $type = '', $name = '', $range = '') + { + $range = $range ?: self::$range; + if (empty($type)) { + $type = pathinfo($config, PATHINFO_EXTENSION); + } + $class = false !== strpos($type, '\\') ? $type : '\\think\\config\\driver\\' . ucwords($type); + return self::set((new $class())->parse($config), $name, $range); + } + + /** + * 加载配置文件(PHP格式) + * @param string $file 配置文件名 + * @param string $name 配置名(如设置即表示二级配置) + * @param string $range 作用域 + * @return mixed + */ + public static function load($file, $name = '', $range = '') + { + $range = $range ?: self::$range; + if (!isset(self::$config[$range])) { + self::$config[$range] = []; + } + if (is_file($file)) { + $name = strtolower($name); + $type = pathinfo($file, PATHINFO_EXTENSION); + if ('php' == $type) { + return self::set(include $file, $name, $range); + } elseif ('yaml' == $type && function_exists('yaml_parse_file')) { + return self::set(yaml_parse_file($file), $name, $range); + } else { + return self::parse($file, $type, $name, $range); + } + } else { + return self::$config[$range]; + } + } + + /** + * 检测配置是否存在 + * @param string $name 配置参数名(支持二级配置 .号分割) + * @param string $range 作用域 + * @return bool + */ + public static function has($name, $range = '') + { + $range = $range ?: self::$range; + + if (!strpos($name, '.')) { + return isset(self::$config[$range][strtolower($name)]); + } else { + // 二维数组设置和获取支持 + $name = explode('.', $name, 2); + return isset(self::$config[$range][strtolower($name[0])][$name[1]]); + } + } + + /** + * 获取配置参数 为空则获取所有配置 + * @param string $name 配置参数名(支持二级配置 .号分割) + * @param string $range 作用域 + * @return mixed + */ + public static function get($name = null, $range = '') + { + $range = $range ?: self::$range; + // 无参数时获取所有 + if (empty($name) && isset(self::$config[$range])) { + return self::$config[$range]; + } + + if (!strpos($name, '.')) { + $name = strtolower($name); + return isset(self::$config[$range][$name]) ? self::$config[$range][$name] : null; + } else { + // 二维数组设置和获取支持 + $name = explode('.', $name, 2); + $name[0] = strtolower($name[0]); + return isset(self::$config[$range][$name[0]][$name[1]]) ? self::$config[$range][$name[0]][$name[1]] : null; + } + } + + /** + * 设置配置参数 name为数组则为批量设置 + * @param string|array $name 配置参数名(支持二级配置 .号分割) + * @param mixed $value 配置值 + * @param string $range 作用域 + * @return mixed + */ + public static function set($name, $value = null, $range = '') + { + $range = $range ?: self::$range; + if (!isset(self::$config[$range])) { + self::$config[$range] = []; + } + if (is_string($name)) { + if (!strpos($name, '.')) { + self::$config[$range][strtolower($name)] = $value; + } else { + // 二维数组设置和获取支持 + $name = explode('.', $name, 2); + self::$config[$range][strtolower($name[0])][$name[1]] = $value; + } + return; + } elseif (is_array($name)) { + // 批量设置 + if (!empty($value)) { + self::$config[$range][$value] = isset(self::$config[$range][$value]) ? + array_merge(self::$config[$range][$value], $name) : + self::$config[$range][$value] = $name; + return self::$config[$range][$value]; + } else { + return self::$config[$range] = array_merge(self::$config[$range], array_change_key_case($name)); + } + } else { + // 为空直接返回 已有配置 + return self::$config[$range]; + } + } + + /** + * 重置配置参数 + */ + public static function reset($range = '') + { + $range = $range ?: self::$range; + if (true === $range) { + self::$config = []; + } else { + self::$config[$range] = []; + } + } +} diff --git a/thinkphp/library/think/Console.php b/thinkphp/library/think/Console.php new file mode 100644 index 0000000..1d97ab2 --- /dev/null +++ b/thinkphp/library/think/Console.php @@ -0,0 +1,719 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\console\Command; +use think\console\command\Help as HelpCommand; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Definition as InputDefinition; +use think\console\input\Option as InputOption; +use think\console\Output; +use think\console\output\driver\Buffer; + +class Console +{ + + private $name; + private $version; + + /** @var Command[] */ + private $commands = []; + + private $wantHelps = false; + + private $catchExceptions = true; + private $autoExit = true; + private $definition; + private $defaultCommand; + + private static $defaultCommands = [ + "think\\console\\command\\Help", + "think\\console\\command\\Lists", + "think\\console\\command\\Build", + "think\\console\\command\\Clear", + "think\\console\\command\\make\\Controller", + "think\\console\\command\\make\\Model", + "think\\console\\command\\optimize\\Autoload", + "think\\console\\command\\optimize\\Config", + "think\\console\\command\\optimize\\Route", + "think\\console\\command\\optimize\\Schema", + ]; + + public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') + { + $this->name = $name; + $this->version = $version; + + $this->defaultCommand = 'list'; + $this->definition = $this->getDefaultInputDefinition(); + + foreach ($this->getDefaultCommands() as $command) { + $this->add($command); + } + } + + public static function init($run = true) + { + static $console; + if (!$console) { + // 实例化console + $console = new self('Think Console', '0.1'); + // 读取指令集 + if (is_file(CONF_PATH . 'command' . EXT)) { + $commands = include CONF_PATH . 'command' . EXT; + if (is_array($commands)) { + foreach ($commands as $command) { + if (class_exists($command) && is_subclass_of($command, "\\think\\console\\Command")) { + // 注册指令 + $console->add(new $command()); + } + } + } + } + } + if ($run) { + // 运行 + return $console->run(); + } else { + return $console; + } + } + + /** + * @param $command + * @param array $parameters + * @param string $driver + * @return Output|Buffer + */ + public static function call($command, array $parameters = [], $driver = 'buffer') + { + $console = self::init(false); + + array_unshift($parameters, $command); + + $input = new Input($parameters); + $output = new Output($driver); + + $console->setCatchExceptions(false); + $console->find($command)->run($input, $output); + + return $output; + } + + /** + * 执行当前的指令 + * @return int + * @throws \Exception + * @api + */ + public function run() + { + $input = new Input(); + $output = new Output(); + + $this->configureIO($input, $output); + + try { + $exitCode = $this->doRun($input, $output); + } catch (\Exception $e) { + if (!$this->catchExceptions) { + throw $e; + } + + $output->renderException($e); + + $exitCode = $e->getCode(); + if (is_numeric($exitCode)) { + $exitCode = (int) $exitCode; + if (0 === $exitCode) { + $exitCode = 1; + } + } else { + $exitCode = 1; + } + } + + if ($this->autoExit) { + if ($exitCode > 255) { + $exitCode = 255; + } + + exit($exitCode); + } + + return $exitCode; + } + + /** + * 执行指令 + * @param Input $input + * @param Output $output + * @return int + */ + public function doRun(Input $input, Output $output) + { + if (true === $input->hasParameterOption(['--version', '-V'])) { + $output->writeln($this->getLongVersion()); + + return 0; + } + + $name = $this->getCommandName($input); + + if (true === $input->hasParameterOption(['--help', '-h'])) { + if (!$name) { + $name = 'help'; + $input = new Input(['help']); + } else { + $this->wantHelps = true; + } + } + + if (!$name) { + $name = $this->defaultCommand; + $input = new Input([$this->defaultCommand]); + } + + $command = $this->find($name); + + $exitCode = $this->doRunCommand($command, $input, $output); + + return $exitCode; + } + + /** + * 设置输入参数定义 + * @param InputDefinition $definition + */ + public function setDefinition(InputDefinition $definition) + { + $this->definition = $definition; + } + + /** + * 获取输入参数定义 + * @return InputDefinition The InputDefinition instance + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Gets the help message. + * @return string A help message. + */ + public function getHelp() + { + return $this->getLongVersion(); + } + + /** + * 是否捕获异常 + * @param bool $boolean + * @api + */ + public function setCatchExceptions($boolean) + { + $this->catchExceptions = (bool) $boolean; + } + + /** + * 是否自动退出 + * @param bool $boolean + * @api + */ + public function setAutoExit($boolean) + { + $this->autoExit = (bool) $boolean; + } + + /** + * 获取名称 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 设置名称 + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * 获取版本 + * @return string + * @api + */ + public function getVersion() + { + return $this->version; + } + + /** + * 设置版本 + * @param string $version + */ + public function setVersion($version) + { + $this->version = $version; + } + + /** + * 获取完整的版本号 + * @return string + */ + public function getLongVersion() + { + if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) { + return sprintf('%s version %s', $this->getName(), $this->getVersion()); + } + + return 'Console Tool'; + } + + /** + * 注册一个指令 + * @param string $name + * @return Command + */ + public function register($name) + { + return $this->add(new Command($name)); + } + + /** + * 添加指令 + * @param Command[] $commands + */ + public function addCommands(array $commands) + { + foreach ($commands as $command) { + $this->add($command); + } + } + + /** + * 添加一个指令 + * @param Command $command + * @return Command + */ + public function add(Command $command) + { + $command->setConsole($this); + + if (!$command->isEnabled()) { + $command->setConsole(null); + return; + } + + if (null === $command->getDefinition()) { + throw new \LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command))); + } + + $this->commands[$command->getName()] = $command; + + foreach ($command->getAliases() as $alias) { + $this->commands[$alias] = $command; + } + + return $command; + } + + /** + * 获取指令 + * @param string $name 指令名称 + * @return Command + * @throws \InvalidArgumentException + */ + public function get($name) + { + if (!isset($this->commands[$name])) { + throw new \InvalidArgumentException(sprintf('The command "%s" does not exist.', $name)); + } + + $command = $this->commands[$name]; + + if ($this->wantHelps) { + $this->wantHelps = false; + + /** @var HelpCommand $helpCommand */ + $helpCommand = $this->get('help'); + $helpCommand->setCommand($command); + + return $helpCommand; + } + + return $command; + } + + /** + * 某个指令是否存在 + * @param string $name 指令名称 + * @return bool + */ + public function has($name) + { + return isset($this->commands[$name]); + } + + /** + * 获取所有的命名空间 + * @return array + */ + public function getNamespaces() + { + $namespaces = []; + foreach ($this->commands as $command) { + $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName())); + + foreach ($command->getAliases() as $alias) { + $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias)); + } + } + + return array_values(array_unique(array_filter($namespaces))); + } + + /** + * 查找注册命名空间中的名称或缩写。 + * @param string $namespace + * @return string + * @throws \InvalidArgumentException + */ + public function findNamespace($namespace) + { + $allNamespaces = $this->getNamespaces(); + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + return preg_quote($matches[1]) . '[^:]*'; + }, $namespace); + $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces); + + if (empty($namespaces)) { + $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); + + if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + + $message .= implode("\n ", $alternatives); + } + + throw new \InvalidArgumentException($message); + } + + $exact = in_array($namespace, $namespaces, true); + if (count($namespaces) > 1 && !$exact) { + throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces)))); + } + + return $exact ? $namespace : reset($namespaces); + } + + /** + * 查找指令 + * @param string $name 名称或者别名 + * @return Command + * @throws \InvalidArgumentException + */ + public function find($name) + { + $allCommands = array_keys($this->commands); + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + return preg_quote($matches[1]) . '[^:]*'; + }, $name); + $commands = preg_grep('{^' . $expr . '}', $allCommands); + + if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) { + if (false !== $pos = strrpos($name, ':')) { + $this->findNamespace(substr($name, 0, $pos)); + } + + $message = sprintf('Command "%s" is not defined.', $name); + + if ($alternatives = $this->findAlternatives($name, $allCommands)) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + $message .= implode("\n ", $alternatives); + } + + throw new \InvalidArgumentException($message); + } + + if (count($commands) > 1) { + $commandList = $this->commands; + $commands = array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) { + $commandName = $commandList[$nameOrAlias]->getName(); + + return $commandName === $nameOrAlias || !in_array($commandName, $commands); + }); + } + + $exact = in_array($name, $commands, true); + if (count($commands) > 1 && !$exact) { + $suggestions = $this->getAbbreviationSuggestions(array_values($commands)); + + throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions)); + } + + return $this->get($exact ? $name : reset($commands)); + } + + /** + * 获取所有的指令 + * @param string $namespace 命名空间 + * @return Command[] + * @api + */ + public function all($namespace = null) + { + if (null === $namespace) { + return $this->commands; + } + + $commands = []; + foreach ($this->commands as $name => $command) { + if ($this->extractNamespace($name, substr_count($namespace, ':') + 1) === $namespace) { + $commands[$name] = $command; + } + } + + return $commands; + } + + /** + * 获取可能的指令名 + * @param array $names + * @return array + */ + public static function getAbbreviations($names) + { + $abbrevs = []; + foreach ($names as $name) { + for ($len = strlen($name); $len > 0; --$len) { + $abbrev = substr($name, 0, $len); + $abbrevs[$abbrev][] = $name; + } + } + + return $abbrevs; + } + + /** + * 配置基于用户的参数和选项的输入和输出实例。 + * @param Input $input 输入实例 + * @param Output $output 输出实例 + */ + protected function configureIO(Input $input, Output $output) + { + if (true === $input->hasParameterOption(['--ansi'])) { + $output->setDecorated(true); + } elseif (true === $input->hasParameterOption(['--no-ansi'])) { + $output->setDecorated(false); + } + + if (true === $input->hasParameterOption(['--no-interaction', '-n'])) { + $input->setInteractive(false); + } + + if (true === $input->hasParameterOption(['--quiet', '-q'])) { + $output->setVerbosity(Output::VERBOSITY_QUIET); + } else { + if ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) { + $output->setVerbosity(Output::VERBOSITY_DEBUG); + } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) { + $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE); + } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) { + $output->setVerbosity(Output::VERBOSITY_VERBOSE); + } + } + } + + /** + * 执行指令 + * @param Command $command 指令实例 + * @param Input $input 输入实例 + * @param Output $output 输出实例 + * @return int + * @throws \Exception + */ + protected function doRunCommand(Command $command, Input $input, Output $output) + { + return $command->run($input, $output); + } + + /** + * 获取指令的基础名称 + * @param Input $input + * @return string + */ + protected function getCommandName(Input $input) + { + return $input->getFirstArgument(); + } + + /** + * 获取默认输入定义 + * @return InputDefinition + */ + protected function getDefaultInputDefinition() + { + return new InputDefinition([ + new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), + new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'), + new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this console version'), + new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'), + new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), + new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'), + new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'), + new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'), + ]); + } + + /** + * 设置默认命令 + * @return Command[] An array of default Command instances + */ + protected function getDefaultCommands() + { + $defaultCommands = []; + + foreach (self::$defaultCommands as $classname) { + if (class_exists($classname) && is_subclass_of($classname, "think\\console\\Command")) { + $defaultCommands[] = new $classname(); + } + } + + return $defaultCommands; + } + + public static function addDefaultCommands(array $classnames) + { + self::$defaultCommands = array_merge(self::$defaultCommands, $classnames); + } + + /** + * 获取可能的建议 + * @param array $abbrevs + * @return string + */ + private function getAbbreviationSuggestions($abbrevs) + { + return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : ''); + } + + /** + * 返回命名空间部分 + * @param string $name 指令 + * @param string $limit 部分的命名空间的最大数量 + * @return string + */ + public function extractNamespace($name, $limit = null) + { + $parts = explode(':', $name); + array_pop($parts); + + return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit)); + } + + /** + * 查找可替代的建议 + * @param string $name + * @param array|\Traversable $collection + * @return array + */ + private function findAlternatives($name, $collection) + { + $threshold = 1e3; + $alternatives = []; + + $collectionParts = []; + foreach ($collection as $item) { + $collectionParts[$item] = explode(':', $item); + } + + foreach (explode(':', $name) as $i => $subname) { + foreach ($collectionParts as $collectionName => $parts) { + $exists = isset($alternatives[$collectionName]); + if (!isset($parts[$i]) && $exists) { + $alternatives[$collectionName] += $threshold; + continue; + } elseif (!isset($parts[$i])) { + continue; + } + + $lev = levenshtein($subname, $parts[$i]); + if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) { + $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; + } elseif ($exists) { + $alternatives[$collectionName] += $threshold; + } + } + } + + foreach ($collection as $item) { + $lev = levenshtein($name, $item); + if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) { + $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; + } + } + + $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { + return $lev < 2 * $threshold; + }); + asort($alternatives); + + return array_keys($alternatives); + } + + /** + * 设置默认的指令 + * @param string $commandName The Command name + */ + public function setDefaultCommand($commandName) + { + $this->defaultCommand = $commandName; + } + + /** + * 返回所有的命名空间 + * @param string $name + * @return array + */ + private function extractAllNamespaces($name) + { + $parts = explode(':', $name, -1); + $namespaces = []; + + foreach ($parts as $part) { + if (count($namespaces)) { + $namespaces[] = end($namespaces) . ':' . $part; + } else { + $namespaces[] = $part; + } + } + + return $namespaces; + } + +} diff --git a/thinkphp/library/think/Controller.php b/thinkphp/library/think/Controller.php new file mode 100644 index 0000000..7aba39c --- /dev/null +++ b/thinkphp/library/think/Controller.php @@ -0,0 +1,212 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +\think\Loader::import('controller/Jump', TRAIT_PATH, EXT); + +use think\exception\ValidateException; + +class Controller +{ + use \traits\controller\Jump; + + /** + * @var \think\View 视图类实例 + */ + protected $view; + /** + * @var \think\Request Request实例 + */ + protected $request; + // 验证失败是否抛出异常 + protected $failException = false; + // 是否批量验证 + protected $batchValidate = false; + + /** + * 前置操作方法列表 + * @var array $beforeActionList + * @access protected + */ + protected $beforeActionList = []; + + /** + * 构造方法 + * @param Request $request Request对象 + * @access public + */ + public function __construct(Request $request = null) + { + if (is_null($request)) { + $request = Request::instance(); + } + $this->view = View::instance(Config::get('template'), Config::get('view_replace_str')); + $this->request = $request; + + // 控制器初始化 + $this->_initialize(); + + // 前置操作方法 + if ($this->beforeActionList) { + foreach ($this->beforeActionList as $method => $options) { + is_numeric($method) ? + $this->beforeAction($options) : + $this->beforeAction($method, $options); + } + } + } + + // 初始化 + protected function _initialize() + { + } + + /** + * 前置操作 + * @access protected + * @param string $method 前置操作方法名 + * @param array $options 调用参数 ['only'=>[...]] 或者['except'=>[...]] + */ + protected function beforeAction($method, $options = []) + { + if (isset($options['only'])) { + if (is_string($options['only'])) { + $options['only'] = explode(',', $options['only']); + } + if (!in_array($this->request->action(), $options['only'])) { + return; + } + } elseif (isset($options['except'])) { + if (is_string($options['except'])) { + $options['except'] = explode(',', $options['except']); + } + if (in_array($this->request->action(), $options['except'])) { + return; + } + } + + call_user_func([$this, $method]); + } + + /** + * 加载模板输出 + * @access protected + * @param string $template 模板文件名 + * @param array $vars 模板输出变量 + * @param array $replace 模板替换 + * @param array $config 模板参数 + * @return mixed + */ + protected function fetch($template = '', $vars = [], $replace = [], $config = []) + { + return $this->view->fetch($template, $vars, $replace, $config); + } + + /** + * 渲染内容输出 + * @access protected + * @param string $content 模板内容 + * @param array $vars 模板输出变量 + * @param array $replace 替换内容 + * @param array $config 模板参数 + * @return mixed + */ + protected function display($content = '', $vars = [], $replace = [], $config = []) + { + return $this->view->display($content, $vars, $replace, $config); + } + + /** + * 模板变量赋值 + * @access protected + * @param mixed $name 要显示的模板变量 + * @param mixed $value 变量的值 + * @return void + */ + protected function assign($name, $value = '') + { + $this->view->assign($name, $value); + } + + /** + * 初始化模板引擎 + * @access protected + * @param array|string $engine 引擎参数 + * @return void + */ + protected function engine($engine) + { + $this->view->engine($engine); + } + + /** + * 设置验证失败后是否抛出异常 + * @access protected + * @param bool $fail 是否抛出异常 + * @return $this + */ + protected function validateFailException($fail = true) + { + $this->failException = $fail; + return $this; + } + + /** + * 验证数据 + * @access protected + * @param array $data 数据 + * @param string|array $validate 验证器名或者验证规则数组 + * @param array $message 提示信息 + * @param bool $batch 是否批量验证 + * @param mixed $callback 回调方法(闭包) + * @return array|string|true + * @throws ValidateException + */ + protected function validate($data, $validate, $message = [], $batch = false, $callback = null) + { + if (is_array($validate)) { + $v = Loader::validate(); + $v->rule($validate); + } else { + if (strpos($validate, '.')) { + // 支持场景 + list($validate, $scene) = explode('.', $validate); + } + $v = Loader::validate($validate); + if (!empty($scene)) { + $v->scene($scene); + } + } + // 是否批量验证 + if ($batch || $this->batchValidate) { + $v->batch(true); + } + + if (is_array($message)) { + $v->message($message); + } + + if ($callback && is_callable($callback)) { + call_user_func_array($callback, [$v, &$data]); + } + + if (!$v->check($data)) { + if ($this->failException) { + throw new ValidateException($v->getError()); + } else { + return $v->getError(); + } + } else { + return true; + } + } +} diff --git a/thinkphp/library/think/Cookie.php b/thinkphp/library/think/Cookie.php new file mode 100644 index 0000000..3205fcd --- /dev/null +++ b/thinkphp/library/think/Cookie.php @@ -0,0 +1,224 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Cookie +{ + protected static $config = [ + // cookie 名称前缀 + 'prefix' => '', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => '', + // 是否使用 setcookie + 'setcookie' => true, + ]; + + protected static $init; + + /** + * Cookie初始化 + * @param array $config + * @return void + */ + public static function init(array $config = []) + { + if (empty($config)) { + $config = Config::get('cookie'); + } + self::$config = array_merge(self::$config, array_change_key_case($config)); + if (!empty(self::$config['httponly'])) { + ini_set('session.cookie_httponly', 1); + } + self::$init = true; + } + + /** + * 设置或者获取cookie作用域(前缀) + * @param string $prefix + * @return string|void + */ + public static function prefix($prefix = '') + { + if (empty($prefix)) { + return self::$config['prefix']; + } + self::$config['prefix'] = $prefix; + } + + /** + * Cookie 设置、获取、删除 + * + * @param string $name cookie名称 + * @param mixed $value cookie值 + * @param mixed $option 可选参数 可能会是 null|integer|string + * + * @return mixed + * @internal param mixed $options cookie参数 + */ + public static function set($name, $value = '', $option = null) + { + !isset(self::$init) && self::init(); + // 参数设置(会覆盖黙认设置) + if (!is_null($option)) { + if (is_numeric($option)) { + $option = ['expire' => $option]; + } elseif (is_string($option)) { + parse_str($option, $option); + } + $config = array_merge(self::$config, array_change_key_case($option)); + } else { + $config = self::$config; + } + $name = $config['prefix'] . $name; + // 设置cookie + if (is_array($value)) { + array_walk_recursive($value, 'self::jsonFormatProtect', 'encode'); + $value = 'think:' . json_encode($value); + } + $expire = !empty($config['expire']) ? $_SERVER['REQUEST_TIME'] + intval($config['expire']) : 0; + if ($config['setcookie']) { + setcookie($name, $value, $expire, $config['path'], $config['domain'], $config['secure'], $config['httponly']); + } + $_COOKIE[$name] = $value; + } + + /** + * 永久保存Cookie数据 + * @param string $name cookie名称 + * @param mixed $value cookie值 + * @param mixed $option 可选参数 可能会是 null|integer|string + * @return void + */ + public static function forever($name, $value = '', $option = null) + { + if (is_null($option) || is_numeric($option)) { + $option = []; + } + $option['expire'] = 315360000; + self::set($name, $value, $option); + } + + /** + * 判断Cookie数据 + * @param string $name cookie名称 + * @param string|null $prefix cookie前缀 + * @return bool + */ + public static function has($name, $prefix = null) + { + !isset(self::$init) && self::init(); + $prefix = !is_null($prefix) ? $prefix : self::$config['prefix']; + $name = $prefix . $name; + return isset($_COOKIE[$name]); + } + + /** + * Cookie获取 + * @param string $name cookie名称 + * @param string|null $prefix cookie前缀 + * @return mixed + */ + public static function get($name = '', $prefix = null) + { + !isset(self::$init) && self::init(); + $prefix = !is_null($prefix) ? $prefix : self::$config['prefix']; + $key = $prefix . $name; + + if ('' == $name) { + // 获取全部 + if ($prefix) { + $value = []; + foreach ($_COOKIE as $k => $val) { + if (0 === strpos($k, $prefix)) { + $value[$k] = $val; + } + } + } else { + $value = $_COOKIE; + } + } elseif (isset($_COOKIE[$key])) { + $value = $_COOKIE[$key]; + if (0 === strpos($value, 'think:')) { + $value = substr($value, 6); + $value = json_decode($value, true); + array_walk_recursive($value, 'self::jsonFormatProtect', 'decode'); + } + } else { + $value = null; + } + return $value; + } + + /** + * Cookie删除 + * @param string $name cookie名称 + * @param string|null $prefix cookie前缀 + * @return mixed + */ + public static function delete($name, $prefix = null) + { + !isset(self::$init) && self::init(); + $config = self::$config; + $prefix = !is_null($prefix) ? $prefix : $config['prefix']; + $name = $prefix . $name; + if ($config['setcookie']) { + setcookie($name, '', $_SERVER['REQUEST_TIME'] - 3600, $config['path'], $config['domain'], $config['secure'], $config['httponly']); + } + // 删除指定cookie + unset($_COOKIE[$name]); + } + + /** + * Cookie清空 + * @param string|null $prefix cookie前缀 + * @return mixed + */ + public static function clear($prefix = null) + { + // 清除指定前缀的所有cookie + if (empty($_COOKIE)) { + return; + } + !isset(self::$init) && self::init(); + // 要删除的cookie前缀,不指定则删除config设置的指定前缀 + $config = self::$config; + $prefix = !is_null($prefix) ? $prefix : $config['prefix']; + if ($prefix) { + // 如果前缀为空字符串将不作处理直接返回 + foreach ($_COOKIE as $key => $val) { + if (0 === strpos($key, $prefix)) { + if ($config['setcookie']) { + setcookie($key, '', $_SERVER['REQUEST_TIME'] - 3600, $config['path'], $config['domain'], $config['secure'], $config['httponly']); + } + unset($_COOKIE[$key]); + } + } + } + return; + } + + private static function jsonFormatProtect(&$val, $key, $type = 'encode') + { + if (!empty($val) && true !== $val) { + $val = 'decode' == $type ? urldecode($val) : urlencode($val); + } + } + +} diff --git a/thinkphp/library/think/Db.php b/thinkphp/library/think/Db.php new file mode 100644 index 0000000..8fa799e --- /dev/null +++ b/thinkphp/library/think/Db.php @@ -0,0 +1,152 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\db\Connection; +use think\db\Query; + +/** + * Class Db + * @package think + * @method Query table(string $table) static 指定数据表(含前缀) + * @method Query name(string $name) static 指定数据表(不含前缀) + * @method Query where(mixed $field, string $op = null, mixed $condition = null) static 查询条件 + * @method Query join(mixed $join, mixed $condition = null, string $type = 'INNER') static JOIN查询 + * @method Query union(mixed $union, boolean $all = false) static UNION查询 + * @method Query limit(mixed $offset, integer $length = null) static 查询LIMIT + * @method Query order(mixed $field, string $order = null) static 查询ORDER + * @method Query cache(mixed $key = null , integer $expire = null) static 设置查询缓存 + * @method mixed value(string $field) static 获取某个字段的值 + * @method array column(string $field, string $key = '') static 获取某个列的值 + * @method Query view(mixed $join, mixed $field = null, mixed $on = null, string $type = 'INNER') static 视图查询 + * @method mixed find(mixed $data = null) static 查询单个记录 + * @method mixed select(mixed $data = null) static 查询多个记录 + * @method integer insert(array $data, boolean $replace = false, boolean $getLastInsID = false, string $sequence = null) static 插入一条记录 + * @method integer insertGetId(array $data, boolean $replace = false, string $sequence = null) static 插入一条记录并返回自增ID + * @method integer insertAll(array $dataSet) static 插入多条记录 + * @method integer update(array $data) static 更新记录 + * @method integer delete(mixed $data = null) static 删除记录 + * @method boolean chunk(integer $count, callable $callback, string $column = null) static 分块获取数据 + * @method mixed query(string $sql, array $bind = [], boolean $master = false, bool $pdo = false) static SQL查询 + * @method integer execute(string $sql, array $bind = [], boolean $fetch = false, boolean $getLastInsID = false, string $sequence = null) static SQL执行 + * @method Paginator paginate(integer $listRows = 15, mixed $simple = null, array $config = []) static 分页查询 + * @method mixed transaction(callable $callback) static 执行数据库事务 + * @method void startTrans() static 启动事务 + * @method void commit() static 用于非自动提交状态下面的查询提交 + * @method void rollback() static 事务回滚 + * @method boolean batchQuery(array $sqlArray) static 批处理执行SQL语句 + * @method string quote(string $str) static SQL指令安全过滤 + * @method string getLastInsID($sequence = null) static 获取最近插入的ID + */ +class Db +{ + // 数据库连接实例 + private static $instance = []; + // 查询次数 + public static $queryTimes = 0; + // 执行次数 + public static $executeTimes = 0; + + /** + * 数据库初始化 并取得数据库类实例 + * @static + * @access public + * @param mixed $config 连接配置 + * @param bool|string $name 连接标识 true 强制重新连接 + * @return Connection + * @throws Exception + */ + public static function connect($config = [], $name = false) + { + if (false === $name) { + $name = md5(serialize($config)); + } + if (true === $name || !isset(self::$instance[$name])) { + // 解析连接参数 支持数组和字符串 + $options = self::parseConfig($config); + if (empty($options['type'])) { + throw new \InvalidArgumentException('Undefined db type'); + } + $class = false !== strpos($options['type'], '\\') ? $options['type'] : '\\think\\db\\connector\\' . ucwords($options['type']); + // 记录初始化信息 + if (App::$debug) { + Log::record('[ DB ] INIT ' . $options['type'], 'info'); + } + if (true === $name) { + $name = md5(serialize($config)); + } + self::$instance[$name] = new $class($options); + } + return self::$instance[$name]; + } + + /** + * 数据库连接参数解析 + * @static + * @access private + * @param mixed $config + * @return array + */ + private static function parseConfig($config) + { + if (empty($config)) { + $config = Config::get('database'); + } elseif (is_string($config) && false === strpos($config, '/')) { + // 支持读取配置参数 + $config = Config::get($config); + } + if (is_string($config)) { + return self::parseDsn($config); + } else { + return $config; + } + } + + /** + * DSN解析 + * 格式: mysql://username:passwd@localhost:3306/DbName?param1=val1¶m2=val2#utf8 + * @static + * @access private + * @param string $dsnStr + * @return array + */ + private static function parseDsn($dsnStr) + { + $info = parse_url($dsnStr); + if (!$info) { + return []; + } + $dsn = [ + 'type' => $info['scheme'], + 'username' => isset($info['user']) ? $info['user'] : '', + 'password' => isset($info['pass']) ? $info['pass'] : '', + 'hostname' => isset($info['host']) ? $info['host'] : '', + 'hostport' => isset($info['port']) ? $info['port'] : '', + 'database' => !empty($info['path']) ? ltrim($info['path'], '/') : '', + 'charset' => isset($info['fragment']) ? $info['fragment'] : 'utf8', + ]; + + if (isset($info['query'])) { + parse_str($info['query'], $dsn['params']); + } else { + $dsn['params'] = []; + } + return $dsn; + } + + // 调用驱动类的方法 + public static function __callStatic($method, $params) + { + // 自动初始化数据库 + return call_user_func_array([self::connect(), $method], $params); + } +} diff --git a/thinkphp/library/think/Debug.php b/thinkphp/library/think/Debug.php new file mode 100644 index 0000000..9994e20 --- /dev/null +++ b/thinkphp/library/think/Debug.php @@ -0,0 +1,212 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; +use think\response\Redirect; + +class Debug +{ + // 区间时间信息 + protected static $info = []; + // 区间内存信息 + protected static $mem = []; + + /** + * 记录时间(微秒)和内存使用情况 + * @param string $name 标记位置 + * @param mixed $value 标记值 留空则取当前 time 表示仅记录时间 否则同时记录时间和内存 + * @return mixed + */ + public static function remark($name, $value = '') + { + // 记录时间和内存使用 + self::$info[$name] = is_float($value) ? $value : microtime(true); + if ('time' != $value) { + self::$mem['mem'][$name] = is_float($value) ? $value : memory_get_usage(); + self::$mem['peak'][$name] = memory_get_peak_usage(); + } + } + + /** + * 统计某个区间的时间(微秒)使用情况 返回值以秒为单位 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 + * @return integer + */ + public static function getRangeTime($start, $end, $dec = 6) + { + if (!isset(self::$info[$end])) { + self::$info[$end] = microtime(true); + } + return number_format((self::$info[$end] - self::$info[$start]), $dec); + } + + /** + * 统计从开始到统计时的时间(微秒)使用情况 返回值以秒为单位 + * @param integer|string $dec 小数位 + * @return integer + */ + public static function getUseTime($dec = 6) + { + return number_format((microtime(true) - THINK_START_TIME), $dec); + } + + /** + * 获取当前访问的吞吐率情况 + * @return string + */ + public static function getThroughputRate() + { + return number_format(1 / self::getUseTime(), 2) . 'req/s'; + } + + /** + * 记录区间的内存使用情况 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 + * @return string + */ + public static function getRangeMem($start, $end, $dec = 2) + { + if (!isset(self::$mem['mem'][$end])) { + self::$mem['mem'][$end] = memory_get_usage(); + } + $size = self::$mem['mem'][$end] - self::$mem['mem'][$start]; + $a = ['B', 'KB', 'MB', 'GB', 'TB']; + $pos = 0; + while ($size >= 1024) { + $size /= 1024; + $pos++; + } + return round($size, $dec) . " " . $a[$pos]; + } + + /** + * 统计从开始到统计时的内存使用情况 + * @param integer|string $dec 小数位 + * @return string + */ + public static function getUseMem($dec = 2) + { + $size = memory_get_usage() - THINK_START_MEM; + $a = ['B', 'KB', 'MB', 'GB', 'TB']; + $pos = 0; + while ($size >= 1024) { + $size /= 1024; + $pos++; + } + return round($size, $dec) . " " . $a[$pos]; + } + + /** + * 统计区间的内存峰值情况 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 + * @return mixed + */ + public static function getMemPeak($start, $end, $dec = 2) + { + if (!isset(self::$mem['peak'][$end])) { + self::$mem['peak'][$end] = memory_get_peak_usage(); + } + $size = self::$mem['peak'][$end] - self::$mem['peak'][$start]; + $a = ['B', 'KB', 'MB', 'GB', 'TB']; + $pos = 0; + while ($size >= 1024) { + $size /= 1024; + $pos++; + } + return round($size, $dec) . " " . $a[$pos]; + } + + /** + * 获取文件加载信息 + * @param bool $detail 是否显示详细 + * @return integer|array + */ + public static function getFile($detail = false) + { + if ($detail) { + $files = get_included_files(); + $info = []; + foreach ($files as $key => $file) { + $info[] = $file . ' ( ' . number_format(filesize($file) / 1024, 2) . ' KB )'; + } + return $info; + } + return count(get_included_files()); + } + + /** + * 浏览器友好的变量输出 + * @param mixed $var 变量 + * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 + * @param string $label 标签 默认为空 + * @param integer $flags htmlspecialchars flags + * @return void|string + */ + public static function dump($var, $echo = true, $label = null, $flags = ENT_SUBSTITUTE) + { + $label = (null === $label) ? '' : rtrim($label) . ':'; + ob_start(); + var_dump($var); + $output = ob_get_clean(); + $output = preg_replace('/\]\=\>\n(\s+)/m', '] => ', $output); + if (IS_CLI) { + $output = PHP_EOL . $label . $output . PHP_EOL; + } else { + if (!extension_loaded('xdebug')) { + $output = htmlspecialchars($output, $flags); + } + $output = '
    ' . $label . $output . '
    '; + } + if ($echo) { + echo($output); + return; + } else { + return $output; + } + } + + public static function inject(Response $response, &$content) + { + $config = Config::get('trace'); + $type = isset($config['type']) ? $config['type'] : 'Html'; + $request = Request::instance(); + $class = false !== strpos($type, '\\') ? $type : '\\think\\debug\\' . ucwords($type); + unset($config['type']); + if (class_exists($class)) { + $trace = new $class($config); + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + + if ($response instanceof Redirect) { + //TODO 记录 + } else { + $output = $trace->output($response, Log::getLog()); + if (is_string($output)) { + // trace调试信息注入 + $pos = strripos($content, ''); + if (false !== $pos) { + $content = substr($content, 0, $pos) . $output . substr($content, $pos); + } else { + $content = $content . $output; + } + } + } + } +} diff --git a/thinkphp/library/think/Env.php b/thinkphp/library/think/Env.php new file mode 100644 index 0000000..fa87897 --- /dev/null +++ b/thinkphp/library/think/Env.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Env +{ + /** + * 获取环境变量值 + * @param string $name 环境变量名(支持二级 .号分割) + * @param string $default 默认值 + * @return mixed + */ + public static function get($name, $default = null) + { + $result = getenv(ENV_PREFIX . strtoupper(str_replace('.', '_', $name))); + if (false !== $result) { + if ('false' === $result) { + $result = false; + } elseif ('true' === $result) { + $result = true; + } + return $result; + } else { + return $default; + } + } +} diff --git a/thinkphp/library/think/Error.php b/thinkphp/library/think/Error.php new file mode 100644 index 0000000..c17292b --- /dev/null +++ b/thinkphp/library/think/Error.php @@ -0,0 +1,117 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\console\Output as ConsoleOutput; +use think\exception\ErrorException; +use think\exception\Handle; +use think\exception\ThrowableError; + +class Error +{ + /** + * 注册异常处理 + * @return void + */ + public static function register() + { + error_reporting(E_ALL); + set_error_handler([__CLASS__, 'appError']); + set_exception_handler([__CLASS__, 'appException']); + register_shutdown_function([__CLASS__, 'appShutdown']); + } + + /** + * Exception Handler + * @param \Exception|\Throwable $e + */ + public static function appException($e) + { + if (!$e instanceof \Exception) { + $e = new ThrowableError($e); + } + + self::getExceptionHandler()->report($e); + if (IS_CLI) { + self::getExceptionHandler()->renderForConsole(new ConsoleOutput, $e); + } else { + self::getExceptionHandler()->render($e)->send(); + } + } + + /** + * Error Handler + * @param integer $errno 错误编号 + * @param integer $errstr 详细错误信息 + * @param string $errfile 出错的文件 + * @param integer $errline 出错行号 + * @param array $errcontext + * @throws ErrorException + */ + public static function appError($errno, $errstr, $errfile = '', $errline = 0, $errcontext = []) + { + $exception = new ErrorException($errno, $errstr, $errfile, $errline, $errcontext); + if (error_reporting() & $errno) { + // 将错误信息托管至 think\exception\ErrorException + throw $exception; + } else { + self::getExceptionHandler()->report($exception); + } + } + + /** + * Shutdown Handler + */ + public static function appShutdown() + { + if (!is_null($error = error_get_last()) && self::isFatal($error['type'])) { + // 将错误信息托管至think\ErrorException + $exception = new ErrorException($error['type'], $error['message'], $error['file'], $error['line']); + + self::appException($exception); + } + + // 写入日志 + Log::save(); + } + + /** + * 确定错误类型是否致命 + * + * @param int $type + * @return bool + */ + protected static function isFatal($type) + { + return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]); + } + + /** + * Get an instance of the exception handler. + * + * @return Handle + */ + public static function getExceptionHandler() + { + static $handle; + if (!$handle) { + // 异常处理handle + $class = Config::get('exception_handle'); + if ($class && class_exists($class) && is_subclass_of($class, "\\think\\exception\\Handle")) { + $handle = new $class; + } else { + $handle = new Handle; + } + } + return $handle; + } +} diff --git a/thinkphp/library/think/Exception.php b/thinkphp/library/think/Exception.php new file mode 100644 index 0000000..034c85b --- /dev/null +++ b/thinkphp/library/think/Exception.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Exception extends \Exception +{ + + /** + * 保存异常页面显示的额外Debug数据 + * @var array + */ + protected $data = []; + + /** + * 设置异常额外的Debug数据 + * 数据将会显示为下面的格式 + * + * Exception Data + * -------------------------------------------------- + * Label 1 + * key1 value1 + * key2 value2 + * Label 2 + * key1 value1 + * key2 value2 + * + * @param string $label 数据分类,用于异常页面显示 + * @param array $data 需要显示的数据,必须为关联数组 + */ + final protected function setData($label, array $data) + { + $this->data[$label] = $data; + } + + /** + * 获取异常额外Debug数据 + * 主要用于输出到异常页面便于调试 + * @return array 由setData设置的Debug数据 + */ + final public function getData() + { + return $this->data; + } + +} diff --git a/thinkphp/library/think/File.php b/thinkphp/library/think/File.php new file mode 100644 index 0000000..636a343 --- /dev/null +++ b/thinkphp/library/think/File.php @@ -0,0 +1,410 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use SplFileObject; + +class File extends SplFileObject +{ + /** + * 错误信息 + * @var string + */ + private $error = ''; + // 当前完整文件名 + protected $filename; + // 上传文件名 + protected $saveName; + // 文件上传命名规则 + protected $rule = 'date'; + // 文件上传验证规则 + protected $validate = []; + // 单元测试 + protected $isTest; + // 上传文件信息 + protected $info; + // 文件hash信息 + protected $hash = []; + + public function __construct($filename, $mode = 'r') + { + parent::__construct($filename, $mode); + $this->filename = $this->getRealPath() ?: $this->getPathname(); + } + + /** + * 是否测试 + * @param bool $test 是否测试 + * @return $this + */ + public function isTest($test = false) + { + $this->isTest = $test; + return $this; + } + + /** + * 设置上传信息 + * @param array $info 上传文件信息 + * @return $this + */ + public function setUploadInfo($info) + { + $this->info = $info; + return $this; + } + + /** + * 获取上传文件的信息 + * @param string $name + * @return array|string + */ + public function getInfo($name = '') + { + return isset($this->info[$name]) ? $this->info[$name] : $this->info; + } + + /** + * 获取上传文件的文件名 + * @return string + */ + public function getSaveName() + { + return $this->saveName; + } + + /** + * 设置上传文件的保存文件名 + * @param string $saveName + * @return $this + */ + public function setSaveName($saveName) + { + $this->saveName = $saveName; + return $this; + } + + /** + * 获取文件的哈希散列值 + * @return $string + */ + public function hash($type = 'sha1') + { + if (!isset($this->hash[$type])) { + $this->hash[$type] = hash_file($type, $this->filename); + } + return $this->hash[$type]; + } + + /** + * 检查目录是否可写 + * @param string $path 目录 + * @return boolean + */ + protected function checkPath($path) + { + if (is_dir($path)) { + return true; + } + + if (mkdir($path, 0755, true)) { + return true; + } else { + $this->error = "目录 {$path} 创建失败!"; + return false; + } + } + + /** + * 获取文件类型信息 + * @return string + */ + public function getMime() + { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + return finfo_file($finfo, $this->filename); + } + + /** + * 设置文件的命名规则 + * @param string $rule 文件命名规则 + * @return $this + */ + public function rule($rule) + { + $this->rule = $rule; + return $this; + } + + /** + * 设置上传文件的验证规则 + * @param array $rule 验证规则 + * @return $this + */ + public function validate($rule = []) + { + $this->validate = $rule; + return $this; + } + + /** + * 检测是否合法的上传文件 + * @return bool + */ + public function isValid() + { + if ($this->isTest) { + return is_file($this->filename); + } + return is_uploaded_file($this->filename); + } + + /** + * 检测上传文件 + * @param array $rule 验证规则 + * @return bool + */ + public function check($rule = []) + { + $rule = $rule ?: $this->validate; + + /* 检查文件大小 */ + if (isset($rule['size']) && !$this->checkSize($rule['size'])) { + $this->error = '上传文件大小不符!'; + return false; + } + + /* 检查文件Mime类型 */ + if (isset($rule['type']) && !$this->checkMime($rule['type'])) { + $this->error = '上传文件MIME类型不允许!'; + return false; + } + + /* 检查文件后缀 */ + if (isset($rule['ext']) && !$this->checkExt($rule['ext'])) { + $this->error = '上传文件后缀不允许'; + return false; + } + + /* 检查图像文件 */ + if (!$this->checkImg()) { + $this->error = '非法图像文件!'; + return false; + } + + return true; + } + + /** + * 检测上传文件后缀 + * @param array|string $ext 允许后缀 + * @return bool + */ + public function checkExt($ext) + { + if (is_string($ext)) { + $ext = explode(',', $ext); + } + $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION)); + if (!in_array($extension, $ext)) { + return false; + } + return true; + } + + /** + * 检测图像文件 + * @return bool + */ + public function checkImg() + { + $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION)); + /* 对图像文件进行严格检测 */ + if (in_array($extension, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf']) && !in_array($this->getImageType($this->filename), [1, 2, 3, 4, 6])) { + return false; + } + return true; + } + + // 判断图像类型 + protected function getImageType($image) + { + if (function_exists('exif_imagetype')) { + return exif_imagetype($image); + } else { + $info = getimagesize($image); + return $info[2]; + } + } + + /** + * 检测上传文件大小 + * @param integer $size 最大大小 + * @return bool + */ + public function checkSize($size) + { + if ($this->getSize() > $size) { + return false; + } + return true; + } + + /** + * 检测上传文件类型 + * @param array|string $mime 允许类型 + * @return bool + */ + public function checkMime($mime) + { + if (is_string($mime)) { + $mime = explode(',', $mime); + } + if (!in_array(strtolower($this->getMime()), $mime)) { + return false; + } + return true; + } + + /** + * 移动文件 + * @param string $path 保存路径 + * @param string|bool $savename 保存的文件名 默认自动生成 + * @param boolean $replace 同名文件是否覆盖 + * @return false|File false-失败 否则返回File实例 + */ + public function move($path, $savename = true, $replace = true) + { + // 文件上传失败,捕获错误代码 + if (!empty($this->info['error'])) { + $this->error($this->info['error']); + return false; + } + + // 检测合法性 + if (!$this->isValid()) { + $this->error = '非法上传文件'; + return false; + } + + // 验证上传 + if (!$this->check()) { + return false; + } + $path = rtrim($path, DS) . DS; + // 文件保存命名规则 + $saveName = $this->buildSaveName($savename); + $filename = $path . $saveName; + + // 检测目录 + if (false === $this->checkPath(dirname($filename))) { + return false; + } + + /* 不覆盖同名文件 */ + if (!$replace && is_file($filename)) { + $this->error = '存在同名文件' . $filename; + return false; + } + + /* 移动文件 */ + if ($this->isTest) { + rename($this->filename, $filename); + } elseif (!move_uploaded_file($this->filename, $filename)) { + $this->error = '文件上传保存错误!'; + return false; + } + // 返回 File对象实例 + $file = new self($filename); + $file->setSaveName($saveName); + $file->setUploadInfo($this->info); + return $file; + } + + /** + * 获取保存文件名 + * @param string|bool $savename 保存的文件名 默认自动生成 + * @return string + */ + protected function buildSaveName($savename) + { + if (true === $savename) { + // 自动生成文件名 + if ($this->rule instanceof \Closure) { + $savename = call_user_func_array($this->rule, [$this]); + } else { + switch ($this->rule) { + case 'date': + $savename = date('Ymd') . DS . md5(microtime(true)); + break; + default: + if (in_array($this->rule, hash_algos())) { + $hash = $this->hash($this->rule); + $savename = substr($hash, 0, 2) . DS . substr($hash, 2); + } elseif (is_callable($this->rule)) { + $savename = call_user_func($this->rule); + } else { + $savename = date('Ymd') . DS . md5(microtime(true)); + } + } + } + } elseif ('' === $savename) { + $savename = $this->getInfo('name'); + } + if (!strpos($savename, '.')) { + $savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION); + } + return $savename; + } + + /** + * 获取错误代码信息 + * @param int $errorNo 错误号 + */ + private function error($errorNo) + { + switch ($errorNo) { + case 1: + case 2: + $this->error = '上传文件大小超过了最大值!'; + break; + case 3: + $this->error = '文件只有部分被上传!'; + break; + case 4: + $this->error = '没有文件被上传!'; + break; + case 6: + $this->error = '找不到临时文件夹!'; + break; + case 7: + $this->error = '文件写入失败!'; + break; + default: + $this->error = '未知上传错误!'; + } + } + + /** + * 获取错误信息 + * @return mixed + */ + public function getError() + { + return $this->error; + } + + public function __call($method, $args) + { + return $this->hash($method); + } +} diff --git a/thinkphp/library/think/Hook.php b/thinkphp/library/think/Hook.php new file mode 100644 index 0000000..f06196e --- /dev/null +++ b/thinkphp/library/think/Hook.php @@ -0,0 +1,136 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Hook +{ + + private static $tags = []; + + /** + * 动态添加行为扩展到某个标签 + * @param string $tag 标签名称 + * @param mixed $behavior 行为名称 + * @param bool $first 是否放到开头执行 + * @return void + */ + public static function add($tag, $behavior, $first = false) + { + isset(self::$tags[$tag]) || self::$tags[$tag] = []; + if (is_array($behavior) && !is_callable($behavior)) { + if (!array_key_exists('_overlay', $behavior) || !$behavior['_overlay']) { + unset($behavior['_overlay']); + self::$tags[$tag] = array_merge(self::$tags[$tag], $behavior); + } else { + unset($behavior['_overlay']); + self::$tags[$tag] = $behavior; + } + } elseif ($first) { + array_unshift(self::$tags[$tag], $behavior); + } else { + self::$tags[$tag][] = $behavior; + } + } + + /** + * 批量导入插件 + * @param array $tags 插件信息 + * @param boolean $recursive 是否递归合并 + */ + public static function import(array $tags, $recursive = true) + { + if ($recursive) { + foreach ($tags as $tag => $behavior) { + self::add($tag, $behavior); + } + } else { + self::$tags = $tags + self::$tags; + } + } + + /** + * 获取插件信息 + * @param string $tag 插件位置 留空获取全部 + * @return array + */ + public static function get($tag = '') + { + if (empty($tag)) { + //获取全部的插件信息 + return self::$tags; + } else { + return array_key_exists($tag, self::$tags) ? self::$tags[$tag] : []; + } + } + + /** + * 监听标签的行为 + * @param string $tag 标签名称 + * @param mixed $params 传入参数 + * @param mixed $extra 额外参数 + * @param bool $once 只获取一个有效返回值 + * @return mixed + */ + public static function listen($tag, &$params = null, $extra = null, $once = false) + { + $results = []; + $tags = static::get($tag); + foreach ($tags as $key => $name) { + $results[$key] = self::exec($name, $tag, $params, $extra); + if (false === $results[$key]) { + // 如果返回false 则中断行为执行 + break; + } elseif (!is_null($results[$key]) && $once) { + break; + } + } + return $once ? end($results) : $results; + } + + /** + * 执行某个行为 + * @param mixed $class 要执行的行为 + * @param string $tag 方法名(标签名) + * @param Mixed $params 传人的参数 + * @param mixed $extra 额外参数 + * @return mixed + */ + public static function exec($class, $tag = '', &$params = null, $extra = null) + { + App::$debug && Debug::remark('behavior_start', 'time'); + $method = Loader::parseName($tag, 1, false); + if ($class instanceof \Closure) { + $result = call_user_func_array($class, [ & $params, $extra]); + $class = 'Closure'; + } elseif (is_array($class)) { + list($class, $method) = $class; + + $result = (new $class())->$method($params, $extra); + $class = $class . '->' . $method; + } elseif (is_object($class)) { + $result = $class->$method($params, $extra); + $class = get_class($class); + } elseif (strpos($class, '::')) { + $result = call_user_func_array($class, [ & $params, $extra]); + } else { + $obj = new $class(); + $method = ($tag && is_callable([$obj, $method])) ? $method : 'run'; + $result = $obj->$method($params, $extra); + } + if (App::$debug) { + Debug::remark('behavior_end', 'time'); + Log::record('[ BEHAVIOR ] Run ' . $class . ' @' . $tag . ' [ RunTime:' . Debug::getRangeTime('behavior_start', 'behavior_end') . 's ]', 'info'); + } + return $result; + } + +} diff --git a/thinkphp/library/think/Lang.php b/thinkphp/library/think/Lang.php new file mode 100644 index 0000000..2df3767 --- /dev/null +++ b/thinkphp/library/think/Lang.php @@ -0,0 +1,220 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Lang +{ + // 语言数据 + private static $lang = []; + // 语言作用域 + private static $range = 'zh-cn'; + // 语言自动侦测的变量 + protected static $langDetectVar = 'lang'; + // 语言Cookie变量 + protected static $langCookieVar = 'think_var'; + // 语言Cookie的过期时间 + protected static $langCookieExpire = 3600; + // 允许语言列表 + protected static $allowLangList = []; + // Accept-Language转义为对应语言包名称 系统默认配置 + protected static $acceptLanguage = [ + 'zh-hans-cn' => 'zh-cn', + ]; + + // 设定当前的语言 + public static function range($range = '') + { + if ('' == $range) { + return self::$range; + } else { + self::$range = $range; + } + return self::$range; + } + + /** + * 设置语言定义(不区分大小写) + * @param string|array $name 语言变量 + * @param string $value 语言值 + * @param string $range 语言作用域 + * @return mixed + */ + public static function set($name, $value = null, $range = '') + { + $range = $range ?: self::$range; + // 批量定义 + if (!isset(self::$lang[$range])) { + self::$lang[$range] = []; + } + if (is_array($name)) { + return self::$lang[$range] = array_change_key_case($name) + self::$lang[$range]; + } else { + return self::$lang[$range][strtolower($name)] = $value; + } + } + + /** + * 加载语言定义(不区分大小写) + * @param string $file 语言文件 + * @param string $range 语言作用域 + * @return mixed + */ + public static function load($file, $range = '') + { + $range = $range ?: self::$range; + if (!isset(self::$lang[$range])) { + self::$lang[$range] = []; + } + // 批量定义 + if (is_string($file)) { + $file = [$file]; + } + $lang = []; + foreach ($file as $_file) { + if (is_file($_file)) { + // 记录加载信息 + App::$debug && Log::record('[ LANG ] ' . $_file, 'info'); + $_lang = include $_file; + if (is_array($_lang)) { + $lang = array_change_key_case($_lang) + $lang; + } + } + } + if (!empty($lang)) { + self::$lang[$range] = $lang + self::$lang[$range]; + } + return self::$lang[$range]; + } + + /** + * 获取语言定义(不区分大小写) + * @param string|null $name 语言变量 + * @param string $range 语言作用域 + * @return mixed + */ + public static function has($name, $range = '') + { + $range = $range ?: self::$range; + return isset(self::$lang[$range][strtolower($name)]); + } + + /** + * 获取语言定义(不区分大小写) + * @param string|null $name 语言变量 + * @param array $vars 变量替换 + * @param string $range 语言作用域 + * @return mixed + */ + public static function get($name = null, $vars = [], $range = '') + { + $range = $range ?: self::$range; + // 空参数返回所有定义 + if (empty($name)) { + return self::$lang[$range]; + } + $key = strtolower($name); + $value = isset(self::$lang[$range][$key]) ? self::$lang[$range][$key] : $name; + + // 变量解析 + if (!empty($vars) && is_array($vars)) { + /** + * Notes: + * 为了检测的方便,数字索引的判断仅仅是参数数组的第一个元素的key为数字0 + * 数字索引采用的是系统的 sprintf 函数替换,用法请参考 sprintf 函数 + */ + if (key($vars) === 0) { + // 数字索引解析 + array_unshift($vars, $value); + $value = call_user_func_array('sprintf', $vars); + } else { + // 关联索引解析 + $replace = array_keys($vars); + foreach ($replace as &$v) { + $v = "{:{$v}}"; + } + $value = str_replace($replace, $vars, $value); + } + + } + return $value; + } + + /** + * 自动侦测设置获取语言选择 + * @return string + */ + public static function detect() + { + // 自动侦测设置获取语言选择 + $langSet = ''; + + if (isset($_GET[self::$langDetectVar])) { + // url中设置了语言变量 + $langSet = strtolower($_GET[self::$langDetectVar]); + } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + // 自动侦测浏览器语言 + preg_match('/^([a-z\d\-]+)/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches); + $langSet = strtolower($matches[1]); + $acceptLangs = Config::get('header_accept_lang'); + if (isset($acceptLangs[$langSet])) { + $langSet = $acceptLangs[$langSet]; + } elseif (isset(self::$acceptLanguage[$langSet])) { + $langSet = self::$acceptLanguage[$langSet]; + } + } + if (empty(self::$allowLangList) || in_array($langSet, self::$allowLangList)) { + // 合法的语言 + self::$range = $langSet ?: self::$range; + } + return self::$range; + } + + /** + * 设置语言自动侦测的变量 + * @param string $var 变量名称 + * @return void + */ + public static function setLangDetectVar($var) + { + self::$langDetectVar = $var; + } + + /** + * 设置语言的cookie保存变量 + * @param string $var 变量名称 + * @return void + */ + public static function setLangCookieVar($var) + { + self::$langCookieVar = $var; + } + + /** + * 设置语言的cookie的过期时间 + * @param string $expire 过期时间 + * @return void + */ + public static function setLangCookieExpire($expire) + { + self::$langCookieExpire = $expire; + } + + /** + * 设置允许的语言列表 + * @param array $list 语言列表 + * @return void + */ + public static function setAllowLangList($list) + { + self::$allowLangList = $list; + } +} diff --git a/thinkphp/library/think/Loader.php b/thinkphp/library/think/Loader.php new file mode 100644 index 0000000..62e876e --- /dev/null +++ b/thinkphp/library/think/Loader.php @@ -0,0 +1,570 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; + +class Loader +{ + protected static $instance = []; + // 类名映射 + protected static $map = []; + + // 命名空间别名 + protected static $namespaceAlias = []; + + // PSR-4 + private static $prefixLengthsPsr4 = []; + private static $prefixDirsPsr4 = []; + private static $fallbackDirsPsr4 = []; + + // PSR-0 + private static $prefixesPsr0 = []; + private static $fallbackDirsPsr0 = []; + + // 自动加载的文件 + private static $autoloadFiles = []; + + // 自动加载 + public static function autoload($class) + { + // 检测命名空间别名 + if (!empty(self::$namespaceAlias)) { + $namespace = dirname($class); + if (isset(self::$namespaceAlias[$namespace])) { + $original = self::$namespaceAlias[$namespace] . '\\' . basename($class); + if (class_exists($original)) { + return class_alias($original, $class, false); + } + } + } + + if ($file = self::findFile($class)) { + + // Win环境严格区分大小写 + if (IS_WIN && pathinfo($file, PATHINFO_FILENAME) != pathinfo(realpath($file), PATHINFO_FILENAME)) { + return false; + } + + __include_file($file); + return true; + } + } + + /** + * 查找文件 + * @param $class + * @return bool + */ + private static function findFile($class) + { + if (!empty(self::$map[$class])) { + // 类库映射 + return self::$map[$class]; + } + + // 查找 PSR-4 + $logicalPathPsr4 = strtr($class, '\\', DS) . EXT; + + $first = $class[0]; + if (isset(self::$prefixLengthsPsr4[$first])) { + foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach (self::$prefixDirsPsr4[$prefix] as $dir) { + if (is_file($file = $dir . DS . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // 查找 PSR-4 fallback dirs + foreach (self::$fallbackDirsPsr4 as $dir) { + if (is_file($file = $dir . DS . $logicalPathPsr4)) { + return $file; + } + } + + // 查找 PSR-0 + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DS); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DS) . EXT; + } + + if (isset(self::$prefixesPsr0[$first])) { + foreach (self::$prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (is_file($file = $dir . DS . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // 查找 PSR-0 fallback dirs + foreach (self::$fallbackDirsPsr0 as $dir) { + if (is_file($file = $dir . DS . $logicalPathPsr0)) { + return $file; + } + } + + return self::$map[$class] = false; + } + + // 注册classmap + public static function addClassMap($class, $map = '') + { + if (is_array($class)) { + self::$map = array_merge(self::$map, $class); + } else { + self::$map[$class] = $map; + } + } + + // 注册命名空间 + public static function addNamespace($namespace, $path = '') + { + if (is_array($namespace)) { + foreach ($namespace as $prefix => $paths) { + self::addPsr4($prefix . '\\', rtrim($paths, DS), true); + } + } else { + self::addPsr4($namespace . '\\', rtrim($path, DS), true); + } + } + + // 添加Ps0空间 + private static function addPsr0($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + self::$fallbackDirsPsr0 = array_merge( + (array) $paths, + self::$fallbackDirsPsr0 + ); + } else { + self::$fallbackDirsPsr0 = array_merge( + self::$fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset(self::$prefixesPsr0[$first][$prefix])) { + self::$prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + self::$prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + self::$prefixesPsr0[$first][$prefix] + ); + } else { + self::$prefixesPsr0[$first][$prefix] = array_merge( + self::$prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + // 添加Psr4空间 + private static function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + self::$fallbackDirsPsr4 = array_merge( + (array) $paths, + self::$fallbackDirsPsr4 + ); + } else { + self::$fallbackDirsPsr4 = array_merge( + self::$fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset(self::$prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + self::$prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + self::$prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + self::$prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + self::$prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + self::$prefixDirsPsr4[$prefix] = array_merge( + self::$prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + // 注册命名空间别名 + public static function addNamespaceAlias($namespace, $original = '') + { + if (is_array($namespace)) { + self::$namespaceAlias = array_merge(self::$namespaceAlias, $namespace); + } else { + self::$namespaceAlias[$namespace] = $original; + } + } + + // 注册自动加载机制 + public static function register($autoload = '') + { + // 注册系统自动加载 + spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true); + // 注册命名空间定义 + self::addNamespace([ + 'think' => LIB_PATH . 'think' . DS, + 'behavior' => LIB_PATH . 'behavior' . DS, + 'traits' => LIB_PATH . 'traits' . DS, + ]); + // 加载类库映射文件 + if (is_file(RUNTIME_PATH . 'classmap' . EXT)) { + self::addClassMap(__include_file(RUNTIME_PATH . 'classmap' . EXT)); + } + + // Composer自动加载支持 + if (is_dir(VENDOR_PATH . 'composer')) { + self::registerComposerLoader(); + } + + // 自动加载extend目录 + self::$fallbackDirsPsr4[] = rtrim(EXTEND_PATH, DS); + } + + // 注册composer自动加载 + private static function registerComposerLoader() + { + if (is_file(VENDOR_PATH . 'composer/autoload_namespaces.php')) { + $map = require VENDOR_PATH . 'composer/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + self::addPsr0($namespace, $path); + } + } + + if (is_file(VENDOR_PATH . 'composer/autoload_psr4.php')) { + $map = require VENDOR_PATH . 'composer/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + self::addPsr4($namespace, $path); + } + } + + if (is_file(VENDOR_PATH . 'composer/autoload_classmap.php')) { + $classMap = require VENDOR_PATH . 'composer/autoload_classmap.php'; + if ($classMap) { + self::addClassMap($classMap); + } + } + + if (is_file(VENDOR_PATH . 'composer/autoload_files.php')) { + $includeFiles = require VENDOR_PATH . 'composer/autoload_files.php'; + foreach ($includeFiles as $fileIdentifier => $file) { + if (empty(self::$autoloadFiles[$fileIdentifier])) { + __require_file($file); + self::$autoloadFiles[$fileIdentifier] = true; + } + } + } + } + + /** + * 导入所需的类库 同java的Import 本函数有缓存功能 + * @param string $class 类库命名空间字符串 + * @param string $baseUrl 起始路径 + * @param string $ext 导入的文件扩展名 + * @return boolean + */ + public static function import($class, $baseUrl = '', $ext = EXT) + { + static $_file = []; + $key = $class . $baseUrl; + $class = str_replace(['.', '#'], [DS, '.'], $class); + if (isset($_file[$key])) { + return true; + } + + if (empty($baseUrl)) { + list($name, $class) = explode(DS, $class, 2); + + if (isset(self::$prefixDirsPsr4[$name . '\\'])) { + // 注册的命名空间 + $baseUrl = self::$prefixDirsPsr4[$name . '\\']; + } elseif ('@' == $name) { + //加载当前模块应用类库 + $baseUrl = App::$modulePath; + } elseif (is_dir(EXTEND_PATH . $name)) { + $baseUrl = EXTEND_PATH . $name . DS; + } else { + // 加载其它模块的类库 + $baseUrl = APP_PATH . $name . DS; + } + } elseif (substr($baseUrl, -1) != DS) { + $baseUrl .= DS; + } + // 如果类存在 则导入类库文件 + if (is_array($baseUrl)) { + foreach ($baseUrl as $path) { + $filename = $path . DS . $class . $ext; + if (is_file($filename)) { + break; + } + } + } else { + $filename = $baseUrl . $class . $ext; + } + + if (!empty($filename) && is_file($filename)) { + // 开启调试模式Win环境严格区分大小写 + if (IS_WIN && pathinfo($filename, PATHINFO_FILENAME) != pathinfo(realpath($filename), PATHINFO_FILENAME)) { + return false; + } + __include_file($filename); + $_file[$key] = true; + return true; + } + return false; + } + + /** + * 实例化(分层)模型 + * @param string $name Model名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return Object + * @throws ClassNotFoundException + */ + public static function model($name = '', $layer = 'model', $appendSuffix = false, $common = 'common') + { + $guid = $name . $layer; + if (isset(self::$instance[$guid])) { + return self::$instance[$guid]; + } + if (false !== strpos($name, '\\')) { + $class = $name; + $module = Request::instance()->module(); + } else { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name, 2); + } else { + $module = Request::instance()->module(); + } + $class = self::parseClass($module, $layer, $name, $appendSuffix); + } + if (class_exists($class)) { + $model = new $class(); + } else { + $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class); + if (class_exists($class)) { + $model = new $class(); + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + } + self::$instance[$guid] = $model; + return $model; + } + + /** + * 实例化(分层)控制器 格式:[模块名/]控制器名 + * @param string $name 资源地址 + * @param string $layer 控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $empty 空控制器名称 + * @return Object + * @throws ClassNotFoundException + */ + public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '') + { + if (false !== strpos($name, '\\')) { + $class = $name; + $module = Request::instance()->module(); + } else { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name); + } else { + $module = Request::instance()->module(); + } + $class = self::parseClass($module, $layer, $name, $appendSuffix); + } + if (class_exists($class)) { + return App::invokeClass($class); + } elseif ($empty && class_exists($emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix))) { + return new $emptyClass(Request::instance()); + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + } + + /** + * 实例化验证类 格式:[模块名/]验证器名 + * @param string $name 资源地址 + * @param string $layer 验证层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return Object|false + * @throws ClassNotFoundException + */ + public static function validate($name = '', $layer = 'validate', $appendSuffix = false, $common = 'common') + { + $name = $name ?: Config::get('default_validate'); + if (empty($name)) { + return new Validate; + } + $guid = $name . $layer; + if (isset(self::$instance[$guid])) { + return self::$instance[$guid]; + } + if (false !== strpos($name, '\\')) { + $class = $name; + $module = Request::instance()->module(); + } else { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name); + } else { + $module = Request::instance()->module(); + } + $class = self::parseClass($module, $layer, $name, $appendSuffix); + } + if (class_exists($class)) { + $validate = new $class; + } else { + $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class); + if (class_exists($class)) { + $validate = new $class; + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + } + self::$instance[$guid] = $validate; + return $validate; + } + + /** + * 数据库初始化 并取得数据库类实例 + * @param mixed $config 数据库配置 + * @param bool|string $name 连接标识 true 强制重新连接 + * @return \think\db\Connection + */ + public static function db($config = [], $name = false) + { + return Db::connect($config, $name); + } + + /** + * 远程调用模块的操作方法 参数格式 [模块/控制器/]操作 + * @param string $url 调用地址 + * @param string|array $vars 调用参数 支持字符串和数组 + * @param string $layer 要调用的控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return mixed + */ + public static function action($url, $vars = [], $layer = 'controller', $appendSuffix = false) + { + $info = pathinfo($url); + $action = $info['basename']; + $module = '.' != $info['dirname'] ? $info['dirname'] : Request::instance()->controller(); + $class = self::controller($module, $layer, $appendSuffix); + if ($class) { + if (is_scalar($vars)) { + if (strpos($vars, '=')) { + parse_str($vars, $vars); + } else { + $vars = [$vars]; + } + } + return App::invokeMethod([$class, $action . Config::get('action_suffix')], $vars); + } + } + + /** + * 字符串命名风格转换 + * type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格 + * @param string $name 字符串 + * @param integer $type 转换类型 + * @param bool $ucfirst 首字母是否大写(驼峰规则) + * @return string + */ + public static function parseName($name, $type = 0, $ucfirst = true) + { + if ($type) { + $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) { + return strtoupper($match[1]); + }, $name); + return $ucfirst ? ucfirst($name) : lcfirst($name); + } else { + return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_")); + } + } + + /** + * 解析应用类的类名 + * @param string $module 模块名 + * @param string $layer 层名 controller model ... + * @param string $name 类名 + * @param bool $appendSuffix + * @return string + */ + public static function parseClass($module, $layer, $name, $appendSuffix = false) + { + $name = str_replace(['/', '.'], '\\', $name); + $array = explode('\\', $name); + $class = self::parseName(array_pop($array), 1) . (App::$suffix || $appendSuffix ? ucfirst($layer) : ''); + $path = $array ? implode('\\', $array) . '\\' : ''; + return App::$namespace . '\\' . ($module ? $module . '\\' : '') . $layer . '\\' . $path . $class; + } + + /** + * 初始化类的实例 + * @return void + */ + public static function clearInstance() + { + self::$instance = []; + } +} + +/** + * 作用范围隔离 + * + * @param $file + * @return mixed + */ +function __include_file($file) +{ + return include $file; +} + +function __require_file($file) +{ + return require $file; +} diff --git a/thinkphp/library/think/Log.php b/thinkphp/library/think/Log.php new file mode 100644 index 0000000..a20ab26 --- /dev/null +++ b/thinkphp/library/think/Log.php @@ -0,0 +1,213 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; + +/** + * Class Log + * @package think + * + * @method void log($msg) static + * @method void error($msg) static + * @method void info($msg) static + * @method void sql($msg) static + * @method void notice($msg) static + * @method void alert($msg) static + */ +class Log +{ + const LOG = 'log'; + const ERROR = 'error'; + const INFO = 'info'; + const SQL = 'sql'; + const NOTICE = 'notice'; + const ALERT = 'alert'; + const DEBUG = 'debug'; + + // 日志信息 + protected static $log = []; + // 配置参数 + protected static $config = []; + // 日志类型 + protected static $type = ['log', 'error', 'info', 'sql', 'notice', 'alert', 'debug']; + // 日志写入驱动 + protected static $driver; + + // 当前日志授权key + protected static $key; + + /** + * 日志初始化 + * @param array $config + */ + public static function init($config = []) + { + $type = isset($config['type']) ? $config['type'] : 'File'; + $class = false !== strpos($type, '\\') ? $type : '\\think\\log\\driver\\' . ucwords($type); + self::$config = $config; + unset($config['type']); + if (class_exists($class)) { + self::$driver = new $class($config); + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + // 记录初始化信息 + App::$debug && Log::record('[ LOG ] INIT ' . $type, 'info'); + } + + /** + * 获取日志信息 + * @param string $type 信息类型 + * @return array + */ + public static function getLog($type = '') + { + return $type ? self::$log[$type] : self::$log; + } + + /** + * 记录调试信息 + * @param mixed $msg 调试信息 + * @param string $type 信息类型 + * @return void + */ + public static function record($msg, $type = 'log') + { + self::$log[$type][] = $msg; + if (IS_CLI) { + // 命令行下面日志写入改进 + self::save(); + } + } + + /** + * 清空日志信息 + * @return void + */ + public static function clear() + { + self::$log = []; + } + + /** + * 当前日志记录的授权key + * @param string $key 授权key + * @return void + */ + public static function key($key) + { + self::$key = $key; + } + + /** + * 检查日志写入权限 + * @param array $config 当前日志配置参数 + * @return bool + */ + public static function check($config) + { + if (self::$key && !empty($config['allow_key']) && !in_array(self::$key, $config['allow_key'])) { + return false; + } + return true; + } + + /** + * 保存调试信息 + * @return bool + */ + public static function save() + { + if (!empty(self::$log)) { + if (is_null(self::$driver)) { + self::init(Config::get('log')); + } + + if (!self::check(self::$config)) { + // 检测日志写入权限 + return false; + } + + if (empty(self::$config['level'])) { + // 获取全部日志 + $log = self::$log; + if (!App::$debug && isset($log['debug'])) { + unset($log['debug']); + } + } else { + // 记录允许级别 + $log = []; + foreach (self::$config['level'] as $level) { + if (isset(self::$log[$level])) { + $log[$level] = self::$log[$level]; + } + } + } + + $result = self::$driver->save($log); + if ($result) { + self::$log = []; + } + Hook::listen('log_write_done', $log); + return $result; + } + return true; + } + + /** + * 实时写入日志信息 并支持行为 + * @param mixed $msg 调试信息 + * @param string $type 信息类型 + * @param bool $force 是否强制写入 + * @return bool + */ + public static function write($msg, $type = 'log', $force = false) + { + $log = self::$log; + // 封装日志信息 + if (true === $force || empty(self::$config['level'])) { + $log[$type][] = $msg; + } elseif (in_array($type, self::$config['level'])) { + $log[$type][] = $msg; + } else { + return false; + } + + // 监听log_write + Hook::listen('log_write', $log); + if (is_null(self::$driver)) { + self::init(Config::get('log')); + } + // 写入日志 + $result = self::$driver->save($log); + if ($result) { + self::$log = []; + } + return $result; + } + + /** + * 静态调用 + * @param $method + * @param $args + * @return mixed + */ + public static function __callStatic($method, $args) + { + if (in_array($method, self::$type)) { + array_push($args, $method); + return call_user_func_array('\\think\\Log::record', $args); + } + } + +} diff --git a/thinkphp/library/think/Model.php b/thinkphp/library/think/Model.php new file mode 100644 index 0000000..7777e23 --- /dev/null +++ b/thinkphp/library/think/Model.php @@ -0,0 +1,2200 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use InvalidArgumentException; +use think\db\Query; +use think\exception\ValidateException; +use think\model\Collection as ModelCollection; +use think\model\Relation; +use think\model\relation\BelongsTo; +use think\model\relation\BelongsToMany; +use think\model\relation\HasMany; +use think\model\relation\HasManyThrough; +use think\model\relation\HasOne; +use think\model\relation\MorphMany; +use think\model\relation\MorphOne; +use think\model\relation\MorphTo; + +/** + * Class Model + * @package think + * @mixin Query + */ +abstract class Model implements \JsonSerializable, \ArrayAccess +{ + // 数据库查询对象池 + protected static $links = []; + // 数据库配置 + protected $connection = []; + // 父关联模型对象 + protected $parent; + // 数据库查询对象 + protected $query; + // 当前模型名称 + protected $name; + // 数据表名称 + protected $table; + // 当前类名称 + protected $class; + // 回调事件 + private static $event = []; + // 错误信息 + protected $error; + // 字段验证规则 + protected $validate; + // 数据表主键 复合主键使用数组定义 不设置则自动获取 + protected $pk; + // 数据表字段信息 留空则自动获取 + protected $field = []; + // 只读字段 + protected $readonly = []; + // 显示属性 + protected $visible = []; + // 隐藏属性 + protected $hidden = []; + // 追加属性 + protected $append = []; + // 数据信息 + protected $data = []; + // 原始数据 + protected $origin = []; + // 关联模型 + protected $relation = []; + + // 保存自动完成列表 + protected $auto = []; + // 新增自动完成列表 + protected $insert = []; + // 更新自动完成列表 + protected $update = []; + // 是否需要自动写入时间戳 如果设置为字符串 则表示时间字段的类型 + protected $autoWriteTimestamp; + // 创建时间字段 + protected $createTime = 'create_time'; + // 更新时间字段 + protected $updateTime = 'update_time'; + // 时间字段取出后的默认时间格式 + protected $dateFormat; + // 字段类型或者格式转换 + protected $type = []; + // 是否为更新数据 + protected $isUpdate = false; + // 更新条件 + protected $updateWhere; + // 验证失败是否抛出异常 + protected $failException = false; + // 全局查询范围 + protected $useGlobalScope = true; + // 是否采用批量验证 + protected $batchValidate = false; + // 查询数据集对象 + protected $resultSetType; + // 关联自动写入 + protected $relationWrite; + + /** + * 初始化过的模型. + * + * @var array + */ + protected static $initialized = []; + + /** + * 构造方法 + * @access public + * @param array|object $data 数据 + */ + public function __construct($data = []) + { + if (is_object($data)) { + $this->data = get_object_vars($data); + } else { + $this->data = $data; + } + // 记录原始数据 + $this->origin = $this->data; + + // 当前类名 + $this->class = get_called_class(); + + if (empty($this->name)) { + // 当前模型名 + $name = str_replace('\\', '/', $this->class); + $this->name = basename($name); + if (Config::get('class_suffix')) { + $suffix = basename(dirname($name)); + $this->name = substr($this->name, 0, -strlen($suffix)); + } + } + + if (is_null($this->autoWriteTimestamp)) { + // 自动写入时间戳 + $this->autoWriteTimestamp = $this->getQuery()->getConfig('auto_timestamp'); + } + + if (is_null($this->dateFormat)) { + // 设置时间戳格式 + $this->dateFormat = $this->getQuery()->getConfig('datetime_format'); + } + + if (is_null($this->resultSetType)) { + $this->resultSetType = $this->getQuery()->getConfig('resultset_type'); + } + // 执行初始化操作 + $this->initialize(); + } + + /** + * 创建模型的查询对象 + * @access protected + * @return Query + */ + protected function buildQuery() + { + // 合并数据库配置 + if (!empty($this->connection)) { + if (is_array($this->connection)) { + $connection = array_merge(Config::get('database'), $this->connection); + } else { + $connection = $this->connection; + } + } else { + $connection = []; + } + + $con = Db::connect($connection); + // 设置当前模型 确保查询返回模型对象 + $queryClass = $this->query ?: $con->getConfig('query'); + $query = new $queryClass($con, $this->class); + + // 设置当前数据表和模型名 + if (!empty($this->table)) { + $query->setTable($this->table); + } else { + $query->name($this->name); + } + + if (!empty($this->pk)) { + $query->pk($this->pk); + } + + return $query; + } + + /** + * 获取当前模型的查询对象 + * @access public + * @param bool $buildNewQuery 创建新的查询对象 + * @return Query + */ + public function getQuery($buildNewQuery = false) + { + if ($buildNewQuery) { + return $this->buildQuery(); + } elseif (!isset(self::$links[$this->class])) { + // 创建模型查询对象 + self::$links[$this->class] = $this->buildQuery(); + } + + return self::$links[$this->class]; + } + + /** + * 获取当前模型的数据库查询对象 + * @access public + * @param bool $useBaseQuery 是否调用全局查询范围 + * @param bool $buildNewQuery 创建新的查询对象 + * @return Query + */ + public function db($useBaseQuery = true, $buildNewQuery = true) + { + $query = $this->getQuery($buildNewQuery); + + // 全局作用域 + if ($useBaseQuery && method_exists($this, 'base')) { + call_user_func_array([$this, 'base'], [ & $query]); + } + + // 返回当前模型的数据库查询对象 + return $query; + } + + /** + * 初始化模型 + * @access protected + * @return void + */ + protected function initialize() + { + $class = get_class($this); + if (!isset(static::$initialized[$class])) { + static::$initialized[$class] = true; + static::init(); + } + } + + /** + * 初始化处理 + * @access protected + * @return void + */ + protected static function init() + { + } + + /** + * 设置父关联对象 + * @access public + * @param Model $model 模型对象 + * @return $this + */ + public function setParent($model) + { + $this->parent = $model; + + return $this; + } + + /** + * 获取父关联对象 + * @access public + * @return Model + */ + public function getParent() + { + return $this->parent; + } + + /** + * 设置数据对象值 + * @access public + * @param mixed $data 数据或者属性名 + * @param mixed $value 值 + * @return $this + */ + public function data($data, $value = null) + { + if (is_string($data)) { + $this->data[$data] = $value; + } else { + // 清空数据 + $this->data = []; + if (is_object($data)) { + $data = get_object_vars($data); + } + if (true === $value) { + // 数据对象赋值 + foreach ($data as $key => $value) { + $this->setAttr($key, $value, $data); + } + } else { + $this->data = $data; + } + } + return $this; + } + + /** + * 获取对象原始数据 如果不存在指定字段返回false + * @access public + * @param string $name 字段名 留空获取全部 + * @return mixed + * @throws InvalidArgumentException + */ + public function getData($name = null) + { + if (is_null($name)) { + return $this->data; + } elseif (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } elseif (array_key_exists($name, $this->relation)) { + return $this->relation[$name]; + } else { + throw new InvalidArgumentException('property not exists:' . $this->class . '->' . $name); + } + } + + /** + * 是否需要自动写入时间字段 + * @access public + * @param bool $auto + * @return $this + */ + public function isAutoWriteTimestamp($auto) + { + $this->autoWriteTimestamp = $auto; + return $this; + } + + /** + * 修改器 设置数据对象值 + * @access public + * @param string $name 属性名 + * @param mixed $value 属性值 + * @param array $data 数据 + * @return $this + */ + public function setAttr($name, $value, $data = []) + { + if (is_null($value) && $this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) { + // 自动写入的时间戳字段 + $value = $this->autoWriteTimestamp($name); + } else { + // 检测修改器 + $method = 'set' . Loader::parseName($name, 1) . 'Attr'; + if (method_exists($this, $method)) { + $value = $this->$method($value, array_merge($this->data, $data), $this->relation); + } elseif (isset($this->type[$name])) { + // 类型转换 + $value = $this->writeTransform($value, $this->type[$name]); + } + } + + // 设置数据对象属性 + $this->data[$name] = $value; + return $this; + } + + /** + * 获取当前模型的关联模型数据 + * @access public + * @param string $name 关联方法名 + * @return mixed + */ + public function getRelation($name = null) + { + if (is_null($name)) { + return $this->relation; + } elseif (array_key_exists($name, $this->relation)) { + return $this->relation[$name]; + } else { + return; + } + } + + /** + * 设置关联数据对象值 + * @access public + * @param string $name 属性名 + * @param mixed $value 属性值 + * @return $this + */ + public function setRelation($name, $value) + { + $this->relation[$name] = $value; + return $this; + } + + /** + * 自动写入时间戳 + * @access public + * @param string $name 时间戳字段 + * @return mixed + */ + protected function autoWriteTimestamp($name) + { + if (isset($this->type[$name])) { + $type = $this->type[$name]; + if (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + switch ($type) { + case 'datetime': + case 'date': + $format = !empty($param) ? $param : $this->dateFormat; + $value = $this->formatDateTime(time(), $format); + break; + case 'timestamp': + case 'integer': + default: + $value = time(); + break; + } + } elseif (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [ + 'datetime', + 'date', + 'timestamp', + ]) + ) { + $value = $this->formatDateTime(time(), $this->dateFormat); + } else { + $value = $this->formatDateTime(time(), $this->dateFormat, true); + } + return $value; + } + + /** + * 时间日期字段格式化处理 + * @access public + * @param mixed $time 时间日期表达式 + * @param mixed $format 日期格式 + * @param bool $timestamp 是否进行时间戳转换 + * @return mixed + */ + protected function formatDateTime($time, $format, $timestamp = false) + { + if (false !== strpos($format, '\\')) { + $time = new $format($time); + } elseif (!$timestamp && false !== $format) { + $time = date($format, $time); + } + return $time; + } + + /** + * 数据写入 类型转换 + * @access public + * @param mixed $value 值 + * @param string|array $type 要转换的类型 + * @return mixed + */ + protected function writeTransform($value, $type) + { + if (is_null($value)) { + return; + } + + if (is_array($type)) { + list($type, $param) = $type; + } elseif (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + switch ($type) { + case 'integer': + $value = (int) $value; + break; + case 'float': + if (empty($param)) { + $value = (float) $value; + } else { + $value = (float) number_format($value, $param, '.', ''); + } + break; + case 'boolean': + $value = (bool) $value; + break; + case 'timestamp': + if (!is_numeric($value)) { + $value = strtotime($value); + } + break; + case 'datetime': + $format = !empty($param) ? $param : $this->dateFormat; + $value = is_numeric($value) ? $value : strtotime($value); + $value = $this->formatDateTime($value, $format); + break; + case 'object': + if (is_object($value)) { + $value = json_encode($value, JSON_FORCE_OBJECT); + } + break; + case 'array': + $value = (array) $value; + case 'json': + $option = !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE; + $value = json_encode($value, $option); + break; + case 'serialize': + $value = serialize($value); + break; + + } + return $value; + } + + /** + * 获取器 获取数据对象的值 + * @access public + * @param string $name 名称 + * @return mixed + * @throws InvalidArgumentException + */ + public function getAttr($name) + { + try { + $notFound = false; + $value = $this->getData($name); + } catch (InvalidArgumentException $e) { + $notFound = true; + $value = null; + } + + // 检测属性获取器 + $method = 'get' . Loader::parseName($name, 1) . 'Attr'; + if (method_exists($this, $method)) { + $value = $this->$method($value, $this->data, $this->relation); + } elseif (isset($this->type[$name])) { + // 类型转换 + $value = $this->readTransform($value, $this->type[$name]); + } elseif (in_array($name, [$this->createTime, $this->updateTime])) { + if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [ + 'datetime', + 'date', + 'timestamp', + ]) + ) { + $value = $this->formatDateTime(strtotime($value), $this->dateFormat); + } else { + $value = $this->formatDateTime($value, $this->dateFormat); + } + } elseif ($notFound) { + $relation = Loader::parseName($name, 1, false); + if (method_exists($this, $relation)) { + $modelRelation = $this->$relation(); + // 不存在该字段 获取关联数据 + $value = $this->getRelationData($modelRelation); + // 保存关联对象值 + $this->relation[$name] = $value; + } else { + throw new InvalidArgumentException('property not exists:' . $this->class . '->' . $name); + } + } + return $value; + } + + /** + * 获取关联模型数据 + * @access public + * @param Relation $modelRelation 模型关联对象 + * @return mixed + */ + protected function getRelationData(Relation $modelRelation) + { + if ($this->parent && get_class($this->parent) == $modelRelation->getModel()) { + $value = $this->parent; + } else { + // 首先获取关联数据 + $value = $modelRelation->getRelation(); + } + return $value; + } + + /** + * 数据读取 类型转换 + * @access public + * @param mixed $value 值 + * @param string|array $type 要转换的类型 + * @return mixed + */ + protected function readTransform($value, $type) + { + if (is_null($value)) { + return; + } + + if (is_array($type)) { + list($type, $param) = $type; + } elseif (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + switch ($type) { + case 'integer': + $value = (int) $value; + break; + case 'float': + if (empty($param)) { + $value = (float) $value; + } else { + $value = (float) number_format($value, $param, '.', ''); + } + break; + case 'boolean': + $value = (bool) $value; + break; + case 'timestamp': + if (!is_null($value)) { + $format = !empty($param) ? $param : $this->dateFormat; + $value = $this->formatDateTime($value, $format); + } + break; + case 'datetime': + if (!is_null($value)) { + $format = !empty($param) ? $param : $this->dateFormat; + $value = $this->formatDateTime(strtotime($value), $format); + } + break; + case 'json': + $value = json_decode($value, true); + break; + case 'array': + $value = empty($value) ? [] : json_decode($value, true); + break; + case 'object': + $value = empty($value) ? new \stdClass() : json_decode($value); + break; + case 'serialize': + $value = unserialize($value); + break; + default: + if (false !== strpos($type, '\\')) { + // 对象类型 + $value = new $type($value); + } + } + return $value; + } + + /** + * 设置需要追加的输出属性 + * @access public + * @param array $append 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function append($append = [], $override = false) + { + $this->append = $override ? $append : array_merge($this->append, $append); + return $this; + } + + /** + * 设置附加关联对象的属性 + * @access public + * @param string $relation 关联方法 + * @param string|array $append 追加属性名 + * @return $this + * @throws Exception + */ + public function appendRelationAttr($relation, $append) + { + if (is_string($append)) { + $append = explode(',', $append); + } + + $relation = Loader::parseName($relation, 1, false); + + // 获取关联数据 + if (isset($this->relation[$relation])) { + $model = $this->relation[$relation]; + } else { + $model = $this->getRelationData($this->$relation()); + } + + if ($model instanceof Model) { + foreach ($append as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + if (isset($this->data[$key])) { + throw new Exception('bind attr has exists:' . $key); + } else { + $this->data[$key] = $model->$attr; + } + } + } + return $this; + } + + /** + * 设置需要隐藏的输出属性 + * @access public + * @param array $hidden 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function hidden($hidden = [], $override = false) + { + $this->hidden = $override ? $hidden : array_merge($this->hidden, $hidden); + return $this; + } + + /** + * 设置需要输出的属性 + * @access public + * @param array $visible + * @param bool $override 是否覆盖 + * @return $this + */ + public function visible($visible = [], $override = false) + { + $this->visible = $override ? $visible : array_merge($this->visible, $visible); + return $this; + } + + /** + * 解析隐藏及显示属性 + * @access protected + * @param array $attrs 属性 + * @param array $result 结果集 + * @param bool $visible + * @return array + */ + protected function parseAttr($attrs, &$result, $visible = true) + { + $array = []; + foreach ($attrs as $key => $val) { + if (is_array($val)) { + if ($visible) { + $array[] = $key; + } + $result[$key] = $val; + } elseif (strpos($val, '.')) { + list($key, $name) = explode('.', $val); + if ($visible) { + $array[] = $key; + } + $result[$key][] = $name; + } else { + $array[] = $val; + } + } + return $array; + } + + /** + * 转换子模型对象 + * @access protected + * @param Model|ModelCollection $model + * @param $visible + * @param $hidden + * @param $key + * @return array + */ + protected function subToArray($model, $visible, $hidden, $key) + { + // 关联模型对象 + if (isset($visible[$key])) { + $model->visible($visible[$key]); + } elseif (isset($hidden[$key])) { + $model->hidden($hidden[$key]); + } + return $model->toArray(); + } + + /** + * 转换当前模型对象为数组 + * @access public + * @return array + */ + public function toArray() + { + $item = []; + $visible = []; + $hidden = []; + + $data = array_merge($this->data, $this->relation); + + // 过滤属性 + if (!empty($this->visible)) { + $array = $this->parseAttr($this->visible, $visible); + $data = array_intersect_key($data, array_flip($array)); + } elseif (!empty($this->hidden)) { + $array = $this->parseAttr($this->hidden, $hidden, false); + $data = array_diff_key($data, array_flip($array)); + } + + foreach ($data as $key => $val) { + if ($val instanceof Model || $val instanceof ModelCollection) { + // 关联模型对象 + $item[$key] = $this->subToArray($val, $visible, $hidden, $key); + } elseif (is_array($val) && reset($val) instanceof Model) { + // 关联模型数据集 + $arr = []; + foreach ($val as $k => $value) { + $arr[$k] = $this->subToArray($value, $visible, $hidden, $key); + } + $item[$key] = $arr; + } else { + // 模型属性 + $item[$key] = $this->getAttr($key); + } + } + // 追加属性(必须定义获取器) + if (!empty($this->append)) { + foreach ($this->append as $key => $name) { + if (is_array($name)) { + // 追加关联对象属性 + $relation = $this->getAttr($key); + $item[$key] = $relation->append($name)->toArray(); + } elseif (strpos($name, '.')) { + list($key, $attr) = explode('.', $name); + // 追加关联对象属性 + $relation = $this->getAttr($key); + $item[$key] = $relation->append([$attr])->toArray(); + } else { + $item[$name] = $this->getAttr($name); + } + } + } + return !empty($item) ? $item : []; + } + + /** + * 转换当前模型对象为JSON字符串 + * @access public + * @param integer $options json参数 + * @return string + */ + public function toJson($options = JSON_UNESCAPED_UNICODE) + { + return json_encode($this->toArray(), $options); + } + + /** + * 移除当前模型的关联属性 + * @access public + * @return $this + */ + public function removeRelation() + { + $this->relation = []; + return $this; + } + + /** + * 转换当前模型数据集为数据集对象 + * @access public + * @param array|\think\Collection $collection 数据集 + * @return \think\Collection + */ + public function toCollection($collection) + { + if ($this->resultSetType) { + if ('collection' == $this->resultSetType) { + $collection = new ModelCollection($collection); + } elseif (false !== strpos($this->resultSetType, '\\')) { + $class = $this->resultSetType; + $collection = new $class($collection); + } + } + return $collection; + } + + /** + * 关联数据一起更新 + * @access public + * @param mixed $relation 关联 + * @return $this + */ + public function together($relation) + { + if (is_string($relation)) { + $relation = explode(',', $relation); + } + $this->relationWrite = $relation; + return $this; + } + + /** + * 获取模型对象的主键 + * @access public + * @param string $name 模型名 + * @return mixed + */ + public function getPk($name = '') + { + if (!empty($name)) { + $table = $this->getQuery()->getTable($name); + return $this->getQuery()->getPk($table); + } elseif (empty($this->pk)) { + $this->pk = $this->getQuery()->getPk(); + } + return $this->pk; + } + + /** + * 判断一个字段名是否为主键字段 + * @access public + * @param string $key 名称 + * @return bool + */ + protected function isPk($key) + { + $pk = $this->getPk(); + if (is_string($pk) && $pk == $key) { + return true; + } elseif (is_array($pk) && in_array($key, $pk)) { + return true; + } + return false; + } + + /** + * 保存当前数据对象 + * @access public + * @param array $data 数据 + * @param array $where 更新条件 + * @param string $sequence 自增序列名 + * @return integer|false + */ + public function save($data = [], $where = [], $sequence = null) + { + if (!empty($data)) { + // 数据自动验证 + if (!$this->validateData($data)) { + return false; + } + // 数据对象赋值 + foreach ($data as $key => $value) { + $this->setAttr($key, $value, $data); + } + if (!empty($where)) { + $this->isUpdate = true; + } + } + + // 自动关联写入 + if (!empty($this->relationWrite)) { + $relation = []; + foreach ($this->relationWrite as $key => $name) { + if (is_array($name)) { + if (key($name) === 0) { + $relation[$key] = []; + foreach ($name as $val) { + if (isset($this->data[$val])) { + $relation[$key][$val] = $this->data[$val]; + unset($this->data[$val]); + } + } + } else { + $relation[$key] = $name; + } + } elseif (isset($this->relation[$name])) { + $relation[$name] = $this->relation[$name]; + } elseif (isset($this->data[$name])) { + $relation[$name] = $this->data[$name]; + unset($this->data[$name]); + } + } + } + + // 数据自动完成 + $this->autoCompleteData($this->auto); + + // 事件回调 + if (false === $this->trigger('before_write', $this)) { + return false; + } + $pk = $this->getPk(); + if ($this->isUpdate) { + // 自动更新 + $this->autoCompleteData($this->update); + + // 事件回调 + if (false === $this->trigger('before_update', $this)) { + return false; + } + + // 获取有更新的数据 + $data = $this->getChangedData(); + + if (empty($data) || (count($data) == 1 && is_string($pk) && isset($data[$pk]))) { + // 关联更新 + if (isset($relation)) { + $this->autoRelationUpdate($relation); + } + return 0; + } elseif ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) { + // 自动写入更新时间 + $data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); + $this->data[$this->updateTime] = $data[$this->updateTime]; + } + + if (empty($where) && !empty($this->updateWhere)) { + $where = $this->updateWhere; + } + + // 保留主键数据 + foreach ($this->data as $key => $val) { + if ($this->isPk($key)) { + $data[$key] = $val; + } + } + + if (is_string($pk) && isset($data[$pk])) { + if (!isset($where[$pk])) { + unset($where); + $where[$pk] = $data[$pk]; + } + unset($data[$pk]); + } + + // 检测字段 + $allowFields = $this->checkAllowField(array_merge($this->auto, $this->update)); + + // 模型更新 + if (!empty($allowFields)) { + $result = $this->getQuery()->where($where)->strict(false)->field($allowFields)->update($data); + } else { + $result = $this->getQuery()->where($where)->update($data); + } + + // 关联更新 + if (isset($relation)) { + $this->autoRelationUpdate($relation); + } + + // 更新回调 + $this->trigger('after_update', $this); + + } else { + // 自动写入 + $this->autoCompleteData($this->insert); + + // 自动写入创建时间和更新时间 + if ($this->autoWriteTimestamp) { + if ($this->createTime && !isset($this->data[$this->createTime])) { + $this->data[$this->createTime] = $this->autoWriteTimestamp($this->createTime); + } + if ($this->updateTime && !isset($this->data[$this->updateTime])) { + $this->data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); + } + } + + if (false === $this->trigger('before_insert', $this)) { + return false; + } + + // 检测字段 + $allowFields = $this->checkAllowField(array_merge($this->auto, $this->insert)); + if (!empty($allowFields)) { + $result = $this->getQuery()->strict(false)->field($allowFields)->insert($this->data); + } else { + $result = $this->getQuery()->insert($this->data); + } + + // 获取自动增长主键 + if ($result && is_string($pk) && (!isset($this->data[$pk]) || '' == $this->data[$pk])) { + $insertId = $this->getQuery()->getLastInsID($sequence); + if ($insertId) { + $this->data[$pk] = $insertId; + } + } + + // 关联写入 + if (isset($relation)) { + foreach ($relation as $name => $val) { + $method = Loader::parseName($name, 1, false); + $this->$method()->save($val); + } + } + + // 标记为更新 + $this->isUpdate = true; + + // 新增回调 + $this->trigger('after_insert', $this); + } + // 写入回调 + $this->trigger('after_write', $this); + + // 重新记录原始数据 + $this->origin = $this->data; + + return $result; + } + + protected function checkAllowField($auto = []) + { + if (true === $this->field) { + $this->field = $this->getQuery()->getTableInfo('', 'fields'); + $field = $this->field; + } elseif (!empty($this->field)) { + $field = array_merge($this->field, $auto); + if ($this->autoWriteTimestamp) { + array_push($field, $this->createTime, $this->updateTime); + } + } else { + $field = []; + } + + return $field; + } + + protected function autoRelationUpdate($relation) + { + foreach ($relation as $name => $val) { + if ($val instanceof Model) { + $val->save(); + } else { + unset($this->data[$name]); + $model = $this->getAttr($name); + if ($model instanceof Model) { + $model->save($val); + } + } + } + } + + /** + * 获取变化的数据 并排除只读数据 + * @access public + * @return array + */ + public function getChangedData() + { + $data = array_udiff_assoc($this->data, $this->origin, function ($a, $b) { + if ((empty($a) || empty($b)) && $a !== $b) { + return 1; + } + return is_object($a) || $a != $b ? 1 : 0; + }); + + if (!empty($this->readonly)) { + // 只读字段不允许更新 + foreach ($this->readonly as $key => $field) { + if (isset($data[$field])) { + unset($data[$field]); + } + } + } + + return $data; + } + + /** + * 字段值(延迟)增长 + * @access public + * @param string $field 字段名 + * @param integer $step 增长值 + * @param integer $lazyTime 延时时间(s) + * @return integer|true + * @throws Exception + */ + public function setInc($field, $step = 1, $lazyTime = 0) + { + // 更新条件 + $where = $this->getWhere(); + + $result = $this->getQuery()->where($where)->setInc($field, $step, $lazyTime); + if (true !== $result) { + $this->data[$field] += $step; + } + + return $result; + } + + /** + * 字段值(延迟)增长 + * @access public + * @param string $field 字段名 + * @param integer $step 增长值 + * @param integer $lazyTime 延时时间(s) + * @return integer|true + * @throws Exception + */ + public function setDec($field, $step = 1, $lazyTime = 0) + { + // 更新条件 + $where = $this->getWhere(); + $result = $this->getQuery()->where($where)->setDec($field, $step, $lazyTime); + if (true !== $result) { + $this->data[$field] -= $step; + } + + return $result; + } + + /** + * 获取更新条件 + * @access protected + * @return mixed + */ + protected function getWhere() + { + // 删除条件 + $pk = $this->getPk(); + + if (is_string($pk) && isset($this->data[$pk])) { + $where = [$pk => $this->data[$pk]]; + } elseif (!empty($this->updateWhere)) { + $where = $this->updateWhere; + } else { + $where = null; + } + return $where; + } + + /** + * 保存多个数据到当前数据对象 + * @access public + * @param array $dataSet 数据 + * @param boolean $replace 是否自动识别更新和写入 + * @return array|false + * @throws \Exception + */ + public function saveAll($dataSet, $replace = true) + { + if ($this->validate) { + // 数据批量验证 + $validate = $this->validate; + foreach ($dataSet as $data) { + if (!$this->validateData($data, $validate)) { + return false; + } + } + } + + $result = []; + $db = $this->getQuery(); + $db->startTrans(); + try { + $pk = $this->getPk(); + if (is_string($pk) && $replace) { + $auto = true; + } + foreach ($dataSet as $key => $data) { + if (!empty($auto) && isset($data[$pk])) { + $result[$key] = self::update($data, [], $this->field); + } else { + $result[$key] = self::create($data, $this->field); + } + } + $db->commit(); + return $result; + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + + /** + * 设置允许写入的字段 + * @access public + * @param mixed $field 允许写入的字段 如果为true只允许写入数据表字段 + * @return $this + */ + public function allowField($field) + { + if (is_string($field)) { + $field = explode(',', $field); + } + $this->field = $field; + return $this; + } + + /** + * 设置只读字段 + * @access public + * @param mixed $field 只读字段 + * @return $this + */ + public function readonly($field) + { + if (is_string($field)) { + $field = explode(',', $field); + } + $this->readonly = $field; + return $this; + } + + /** + * 是否为更新数据 + * @access public + * @param bool $update + * @param mixed $where + * @return $this + */ + public function isUpdate($update = true, $where = null) + { + $this->isUpdate = $update; + if (!empty($where)) { + $this->updateWhere = $where; + } + return $this; + } + + /** + * 数据自动完成 + * @access public + * @param array $auto 要自动更新的字段列表 + * @return void + */ + protected function autoCompleteData($auto = []) + { + foreach ($auto as $field => $value) { + if (is_integer($field)) { + $field = $value; + $value = null; + } + + if (!isset($this->data[$field])) { + $default = null; + } else { + $default = $this->data[$field]; + } + + $this->setAttr($field, !is_null($value) ? $value : $default); + } + } + + /** + * 删除当前的记录 + * @access public + * @return integer + */ + public function delete() + { + if (false === $this->trigger('before_delete', $this)) { + return false; + } + + // 删除条件 + $where = $this->getWhere(); + + // 删除当前模型数据 + $result = $this->getQuery()->where($where)->delete(); + + // 关联删除 + if (!empty($this->relationWrite)) { + foreach ($this->relationWrite as $key => $name) { + $name = is_numeric($key) ? $name : $key; + $model = $this->getAttr($name); + if ($model instanceof Model) { + $model->delete(); + } + } + } + + $this->trigger('after_delete', $this); + // 清空原始数据 + $this->origin = []; + + return $result; + } + + /** + * 设置自动完成的字段( 规则通过修改器定义) + * @access public + * @param array $fields 需要自动完成的字段 + * @return $this + */ + public function auto($fields) + { + $this->auto = $fields; + return $this; + } + + /** + * 设置字段验证 + * @access public + * @param array|string|bool $rule 验证规则 true表示自动读取验证器类 + * @param array $msg 提示信息 + * @param bool $batch 批量验证 + * @return $this + */ + public function validate($rule = true, $msg = [], $batch = false) + { + if (is_array($rule)) { + $this->validate = [ + 'rule' => $rule, + 'msg' => $msg, + ]; + } else { + $this->validate = true === $rule ? $this->name : $rule; + } + $this->batchValidate = $batch; + return $this; + } + + /** + * 设置验证失败后是否抛出异常 + * @access public + * @param bool $fail 是否抛出异常 + * @return $this + */ + public function validateFailException($fail = true) + { + $this->failException = $fail; + return $this; + } + + /** + * 自动验证数据 + * @access protected + * @param array $data 验证数据 + * @param mixed $rule 验证规则 + * @param bool $batch 批量验证 + * @return bool + */ + protected function validateData($data, $rule = null, $batch = null) + { + $info = is_null($rule) ? $this->validate : $rule; + + if (!empty($info)) { + if (is_array($info)) { + $validate = Loader::validate(); + $validate->rule($info['rule']); + $validate->message($info['msg']); + } else { + $name = is_string($info) ? $info : $this->name; + if (strpos($name, '.')) { + list($name, $scene) = explode('.', $name); + } + $validate = Loader::validate($name); + if (!empty($scene)) { + $validate->scene($scene); + } + } + $batch = is_null($batch) ? $this->batchValidate : $batch; + + if (!$validate->batch($batch)->check($data)) { + $this->error = $validate->getError(); + if ($this->failException) { + throw new ValidateException($this->error); + } else { + return false; + } + } + $this->validate = null; + } + return true; + } + + /** + * 返回模型的错误信息 + * @access public + * @return string|array + */ + public function getError() + { + return $this->error; + } + + /** + * 注册回调方法 + * @access public + * @param string $event 事件名 + * @param callable $callback 回调方法 + * @param bool $override 是否覆盖 + * @return void + */ + public static function event($event, $callback, $override = false) + { + $class = get_called_class(); + if ($override) { + self::$event[$class][$event] = []; + } + self::$event[$class][$event][] = $callback; + } + + /** + * 触发事件 + * @access protected + * @param string $event 事件名 + * @param mixed $params 传入参数(引用) + * @return bool + */ + protected function trigger($event, &$params) + { + if (isset(self::$event[$this->class][$event])) { + foreach (self::$event[$this->class][$event] as $callback) { + if (is_callable($callback)) { + $result = call_user_func_array($callback, [ & $params]); + if (false === $result) { + return false; + } + } + } + } + return true; + } + + /** + * 写入数据 + * @access public + * @param array $data 数据数组 + * @param array|true $field 允许字段 + * @return $this + */ + public static function create($data = [], $field = null) + { + $model = new static(); + if (!empty($field)) { + $model->allowField($field); + } + $model->isUpdate(false)->save($data, []); + return $model; + } + + /** + * 更新数据 + * @access public + * @param array $data 数据数组 + * @param array $where 更新条件 + * @param array|true $field 允许字段 + * @return $this + */ + public static function update($data = [], $where = [], $field = null) + { + $model = new static(); + if (!empty($field)) { + $model->allowField($field); + } + $result = $model->isUpdate(true)->save($data, $where); + return $model; + } + + /** + * 查找单条记录 + * @access public + * @param mixed $data 主键值或者查询条件(闭包) + * @param array|string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return static|null + * @throws exception\DbException + */ + public static function get($data, $with = [], $cache = false) + { + if (is_null($data)) { + return; + } + + if (true === $with || is_int($with)) { + $cache = $with; + $with = []; + } + $query = static::parseQuery($data, $with, $cache); + return $query->find($data); + } + + /** + * 查找所有记录 + * @access public + * @param mixed $data 主键列表或者查询条件(闭包) + * @param array|string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return static[]|false + * @throws exception\DbException + */ + public static function all($data = null, $with = [], $cache = false) + { + if (true === $with || is_int($with)) { + $cache = $with; + $with = []; + } + $query = static::parseQuery($data, $with, $cache); + return $query->select($data); + } + + /** + * 分析查询表达式 + * @access public + * @param mixed $data 主键列表或者查询条件(闭包) + * @param string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return Query + */ + protected static function parseQuery(&$data, $with, $cache) + { + $result = self::with($with)->cache($cache); + if (is_array($data) && key($data) !== 0) { + $result = $result->where($data); + $data = null; + } elseif ($data instanceof \Closure) { + call_user_func_array($data, [ & $result]); + $data = null; + } elseif ($data instanceof Query) { + $result = $data->with($with)->cache($cache); + $data = null; + } + return $result; + } + + /** + * 删除记录 + * @access public + * @param mixed $data 主键列表 支持闭包查询条件 + * @return integer 成功删除的记录数 + */ + public static function destroy($data) + { + $model = new static(); + $query = $model->db(); + if (is_array($data) && key($data) !== 0) { + $query->where($data); + $data = null; + } elseif ($data instanceof \Closure) { + call_user_func_array($data, [ & $query]); + $data = null; + } elseif (empty($data) && 0 !== $data) { + return 0; + } + $resultSet = $query->select($data); + $count = 0; + if ($resultSet) { + foreach ($resultSet as $data) { + $result = $data->delete(); + $count += $result; + } + } + return $count; + } + + /** + * 命名范围 + * @access public + * @param string|array|\Closure $name 命名范围名称 逗号分隔 + * @internal mixed ...$params 参数调用 + * @return Query + */ + public static function scope($name) + { + $model = new static(); + $query = $model->db(); + $params = func_get_args(); + array_shift($params); + array_unshift($params, $query); + if ($name instanceof \Closure) { + call_user_func_array($name, $params); + } elseif (is_string($name)) { + $name = explode(',', $name); + } + if (is_array($name)) { + foreach ($name as $scope) { + $method = 'scope' . trim($scope); + if (method_exists($model, $method)) { + call_user_func_array([$model, $method], $params); + } + } + } + return $query; + } + + /** + * 设置是否使用全局查询范围 + * @param bool $use 是否启用全局查询范围 + * @access public + * @return Model + */ + public static function useGlobalScope($use) + { + $model = new static(); + return $model->db($use); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $relation 关联方法名 + * @param mixed $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @return Relation|Query + */ + public static function has($relation, $operator = '>=', $count = 1, $id = '*') + { + $relation = (new static())->$relation(); + if (is_array($operator) || $operator instanceof \Closure) { + return $relation->hasWhere($operator); + } + return $relation->has($operator, $count, $id); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $relation 关联方法名 + * @param mixed $where 查询条件(数组或者闭包) + * @return Relation|Query + */ + public static function hasWhere($relation, $where = []) + { + return (new static())->$relation()->hasWhere($where); + } + + /** + * 解析模型的完整命名空间 + * @access public + * @param string $model 模型名(或者完整类名) + * @return string + */ + protected function parseModel($model) + { + if (false === strpos($model, '\\')) { + $path = explode('\\', get_called_class()); + array_pop($path); + array_push($path, Loader::parseName($model, 1)); + $model = implode('\\', $path); + } + return $model; + } + + /** + * 查询当前模型的关联数据 + * @access public + * @param string|array $relations 关联名 + * @return $this + */ + public function relationQuery($relations) + { + if (is_string($relations)) { + $relations = explode(',', $relations); + } + + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = null; + if ($relation instanceof \Closure) { + // 支持闭包查询过滤关联条件 + $closure = $relation; + $relation = $key; + } + if (is_array($relation)) { + $subRelation = $relation; + $relation = $key; + } elseif (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + $method = Loader::parseName($relation, 1, false); + $this->data[$relation] = $this->$method()->getRelation($subRelation, $closure); + } + return $this; + } + + /** + * 预载入关联查询 返回数据集 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 关联名 + * @return array + */ + public function eagerlyResultSet(&$resultSet, $relation) + { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = false; + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } + if (is_array($relation)) { + $subRelation = $relation; + $relation = $key; + } elseif (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + $relation = Loader::parseName($relation, 1, false); + $this->$relation()->eagerlyResultSet($resultSet, $relation, $subRelation, $closure); + } + } + + /** + * 预载入关联查询 返回模型对象 + * @access public + * @param Model $result 数据对象 + * @param string $relation 关联名 + * @return Model + */ + public function eagerlyResult(&$result, $relation) + { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = false; + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } + if (is_array($relation)) { + $subRelation = $relation; + $relation = $key; + } elseif (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + $relation = Loader::parseName($relation, 1, false); + $this->$relation()->eagerlyResult($result, $relation, $subRelation, $closure); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param string|array $relation 关联名 + * @return void + */ + public function relationCount(&$result, $relation) + { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + + foreach ($relations as $key => $relation) { + $closure = false; + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } elseif (is_string($key)) { + $name = $relation; + $relation = $key; + } + $relation = Loader::parseName($relation, 1, false); + $count = $this->$relation()->relationCount($result, $closure); + if (!isset($name)) { + $name = Loader::parseName($relation) . '_count'; + } + $result->setAttr($name, $count); + } + } + + /** + * 获取模型的默认外键名 + * @access public + * @param string $name 模型名 + * @return string + */ + protected function getForeignKey($name) + { + if (strpos($name, '\\')) { + $name = basename(str_replace('\\', '/', $name)); + } + return Loader::parseName($name) . '_id'; + } + + /** + * HAS ONE 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型主键 + * @param array $alias 别名定义(已经废弃) + * @param string $joinType JOIN类型 + * @return HasOne + */ + public function hasOne($model, $foreignKey = '', $localKey = '', $alias = [], $joinType = 'INNER') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $localKey = $localKey ?: $this->getPk(); + $foreignKey = $foreignKey ?: $this->getForeignKey($this->name); + return new HasOne($this, $model, $foreignKey, $localKey, $joinType); + } + + /** + * BELONGS TO 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 关联主键 + * @param array $alias 别名定义(已经废弃) + * @param string $joinType JOIN类型 + * @return BelongsTo + */ + public function belongsTo($model, $foreignKey = '', $localKey = '', $alias = [], $joinType = 'INNER') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $foreignKey = $foreignKey ?: $this->getForeignKey($model); + $localKey = $localKey ?: (new $model)->getPk(); + $trace = debug_backtrace(false, 2); + $relation = Loader::parseName($trace[1]['function']); + return new BelongsTo($this, $model, $foreignKey, $localKey, $joinType, $relation); + } + + /** + * HAS MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型主键 + * @return HasMany + */ + public function hasMany($model, $foreignKey = '', $localKey = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $localKey = $localKey ?: $this->getPk(); + $foreignKey = $foreignKey ?: $this->getForeignKey($this->name); + return new HasMany($this, $model, $foreignKey, $localKey); + } + + /** + * HAS MANY 远程关联定义 + * @access public + * @param string $model 模型名 + * @param string $through 中间模型名 + * @param string $foreignKey 关联外键 + * @param string $throughKey 关联外键 + * @param string $localKey 当前模型主键 + * @return HasManyThrough + */ + public function hasManyThrough($model, $through, $foreignKey = '', $throughKey = '', $localKey = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $through = $this->parseModel($through); + $localKey = $localKey ?: $this->getPk(); + $foreignKey = $foreignKey ?: $this->getForeignKey($this->name); + $throughKey = $throughKey ?: $this->getForeignKey($through); + return new HasManyThrough($this, $model, $through, $foreignKey, $throughKey, $localKey); + } + + /** + * BELONGS TO MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string $table 中间表名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型关联键 + * @return BelongsToMany + */ + public function belongsToMany($model, $table = '', $foreignKey = '', $localKey = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $name = Loader::parseName(basename(str_replace('\\', '/', $model))); + $table = $table ?: Loader::parseName($this->name) . '_' . $name; + $foreignKey = $foreignKey ?: $name . '_id'; + $localKey = $localKey ?: $this->getForeignKey($this->name); + return new BelongsToMany($this, $model, $table, $foreignKey, $localKey); + } + + /** + * MORPH MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string|array $morph 多态字段信息 + * @param string $type 多态类型 + * @return MorphMany + */ + public function morphMany($model, $morph = null, $type = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + if (is_null($morph)) { + $trace = debug_backtrace(false, 2); + $morph = Loader::parseName($trace[1]['function']); + } + $type = $type ?: Loader::parseName($this->name); + if (is_array($morph)) { + list($morphType, $foreignKey) = $morph; + } else { + $morphType = $morph . '_type'; + $foreignKey = $morph . '_id'; + } + return new MorphMany($this, $model, $foreignKey, $morphType, $type); + } + + /** + * MORPH One 关联定义 + * @access public + * @param string $model 模型名 + * @param string|array $morph 多态字段信息 + * @param string $type 多态类型 + * @return MorphOne + */ + public function morphOne($model, $morph = null, $type = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + if (is_null($morph)) { + $trace = debug_backtrace(false, 2); + $morph = Loader::parseName($trace[1]['function']); + } + $type = $type ?: Loader::parseName($this->name); + if (is_array($morph)) { + list($morphType, $foreignKey) = $morph; + } else { + $morphType = $morph . '_type'; + $foreignKey = $morph . '_id'; + } + return new MorphOne($this, $model, $foreignKey, $morphType, $type); + } + + /** + * MORPH TO 关联定义 + * @access public + * @param string|array $morph 多态字段信息 + * @param array $alias 多态别名定义 + * @return MorphTo + */ + public function morphTo($morph = null, $alias = []) + { + $trace = debug_backtrace(false, 2); + $relation = Loader::parseName($trace[1]['function']); + + if (is_null($morph)) { + $morph = $relation; + } + // 记录当前关联信息 + if (is_array($morph)) { + list($morphType, $foreignKey) = $morph; + } else { + $morphType = $morph . '_type'; + $foreignKey = $morph . '_id'; + } + return new MorphTo($this, $morphType, $foreignKey, $alias, $relation); + } + + public function __call($method, $args) + { + $query = $this->db(true, false); + + if (method_exists($this, 'scope' . $method)) { + // 动态调用命名范围 + $method = 'scope' . $method; + array_unshift($args, $query); + call_user_func_array([$this, $method], $args); + return $this; + } else { + return call_user_func_array([$query, $method], $args); + } + } + + public static function __callStatic($method, $args) + { + $model = new static(); + $query = $model->db(); + + if (method_exists($model, 'scope' . $method)) { + // 动态调用命名范围 + $method = 'scope' . $method; + array_unshift($args, $query); + + call_user_func_array([$model, $method], $args); + return $query; + } else { + return call_user_func_array([$query, $method], $args); + } + } + + /** + * 修改器 设置数据对象的值 + * @access public + * @param string $name 名称 + * @param mixed $value 值 + * @return void + */ + public function __set($name, $value) + { + $this->setAttr($name, $value); + } + + /** + * 获取器 获取数据对象的值 + * @access public + * @param string $name 名称 + * @return mixed + */ + public function __get($name) + { + return $this->getAttr($name); + } + + /** + * 检测数据对象的值 + * @access public + * @param string $name 名称 + * @return boolean + */ + public function __isset($name) + { + try { + if (array_key_exists($name, $this->data) || array_key_exists($name, $this->relation)) { + return true; + } else { + $this->getAttr($name); + return true; + } + } catch (InvalidArgumentException $e) { + return false; + } + + } + + /** + * 销毁数据对象的值 + * @access public + * @param string $name 名称 + * @return void + */ + public function __unset($name) + { + unset($this->data[$name], $this->relation[$name]); + } + + public function __toString() + { + return $this->toJson(); + } + + // JsonSerializable + public function jsonSerialize() + { + return $this->toArray(); + } + + // ArrayAccess + public function offsetSet($name, $value) + { + $this->setAttr($name, $value); + } + + public function offsetExists($name) + { + return $this->__isset($name); + } + + public function offsetUnset($name) + { + $this->__unset($name); + } + + public function offsetGet($name) + { + return $this->getAttr($name); + } + + /** + * 解序列化后处理 + */ + public function __wakeup() + { + $this->initialize(); + } + + /** + * 模型事件快捷方法 + * @param $callback + * @param bool $override + */ + protected static function beforeInsert($callback, $override = false) + { + self::event('before_insert', $callback, $override); + } + + protected static function afterInsert($callback, $override = false) + { + self::event('after_insert', $callback, $override); + } + + protected static function beforeUpdate($callback, $override = false) + { + self::event('before_update', $callback, $override); + } + + protected static function afterUpdate($callback, $override = false) + { + self::event('after_update', $callback, $override); + } + + protected static function beforeWrite($callback, $override = false) + { + self::event('before_write', $callback, $override); + } + + protected static function afterWrite($callback, $override = false) + { + self::event('after_write', $callback, $override); + } + + protected static function beforeDelete($callback, $override = false) + { + self::event('before_delete', $callback, $override); + } + + protected static function afterDelete($callback, $override = false) + { + self::event('after_delete', $callback, $override); + } + +} diff --git a/thinkphp/library/think/Paginator.php b/thinkphp/library/think/Paginator.php new file mode 100644 index 0000000..5a8fa20 --- /dev/null +++ b/thinkphp/library/think/Paginator.php @@ -0,0 +1,387 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use ArrayAccess; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use JsonSerializable; +use Traversable; + +abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable +{ + /** @var bool 是否为简洁模式 */ + protected $simple = false; + + /** @var Collection 数据集 */ + protected $items; + + /** @var integer 当前页 */ + protected $currentPage; + + /** @var integer 最后一页 */ + protected $lastPage; + + /** @var integer|null 数据总数 */ + protected $total; + + /** @var integer 每页的数量 */ + protected $listRows; + + /** @var bool 是否有下一页 */ + protected $hasMore; + + /** @var array 一些配置 */ + protected $options = [ + 'var_page' => 'page', + 'path' => '/', + 'query' => [], + 'fragment' => '', + ]; + + public function __construct($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) + { + $this->options = array_merge($this->options, $options); + + $this->options['path'] = '/' != $this->options['path'] ? rtrim($this->options['path'], '/') : $this->options['path']; + + $this->simple = $simple; + $this->listRows = $listRows; + + if (!$items instanceof Collection) { + $items = Collection::make($items); + } + + if ($simple) { + $this->currentPage = $this->setCurrentPage($currentPage); + $this->hasMore = count($items) > ($this->listRows); + $items = $items->slice(0, $this->listRows); + } else { + $this->total = $total; + $this->lastPage = (int) ceil($total / $listRows); + $this->currentPage = $this->setCurrentPage($currentPage); + $this->hasMore = $this->currentPage < $this->lastPage; + } + $this->items = $items; + } + + /** + * @param $items + * @param $listRows + * @param null $currentPage + * @param bool $simple + * @param null $total + * @param array $options + * @return Paginator + */ + public static function make($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) + { + return new static($items, $listRows, $currentPage, $total, $simple, $options); + } + + protected function setCurrentPage($currentPage) + { + if (!$this->simple && $currentPage > $this->lastPage) { + return $this->lastPage > 0 ? $this->lastPage : 1; + } + + return $currentPage; + } + + /** + * 获取页码对应的链接 + * + * @param $page + * @return string + */ + protected function url($page) + { + if ($page <= 0) { + $page = 1; + } + + if (strpos($this->options['path'], '[PAGE]') === false) { + $parameters = [$this->options['var_page'] => $page]; + $path = $this->options['path']; + } else { + $parameters = []; + $path = str_replace('[PAGE]', $page, $this->options['path']); + } + if (count($this->options['query']) > 0) { + $parameters = array_merge($this->options['query'], $parameters); + } + $url = $path; + if (!empty($parameters)) { + $url .= '?' . urldecode(http_build_query($parameters, null, '&')); + } + return $url . $this->buildFragment(); + } + + /** + * 自动获取当前页码 + * @param string $varPage + * @param int $default + * @return int + */ + public static function getCurrentPage($varPage = 'page', $default = 1) + { + $page = Request::instance()->request($varPage); + + if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) { + return $page; + } + + return $default; + } + + /** + * 自动获取当前的path + * @return string + */ + public static function getCurrentPath() + { + return Request::instance()->baseUrl(); + } + + public function total() + { + if ($this->simple) { + throw new \DomainException('not support total'); + } + return $this->total; + } + + public function listRows() + { + return $this->listRows; + } + + public function currentPage() + { + return $this->currentPage; + } + + public function lastPage() + { + if ($this->simple) { + throw new \DomainException('not support last'); + } + return $this->lastPage; + } + + /** + * 数据是否足够分页 + * @return boolean + */ + public function hasPages() + { + return !(1 == $this->currentPage && !$this->hasMore); + } + + /** + * 创建一组分页链接 + * + * @param int $start + * @param int $end + * @return array + */ + public function getUrlRange($start, $end) + { + $urls = []; + + for ($page = $start; $page <= $end; $page++) { + $urls[$page] = $this->url($page); + } + + return $urls; + } + + /** + * 设置URL锚点 + * + * @param string|null $fragment + * @return $this + */ + public function fragment($fragment) + { + $this->options['fragment'] = $fragment; + return $this; + } + + /** + * 添加URL参数 + * + * @param array|string $key + * @param string|null $value + * @return $this + */ + public function appends($key, $value = null) + { + if (!is_array($key)) { + $queries = [$key => $value]; + } else { + $queries = $key; + } + + foreach ($queries as $k => $v) { + if ($k !== $this->options['var_page']) { + $this->options['query'][$k] = $v; + } + } + + return $this; + } + + /** + * 构造锚点字符串 + * + * @return string + */ + protected function buildFragment() + { + return $this->options['fragment'] ? '#' . $this->options['fragment'] : ''; + } + + /** + * 渲染分页html + * @return mixed + */ + abstract public function render(); + + public function items() + { + return $this->items->all(); + } + + public function getCollection() + { + return $this->items; + } + + public function isEmpty() + { + return $this->items->isEmpty(); + } + + /** + * 给每个元素执行个回调 + * + * @param callable $callback + * @return $this + */ + public function each(callable $callback) + { + foreach ($this->items as $key => $item) { + if ($callback($item, $key) === false) { + break; + } + } + + return $this; + } + + /** + * Retrieve an external iterator + * @return Traversable An instance of an object implementing Iterator or + * Traversable + */ + public function getIterator() + { + return new ArrayIterator($this->items->all()); + } + + /** + * Whether a offset exists + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + return $this->items->offsetExists($offset); + } + + /** + * Offset to retrieve + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + return $this->items->offsetGet($offset); + } + + /** + * Offset to set + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + $this->items->offsetSet($offset, $value); + } + + /** + * Offset to unset + * @param mixed $offset + * @return void + * @since 5.0.0 + */ + public function offsetUnset($offset) + { + $this->items->offsetUnset($offset); + } + + /** + * Count elements of an object + */ + public function count() + { + return $this->items->count(); + } + + public function __toString() + { + return (string) $this->render(); + } + + public function toArray() + { + try { + $total = $this->total(); + } catch (\DomainException $e) { + $total = null; + } + + return [ + 'total' => $total, + 'per_page' => $this->listRows(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage, + 'data' => $this->items->toArray(), + ]; + } + + /** + * Specify data which should be serialized to JSON + */ + public function jsonSerialize() + { + return $this->toArray(); + } + + public function __call($name, $arguments) + { + return call_user_func_array([$this->getCollection(), $name], $arguments); + } + +} diff --git a/thinkphp/library/think/Process.php b/thinkphp/library/think/Process.php new file mode 100644 index 0000000..6f3faa3 --- /dev/null +++ b/thinkphp/library/think/Process.php @@ -0,0 +1,1205 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\process\exception\Failed as ProcessFailedException; +use think\process\exception\Timeout as ProcessTimeoutException; +use think\process\pipes\Pipes; +use think\process\pipes\Unix as UnixPipes; +use think\process\pipes\Windows as WindowsPipes; +use think\process\Utils; + +class Process +{ + + const ERR = 'err'; + const OUT = 'out'; + + const STATUS_READY = 'ready'; + const STATUS_STARTED = 'started'; + const STATUS_TERMINATED = 'terminated'; + + const STDIN = 0; + const STDOUT = 1; + const STDERR = 2; + + const TIMEOUT_PRECISION = 0.2; + + private $callback; + private $commandline; + private $cwd; + private $env; + private $input; + private $starttime; + private $lastOutputTime; + private $timeout; + private $idleTimeout; + private $options; + private $exitcode; + private $fallbackExitcode; + private $processInformation; + private $outputDisabled = false; + private $stdout; + private $stderr; + private $enhanceWindowsCompatibility = true; + private $enhanceSigchildCompatibility; + private $process; + private $status = self::STATUS_READY; + private $incrementalOutputOffset = 0; + private $incrementalErrorOutputOffset = 0; + private $tty; + private $pty; + + private $useFileHandles = false; + + /** @var Pipes */ + private $processPipes; + + private $latestSignal; + + private static $sigchild; + + /** + * @var array + */ + public static $exitCodes = [ + 0 => 'OK', + 1 => 'General error', + 2 => 'Misuse of shell builtins', + 126 => 'Invoked command cannot execute', + 127 => 'Command not found', + 128 => 'Invalid exit argument', + // signals + 129 => 'Hangup', + 130 => 'Interrupt', + 131 => 'Quit and dump core', + 132 => 'Illegal instruction', + 133 => 'Trace/breakpoint trap', + 134 => 'Process aborted', + 135 => 'Bus error: "access to undefined portion of memory object"', + 136 => 'Floating point exception: "erroneous arithmetic operation"', + 137 => 'Kill (terminate immediately)', + 138 => 'User-defined 1', + 139 => 'Segmentation violation', + 140 => 'User-defined 2', + 141 => 'Write to pipe with no one reading', + 142 => 'Signal raised by alarm', + 143 => 'Termination (request to terminate)', + // 144 - not defined + 145 => 'Child process terminated, stopped (or continued*)', + 146 => 'Continue if stopped', + 147 => 'Stop executing temporarily', + 148 => 'Terminal stop signal', + 149 => 'Background process attempting to read from tty ("in")', + 150 => 'Background process attempting to write to tty ("out")', + 151 => 'Urgent data available on socket', + 152 => 'CPU time limit exceeded', + 153 => 'File size limit exceeded', + 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', + 155 => 'Profiling timer expired', + // 156 - not defined + 157 => 'Pollable event', + // 158 - not defined + 159 => 'Bad syscall', + ]; + + /** + * 构造方法 + * @param string $commandline 指令 + * @param string|null $cwd 工作目录 + * @param array|null $env 环境变量 + * @param string|null $input 输入 + * @param int|float|null $timeout 超时时间 + * @param array $options proc_open的选项 + * @throws \RuntimeException + * @api + */ + public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = []) + { + if (!function_exists('proc_open')) { + throw new \RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); + } + + $this->commandline = $commandline; + $this->cwd = $cwd; + + if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DS)) { + $this->cwd = getcwd(); + } + if (null !== $env) { + $this->setEnv($env); + } + + $this->input = $input; + $this->setTimeout($timeout); + $this->useFileHandles = '\\' === DS; + $this->pty = false; + $this->enhanceWindowsCompatibility = true; + $this->enhanceSigchildCompatibility = '\\' !== DS && $this->isSigchildEnabled(); + $this->options = array_replace([ + 'suppress_errors' => true, + 'binary_pipes' => true, + ], $options); + } + + public function __destruct() + { + $this->stop(); + } + + public function __clone() + { + $this->resetProcessData(); + } + + /** + * 运行指令 + * @param callback|null $callback + * @return int + */ + public function run($callback = null) + { + $this->start($callback); + + return $this->wait(); + } + + /** + * 运行指令 + * @param callable|null $callback + * @return self + * @throws \RuntimeException + * @throws ProcessFailedException + */ + public function mustRun($callback = null) + { + if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + + if (0 !== $this->run($callback)) { + throw new ProcessFailedException($this); + } + + return $this; + } + + /** + * 启动进程并写到 STDIN 输入后返回。 + * @param callable|null $callback + * @throws \RuntimeException + * @throws \RuntimeException + * @throws \LogicException + */ + public function start($callback = null) + { + if ($this->isRunning()) { + throw new \RuntimeException('Process is already running'); + } + if ($this->outputDisabled && null !== $callback) { + throw new \LogicException('Output has been disabled, enable it to allow the use of a callback.'); + } + + $this->resetProcessData(); + $this->starttime = $this->lastOutputTime = microtime(true); + $this->callback = $this->buildCallback($callback); + $descriptors = $this->getDescriptors(); + + $commandline = $this->commandline; + + if ('\\' === DS && $this->enhanceWindowsCompatibility) { + $commandline = 'cmd /V:ON /E:ON /C "(' . $commandline . ')'; + foreach ($this->processPipes->getFiles() as $offset => $filename) { + $commandline .= ' ' . $offset . '>' . Utils::escapeArgument($filename); + } + $commandline .= '"'; + + if (!isset($this->options['bypass_shell'])) { + $this->options['bypass_shell'] = true; + } + } + + $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options); + + if (!is_resource($this->process)) { + throw new \RuntimeException('Unable to launch a new process.'); + } + $this->status = self::STATUS_STARTED; + + if ($this->tty) { + return; + } + + $this->updateStatus(false); + $this->checkTimeout(); + } + + /** + * 重启进程 + * @param callable|null $callback + * @return Process + * @throws \RuntimeException + * @throws \RuntimeException + */ + public function restart($callback = null) + { + if ($this->isRunning()) { + throw new \RuntimeException('Process is already running'); + } + + $process = clone $this; + $process->start($callback); + + return $process; + } + + /** + * 等待要终止的进程 + * @param callable|null $callback + * @return int + */ + public function wait($callback = null) + { + $this->requireProcessIsStarted(__FUNCTION__); + + $this->updateStatus(false); + if (null !== $callback) { + $this->callback = $this->buildCallback($callback); + } + + do { + $this->checkTimeout(); + $running = '\\' === DS ? $this->isRunning() : $this->processPipes->areOpen(); + $close = '\\' !== DS || !$running; + $this->readPipes(true, $close); + } while ($running); + + while ($this->isRunning()) { + usleep(1000); + } + + if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { + throw new \RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); + } + + return $this->exitcode; + } + + /** + * 获取PID + * @return int|null + * @throws \RuntimeException + */ + public function getPid() + { + if ($this->isSigchildEnabled()) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->isRunning() ? $this->processInformation['pid'] : null; + } + + /** + * 将一个 POSIX 信号发送到进程中 + * @param int $signal + * @return Process + */ + public function signal($signal) + { + $this->doSignal($signal, true); + + return $this; + } + + /** + * 禁用从底层过程获取输出和错误输出。 + * @return Process + */ + public function disableOutput() + { + if ($this->isRunning()) { + throw new \RuntimeException('Disabling output while the process is running is not possible.'); + } + if (null !== $this->idleTimeout) { + throw new \LogicException('Output can not be disabled while an idle timeout is set.'); + } + + $this->outputDisabled = true; + + return $this; + } + + /** + * 开启从底层过程获取输出和错误输出。 + * @return Process + * @throws \RuntimeException + */ + public function enableOutput() + { + if ($this->isRunning()) { + throw new \RuntimeException('Enabling output while the process is running is not possible.'); + } + + $this->outputDisabled = false; + + return $this; + } + + /** + * 输出是否禁用 + * @return bool + */ + public function isOutputDisabled() + { + return $this->outputDisabled; + } + + /** + * 获取当前的输出管道 + * @return string + * @throws \LogicException + * @throws \LogicException + * @api + */ + public function getOutput() + { + if ($this->outputDisabled) { + throw new \LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted(__FUNCTION__); + + $this->readPipes(false, '\\' === DS ? !$this->processInformation['running'] : true); + + return $this->stdout; + } + + /** + * 以增量方式返回的输出结果。 + * @return string + */ + public function getIncrementalOutput() + { + $this->requireProcessIsStarted(__FUNCTION__); + + $data = $this->getOutput(); + + $latest = substr($data, $this->incrementalOutputOffset); + + if (false === $latest) { + return ''; + } + + $this->incrementalOutputOffset = strlen($data); + + return $latest; + } + + /** + * 清空输出 + * @return Process + */ + public function clearOutput() + { + $this->stdout = ''; + $this->incrementalOutputOffset = 0; + + return $this; + } + + /** + * 返回当前的错误输出的过程 (STDERR)。 + * @return string + */ + public function getErrorOutput() + { + if ($this->outputDisabled) { + throw new \LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted(__FUNCTION__); + + $this->readPipes(false, '\\' === DS ? !$this->processInformation['running'] : true); + + return $this->stderr; + } + + /** + * 以增量方式返回 errorOutput + * @return string + */ + public function getIncrementalErrorOutput() + { + $this->requireProcessIsStarted(__FUNCTION__); + + $data = $this->getErrorOutput(); + + $latest = substr($data, $this->incrementalErrorOutputOffset); + + if (false === $latest) { + return ''; + } + + $this->incrementalErrorOutputOffset = strlen($data); + + return $latest; + } + + /** + * 清空 errorOutput + * @return Process + */ + public function clearErrorOutput() + { + $this->stderr = ''; + $this->incrementalErrorOutputOffset = 0; + + return $this; + } + + /** + * 获取退出码 + * @return null|int + */ + public function getExitCode() + { + if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + + $this->updateStatus(false); + + return $this->exitcode; + } + + /** + * 获取退出文本 + * @return null|string + */ + public function getExitCodeText() + { + if (null === $exitcode = $this->getExitCode()) { + return; + } + + return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; + } + + /** + * 检查是否成功 + * @return bool + */ + public function isSuccessful() + { + return 0 === $this->getExitCode(); + } + + /** + * 是否未捕获的信号已被终止子进程 + * @return bool + */ + public function hasBeenSignaled() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled()) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->processInformation['signaled']; + } + + /** + * 返回导致子进程终止其执行的数。 + * @return int + */ + public function getTermSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled()) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->processInformation['termsig']; + } + + /** + * 检查子进程信号是否已停止 + * @return bool + */ + public function hasBeenStopped() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + $this->updateStatus(false); + + return $this->processInformation['stopped']; + } + + /** + * 返回导致子进程停止其执行的数。 + * @return int + */ + public function getStopSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + $this->updateStatus(false); + + return $this->processInformation['stopsig']; + } + + /** + * 检查是否正在运行 + * @return bool + */ + public function isRunning() + { + if (self::STATUS_STARTED !== $this->status) { + return false; + } + + $this->updateStatus(false); + + return $this->processInformation['running']; + } + + /** + * 检查是否已开始 + * @return bool + */ + public function isStarted() + { + return self::STATUS_READY != $this->status; + } + + /** + * 检查是否已终止 + * @return bool + */ + public function isTerminated() + { + $this->updateStatus(false); + + return self::STATUS_TERMINATED == $this->status; + } + + /** + * 获取当前的状态 + * @return string + */ + public function getStatus() + { + $this->updateStatus(false); + + return $this->status; + } + + /** + * 终止进程 + */ + public function stop() + { + if ($this->isRunning()) { + if ('\\' === DS && !$this->isSigchildEnabled()) { + exec(sprintf('taskkill /F /T /PID %d 2>&1', $this->getPid()), $output, $exitCode); + if ($exitCode > 0) { + throw new \RuntimeException('Unable to kill the process'); + } + } else { + $pids = preg_split('/\s+/', `ps -o pid --no-heading --ppid {$this->getPid()}`); + foreach ($pids as $pid) { + if (is_numeric($pid)) { + posix_kill($pid, 9); + } + } + } + } + + $this->updateStatus(false); + if ($this->processInformation['running']) { + $this->close(); + } + + return $this->exitcode; + } + + /** + * 添加一行输出 + * @param string $line + */ + public function addOutput($line) +{ + $this->lastOutputTime = microtime(true); + $this->stdout .= $line; + } + + /** + * 添加一行错误输出 + * @param string $line + */ + public function addErrorOutput($line) +{ + $this->lastOutputTime = microtime(true); + $this->stderr .= $line; + } + + /** + * 获取被执行的指令 + * @return string + */ + public function getCommandLine() +{ + return $this->commandline; + } + + /** + * 设置指令 + * @param string $commandline + * @return self + */ + public function setCommandLine($commandline) +{ + $this->commandline = $commandline; + + return $this; + } + + /** + * 获取超时时间 + * @return float|null + */ + public function getTimeout() +{ + return $this->timeout; + } + + /** + * 获取idle超时时间 + * @return float|null + */ + public function getIdleTimeout() +{ + return $this->idleTimeout; + } + + /** + * 设置超时时间 + * @param int|float|null $timeout + * @return self + */ + public function setTimeout($timeout) +{ + $this->timeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * 设置idle超时时间 + * @param int|float|null $timeout + * @return self + */ + public function setIdleTimeout($timeout) +{ + if (null !== $timeout && $this->outputDisabled) { + throw new \LogicException('Idle timeout can not be set while the output is disabled.'); + } + + $this->idleTimeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * 设置TTY + * @param bool $tty + * @return self + */ + public function setTty($tty) +{ + if ('\\' === DS && $tty) { + throw new \RuntimeException('TTY mode is not supported on Windows platform.'); + } + if ($tty && (!file_exists('/dev/tty') || !is_readable('/dev/tty'))) { + throw new \RuntimeException('TTY mode requires /dev/tty to be readable.'); + } + + $this->tty = (bool) $tty; + + return $this; + } + + /** + * 检查是否是tty模式 + * @return bool + */ + public function isTty() +{ + return $this->tty; + } + + /** + * 设置pty模式 + * @param bool $bool + * @return self + */ + public function setPty($bool) +{ + $this->pty = (bool) $bool; + + return $this; + } + + /** + * 是否是pty模式 + * @return bool + */ + public function isPty() +{ + return $this->pty; + } + + /** + * 获取工作目录 + * @return string|null + */ + public function getWorkingDirectory() +{ + if (null === $this->cwd) { + return getcwd() ?: null; + } + + return $this->cwd; + } + + /** + * 设置工作目录 + * @param string $cwd + * @return self + */ + public function setWorkingDirectory($cwd) +{ + $this->cwd = $cwd; + + return $this; + } + + /** + * 获取环境变量 + * @return array + */ + public function getEnv() +{ + return $this->env; + } + + /** + * 设置环境变量 + * @param array $env + * @return self + */ + public function setEnv(array $env) +{ + $env = array_filter($env, function ($value) { + return !is_array($value); + }); + + $this->env = []; + foreach ($env as $key => $value) { + $this->env[(binary) $key] = (binary) $value; + } + + return $this; + } + + /** + * 获取输入 + * @return null|string + */ + public function getInput() +{ + return $this->input; + } + + /** + * 设置输入 + * @param mixed $input + * @return self + */ + public function setInput($input) +{ + if ($this->isRunning()) { + throw new \LogicException('Input can not be set while the process is running.'); + } + + $this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input); + + return $this; + } + + /** + * 获取proc_open的选项 + * @return array + */ + public function getOptions() +{ + return $this->options; + } + + /** + * 设置proc_open的选项 + * @param array $options + * @return self + */ + public function setOptions(array $options) +{ + $this->options = $options; + + return $this; + } + + /** + * 是否兼容windows + * @return bool + */ + public function getEnhanceWindowsCompatibility() +{ + return $this->enhanceWindowsCompatibility; + } + + /** + * 设置是否兼容windows + * @param bool $enhance + * @return self + */ + public function setEnhanceWindowsCompatibility($enhance) +{ + $this->enhanceWindowsCompatibility = (bool) $enhance; + + return $this; + } + + /** + * 返回是否 sigchild 兼容模式激活 + * @return bool + */ + public function getEnhanceSigchildCompatibility() +{ + return $this->enhanceSigchildCompatibility; + } + + /** + * 激活 sigchild 兼容性模式。 + * @param bool $enhance + * @return self + */ + public function setEnhanceSigchildCompatibility($enhance) +{ + $this->enhanceSigchildCompatibility = (bool) $enhance; + + return $this; + } + + /** + * 是否超时 + */ + public function checkTimeout() +{ + if (self::STATUS_STARTED !== $this->status) { + return; + } + + if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { + $this->stop(); + + throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_GENERAL); + } + + if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { + $this->stop(); + + throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_IDLE); + } + } + + /** + * 是否支持pty + * @return bool + */ + public static function isPtySupported() +{ + static $result; + + if (null !== $result) { + return $result; + } + + if ('\\' === DS) { + return $result = false; + } + + $proc = @proc_open('echo 1', [['pty'], ['pty'], ['pty']], $pipes); + if (is_resource($proc)) { + proc_close($proc); + + return $result = true; + } + + return $result = false; + } + + /** + * 创建所需的 proc_open 的描述符 + * @return array + */ + private function getDescriptors() +{ + if ('\\' === DS) { + $this->processPipes = WindowsPipes::create($this, $this->input); + } else { + $this->processPipes = UnixPipes::create($this, $this->input); + } + $descriptors = $this->processPipes->getDescriptors($this->outputDisabled); + + if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + + $descriptors = array_merge($descriptors, [['pipe', 'w']]); + + $this->commandline = '(' . $this->commandline . ') 3>/dev/null; code=$?; echo $code >&3; exit $code'; + } + + return $descriptors; + } + + /** + * 建立 wait () 使用的回调。 + * @param callable|null $callback + * @return callable + */ + protected function buildCallback($callback) +{ + $out = self::OUT; + $callback = function ($type, $data) use ($callback, $out) { + if ($out == $type) { + $this->addOutput($data); + } else { + $this->addErrorOutput($data); + } + + if (null !== $callback) { + call_user_func($callback, $type, $data); + } + }; + + return $callback; + } + + /** + * 更新状态 + * @param bool $blocking + */ + protected function updateStatus($blocking) +{ + if (self::STATUS_STARTED !== $this->status) { + return; + } + + $this->processInformation = proc_get_status($this->process); + $this->captureExitCode(); + + $this->readPipes($blocking, '\\' === DS ? !$this->processInformation['running'] : true); + + if (!$this->processInformation['running']) { + $this->close(); + } + } + + /** + * 是否开启 '--enable-sigchild' + * @return bool + */ + protected function isSigchildEnabled() +{ + if (null !== self::$sigchild) { + return self::$sigchild; + } + + if (!function_exists('phpinfo')) { + return self::$sigchild = false; + } + + ob_start(); + phpinfo(INFO_GENERAL); + + return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); + } + + /** + * 验证是否超时 + * @param int|float|null $timeout + * @return float|null + */ + private function validateTimeout($timeout) +{ + $timeout = (float) $timeout; + + if (0.0 === $timeout) { + $timeout = null; + } elseif ($timeout < 0) { + throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + return $timeout; + } + + /** + * 读取pipes + * @param bool $blocking + * @param bool $close + */ + private function readPipes($blocking, $close) +{ + $result = $this->processPipes->readAndWrite($blocking, $close); + + $callback = $this->callback; + foreach ($result as $type => $data) { + if (3 == $type) { + $this->fallbackExitcode = (int) $data; + } else { + $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); + } + } + } + + /** + * 捕获退出码 + */ + private function captureExitCode() +{ + if (isset($this->processInformation['exitcode']) && -1 != $this->processInformation['exitcode']) { + $this->exitcode = $this->processInformation['exitcode']; + } + } + + /** + * 关闭资源 + * @return int 退出码 + */ + private function close() +{ + $this->processPipes->close(); + if (is_resource($this->process)) { + $exitcode = proc_close($this->process); + } else { + $exitcode = -1; + } + + $this->exitcode = -1 !== $exitcode ? $exitcode : (null !== $this->exitcode ? $this->exitcode : -1); + $this->status = self::STATUS_TERMINATED; + + if (-1 === $this->exitcode && null !== $this->fallbackExitcode) { + $this->exitcode = $this->fallbackExitcode; + } elseif (-1 === $this->exitcode && $this->processInformation['signaled'] + && 0 < $this->processInformation['termsig'] + ) { + $this->exitcode = 128 + $this->processInformation['termsig']; + } + + return $this->exitcode; + } + + /** + * 重置数据 + */ + private function resetProcessData() +{ + $this->starttime = null; + $this->callback = null; + $this->exitcode = null; + $this->fallbackExitcode = null; + $this->processInformation = null; + $this->stdout = null; + $this->stderr = null; + $this->process = null; + $this->latestSignal = null; + $this->status = self::STATUS_READY; + $this->incrementalOutputOffset = 0; + $this->incrementalErrorOutputOffset = 0; + } + + /** + * 将一个 POSIX 信号发送到进程中。 + * @param int $signal + * @param bool $throwException + * @return bool + */ + private function doSignal($signal, $throwException) +{ + if (!$this->isRunning()) { + if ($throwException) { + throw new \LogicException('Can not send signal on a non running process.'); + } + + return false; + } + + if ($this->isSigchildEnabled()) { + if ($throwException) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } + + return false; + } + + if (true !== @proc_terminate($this->process, $signal)) { + if ($throwException) { + throw new \RuntimeException(sprintf('Error while sending signal `%s`.', $signal)); + } + + return false; + } + + $this->latestSignal = $signal; + + return true; + } + + /** + * 确保进程已经开启 + * @param string $functionName + */ + private function requireProcessIsStarted($functionName) +{ + if (!$this->isStarted()) { + throw new \LogicException(sprintf('Process must be started before calling %s.', $functionName)); + } + } + + /** + * 确保进程已经终止 + * @param string $functionName + */ + private function requireProcessIsTerminated($functionName) +{ + if (!$this->isTerminated()) { + throw new \LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); + } + } +} diff --git a/thinkphp/library/think/Request.php b/thinkphp/library/think/Request.php new file mode 100644 index 0000000..7e9949a --- /dev/null +++ b/thinkphp/library/think/Request.php @@ -0,0 +1,1634 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Request +{ + /** + * @var object 对象实例 + */ + protected static $instance; + + protected $method; + /** + * @var string 域名(含协议和端口) + */ + protected $domain; + + /** + * @var string URL地址 + */ + protected $url; + + /** + * @var string 基础URL + */ + protected $baseUrl; + + /** + * @var string 当前执行的文件 + */ + protected $baseFile; + + /** + * @var string 访问的ROOT地址 + */ + protected $root; + + /** + * @var string pathinfo + */ + protected $pathinfo; + + /** + * @var string pathinfo(不含后缀) + */ + protected $path; + + /** + * @var array 当前路由信息 + */ + protected $routeInfo = []; + + /** + * @var array 当前调度信息 + */ + protected $dispatch = []; + protected $module; + protected $controller; + protected $action; + // 当前语言集 + protected $langset; + + /** + * @var array 请求参数 + */ + protected $param = []; + protected $get = []; + protected $post = []; + protected $request = []; + protected $route = []; + protected $put; + protected $session = []; + protected $file = []; + protected $cookie = []; + protected $server = []; + protected $header = []; + + /** + * @var array 资源类型 + */ + protected $mimeType = [ + 'xml' => 'application/xml,text/xml,application/x-xml', + 'json' => 'application/json,text/x-json,application/jsonrequest,text/json', + 'js' => 'text/javascript,application/javascript,application/x-javascript', + 'css' => 'text/css', + 'rss' => 'application/rss+xml', + 'yaml' => 'application/x-yaml,text/yaml', + 'atom' => 'application/atom+xml', + 'pdf' => 'application/pdf', + 'text' => 'text/plain', + 'image' => 'image/png,image/jpg,image/jpeg,image/pjpeg,image/gif,image/webp,image/*', + 'csv' => 'text/csv', + 'html' => 'text/html,application/xhtml+xml,*/*', + ]; + + protected $content; + + // 全局过滤规则 + protected $filter; + // Hook扩展方法 + protected static $hook = []; + // 绑定的属性 + protected $bind = []; + // php://input + protected $input; + // 请求缓存 + protected $cache; + // 缓存是否检查 + protected $isCheckCache; + + /** + * 构造函数 + * @access protected + * @param array $options 参数 + */ + protected function __construct($options = []) + { + foreach ($options as $name => $item) { + if (property_exists($this, $name)) { + $this->$name = $item; + } + } + if (is_null($this->filter)) { + $this->filter = Config::get('default_filter'); + } + + // 保存 php://input + $this->input = file_get_contents('php://input'); + } + + public function __call($method, $args) + { + if (array_key_exists($method, self::$hook)) { + array_unshift($args, $this); + return call_user_func_array(self::$hook[$method], $args); + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } + + /** + * Hook 方法注入 + * @access public + * @param string|array $method 方法名 + * @param mixed $callback callable + * @return void + */ + public static function hook($method, $callback = null) + { + if (is_array($method)) { + self::$hook = array_merge(self::$hook, $method); + } else { + self::$hook[$method] = $callback; + } + } + + /** + * 初始化 + * @access public + * @param array $options 参数 + * @return \think\Request + */ + public static function instance($options = []) + { + if (is_null(self::$instance)) { + self::$instance = new static($options); + } + return self::$instance; + } + + /** + * 创建一个URL请求 + * @access public + * @param string $uri URL地址 + * @param string $method 请求类型 + * @param array $params 请求参数 + * @param array $cookie + * @param array $files + * @param array $server + * @param string $content + * @return \think\Request + */ + public static function create($uri, $method = 'GET', $params = [], $cookie = [], $files = [], $server = [], $content = null) + { + $server['PATH_INFO'] = ''; + $server['REQUEST_METHOD'] = strtoupper($method); + $info = parse_url($uri); + if (isset($info['host'])) { + $server['SERVER_NAME'] = $info['host']; + $server['HTTP_HOST'] = $info['host']; + } + if (isset($info['scheme'])) { + if ('https' === $info['scheme']) { + $server['HTTPS'] = 'on'; + $server['SERVER_PORT'] = 443; + } else { + unset($server['HTTPS']); + $server['SERVER_PORT'] = 80; + } + } + if (isset($info['port'])) { + $server['SERVER_PORT'] = $info['port']; + $server['HTTP_HOST'] = $server['HTTP_HOST'] . ':' . $info['port']; + } + if (isset($info['user'])) { + $server['PHP_AUTH_USER'] = $info['user']; + } + if (isset($info['pass'])) { + $server['PHP_AUTH_PW'] = $info['pass']; + } + if (!isset($info['path'])) { + $info['path'] = '/'; + } + $options = []; + $options[strtolower($method)] = $params; + $queryString = ''; + if (isset($info['query'])) { + parse_str(html_entity_decode($info['query']), $query); + if (!empty($params)) { + $params = array_replace($query, $params); + $queryString = http_build_query($query, '', '&'); + } else { + $params = $query; + $queryString = $info['query']; + } + } elseif (!empty($params)) { + $queryString = http_build_query($params, '', '&'); + } + if ($queryString) { + parse_str($queryString, $get); + $options['get'] = isset($options['get']) ? array_merge($get, $options['get']) : $get; + } + + $server['REQUEST_URI'] = $info['path'] . ('' !== $queryString ? '?' . $queryString : ''); + $server['QUERY_STRING'] = $queryString; + $options['cookie'] = $cookie; + $options['param'] = $params; + $options['file'] = $files; + $options['server'] = $server; + $options['url'] = $server['REQUEST_URI']; + $options['baseUrl'] = $info['path']; + $options['pathinfo'] = '/' == $info['path'] ? '/' : ltrim($info['path'], '/'); + $options['method'] = $server['REQUEST_METHOD']; + $options['domain'] = isset($info['scheme']) ? $info['scheme'] . '://' . $server['HTTP_HOST'] : ''; + $options['content'] = $content; + self::$instance = new self($options); + return self::$instance; + } + + /** + * 设置或获取当前包含协议的域名 + * @access public + * @param string $domain 域名 + * @return string + */ + public function domain($domain = null) + { + if (!is_null($domain)) { + $this->domain = $domain; + return $this; + } elseif (!$this->domain) { + $this->domain = $this->scheme() . '://' . $this->host(); + } + return $this->domain; + } + + /** + * 设置或获取当前完整URL 包括QUERY_STRING + * @access public + * @param string|true $url URL地址 true 带域名获取 + * @return string + */ + public function url($url = null) + { + if (!is_null($url) && true !== $url) { + $this->url = $url; + return $this; + } elseif (!$this->url) { + if (IS_CLI) { + $this->url = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : ''; + } elseif (isset($_SERVER['HTTP_X_REWRITE_URL'])) { + $this->url = $_SERVER['HTTP_X_REWRITE_URL']; + } elseif (isset($_SERVER['REQUEST_URI'])) { + $this->url = $_SERVER['REQUEST_URI']; + } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { + $this->url = $_SERVER['ORIG_PATH_INFO'] . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : ''); + } else { + $this->url = ''; + } + } + return true === $url ? $this->domain() . $this->url : $this->url; + } + + /** + * 设置或获取当前URL 不含QUERY_STRING + * @access public + * @param string $url URL地址 + * @return string + */ + public function baseUrl($url = null) + { + if (!is_null($url) && true !== $url) { + $this->baseUrl = $url; + return $this; + } elseif (!$this->baseUrl) { + $str = $this->url(); + $this->baseUrl = strpos($str, '?') ? strstr($str, '?', true) : $str; + } + return true === $url ? $this->domain() . $this->baseUrl : $this->baseUrl; + } + + /** + * 设置或获取当前执行的文件 SCRIPT_NAME + * @access public + * @param string $file 当前执行的文件 + * @return string + */ + public function baseFile($file = null) + { + if (!is_null($file) && true !== $file) { + $this->baseFile = $file; + return $this; + } elseif (!$this->baseFile) { + $url = ''; + if (!IS_CLI) { + $script_name = basename($_SERVER['SCRIPT_FILENAME']); + if (basename($_SERVER['SCRIPT_NAME']) === $script_name) { + $url = $_SERVER['SCRIPT_NAME']; + } elseif (basename($_SERVER['PHP_SELF']) === $script_name) { + $url = $_SERVER['PHP_SELF']; + } elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $script_name) { + $url = $_SERVER['ORIG_SCRIPT_NAME']; + } elseif (($pos = strpos($_SERVER['PHP_SELF'], '/' . $script_name)) !== false) { + $url = substr($_SERVER['SCRIPT_NAME'], 0, $pos) . '/' . $script_name; + } elseif (isset($_SERVER['DOCUMENT_ROOT']) && strpos($_SERVER['SCRIPT_FILENAME'], $_SERVER['DOCUMENT_ROOT']) === 0) { + $url = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $_SERVER['SCRIPT_FILENAME'])); + } + } + $this->baseFile = $url; + } + return true === $file ? $this->domain() . $this->baseFile : $this->baseFile; + } + + /** + * 设置或获取URL访问根地址 + * @access public + * @param string $url URL地址 + * @return string + */ + public function root($url = null) + { + if (!is_null($url) && true !== $url) { + $this->root = $url; + return $this; + } elseif (!$this->root) { + $file = $this->baseFile(); + if ($file && 0 !== strpos($this->url(), $file)) { + $file = str_replace('\\', '/', dirname($file)); + } + $this->root = rtrim($file, '/'); + } + return true === $url ? $this->domain() . $this->root : $this->root; + } + + /** + * 获取当前请求URL的pathinfo信息(含URL后缀) + * @access public + * @return string + */ + public function pathinfo() + { + if (is_null($this->pathinfo)) { + if (isset($_GET[Config::get('var_pathinfo')])) { + // 判断URL里面是否有兼容模式参数 + $_SERVER['PATH_INFO'] = $_GET[Config::get('var_pathinfo')]; + unset($_GET[Config::get('var_pathinfo')]); + } elseif (IS_CLI) { + // CLI模式下 index.php module/controller/action/params/... + $_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : ''; + } + + // 分析PATHINFO信息 + if (!isset($_SERVER['PATH_INFO'])) { + foreach (Config::get('pathinfo_fetch') as $type) { + if (!empty($_SERVER[$type])) { + $_SERVER['PATH_INFO'] = (0 === strpos($_SERVER[$type], $_SERVER['SCRIPT_NAME'])) ? + substr($_SERVER[$type], strlen($_SERVER['SCRIPT_NAME'])) : $_SERVER[$type]; + break; + } + } + } + $this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim($_SERVER['PATH_INFO'], '/'); + } + return $this->pathinfo; + } + + /** + * 获取当前请求URL的pathinfo信息(不含URL后缀) + * @access public + * @return string + */ + public function path() + { + if (is_null($this->path)) { + $suffix = Config::get('url_html_suffix'); + $pathinfo = $this->pathinfo(); + if (false === $suffix) { + // 禁止伪静态访问 + $this->path = $pathinfo; + } elseif ($suffix) { + // 去除正常的URL后缀 + $this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo); + } else { + // 允许任何后缀访问 + $this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo); + } + } + return $this->path; + } + + /** + * 当前URL的访问后缀 + * @access public + * @return string + */ + public function ext() + { + return pathinfo($this->pathinfo(), PATHINFO_EXTENSION); + } + + /** + * 获取当前请求的时间 + * @access public + * @param bool $float 是否使用浮点类型 + * @return integer|float + */ + public function time($float = false) + { + return $float ? $_SERVER['REQUEST_TIME_FLOAT'] : $_SERVER['REQUEST_TIME']; + } + + /** + * 当前请求的资源类型 + * @access public + * @return false|string + */ + public function type() + { + $accept = $this->server('HTTP_ACCEPT'); + if (empty($accept)) { + return false; + } + + foreach ($this->mimeType as $key => $val) { + $array = explode(',', $val); + foreach ($array as $k => $v) { + if (stristr($accept, $v)) { + return $key; + } + } + } + return false; + } + + /** + * 设置资源类型 + * @access public + * @param string|array $type 资源类型名 + * @param string $val 资源类型 + * @return void + */ + public function mimeType($type, $val = '') + { + if (is_array($type)) { + $this->mimeType = array_merge($this->mimeType, $type); + } else { + $this->mimeType[$type] = $val; + } + } + + /** + * 当前的请求类型 + * @access public + * @param bool $method true 获取原始请求类型 + * @return string + */ + public function method($method = false) + { + if (true === $method) { + // 获取原始请求类型 + return IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']); + } elseif (!$this->method) { + if (isset($_POST[Config::get('var_method')])) { + $this->method = strtoupper($_POST[Config::get('var_method')]); + $this->{$this->method}($_POST); + } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { + $this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); + } else { + $this->method = IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']); + } + } + return $this->method; + } + + /** + * 是否为GET请求 + * @access public + * @return bool + */ + public function isGet() + { + return $this->method() == 'GET'; + } + + /** + * 是否为POST请求 + * @access public + * @return bool + */ + public function isPost() + { + return $this->method() == 'POST'; + } + + /** + * 是否为PUT请求 + * @access public + * @return bool + */ + public function isPut() + { + return $this->method() == 'PUT'; + } + + /** + * 是否为DELTE请求 + * @access public + * @return bool + */ + public function isDelete() + { + return $this->method() == 'DELETE'; + } + + /** + * 是否为HEAD请求 + * @access public + * @return bool + */ + public function isHead() + { + return $this->method() == 'HEAD'; + } + + /** + * 是否为PATCH请求 + * @access public + * @return bool + */ + public function isPatch() + { + return $this->method() == 'PATCH'; + } + + /** + * 是否为OPTIONS请求 + * @access public + * @return bool + */ + public function isOptions() + { + return $this->method() == 'OPTIONS'; + } + + /** + * 是否为cli + * @access public + * @return bool + */ + public function isCli() + { + return PHP_SAPI == 'cli'; + } + + /** + * 是否为cgi + * @access public + * @return bool + */ + public function isCgi() + { + return strpos(PHP_SAPI, 'cgi') === 0; + } + + /** + * 获取当前请求的参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function param($name = '', $default = null, $filter = '') + { + if (empty($this->param)) { + $method = $this->method(true); + // 自动获取请求变量 + switch ($method) { + case 'POST': + $vars = $this->post(false); + break; + case 'PUT': + case 'DELETE': + case 'PATCH': + $vars = $this->put(false); + break; + default: + $vars = []; + } + // 当前请求参数和URL地址中的参数合并 + $this->param = array_merge($this->get(false), $vars, $this->route(false)); + } + if (true === $name) { + // 获取包含文件上传信息的数组 + $file = $this->file(); + $data = is_array($file) ? array_merge($this->param, $file) : $this->param; + return $this->input($data, '', $default, $filter); + } + return $this->input($this->param, $name, $default, $filter); + } + + /** + * 设置获取路由参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function route($name = '', $default = null, $filter = '') + { + if (is_array($name)) { + $this->param = []; + return $this->route = array_merge($this->route, $name); + } + return $this->input($this->route, $name, $default, $filter); + } + + /** + * 设置获取GET参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function get($name = '', $default = null, $filter = '') + { + if (empty($this->get)) { + $this->get = $_GET; + } + if (is_array($name)) { + $this->param = []; + return $this->get = array_merge($this->get, $name); + } + return $this->input($this->get, $name, $default, $filter); + } + + /** + * 设置获取POST参数 + * @access public + * @param string $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function post($name = '', $default = null, $filter = '') + { + if (empty($this->post)) { + $content = $this->input; + if (empty($_POST) && false !== strpos($this->contentType(), 'application/json')) { + $this->post = (array) json_decode($content, true); + } else { + $this->post = $_POST; + } + } + if (is_array($name)) { + $this->param = []; + return $this->post = array_merge($this->post, $name); + } + return $this->input($this->post, $name, $default, $filter); + } + + /** + * 设置获取PUT参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function put($name = '', $default = null, $filter = '') + { + if (is_null($this->put)) { + $content = $this->input; + if (false !== strpos($this->contentType(), 'application/json')) { + $this->put = (array) json_decode($content, true); + } else { + parse_str($content, $this->put); + } + } + if (is_array($name)) { + $this->param = []; + return $this->put = is_null($this->put) ? $name : array_merge($this->put, $name); + } + + return $this->input($this->put, $name, $default, $filter); + } + + /** + * 设置获取DELETE参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function delete($name = '', $default = null, $filter = '') + { + return $this->put($name, $default, $filter); + } + + /** + * 设置获取PATCH参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function patch($name = '', $default = null, $filter = '') + { + return $this->put($name, $default, $filter); + } + + /** + * 获取request变量 + * @param string $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function request($name = '', $default = null, $filter = '') + { + if (empty($this->request)) { + $this->request = $_REQUEST; + } + if (is_array($name)) { + $this->param = []; + return $this->request = array_merge($this->request, $name); + } + return $this->input($this->request, $name, $default, $filter); + } + + /** + * 获取session数据 + * @access public + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function session($name = '', $default = null, $filter = '') + { + if (empty($this->session)) { + $this->session = Session::get(); + } + if (is_array($name)) { + return $this->session = array_merge($this->session, $name); + } + return $this->input($this->session, $name, $default, $filter); + } + + /** + * 获取cookie参数 + * @access public + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function cookie($name = '', $default = null, $filter = '') + { + if (empty($this->cookie)) { + $this->cookie = Cookie::get(); + } + if (is_array($name)) { + return $this->cookie = array_merge($this->cookie, $name); + } elseif (!empty($name)) { + $data = Cookie::has($name) ? Cookie::get($name) : $default; + } else { + $data = $this->cookie; + } + + // 解析过滤器 + $filter = $this->getFilter($filter, $default); + + if (is_array($data)) { + array_walk_recursive($data, [$this, 'filterValue'], $filter); + reset($data); + } else { + $this->filterValue($data, $name, $filter); + } + return $data; + } + + /** + * 获取server参数 + * @access public + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function server($name = '', $default = null, $filter = '') + { + if (empty($this->server)) { + $this->server = $_SERVER; + } + if (is_array($name)) { + return $this->server = array_merge($this->server, $name); + } + return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter); + } + + /** + * 获取上传的文件信息 + * @access public + * @param string|array $name 名称 + * @return null|array|\think\File + */ + public function file($name = '') + { + if (empty($this->file)) { + $this->file = isset($_FILES) ? $_FILES : []; + } + if (is_array($name)) { + return $this->file = array_merge($this->file, $name); + } + $files = $this->file; + if (!empty($files)) { + // 处理上传文件 + $array = []; + foreach ($files as $key => $file) { + if (is_array($file['name'])) { + $item = []; + $keys = array_keys($file); + $count = count($file['name']); + for ($i = 0; $i < $count; $i++) { + if (empty($file['tmp_name'][$i]) || !is_file($file['tmp_name'][$i])) { + continue; + } + $temp['key'] = $key; + foreach ($keys as $_key) { + $temp[$_key] = $file[$_key][$i]; + } + $item[] = (new File($temp['tmp_name']))->setUploadInfo($temp); + } + $array[$key] = $item; + } else { + if ($file instanceof File) { + $array[$key] = $file; + } else { + if (empty($file['tmp_name']) || !is_file($file['tmp_name'])) { + continue; + } + $array[$key] = (new File($file['tmp_name']))->setUploadInfo($file); + } + } + } + if (strpos($name, '.')) { + list($name, $sub) = explode('.', $name); + } + if ('' === $name) { + // 获取全部文件 + return $array; + } elseif (isset($sub) && isset($array[$name][$sub])) { + return $array[$name][$sub]; + } elseif (isset($array[$name])) { + return $array[$name]; + } + } + return; + } + + /** + * 获取环境变量 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function env($name = '', $default = null, $filter = '') + { + if (empty($this->env)) { + $this->env = $_ENV; + } + if (is_array($name)) { + return $this->env = array_merge($this->env, $name); + } + return $this->input($this->env, false === $name ? false : strtoupper($name), $default, $filter); + } + + /** + * 设置或者获取当前的Header + * @access public + * @param string|array $name header名称 + * @param string $default 默认值 + * @return string + */ + public function header($name = '', $default = null) + { + if (empty($this->header)) { + $header = []; + if (function_exists('apache_request_headers') && $result = apache_request_headers()) { + $header = $result; + } else { + $server = $this->server ?: $_SERVER; + foreach ($server as $key => $val) { + if (0 === strpos($key, 'HTTP_')) { + $key = str_replace('_', '-', strtolower(substr($key, 5))); + $header[$key] = $val; + } + } + if (isset($server['CONTENT_TYPE'])) { + $header['content-type'] = $server['CONTENT_TYPE']; + } + if (isset($server['CONTENT_LENGTH'])) { + $header['content-length'] = $server['CONTENT_LENGTH']; + } + } + $this->header = array_change_key_case($header); + } + if (is_array($name)) { + return $this->header = array_merge($this->header, $name); + } + if ('' === $name) { + return $this->header; + } + $name = str_replace('_', '-', strtolower($name)); + return isset($this->header[$name]) ? $this->header[$name] : $default; + } + + /** + * 获取变量 支持过滤和默认值 + * @param array $data 数据源 + * @param string|false $name 字段名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤函数 + * @return mixed + */ + public function input($data = [], $name = '', $default = null, $filter = '') + { + if (false === $name) { + // 获取原始数据 + return $data; + } + $name = (string) $name; + if ('' != $name) { + // 解析name + if (strpos($name, '/')) { + list($name, $type) = explode('/', $name); + } else { + $type = 's'; + } + // 按.拆分成多维数组进行判断 + foreach (explode('.', $name) as $val) { + if (isset($data[$val])) { + $data = $data[$val]; + } else { + // 无输入数据,返回默认值 + return $default; + } + } + if (is_object($data)) { + return $data; + } + } + + // 解析过滤器 + $filter = $this->getFilter($filter, $default); + + if (is_array($data)) { + array_walk_recursive($data, [$this, 'filterValue'], $filter); + reset($data); + } else { + $this->filterValue($data, $name, $filter); + } + + if (isset($type) && $data !== $default) { + // 强制类型转换 + $this->typeCast($data, $type); + } + return $data; + } + + /** + * 设置或获取当前的过滤规则 + * @param mixed $filter 过滤规则 + * @return mixed + */ + public function filter($filter = null) + { + if (is_null($filter)) { + return $this->filter; + } else { + $this->filter = $filter; + } + } + + protected function getFilter($filter, $default) + { + if (is_null($filter)) { + $filter = []; + } else { + $filter = $filter ?: $this->filter; + if (is_string($filter) && false === strpos($filter, '/')) { + $filter = explode(',', $filter); + } else { + $filter = (array) $filter; + } + } + + $filter[] = $default; + return $filter; + } + + /** + * 递归过滤给定的值 + * @param mixed $value 键值 + * @param mixed $key 键名 + * @param array $filters 过滤方法+默认值 + * @return mixed + */ + private function filterValue(&$value, $key, $filters) + { + $default = array_pop($filters); + foreach ($filters as $filter) { + if (is_callable($filter)) { + // 调用函数或者方法过滤 + $value = call_user_func($filter, $value); + } elseif (is_scalar($value)) { + if (false !== strpos($filter, '/')) { + // 正则过滤 + if (!preg_match($filter, $value)) { + // 匹配不成功返回默认值 + $value = $default; + break; + } + } elseif (!empty($filter)) { + // filter函数不存在时, 则使用filter_var进行过滤 + // filter为非整形值时, 调用filter_id取得过滤id + $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter)); + if (false === $value) { + $value = $default; + break; + } + } + } + } + return $this->filterExp($value); + } + + /** + * 过滤表单中的表达式 + * @param string $value + * @return void + */ + public function filterExp(&$value) + { + // 过滤查询特殊字符 + if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) { + $value .= ' '; + } + // TODO 其他安全过滤 + } + + /** + * 强制类型转换 + * @param string $data + * @param string $type + * @return mixed + */ + private function typeCast(&$data, $type) + { + switch (strtolower($type)) { + // 数组 + case 'a': + $data = (array) $data; + break; + // 数字 + case 'd': + $data = (int) $data; + break; + // 浮点 + case 'f': + $data = (float) $data; + break; + // 布尔 + case 'b': + $data = (boolean) $data; + break; + // 字符串 + case 's': + default: + if (is_scalar($data)) { + $data = (string) $data; + } else { + throw new \InvalidArgumentException('variable type error:' . gettype($data)); + } + } + } + + /** + * 是否存在某个请求参数 + * @access public + * @param string $name 变量名 + * @param string $type 变量类型 + * @param bool $checkEmpty 是否检测空值 + * @return mixed + */ + public function has($name, $type = 'param', $checkEmpty = false) + { + if (empty($this->$type)) { + $param = $this->$type(); + } else { + $param = $this->$type; + } + // 按.拆分成多维数组进行判断 + foreach (explode('.', $name) as $val) { + if (isset($param[$val])) { + $param = $param[$val]; + } else { + return false; + } + } + return ($checkEmpty && '' === $param) ? false : true; + } + + /** + * 获取指定的参数 + * @access public + * @param string|array $name 变量名 + * @param string $type 变量类型 + * @return mixed + */ + public function only($name, $type = 'param') + { + $param = $this->$type(); + if (is_string($name)) { + $name = explode(',', $name); + } + $item = []; + foreach ($name as $key) { + if (isset($param[$key])) { + $item[$key] = $param[$key]; + } + } + return $item; + } + + /** + * 排除指定参数获取 + * @access public + * @param string|array $name 变量名 + * @param string $type 变量类型 + * @return mixed + */ + public function except($name, $type = 'param') + { + $param = $this->$type(); + if (is_string($name)) { + $name = explode(',', $name); + } + foreach ($name as $key) { + if (isset($param[$key])) { + unset($param[$key]); + } + } + return $param; + } + + /** + * 当前是否ssl + * @access public + * @return bool + */ + public function isSsl() + { + $server = array_merge($_SERVER, $this->server); + if (isset($server['HTTPS']) && ('1' == $server['HTTPS'] || 'on' == strtolower($server['HTTPS']))) { + return true; + } elseif (isset($server['REQUEST_SCHEME']) && 'https' == $server['REQUEST_SCHEME']) { + return true; + } elseif (isset($server['SERVER_PORT']) && ('443' == $server['SERVER_PORT'])) { + return true; + } elseif (isset($server['HTTP_X_FORWARDED_PROTO']) && 'https' == $server['HTTP_X_FORWARDED_PROTO']) { + return true; + } elseif (Config::get('https_agent_name') && isset($server[Config::get('https_agent_name')])) { + return true; + } + return false; + } + + /** + * 当前是否Ajax请求 + * @access public + * @param bool $ajax true 获取原始ajax请求 + * @return bool + */ + public function isAjax($ajax = false) + { + $value = $this->server('HTTP_X_REQUESTED_WITH', '', 'strtolower'); + $result = ('xmlhttprequest' == $value) ? true : false; + if (true === $ajax) { + return $result; + } else { + return $this->param(Config::get('var_ajax')) ? true : $result; + } + } + + /** + * 当前是否Pjax请求 + * @access public + * @param bool $pjax true 获取原始pjax请求 + * @return bool + */ + public function isPjax($pjax = false) + { + $result = !is_null($this->server('HTTP_X_PJAX')) ? true : false; + if (true === $pjax) { + return $result; + } else { + return $this->param(Config::get('var_pjax')) ? true : $result; + } + } + + /** + * 获取客户端IP地址 + * @param integer $type 返回类型 0 返回IP地址 1 返回IPV4地址数字 + * @param boolean $adv 是否进行高级模式获取(有可能被伪装) + * @return mixed + */ + public function ip($type = 0, $adv = false) + { + $type = $type ? 1 : 0; + static $ip = null; + if (null !== $ip) { + return $ip[$type]; + } + + if ($adv) { + if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); + $pos = array_search('unknown', $arr); + if (false !== $pos) { + unset($arr[$pos]); + } + $ip = trim(current($arr)); + } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + } elseif (isset($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + } elseif (isset($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + // IP地址合法验证 + $long = sprintf("%u", ip2long($ip)); + $ip = $long ? [$ip, $long] : ['0.0.0.0', 0]; + return $ip[$type]; + } + + /** + * 检测是否使用手机访问 + * @access public + * @return bool + */ + public function isMobile() + { + if (isset($_SERVER['HTTP_VIA']) && stristr($_SERVER['HTTP_VIA'], "wap")) { + return true; + } elseif (isset($_SERVER['HTTP_ACCEPT']) && strpos(strtoupper($_SERVER['HTTP_ACCEPT']), "VND.WAP.WML")) { + return true; + } elseif (isset($_SERVER['HTTP_X_WAP_PROFILE']) || isset($_SERVER['HTTP_PROFILE'])) { + return true; + } elseif (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/(blackberry|configuration\/cldc|hp |hp-|htc |htc_|htc-|iemobile|kindle|midp|mmp|motorola|mobile|nokia|opera mini|opera |Googlebot-Mobile|YahooSeeker\/M1A1-R2D2|android|iphone|ipod|mobi|palm|palmos|pocket|portalmmm|ppc;|smartphone|sonyericsson|sqh|spv|symbian|treo|up.browser|up.link|vodafone|windows ce|xda |xda_)/i', $_SERVER['HTTP_USER_AGENT'])) { + return true; + } else { + return false; + } + } + + /** + * 当前URL地址中的scheme参数 + * @access public + * @return string + */ + public function scheme() + { + return $this->isSsl() ? 'https' : 'http'; + } + + /** + * 当前请求URL地址中的query参数 + * @access public + * @return string + */ + public function query() + { + return $this->server('QUERY_STRING'); + } + + /** + * 当前请求的host + * @access public + * @return string + */ + public function host() + { + return $this->server('HTTP_HOST'); + } + + /** + * 当前请求URL地址中的port参数 + * @access public + * @return integer + */ + public function port() + { + return $this->server('SERVER_PORT'); + } + + /** + * 当前请求 SERVER_PROTOCOL + * @access public + * @return integer + */ + public function protocol() + { + return $this->server('SERVER_PROTOCOL'); + } + + /** + * 当前请求 REMOTE_PORT + * @access public + * @return integer + */ + public function remotePort() + { + return $this->server('REMOTE_PORT'); + } + + /** + * 当前请求 HTTP_CONTENT_TYPE + * @access public + * @return string + */ + public function contentType() + { + $contentType = $this->server('CONTENT_TYPE'); + if ($contentType) { + if (strpos($contentType, ';')) { + list($type) = explode(';', $contentType); + } else { + $type = $contentType; + } + return trim($type); + } + return ''; + } + + /** + * 获取当前请求的路由信息 + * @access public + * @param array $route 路由名称 + * @return array + */ + public function routeInfo($route = []) + { + if (!empty($route)) { + $this->routeInfo = $route; + } else { + return $this->routeInfo; + } + } + + /** + * 设置或者获取当前请求的调度信息 + * @access public + * @param array $dispatch 调度信息 + * @return array + */ + public function dispatch($dispatch = null) + { + if (!is_null($dispatch)) { + $this->dispatch = $dispatch; + } + return $this->dispatch; + } + + /** + * 设置或者获取当前的模块名 + * @access public + * @param string $module 模块名 + * @return string|Request + */ + public function module($module = null) + { + if (!is_null($module)) { + $this->module = $module; + return $this; + } else { + return $this->module ?: ''; + } + } + + /** + * 设置或者获取当前的控制器名 + * @access public + * @param string $controller 控制器名 + * @return string|Request + */ + public function controller($controller = null) + { + if (!is_null($controller)) { + $this->controller = $controller; + return $this; + } else { + return $this->controller ?: ''; + } + } + + /** + * 设置或者获取当前的操作名 + * @access public + * @param string $action 操作名 + * @return string|Request + */ + public function action($action = null) + { + if (!is_null($action)) { + $this->action = $action; + return $this; + } else { + return $this->action ?: ''; + } + } + + /** + * 设置或者获取当前的语言 + * @access public + * @param string $lang 语言名 + * @return string|Request + */ + public function langset($lang = null) + { + if (!is_null($lang)) { + $this->langset = $lang; + return $this; + } else { + return $this->langset ?: ''; + } + } + + /** + * 设置或者获取当前请求的content + * @access public + * @return string + */ + public function getContent() + { + if (is_null($this->content)) { + $this->content = $this->input; + } + return $this->content; + } + + /** + * 获取当前请求的php://input + * @access public + * @return string + */ + public function getInput() + { + return $this->input; + } + + /** + * 生成请求令牌 + * @access public + * @param string $name 令牌名称 + * @param mixed $type 令牌生成方法 + * @return string + */ + public function token($name = '__token__', $type = 'md5') + { + $type = is_callable($type) ? $type : 'md5'; + $token = call_user_func($type, $_SERVER['REQUEST_TIME_FLOAT']); + if ($this->isAjax()) { + header($name . ': ' . $token); + } + Session::set($name, $token); + return $token; + } + + /** + * 设置当前地址的请求缓存 + * @access public + * @param string $key 缓存标识,支持变量规则 ,例如 item/:name/:id + * @param mixed $expire 缓存有效期 + * @param array $except 缓存排除 + * @return void + */ + public function cache($key, $expire = null, $except = []) + { + if (false !== $key && $this->isGet() && !$this->isCheckCache) { + // 标记请求缓存检查 + $this->isCheckCache = true; + if (false === $expire) { + // 关闭当前缓存 + return; + } + if ($key instanceof \Closure) { + $key = call_user_func_array($key, [$this]); + } elseif (true === $key) { + foreach ($except as $rule) { + if (0 === stripos($this->url(), $rule)) { + return; + } + } + // 自动缓存功能 + $key = '__URL__'; + } elseif (strpos($key, '|')) { + list($key, $fun) = explode('|', $key); + } + // 特殊规则替换 + if (false !== strpos($key, '__')) { + $key = str_replace(['__MODULE__', '__CONTROLLER__', '__ACTION__', '__URL__', ''], [$this->module, $this->controller, $this->action, md5($this->url(true))], $key); + } + + if (false !== strpos($key, ':')) { + $param = $this->param(); + foreach ($param as $item => $val) { + if (is_string($val) && false !== strpos($key, ':' . $item)) { + $key = str_replace(':' . $item, $val, $key); + } + } + } elseif (strpos($key, ']')) { + if ('[' . $this->ext() . ']' == $key) { + // 缓存某个后缀的请求 + $key = md5($this->url()); + } else { + return; + } + } + if (isset($fun)) { + $key = $fun($key); + } + + if (strtotime($this->server('HTTP_IF_MODIFIED_SINCE')) + $expire > $_SERVER['REQUEST_TIME']) { + // 读取缓存 + $response = Response::create()->code(304); + throw new \think\exception\HttpResponseException($response); + } elseif (Cache::has($key)) { + list($content, $header) = Cache::get($key); + $response = Response::create($content)->header($header); + throw new \think\exception\HttpResponseException($response); + } else { + $this->cache = [$key, $expire]; + } + } + } + + /** + * 读取请求缓存设置 + * @access public + * @return array + */ + public function getCache() + { + return $this->cache; + } + + /** + * 设置当前请求绑定的对象实例 + * @access public + * @param string $name 绑定的对象标识 + * @param mixed $obj 绑定的对象实例 + * @return mixed + */ + public function bind($name, $obj = null) + { + if (is_array($name)) { + $this->bind = array_merge($this->bind, $name); + } else { + $this->bind[$name] = $obj; + } + } + + public function __set($name, $value) + { + $this->bind[$name] = $value; + } + + public function __get($name) + { + return isset($this->bind[$name]) ? $this->bind[$name] : null; + } + + public function __isset($name) + { + return isset($this->bind[$name]); + } +} diff --git a/thinkphp/library/think/Response.php b/thinkphp/library/think/Response.php new file mode 100644 index 0000000..7ae9fed --- /dev/null +++ b/thinkphp/library/think/Response.php @@ -0,0 +1,334 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\response\Json as JsonResponse; +use think\response\Jsonp as JsonpResponse; +use think\response\Redirect as RedirectResponse; +use think\response\View as ViewResponse; +use think\response\Xml as XmlResponse; + +class Response +{ + // 原始数据 + protected $data; + + // 当前的contentType + protected $contentType = 'text/html'; + + // 字符集 + protected $charset = 'utf-8'; + + //状态 + protected $code = 200; + + // 输出参数 + protected $options = []; + // header参数 + protected $header = []; + + protected $content = null; + + /** + * 构造函数 + * @access public + * @param mixed $data 输出数据 + * @param int $code + * @param array $header + * @param array $options 输出参数 + */ + public function __construct($data = '', $code = 200, array $header = [], $options = []) + { + $this->data($data); + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + $this->contentType($this->contentType, $this->charset); + $this->header = array_merge($this->header, $header); + $this->code = $code; + } + + /** + * 创建Response对象 + * @access public + * @param mixed $data 输出数据 + * @param string $type 输出类型 + * @param int $code + * @param array $header + * @param array $options 输出参数 + * @return Response|JsonResponse|ViewResponse|XmlResponse|RedirectResponse|JsonpResponse + */ + public static function create($data = '', $type = '', $code = 200, array $header = [], $options = []) + { + $type = empty($type) ? 'null' : strtolower($type); + + $class = false !== strpos($type, '\\') ? $type : '\\think\\response\\' . ucfirst($type); + if (class_exists($class)) { + $response = new $class($data, $code, $header, $options); + } else { + $response = new static($data, $code, $header, $options); + } + + return $response; + } + + /** + * 发送数据到客户端 + * @access public + * @return mixed + * @throws \InvalidArgumentException + */ + public function send() + { + // 监听response_send + Hook::listen('response_send', $this); + + // 处理输出数据 + $data = $this->getContent(); + + // Trace调试注入 + if (Env::get('app_trace', Config::get('app_trace'))) { + Debug::inject($this, $data); + } + + if (200 == $this->code) { + $cache = Request::instance()->getCache(); + if ($cache) { + $this->header['Cache-Control'] = 'max-age=' . $cache[1] . ',must-revalidate'; + $this->header['Last-Modified'] = gmdate('D, d M Y H:i:s') . ' GMT'; + $this->header['Expires'] = gmdate('D, d M Y H:i:s', $_SERVER['REQUEST_TIME'] + $cache[1]) . ' GMT'; + Cache::set($cache[0], [$data, $this->header], $cache[1]); + } + } + + if (!headers_sent() && !empty($this->header)) { + // 发送状态码 + http_response_code($this->code); + // 发送头部信息 + foreach ($this->header as $name => $val) { + if (is_null($val)) { + header($name); + } else { + header($name . ':' . $val); + } + } + } + + echo $data; + + if (function_exists('fastcgi_finish_request')) { + // 提高页面响应 + fastcgi_finish_request(); + } + + // 监听response_end + Hook::listen('response_end', $this); + + // 清空当次请求有效的数据 + if (!($this instanceof RedirectResponse)) { + Session::flush(); + } + } + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + return $data; + } + + /** + * 输出的参数 + * @access public + * @param mixed $options 输出参数 + * @return $this + */ + public function options($options = []) + { + $this->options = array_merge($this->options, $options); + return $this; + } + + /** + * 输出数据设置 + * @access public + * @param mixed $data 输出数据 + * @return $this + */ + public function data($data) + { + $this->data = $data; + return $this; + } + + /** + * 设置响应头 + * @access public + * @param string|array $name 参数名 + * @param string $value 参数值 + * @return $this + */ + public function header($name, $value = null) + { + if (is_array($name)) { + $this->header = array_merge($this->header, $name); + } else { + $this->header[$name] = $value; + } + return $this; + } + + /** + * 设置页面输出内容 + * @param $content + * @return $this + */ + public function content($content) + { + if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([ + $content, + '__toString', + ]) + ) { + throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content))); + } + + $this->content = (string) $content; + + return $this; + } + + /** + * 发送HTTP状态 + * @param integer $code 状态码 + * @return $this + */ + public function code($code) + { + $this->code = $code; + return $this; + } + + /** + * LastModified + * @param string $time + * @return $this + */ + public function lastModified($time) + { + $this->header['Last-Modified'] = $time; + return $this; + } + + /** + * Expires + * @param string $time + * @return $this + */ + public function expires($time) + { + $this->header['Expires'] = $time; + return $this; + } + + /** + * ETag + * @param string $eTag + * @return $this + */ + public function eTag($eTag) + { + $this->header['ETag'] = $eTag; + return $this; + } + + /** + * 页面缓存控制 + * @param string $cache 状态码 + * @return $this + */ + public function cacheControl($cache) + { + $this->header['Cache-control'] = $cache; + return $this; + } + + /** + * 页面输出类型 + * @param string $contentType 输出类型 + * @param string $charset 输出编码 + * @return $this + */ + public function contentType($contentType, $charset = 'utf-8') + { + $this->header['Content-Type'] = $contentType . '; charset=' . $charset; + return $this; + } + + /** + * 获取头部信息 + * @param string $name 头部名称 + * @return mixed + */ + public function getHeader($name = '') + { + if (!empty($name)) { + return isset($this->header[$name]) ? $this->header[$name] : null; + } else { + return $this->header; + } + } + + /** + * 获取原始数据 + * @return mixed + */ + public function getData() + { + return $this->data; + } + + /** + * 获取输出数据 + * @return mixed + */ + public function getContent() + { + if (null == $this->content) { + $content = $this->output($this->data); + + if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([ + $content, + '__toString', + ]) + ) { + throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content))); + } + + $this->content = (string) $content; + } + return $this->content; + } + + /** + * 获取状态码 + * @return integer + */ + public function getCode() + { + return $this->code; + } +} diff --git a/thinkphp/library/think/Route.php b/thinkphp/library/think/Route.php new file mode 100644 index 0000000..35a57ee --- /dev/null +++ b/thinkphp/library/think/Route.php @@ -0,0 +1,1600 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\HttpException; + +class Route +{ + // 路由规则 + private static $rules = [ + 'get' => [], + 'post' => [], + 'put' => [], + 'delete' => [], + 'patch' => [], + 'head' => [], + 'options' => [], + '*' => [], + 'alias' => [], + 'domain' => [], + 'pattern' => [], + 'name' => [], + ]; + + // REST路由操作方法定义 + private static $rest = [ + 'index' => ['get', '', 'index'], + 'create' => ['get', '/create', 'create'], + 'edit' => ['get', '/:id/edit', 'edit'], + 'read' => ['get', '/:id', 'read'], + 'save' => ['post', '', 'save'], + 'update' => ['put', '/:id', 'update'], + 'delete' => ['delete', '/:id', 'delete'], + ]; + + // 不同请求类型的方法前缀 + private static $methodPrefix = [ + 'get' => 'get', + 'post' => 'post', + 'put' => 'put', + 'delete' => 'delete', + 'patch' => 'patch', + ]; + + // 子域名 + private static $subDomain = ''; + // 域名绑定 + private static $bind = []; + // 当前分组信息 + private static $group = []; + // 当前子域名绑定 + private static $domainBind; + private static $domainRule; + // 当前域名 + private static $domain; + // 当前路由执行过程中的参数 + private static $option = []; + + /** + * 注册变量规则 + * @access public + * @param string|array $name 变量名 + * @param string $rule 变量规则 + * @return void + */ + public static function pattern($name = null, $rule = '') + { + if (is_array($name)) { + self::$rules['pattern'] = array_merge(self::$rules['pattern'], $name); + } else { + self::$rules['pattern'][$name] = $rule; + } + } + + /** + * 注册子域名部署规则 + * @access public + * @param string|array $domain 子域名 + * @param mixed $rule 路由规则 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function domain($domain, $rule = '', $option = [], $pattern = []) + { + if (is_array($domain)) { + foreach ($domain as $key => $item) { + self::domain($key, $item, $option, $pattern); + } + } elseif ($rule instanceof \Closure) { + // 执行闭包 + self::setDomain($domain); + call_user_func_array($rule, []); + self::setDomain(null); + } elseif (is_array($rule)) { + self::setDomain($domain); + self::group('', function () use ($rule) { + // 动态注册域名的路由规则 + self::registerRules($rule); + }, $option, $pattern); + self::setDomain(null); + } else { + self::$rules['domain'][$domain]['[bind]'] = [$rule, $option, $pattern]; + } + } + + private static function setDomain($domain) + { + self::$domain = $domain; + } + + /** + * 设置路由绑定 + * @access public + * @param mixed $bind 绑定信息 + * @param string $type 绑定类型 默认为module 支持 namespace class controller + * @return mixed + */ + public static function bind($bind, $type = 'module') + { + self::$bind = ['type' => $type, $type => $bind]; + } + + /** + * 设置或者获取路由标识 + * @access public + * @param string|array $name 路由命名标识 数组表示批量设置 + * @param array $value 路由地址及变量信息 + * @return array + */ + public static function name($name = '', $value = null) + { + if (is_array($name)) { + return self::$rules['name'] = $name; + } elseif ('' === $name) { + return self::$rules['name']; + } elseif (!is_null($value)) { + self::$rules['name'][strtolower($name)][] = $value; + } else { + $name = strtolower($name); + return isset(self::$rules['name'][$name]) ? self::$rules['name'][$name] : null; + } + } + + /** + * 读取路由绑定 + * @access public + * @param string $type 绑定类型 + * @return mixed + */ + public static function getBind($type) + { + return isset(self::$bind[$type]) ? self::$bind[$type] : null; + } + + /** + * 导入配置文件的路由规则 + * @access public + * @param array $rule 路由规则 + * @param string $type 请求类型 + * @return void + */ + public static function import(array $rule, $type = '*') + { + // 检查域名部署 + if (isset($rule['__domain__'])) { + self::domain($rule['__domain__']); + unset($rule['__domain__']); + } + + // 检查变量规则 + if (isset($rule['__pattern__'])) { + self::pattern($rule['__pattern__']); + unset($rule['__pattern__']); + } + + // 检查路由别名 + if (isset($rule['__alias__'])) { + self::alias($rule['__alias__']); + unset($rule['__alias__']); + } + + // 检查资源路由 + if (isset($rule['__rest__'])) { + self::resource($rule['__rest__']); + unset($rule['__rest__']); + } + + self::registerRules($rule, strtolower($type)); + } + + // 批量注册路由 + protected static function registerRules($rules, $type = '*') + { + foreach ($rules as $key => $val) { + if (is_numeric($key)) { + $key = array_shift($val); + } + if (empty($val)) { + continue; + } + if (is_string($key) && 0 === strpos($key, '[')) { + $key = substr($key, 1, -1); + self::group($key, $val); + } elseif (is_array($val)) { + self::setRule($key, $val[0], $type, $val[1], isset($val[2]) ? $val[2] : []); + } else { + self::setRule($key, $val, $type); + } + } + } + + /** + * 注册路由规则 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $type 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function rule($rule, $route = '', $type = '*', $option = [], $pattern = []) + { + $group = self::getGroup('name'); + + if (!is_null($group)) { + // 路由分组 + $option = array_merge(self::getGroup('option'), $option); + $pattern = array_merge(self::getGroup('pattern'), $pattern); + } + + $type = strtolower($type); + + if (strpos($type, '|')) { + $option['method'] = $type; + $type = '*'; + } + if (is_array($rule) && empty($route)) { + foreach ($rule as $key => $val) { + if (is_numeric($key)) { + $key = array_shift($val); + } + if (is_array($val)) { + $route = $val[0]; + $option1 = array_merge($option, $val[1]); + $pattern1 = array_merge($pattern, isset($val[2]) ? $val[2] : []); + } else { + $route = $val; + } + self::setRule($key, $route, $type, isset($option1) ? $option1 : $option, isset($pattern1) ? $pattern1 : $pattern, $group); + } + } else { + self::setRule($rule, $route, $type, $option, $pattern, $group); + } + + } + + /** + * 设置路由规则 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $type 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @param string $group 所属分组 + * @return void + */ + protected static function setRule($rule, $route, $type = '*', $option = [], $pattern = [], $group = '') + { + if (is_array($rule)) { + $name = $rule[0]; + $rule = $rule[1]; + } elseif (is_string($route)) { + $name = $route; + } + if (!isset($option['complete_match'])) { + if (Config::get('route_complete_match')) { + $option['complete_match'] = true; + } elseif ('$' == substr($rule, -1, 1)) { + // 是否完整匹配 + $option['complete_match'] = true; + } + } elseif (empty($option['complete_match']) && '$' == substr($rule, -1, 1)) { + // 是否完整匹配 + $option['complete_match'] = true; + } + + if ('$' == substr($rule, -1, 1)) { + $rule = substr($rule, 0, -1); + } + + if ('/' != $rule || $group) { + $rule = trim($rule, '/'); + } + $vars = self::parseVar($rule); + if (isset($name)) { + $key = $group ? $group . ($rule ? '/' . $rule : '') : $rule; + $suffix = isset($option['ext']) ? $option['ext'] : null; + self::name($name, [$key, $vars, self::$domain, $suffix]); + } + if (isset($option['modular'])) { + $route = $option['modular'] . '/' . $route; + } + if ($group) { + if ('*' != $type) { + $option['method'] = $type; + } + if (self::$domain) { + self::$rules['domain'][self::$domain]['*'][$group]['rule'][] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; + } else { + self::$rules['*'][$group]['rule'][] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; + } + } else { + if ('*' != $type && isset(self::$rules['*'][$rule])) { + unset(self::$rules['*'][$rule]); + } + if (self::$domain) { + self::$rules['domain'][self::$domain][$type][$rule] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; + } else { + self::$rules[$type][$rule] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; + } + if ('*' == $type) { + // 注册路由快捷方式 + foreach (['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as $method) { + if (self::$domain) { + self::$rules['domain'][self::$domain][$method][$rule] = true; + } else { + self::$rules[$method][$rule] = true; + } + } + } + } + } + + /** + * 设置当前执行的参数信息 + * @access public + * @param array $options 参数信息 + * @return mixed + */ + protected static function setOption($options = []) + { + self::$option[] = $options; + } + + /** + * 获取当前执行的所有参数信息 + * @access public + * @return array + */ + public static function getOption() + { + return self::$option; + } + + /** + * 获取当前的分组信息 + * @access public + * @param string $type 分组信息名称 name option pattern + * @return mixed + */ + public static function getGroup($type) + { + if (isset(self::$group[$type])) { + return self::$group[$type]; + } else { + return 'name' == $type ? null : []; + } + } + + /** + * 设置当前的路由分组 + * @access public + * @param string $name 分组名称 + * @param array $option 分组路由参数 + * @param array $pattern 分组变量规则 + * @return void + */ + public static function setGroup($name, $option = [], $pattern = []) + { + self::$group['name'] = $name; + self::$group['option'] = $option ?: []; + self::$group['pattern'] = $pattern ?: []; + } + + /** + * 注册路由分组 + * @access public + * @param string|array $name 分组名称或者参数 + * @param array|\Closure $routes 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function group($name, $routes, $option = [], $pattern = []) + { + if (is_array($name)) { + $option = $name; + $name = isset($option['name']) ? $option['name'] : ''; + } + // 分组 + $currentGroup = self::getGroup('name'); + if ($currentGroup) { + $name = $currentGroup . ($name ? '/' . ltrim($name, '/') : ''); + } + if (!empty($name)) { + if ($routes instanceof \Closure) { + $currentOption = self::getGroup('option'); + $currentPattern = self::getGroup('pattern'); + self::setGroup($name, array_merge($currentOption, $option), array_merge($currentPattern, $pattern)); + call_user_func_array($routes, []); + self::setGroup($currentGroup, $currentOption, $currentPattern); + if ($currentGroup != $name) { + self::$rules['*'][$name]['route'] = ''; + self::$rules['*'][$name]['var'] = self::parseVar($name); + self::$rules['*'][$name]['option'] = $option; + self::$rules['*'][$name]['pattern'] = $pattern; + } + } else { + $item = []; + $completeMatch = Config::get('route_complete_match'); + foreach ($routes as $key => $val) { + if (is_numeric($key)) { + $key = array_shift($val); + } + if (is_array($val)) { + $route = $val[0]; + $option1 = array_merge($option, isset($val[1]) ? $val[1] : []); + $pattern1 = array_merge($pattern, isset($val[2]) ? $val[2] : []); + } else { + $route = $val; + } + + $options = isset($option1) ? $option1 : $option; + $patterns = isset($pattern1) ? $pattern1 : $pattern; + if ('$' == substr($key, -1, 1)) { + // 是否完整匹配 + $options['complete_match'] = true; + $key = substr($key, 0, -1); + } elseif ($completeMatch) { + $options['complete_match'] = true; + } + $key = trim($key, '/'); + $vars = self::parseVar($key); + $item[] = ['rule' => $key, 'route' => $route, 'var' => $vars, 'option' => $options, 'pattern' => $patterns]; + // 设置路由标识 + $suffix = isset($options['ext']) ? $options['ext'] : null; + self::name($route, [$name . ($key ? '/' . $key : ''), $vars, self::$domain, $suffix]); + } + self::$rules['*'][$name] = ['rule' => $item, 'route' => '', 'var' => [], 'option' => $option, 'pattern' => $pattern]; + } + + foreach (['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as $method) { + if (!isset(self::$rules[$method][$name])) { + self::$rules[$method][$name] = true; + } elseif (is_array(self::$rules[$method][$name])) { + self::$rules[$method][$name] = array_merge(self::$rules['*'][$name], self::$rules[$method][$name]); + } + } + + } elseif ($routes instanceof \Closure) { + // 闭包注册 + $currentOption = self::getGroup('option'); + $currentPattern = self::getGroup('pattern'); + self::setGroup('', array_merge($currentOption, $option), array_merge($currentPattern, $pattern)); + call_user_func_array($routes, []); + self::setGroup($currentGroup, $currentOption, $currentPattern); + } else { + // 批量注册路由 + self::rule($routes, '', '*', $option, $pattern); + } + } + + /** + * 注册路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function any($rule, $route = '', $option = [], $pattern = []) + { + self::rule($rule, $route, '*', $option, $pattern); + } + + /** + * 注册GET路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function get($rule, $route = '', $option = [], $pattern = []) + { + self::rule($rule, $route, 'GET', $option, $pattern); + } + + /** + * 注册POST路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function post($rule, $route = '', $option = [], $pattern = []) + { + self::rule($rule, $route, 'POST', $option, $pattern); + } + + /** + * 注册PUT路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function put($rule, $route = '', $option = [], $pattern = []) + { + self::rule($rule, $route, 'PUT', $option, $pattern); + } + + /** + * 注册DELETE路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function delete($rule, $route = '', $option = [], $pattern = []) + { + self::rule($rule, $route, 'DELETE', $option, $pattern); + } + + /** + * 注册PATCH路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function patch($rule, $route = '', $option = [], $pattern = []) + { + self::rule($rule, $route, 'PATCH', $option, $pattern); + } + + /** + * 注册资源路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function resource($rule, $route = '', $option = [], $pattern = []) + { + if (is_array($rule)) { + foreach ($rule as $key => $val) { + if (is_array($val)) { + list($val, $option, $pattern) = array_pad($val, 3, []); + } + self::resource($key, $val, $option, $pattern); + } + } else { + if (strpos($rule, '.')) { + // 注册嵌套资源路由 + $array = explode('.', $rule); + $last = array_pop($array); + $item = []; + foreach ($array as $val) { + $item[] = $val . '/:' . (isset($option['var'][$val]) ? $option['var'][$val] : $val . '_id'); + } + $rule = implode('/', $item) . '/' . $last; + } + // 注册资源路由 + foreach (self::$rest as $key => $val) { + if ((isset($option['only']) && !in_array($key, $option['only'])) + || (isset($option['except']) && in_array($key, $option['except']))) { + continue; + } + if (isset($last) && strpos($val[1], ':id') && isset($option['var'][$last])) { + $val[1] = str_replace(':id', ':' . $option['var'][$last], $val[1]); + } elseif (strpos($val[1], ':id') && isset($option['var'][$rule])) { + $val[1] = str_replace(':id', ':' . $option['var'][$rule], $val[1]); + } + $item = ltrim($rule . $val[1], '/'); + $option['rest'] = $key; + self::rule($item . '$', $route . '/' . $val[2], $val[0], $option, $pattern); + } + } + } + + /** + * 注册控制器路由 操作方法对应不同的请求后缀 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function controller($rule, $route = '', $option = [], $pattern = []) + { + foreach (self::$methodPrefix as $type => $val) { + self::$type($rule . '/:action', $route . '/' . $val . ':action', $option, $pattern); + } + } + + /** + * 注册别名路由 + * @access public + * @param string|array $rule 路由别名 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @return void + */ + public static function alias($rule = null, $route = '', $option = []) + { + if (is_array($rule)) { + self::$rules['alias'] = array_merge(self::$rules['alias'], $rule); + } else { + self::$rules['alias'][$rule] = $option ? [$route, $option] : $route; + } + } + + /** + * 设置不同请求类型下面的方法前缀 + * @access public + * @param string $method 请求类型 + * @param string $prefix 类型前缀 + * @return void + */ + public static function setMethodPrefix($method, $prefix = '') + { + if (is_array($method)) { + self::$methodPrefix = array_merge(self::$methodPrefix, array_change_key_case($method)); + } else { + self::$methodPrefix[strtolower($method)] = $prefix; + } + } + + /** + * rest方法定义和修改 + * @access public + * @param string $name 方法名称 + * @param array|bool $resource 资源 + * @return void + */ + public static function rest($name, $resource = []) + { + if (is_array($name)) { + self::$rest = $resource ? $name : array_merge(self::$rest, $name); + } else { + self::$rest[$name] = $resource; + } + } + + /** + * 注册未匹配路由规则后的处理 + * @access public + * @param string $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @return void + */ + public static function miss($route, $method = '*', $option = []) + { + self::rule('__miss__', $route, $method, $option, []); + } + + /** + * 注册一个自动解析的URL路由 + * @access public + * @param string $route 路由地址 + * @return void + */ + public static function auto($route) + { + self::rule('__auto__', $route, '*', [], []); + } + + /** + * 获取或者批量设置路由定义 + * @access public + * @param mixed $rules 请求类型或者路由定义数组 + * @return array + */ + public static function rules($rules = '') + { + if (is_array($rules)) { + self::$rules = $rules; + } elseif ($rules) { + return true === $rules ? self::$rules : self::$rules[strtolower($rules)]; + } else { + $rules = self::$rules; + unset($rules['pattern'], $rules['alias'], $rules['domain'], $rules['name']); + return $rules; + } + } + + /** + * 检测子域名部署 + * @access public + * @param Request $request Request请求对象 + * @param array $currentRules 当前路由规则 + * @param string $method 请求类型 + * @return void + */ + public static function checkDomain($request, &$currentRules, $method = 'get') + { + // 域名规则 + $rules = self::$rules['domain']; + // 开启子域名部署 支持二级和三级域名 + if (!empty($rules)) { + $host = $request->host(); + if (isset($rules[$host])) { + // 完整域名或者IP配置 + $item = $rules[$host]; + } else { + $rootDomain = Config::get('url_domain_root'); + if ($rootDomain) { + // 配置域名根 例如 thinkphp.cn 163.com.cn 如果是国家级域名 com.cn net.cn 之类的域名需要配置 + $domain = explode('.', rtrim(stristr($host, $rootDomain, true), '.')); + } else { + $domain = explode('.', $host, -2); + } + // 子域名配置 + if (!empty($domain)) { + // 当前子域名 + $subDomain = implode('.', $domain); + self::$subDomain = $subDomain; + $domain2 = array_pop($domain); + if ($domain) { + // 存在三级域名 + $domain3 = array_pop($domain); + } + if ($subDomain && isset($rules[$subDomain])) { + // 子域名配置 + $item = $rules[$subDomain]; + } elseif (isset($rules['*.' . $domain2]) && !empty($domain3)) { + // 泛三级域名 + $item = $rules['*.' . $domain2]; + $panDomain = $domain3; + } elseif (isset($rules['*']) && !empty($domain2)) { + // 泛二级域名 + if ('www' != $domain2) { + $item = $rules['*']; + $panDomain = $domain2; + } + } + } + } + if (!empty($item)) { + if (isset($panDomain)) { + // 保存当前泛域名 + $request->route(['__domain__' => $panDomain]); + } + if (isset($item['[bind]'])) { + // 解析子域名部署规则 + list($rule, $option, $pattern) = $item['[bind]']; + if (!empty($option['https']) && !$request->isSsl()) { + // https检测 + throw new HttpException(404, 'must use https request:' . $host); + } + + if (strpos($rule, '?')) { + // 传入其它参数 + $array = parse_url($rule); + $result = $array['path']; + parse_str($array['query'], $params); + if (isset($panDomain)) { + $pos = array_search('*', $params); + if (false !== $pos) { + // 泛域名作为参数 + $params[$pos] = $panDomain; + } + } + $_GET = array_merge($_GET, $params); + } else { + $result = $rule; + } + + if (0 === strpos($result, '\\')) { + // 绑定到命名空间 例如 \app\index\behavior + self::$bind = ['type' => 'namespace', 'namespace' => $result]; + } elseif (0 === strpos($result, '@')) { + // 绑定到类 例如 @app\index\controller\User + self::$bind = ['type' => 'class', 'class' => substr($result, 1)]; + } else { + // 绑定到模块/控制器 例如 index/user + self::$bind = ['type' => 'module', 'module' => $result]; + } + self::$domainBind = true; + } else { + self::$domainRule = $item; + $currentRules = isset($item[$method]) ? $item[$method] : $item['*']; + } + } + } + } + + /** + * 检测URL路由 + * @access public + * @param Request $request Request请求对象 + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @param bool $checkDomain 是否检测域名规则 + * @return false|array + */ + public static function check($request, $url, $depr = '/', $checkDomain = false) + { + // 分隔符替换 确保路由定义使用统一的分隔符 + $url = str_replace($depr, '|', $url); + + if (isset(self::$rules['alias'][$url]) || isset(self::$rules['alias'][strstr($url, '|', true)])) { + // 检测路由别名 + $result = self::checkRouteAlias($request, $url, $depr); + if (false !== $result) { + return $result; + } + } + $method = strtolower($request->method()); + // 获取当前请求类型的路由规则 + $rules = isset(self::$rules[$method]) ? self::$rules[$method] : []; + // 检测域名部署 + if ($checkDomain) { + self::checkDomain($request, $rules, $method); + } + // 检测URL绑定 + $return = self::checkUrlBind($url, $rules, $depr); + if (false !== $return) { + return $return; + } + if ('|' != $url) { + $url = rtrim($url, '|'); + } + $item = str_replace('|', '/', $url); + if (isset($rules[$item])) { + // 静态路由规则检测 + $rule = $rules[$item]; + if (true === $rule) { + $rule = self::getRouteExpress($item); + } + if (!empty($rule['route']) && self::checkOption($rule['option'], $request)) { + self::setOption($rule['option']); + return self::parseRule($item, $rule['route'], $url, $rule['option']); + } + } + + // 路由规则检测 + if (!empty($rules)) { + return self::checkRoute($request, $rules, $url, $depr); + } + return false; + } + + private static function getRouteExpress($key) + { + return self::$domainRule ? self::$domainRule['*'][$key] : self::$rules['*'][$key]; + } + + /** + * 检测路由规则 + * @access private + * @param Request $request + * @param array $rules 路由规则 + * @param string $url URL地址 + * @param string $depr URL分割符 + * @param string $group 路由分组名 + * @param array $options 路由参数(分组) + * @return mixed + */ + private static function checkRoute($request, $rules, $url, $depr = '/', $group = '', $options = []) + { + foreach ($rules as $key => $item) { + if (true === $item) { + $item = self::getRouteExpress($key); + } + if (!isset($item['rule'])) { + continue; + } + $rule = $item['rule']; + $route = $item['route']; + $vars = $item['var']; + $option = $item['option']; + $pattern = $item['pattern']; + + // 检查参数有效性 + if (!self::checkOption($option, $request)) { + continue; + } + + if (isset($option['ext'])) { + // 路由ext参数 优先于系统配置的URL伪静态后缀参数 + $url = preg_replace('/\.' . $request->ext() . '$/i', '', $url); + } + + if (is_array($rule)) { + // 分组路由 + $pos = strpos(str_replace('<', ':', $key), ':'); + if (false !== $pos) { + $str = substr($key, 0, $pos); + } else { + $str = $key; + } + if (is_string($str) && $str && 0 !== stripos(str_replace('|', '/', $url), $str)) { + continue; + } + self::setOption($option); + $result = self::checkRoute($request, $rule, $url, $depr, $key, $option); + if (false !== $result) { + return $result; + } + } elseif ($route) { + if ('__miss__' == $rule || '__auto__' == $rule) { + // 指定特殊路由 + $var = trim($rule, '__'); + ${$var} = $item; + continue; + } + if ($group) { + $rule = $group . ($rule ? '/' . ltrim($rule, '/') : ''); + } + + self::setOption($option); + if (isset($options['bind_model']) && isset($option['bind_model'])) { + $option['bind_model'] = array_merge($options['bind_model'], $option['bind_model']); + } + $result = self::checkRule($rule, $route, $url, $pattern, $option, $depr); + if (false !== $result) { + return $result; + } + } + } + if (isset($auto)) { + // 自动解析URL地址 + return self::parseUrl($auto['route'] . '/' . $url, $depr); + } elseif (isset($miss)) { + // 未匹配所有路由的路由规则处理 + return self::parseRule('', $miss['route'], $url, $miss['option']); + } + return false; + } + + /** + * 检测路由别名 + * @access private + * @param Request $request + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @return mixed + */ + private static function checkRouteAlias($request, $url, $depr) + { + $array = explode('|', $url); + $alias = array_shift($array); + $item = self::$rules['alias'][$alias]; + + if (is_array($item)) { + list($rule, $option) = $item; + $action = $array[0]; + if (isset($option['allow']) && !in_array($action, explode(',', $option['allow']))) { + // 允许操作 + return false; + } elseif (isset($option['except']) && in_array($action, explode(',', $option['except']))) { + // 排除操作 + return false; + } + if (isset($option['method'][$action])) { + $option['method'] = $option['method'][$action]; + } + } else { + $rule = $item; + } + $bind = implode('|', $array); + // 参数有效性检查 + if (isset($option) && !self::checkOption($option, $request)) { + // 路由不匹配 + return false; + } elseif (0 === strpos($rule, '\\')) { + // 路由到类 + return self::bindToClass($bind, substr($rule, 1), $depr); + } elseif (0 === strpos($rule, '@')) { + // 路由到控制器类 + return self::bindToController($bind, substr($rule, 1), $depr); + } else { + // 路由到模块/控制器 + return self::bindToModule($bind, $rule, $depr); + } + } + + /** + * 检测URL绑定 + * @access private + * @param string $url URL地址 + * @param array $rules 路由规则 + * @param string $depr URL分隔符 + * @return mixed + */ + private static function checkUrlBind(&$url, &$rules, $depr = '/') + { + if (!empty(self::$bind)) { + $type = self::$bind['type']; + $bind = self::$bind[$type]; + // 记录绑定信息 + App::$debug && Log::record('[ BIND ] ' . var_export($bind, true), 'info'); + // 如果有URL绑定 则进行绑定检测 + switch ($type) { + case 'class': + // 绑定到类 + return self::bindToClass($url, $bind, $depr); + case 'controller': + // 绑定到控制器类 + return self::bindToController($url, $bind, $depr); + case 'namespace': + // 绑定到命名空间 + return self::bindToNamespace($url, $bind, $depr); + } + } + return false; + } + + /** + * 绑定到类 + * @access public + * @param string $url URL地址 + * @param string $class 类名(带命名空间) + * @param string $depr URL分隔符 + * @return array + */ + public static function bindToClass($url, $class, $depr = '/') + { + $url = str_replace($depr, '|', $url); + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : Config::get('default_action'); + if (!empty($array[1])) { + self::parseUrlParams($array[1]); + } + return ['type' => 'method', 'method' => [$class, $action], 'var' => []]; + } + + /** + * 绑定到命名空间 + * @access public + * @param string $url URL地址 + * @param string $namespace 命名空间 + * @param string $depr URL分隔符 + * @return array + */ + public static function bindToNamespace($url, $namespace, $depr = '/') + { + $url = str_replace($depr, '|', $url); + $array = explode('|', $url, 3); + $class = !empty($array[0]) ? $array[0] : Config::get('default_controller'); + $method = !empty($array[1]) ? $array[1] : Config::get('default_action'); + if (!empty($array[2])) { + self::parseUrlParams($array[2]); + } + return ['type' => 'method', 'method' => [$namespace . '\\' . Loader::parseName($class, 1), $method], 'var' => []]; + } + + /** + * 绑定到控制器类 + * @access public + * @param string $url URL地址 + * @param string $controller 控制器名 (支持带模块名 index/user ) + * @param string $depr URL分隔符 + * @return array + */ + public static function bindToController($url, $controller, $depr = '/') + { + $url = str_replace($depr, '|', $url); + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : Config::get('default_action'); + if (!empty($array[1])) { + self::parseUrlParams($array[1]); + } + return ['type' => 'controller', 'controller' => $controller . '/' . $action, 'var' => []]; + } + + /** + * 绑定到模块/控制器 + * @access public + * @param string $url URL地址 + * @param string $controller 控制器类名(带命名空间) + * @param string $depr URL分隔符 + * @return array + */ + public static function bindToModule($url, $controller, $depr = '/') + { + $url = str_replace($depr, '|', $url); + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : Config::get('default_action'); + if (!empty($array[1])) { + self::parseUrlParams($array[1]); + } + return ['type' => 'module', 'module' => $controller . '/' . $action]; + } + + /** + * 路由参数有效性检查 + * @access private + * @param array $option 路由参数 + * @param Request $request Request对象 + * @return bool + */ + private static function checkOption($option, $request) + { + if ((isset($option['method']) && is_string($option['method']) && false === stripos($option['method'], $request->method())) + || (isset($option['ajax']) && $option['ajax'] && !$request->isAjax()) // Ajax检测 + || (isset($option['ajax']) && !$option['ajax'] && $request->isAjax()) // 非Ajax检测 + || (isset($option['pjax']) && $option['pjax'] && !$request->isPjax()) // Pjax检测 + || (isset($option['pjax']) && !$option['pjax'] && $request->isPjax()) // 非Pjax检测 + || (isset($option['ext']) && false === stripos('|' . $option['ext'] . '|', '|' . $request->ext() . '|')) // 伪静态后缀检测 + || (isset($option['deny_ext']) && false !== stripos('|' . $option['deny_ext'] . '|', '|' . $request->ext() . '|')) + || (isset($option['domain']) && !in_array($option['domain'], [$_SERVER['HTTP_HOST'], self::$subDomain])) // 域名检测 + || (isset($option['https']) && $option['https'] && !$request->isSsl()) // https检测 + || (isset($option['https']) && !$option['https'] && $request->isSsl()) // https检测 + || (!empty($option['before_behavior']) && false === Hook::exec($option['before_behavior'])) // 行为检测 + || (!empty($option['callback']) && is_callable($option['callback']) && false === call_user_func($option['callback'])) // 自定义检测 + ) { + return false; + } + return true; + } + + /** + * 检测路由规则 + * @access private + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $url URL地址 + * @param array $pattern 变量规则 + * @param array $option 路由参数 + * @param string $depr URL分隔符(全局) + * @return array|false + */ + private static function checkRule($rule, $route, $url, $pattern, $option, $depr) + { + // 检查完整规则定义 + if (isset($pattern['__url__']) && !preg_match(0 === strpos($pattern['__url__'], '/') ? $pattern['__url__'] : '/^' . $pattern['__url__'] . '/', str_replace('|', $depr, $url))) { + return false; + } + // 检查路由的参数分隔符 + if (isset($option['param_depr'])) { + $url = str_replace(['|', $option['param_depr']], [$depr, '|'], $url); + } + + $len1 = substr_count($url, '|'); + $len2 = substr_count($rule, '/'); + // 多余参数是否合并 + $merge = !empty($option['merge_extra_vars']); + if ($merge && $len1 > $len2) { + $url = str_replace('|', $depr, $url); + $url = implode('|', explode($depr, $url, $len2 + 1)); + } + + if ($len1 >= $len2 || strpos($rule, '[')) { + if (!empty($option['complete_match'])) { + // 完整匹配 + if (!$merge && $len1 != $len2 && (false === strpos($rule, '[') || $len1 > $len2 || $len1 < $len2 - substr_count($rule, '['))) { + return false; + } + } + $pattern = array_merge(self::$rules['pattern'], $pattern); + if (false !== $match = self::match($url, $rule, $pattern)) { + // 匹配到路由规则 + return self::parseRule($rule, $route, $url, $option, $match); + } + } + return false; + } + + /** + * 解析模块的URL地址 [模块/控制器/操作?]参数1=值1&参数2=值2... + * @access public + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @param bool $autoSearch 是否自动深度搜索控制器 + * @return array + */ + public static function parseUrl($url, $depr = '/', $autoSearch = false) + { + + if (isset(self::$bind['module'])) { + $bind = str_replace('/', $depr, self::$bind['module']); + // 如果有模块/控制器绑定 + $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr); + } + $url = str_replace($depr, '|', $url); + list($path, $var) = self::parseUrlPath($url); + $route = [null, null, null]; + if (isset($path)) { + // 解析模块 + $module = Config::get('app_multi_module') ? array_shift($path) : null; + if ($autoSearch) { + // 自动搜索控制器 + $dir = APP_PATH . ($module ? $module . DS : '') . Config::get('url_controller_layer'); + $suffix = App::$suffix || Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : ''; + $item = []; + $find = false; + foreach ($path as $val) { + $item[] = $val; + $file = $dir . DS . str_replace('.', DS, $val) . $suffix . EXT; + $file = pathinfo($file, PATHINFO_DIRNAME) . DS . Loader::parseName(pathinfo($file, PATHINFO_FILENAME), 1) . EXT; + if (is_file($file)) { + $find = true; + break; + } else { + $dir .= DS . Loader::parseName($val); + } + } + if ($find) { + $controller = implode('.', $item); + $path = array_slice($path, count($item)); + } else { + $controller = array_shift($path); + } + } else { + // 解析控制器 + $controller = !empty($path) ? array_shift($path) : null; + } + // 解析操作 + $action = !empty($path) ? array_shift($path) : null; + // 解析额外参数 + self::parseUrlParams(empty($path) ? '' : implode('|', $path)); + // 封装路由 + $route = [$module, $controller, $action]; + // 检查地址是否被定义过路由 + $name = strtolower($module . '/' . Loader::parseName($controller, 1) . '/' . $action); + $name2 = ''; + if (empty($module) || isset($bind) && $module == $bind) { + $name2 = strtolower(Loader::parseName($controller, 1) . '/' . $action); + } + + if (isset(self::$rules['name'][$name]) || isset(self::$rules['name'][$name2])) { + throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url)); + } + } + return ['type' => 'module', 'module' => $route]; + } + + /** + * 解析URL的pathinfo参数和变量 + * @access private + * @param string $url URL地址 + * @return array + */ + private static function parseUrlPath($url) + { + // 分隔符替换 确保路由定义使用统一的分隔符 + $url = str_replace('|', '/', $url); + $url = trim($url, '/'); + $var = []; + if (false !== strpos($url, '?')) { + // [模块/控制器/操作?]参数1=值1&参数2=值2... + $info = parse_url($url); + $path = explode('/', $info['path']); + parse_str($info['query'], $var); + } elseif (strpos($url, '/')) { + // [模块/控制器/操作] + $path = explode('/', $url); + } else { + $path = [$url]; + } + return [$path, $var]; + } + + /** + * 检测URL和规则路由是否匹配 + * @access private + * @param string $url URL地址 + * @param string $rule 路由规则 + * @param array $pattern 变量规则 + * @return array|false + */ + private static function match($url, $rule, $pattern) + { + $m2 = explode('/', $rule); + $m1 = explode('|', $url); + + $var = []; + foreach ($m2 as $key => $val) { + // val中定义了多个变量 + if (false !== strpos($val, '<') && preg_match_all('/<(\w+(\??))>/', $val, $matches)) { + $value = []; + $replace = []; + foreach ($matches[1] as $name) { + if (strpos($name, '?')) { + $name = substr($name, 0, -1); + $replace[] = '(' . (isset($pattern[$name]) ? $pattern[$name] : '\w+') . ')?'; + } else { + $replace[] = '(' . (isset($pattern[$name]) ? $pattern[$name] : '\w+') . ')'; + } + $value[] = $name; + } + $val = str_replace($matches[0], $replace, $val); + if (preg_match('/^' . $val . '$/', isset($m1[$key]) ? $m1[$key] : '', $match)) { + array_shift($match); + foreach ($value as $k => $name) { + if (isset($match[$k])) { + $var[$name] = $match[$k]; + } + } + continue; + } else { + return false; + } + } + + if (0 === strpos($val, '[:')) { + // 可选参数 + $val = substr($val, 1, -1); + $optional = true; + } else { + $optional = false; + } + if (0 === strpos($val, ':')) { + // URL变量 + $name = substr($val, 1); + if (!$optional && !isset($m1[$key])) { + return false; + } + if (isset($m1[$key]) && isset($pattern[$name])) { + // 检查变量规则 + if ($pattern[$name] instanceof \Closure) { + $result = call_user_func_array($pattern[$name], [$m1[$key]]); + if (false === $result) { + return false; + } + } elseif (!preg_match(0 === strpos($pattern[$name], '/') ? $pattern[$name] : '/^' . $pattern[$name] . '$/', $m1[$key])) { + return false; + } + } + $var[$name] = isset($m1[$key]) ? $m1[$key] : ''; + } elseif (!isset($m1[$key]) || 0 !== strcasecmp($val, $m1[$key])) { + return false; + } + } + // 成功匹配后返回URL中的动态变量数组 + return $var; + } + + /** + * 解析规则路由 + * @access private + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $pathinfo URL地址 + * @param array $option 路由参数 + * @param array $matches 匹配的变量 + * @return array + */ + private static function parseRule($rule, $route, $pathinfo, $option = [], $matches = []) + { + $request = Request::instance(); + // 解析路由规则 + if ($rule) { + $rule = explode('/', $rule); + // 获取URL地址中的参数 + $paths = explode('|', $pathinfo); + foreach ($rule as $item) { + $fun = ''; + if (0 === strpos($item, '[:')) { + $item = substr($item, 1, -1); + } + if (0 === strpos($item, ':')) { + $var = substr($item, 1); + $matches[$var] = array_shift($paths); + } else { + // 过滤URL中的静态变量 + array_shift($paths); + } + } + } else { + $paths = explode('|', $pathinfo); + } + + // 获取路由地址规则 + if (is_string($route) && isset($option['prefix'])) { + // 路由地址前缀 + $route = $option['prefix'] . $route; + } + // 替换路由地址中的变量 + if (is_string($route) && !empty($matches)) { + foreach ($matches as $key => $val) { + if (false !== strpos($route, ':' . $key)) { + $route = str_replace(':' . $key, $val, $route); + } + } + } + + // 绑定模型数据 + if (isset($option['bind_model'])) { + $bind = []; + foreach ($option['bind_model'] as $key => $val) { + if ($val instanceof \Closure) { + $result = call_user_func_array($val, [$matches]); + } else { + if (is_array($val)) { + $fields = explode('&', $val[1]); + $model = $val[0]; + $exception = isset($val[2]) ? $val[2] : true; + } else { + $fields = ['id']; + $model = $val; + $exception = true; + } + $where = []; + $match = true; + foreach ($fields as $field) { + if (!isset($matches[$field])) { + $match = false; + break; + } else { + $where[$field] = $matches[$field]; + } + } + if ($match) { + $query = strpos($model, '\\') ? $model::where($where) : Loader::model($model)->where($where); + $result = $query->failException($exception)->find(); + } + } + if (!empty($result)) { + $bind[$key] = $result; + } + } + $request->bind($bind); + } + + if (!empty($option['response'])) { + Hook::add('response_send', $option['response']); + } + + // 解析额外参数 + self::parseUrlParams(empty($paths) ? '' : implode('|', $paths), $matches); + // 记录匹配的路由信息 + $request->routeInfo(['rule' => $rule, 'route' => $route, 'option' => $option, 'var' => $matches]); + + // 检测路由after行为 + if (!empty($option['after_behavior'])) { + if ($option['after_behavior'] instanceof \Closure) { + $result = call_user_func_array($option['after_behavior'], []); + } else { + foreach ((array) $option['after_behavior'] as $behavior) { + $result = Hook::exec($behavior, ''); + if (!is_null($result)) { + break; + } + } + } + // 路由规则重定向 + if ($result instanceof Response) { + return ['type' => 'response', 'response' => $result]; + } elseif (is_array($result)) { + return $result; + } + } + + if ($route instanceof \Closure) { + // 执行闭包 + $result = ['type' => 'function', 'function' => $route]; + } elseif (0 === strpos($route, '/') || strpos($route, '://')) { + // 路由到重定向地址 + $result = ['type' => 'redirect', 'url' => $route, 'status' => isset($option['status']) ? $option['status'] : 301]; + } elseif (false !== strpos($route, '\\')) { + // 路由到方法 + list($path, $var) = self::parseUrlPath($route); + $route = str_replace('/', '@', implode('/', $path)); + $method = strpos($route, '@') ? explode('@', $route) : $route; + $result = ['type' => 'method', 'method' => $method, 'var' => $var]; + } elseif (0 === strpos($route, '@')) { + // 路由到控制器 + $route = substr($route, 1); + list($route, $var) = self::parseUrlPath($route); + $result = ['type' => 'controller', 'controller' => implode('/', $route), 'var' => $var]; + $request->action(array_pop($route)); + $request->controller($route ? array_pop($route) : Config::get('default_controller')); + $request->module($route ? array_pop($route) : Config::get('default_module')); + App::$modulePath = APP_PATH . (Config::get('app_multi_module') ? $request->module() . DS : ''); + } else { + // 路由到模块/控制器/操作 + $result = self::parseModule($route); + } + // 开启请求缓存 + if ($request->isGet() && isset($option['cache'])) { + $cache = $option['cache']; + if (is_array($cache)) { + list($key, $expire) = $cache; + } else { + $key = str_replace('|', '/', $pathinfo); + $expire = $cache; + } + $request->cache($key, $expire); + } + return $result; + } + + /** + * 解析URL地址为 模块/控制器/操作 + * @access private + * @param string $url URL地址 + * @return array + */ + private static function parseModule($url) + { + list($path, $var) = self::parseUrlPath($url); + $action = array_pop($path); + $controller = !empty($path) ? array_pop($path) : null; + $module = Config::get('app_multi_module') && !empty($path) ? array_pop($path) : null; + $method = Request::instance()->method(); + if (Config::get('use_action_prefix') && !empty(self::$methodPrefix[$method])) { + // 操作方法前缀支持 + $action = 0 !== strpos($action, self::$methodPrefix[$method]) ? self::$methodPrefix[$method] . $action : $action; + } + // 设置当前请求的路由变量 + Request::instance()->route($var); + // 路由到模块/控制器/操作 + return ['type' => 'module', 'module' => [$module, $controller, $action], 'convert' => false]; + } + + /** + * 解析URL地址中的参数Request对象 + * @access private + * @param string $rule 路由规则 + * @param array $var 变量 + * @return void + */ + private static function parseUrlParams($url, &$var = []) + { + if ($url) { + if (Config::get('url_param_type')) { + $var += explode('|', $url); + } else { + preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) { + $var[$match[1]] = strip_tags($match[2]); + }, $url); + } + } + // 设置当前请求的参数 + Request::instance()->route($var); + } + + // 分析路由规则中的变量 + private static function parseVar($rule) + { + // 提取路由规则中的变量 + $var = []; + foreach (explode('/', $rule) as $val) { + $optional = false; + if (false !== strpos($val, '<') && preg_match_all('/<(\w+(\??))>/', $val, $matches)) { + foreach ($matches[1] as $name) { + if (strpos($name, '?')) { + $name = substr($name, 0, -1); + $optional = true; + } else { + $optional = false; + } + $var[$name] = $optional ? 2 : 1; + } + } + + if (0 === strpos($val, '[:')) { + // 可选参数 + $optional = true; + $val = substr($val, 1, -1); + } + if (0 === strpos($val, ':')) { + // URL变量 + $name = substr($val, 1); + $var[$name] = $optional ? 2 : 1; + } + } + return $var; + } +} diff --git a/thinkphp/library/think/Session.php b/thinkphp/library/think/Session.php new file mode 100644 index 0000000..5fcc413 --- /dev/null +++ b/thinkphp/library/think/Session.php @@ -0,0 +1,365 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; + +class Session +{ + protected static $prefix = ''; + protected static $init = null; + + /** + * 设置或者获取session作用域(前缀) + * @param string $prefix + * @return string|void + */ + public static function prefix($prefix = '') + { + if (empty($prefix) && null !== $prefix) { + return self::$prefix; + } else { + self::$prefix = $prefix; + } + } + + /** + * session初始化 + * @param array $config + * @return void + * @throws \think\Exception + */ + public static function init(array $config = []) + { + if (empty($config)) { + $config = Config::get('session'); + } + // 记录初始化信息 + App::$debug && Log::record('[ SESSION ] INIT ' . var_export($config, true), 'info'); + $isDoStart = false; + if (isset($config['use_trans_sid'])) { + ini_set('session.use_trans_sid', $config['use_trans_sid'] ? 1 : 0); + } + + // 启动session + if (!empty($config['auto_start']) && PHP_SESSION_ACTIVE != session_status()) { + ini_set('session.auto_start', 0); + $isDoStart = true; + } + + if (isset($config['prefix']) && (self::$prefix === '' || self::$prefix === null)) { + self::$prefix = $config['prefix']; + } + if (isset($config['var_session_id']) && isset($_REQUEST[$config['var_session_id']])) { + session_id($_REQUEST[$config['var_session_id']]); + } elseif (isset($config['id']) && !empty($config['id'])) { + session_id($config['id']); + } + if (isset($config['name'])) { + session_name($config['name']); + } + if (isset($config['path'])) { + session_save_path($config['path']); + } + if (isset($config['domain'])) { + ini_set('session.cookie_domain', $config['domain']); + } + if (isset($config['expire'])) { + ini_set('session.gc_maxlifetime', $config['expire']); + ini_set('session.cookie_lifetime', $config['expire']); + } + if (isset($config['secure'])) { + ini_set('session.cookie_secure', $config['secure']); + } + if (isset($config['httponly'])) { + ini_set('session.cookie_httponly', $config['httponly']); + } + if (isset($config['use_cookies'])) { + ini_set('session.use_cookies', $config['use_cookies'] ? 1 : 0); + } + if (isset($config['cache_limiter'])) { + session_cache_limiter($config['cache_limiter']); + } + if (isset($config['cache_expire'])) { + session_cache_expire($config['cache_expire']); + } + if (!empty($config['type'])) { + // 读取session驱动 + $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\session\\driver\\' . ucwords($config['type']); + + // 检查驱动类 + if (!class_exists($class) || !session_set_save_handler(new $class($config))) { + throw new ClassNotFoundException('error session handler:' . $class, $class); + } + } + if ($isDoStart) { + session_start(); + self::$init = true; + } else { + self::$init = false; + } + } + + /** + * session自动启动或者初始化 + * @return void + */ + public static function boot() + { + if (is_null(self::$init)) { + self::init(); + } elseif (false === self::$init) { + if (PHP_SESSION_ACTIVE != session_status()) { + session_start(); + } + self::$init = true; + } + } + + /** + * session设置 + * @param string $name session名称 + * @param mixed $value session值 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public static function set($name, $value = '', $prefix = null) + { + empty(self::$init) && self::boot(); + + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if (strpos($name, '.')) { + // 二维数组赋值 + list($name1, $name2) = explode('.', $name); + if ($prefix) { + $_SESSION[$prefix][$name1][$name2] = $value; + } else { + $_SESSION[$name1][$name2] = $value; + } + } elseif ($prefix) { + $_SESSION[$prefix][$name] = $value; + } else { + $_SESSION[$name] = $value; + } + } + + /** + * session获取 + * @param string $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return mixed + */ + public static function get($name = '', $prefix = null) + { + empty(self::$init) && self::boot(); + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if ('' == $name) { + // 获取全部的session + $value = $prefix ? (!empty($_SESSION[$prefix]) ? $_SESSION[$prefix] : []) : $_SESSION; + } elseif ($prefix) { + // 获取session + if (strpos($name, '.')) { + list($name1, $name2) = explode('.', $name); + $value = isset($_SESSION[$prefix][$name1][$name2]) ? $_SESSION[$prefix][$name1][$name2] : null; + } else { + $value = isset($_SESSION[$prefix][$name]) ? $_SESSION[$prefix][$name] : null; + } + } else { + if (strpos($name, '.')) { + list($name1, $name2) = explode('.', $name); + $value = isset($_SESSION[$name1][$name2]) ? $_SESSION[$name1][$name2] : null; + } else { + $value = isset($_SESSION[$name]) ? $_SESSION[$name] : null; + } + } + return $value; + } + + /** + * session获取并删除 + * @param string $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return mixed + */ + public static function pull($name, $prefix = null) + { + $result = self::get($name, $prefix); + if ($result) { + self::delete($name, $prefix); + return $result; + } else { + return; + } + } + + /** + * session设置 下一次请求有效 + * @param string $name session名称 + * @param mixed $value session值 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public static function flash($name, $value) + { + self::set($name, $value); + if (!self::has('__flash__.__time__')) { + self::set('__flash__.__time__', $_SERVER['REQUEST_TIME_FLOAT']); + } + self::push('__flash__', $name); + } + + /** + * 清空当前请求的session数据 + * @return void + */ + public static function flush() + { + if (self::$init) { + $item = self::get('__flash__'); + + if (!empty($item)) { + $time = $item['__time__']; + if ($_SERVER['REQUEST_TIME_FLOAT'] > $time) { + unset($item['__time__']); + self::delete($item); + self::set('__flash__', []); + } + } + } + } + + /** + * 删除session数据 + * @param string|array $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public static function delete($name, $prefix = null) + { + empty(self::$init) && self::boot(); + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if (is_array($name)) { + foreach ($name as $key) { + self::delete($key, $prefix); + } + } elseif (strpos($name, '.')) { + list($name1, $name2) = explode('.', $name); + if ($prefix) { + unset($_SESSION[$prefix][$name1][$name2]); + } else { + unset($_SESSION[$name1][$name2]); + } + } else { + if ($prefix) { + unset($_SESSION[$prefix][$name]); + } else { + unset($_SESSION[$name]); + } + } + } + + /** + * 清空session数据 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public static function clear($prefix = null) + { + empty(self::$init) && self::boot(); + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if ($prefix) { + unset($_SESSION[$prefix]); + } else { + $_SESSION = []; + } + } + + /** + * 判断session数据 + * @param string $name session名称 + * @param string|null $prefix + * @return bool + */ + public static function has($name, $prefix = null) + { + empty(self::$init) && self::boot(); + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if (strpos($name, '.')) { + // 支持数组 + list($name1, $name2) = explode('.', $name); + return $prefix ? isset($_SESSION[$prefix][$name1][$name2]) : isset($_SESSION[$name1][$name2]); + } else { + return $prefix ? isset($_SESSION[$prefix][$name]) : isset($_SESSION[$name]); + } + } + + /** + * 添加数据到一个session数组 + * @param string $key + * @param mixed $value + * @return void + */ + public static function push($key, $value) + { + $array = self::get($key); + if (is_null($array)) { + $array = []; + } + $array[] = $value; + self::set($key, $array); + } + + /** + * 启动session + * @return void + */ + public static function start() + { + session_start(); + self::$init = true; + } + + /** + * 销毁session + * @return void + */ + public static function destroy() + { + if (!empty($_SESSION)) { + $_SESSION = []; + } + session_unset(); + session_destroy(); + self::$init = null; + } + + /** + * 重新生成session_id + * @param bool $delete 是否删除关联会话文件 + * @return void + */ + public static function regenerate($delete = false) + { + session_regenerate_id($delete); + } + + /** + * 暂停session + * @return void + */ + public static function pause() + { + // 暂停session + session_write_close(); + self::$init = false; + } +} diff --git a/thinkphp/library/think/Template.php b/thinkphp/library/think/Template.php new file mode 100644 index 0000000..c74f64f --- /dev/null +++ b/thinkphp/library/think/Template.php @@ -0,0 +1,1146 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\TemplateNotFoundException; + +/** + * ThinkPHP分离出来的模板引擎 + * 支持XML标签和普通标签的模板解析 + * 编译型模板引擎 支持动态缓存 + */ +class Template +{ + // 模板变量 + protected $data = []; + // 引擎配置 + protected $config = [ + 'view_path' => '', // 模板路径 + 'view_base' => '', + 'view_suffix' => 'html', // 默认模板文件后缀 + 'view_depr' => DS, + 'cache_suffix' => 'php', // 默认模板缓存后缀 + 'tpl_deny_func_list' => 'echo,exit', // 模板引擎禁用函数 + 'tpl_deny_php' => false, // 默认模板引擎是否禁用PHP原生代码 + 'tpl_begin' => '{', // 模板引擎普通标签开始标记 + 'tpl_end' => '}', // 模板引擎普通标签结束标记 + 'strip_space' => false, // 是否去除模板文件里面的html空格与换行 + 'tpl_cache' => true, // 是否开启模板编译缓存,设为false则每次都会重新编译 + 'compile_type' => 'file', // 模板编译类型 + 'cache_prefix' => '', // 模板缓存前缀标识,可以动态改变 + 'cache_time' => 0, // 模板缓存有效期 0 为永久,(以数字为值,单位:秒) + 'layout_on' => false, // 布局模板开关 + 'layout_name' => 'layout', // 布局模板入口文件 + 'layout_item' => '{__CONTENT__}', // 布局模板的内容替换标识 + 'taglib_begin' => '{', // 标签库标签开始标记 + 'taglib_end' => '}', // 标签库标签结束标记 + 'taglib_load' => true, // 是否使用内置标签库之外的其它标签库,默认自动检测 + 'taglib_build_in' => 'cx', // 内置标签库名称(标签使用不必指定标签库名称),以逗号分隔 注意解析顺序 + 'taglib_pre_load' => '', // 需要额外加载的标签库(须指定标签库名称),多个以逗号分隔 + 'display_cache' => false, // 模板渲染缓存 + 'cache_id' => '', // 模板缓存ID + 'tpl_replace_string' => [], + 'tpl_var_identify' => 'array', // .语法变量识别,array|object|'', 为空时自动识别 + ]; + + private $literal = []; + private $includeFile = []; // 记录所有模板包含的文件路径及更新时间 + protected $storage; + + /** + * 构造函数 + * @access public + */ + public function __construct(array $config = []) + { + $this->config['cache_path'] = TEMP_PATH; + $this->config = array_merge($this->config, $config); + $this->config['taglib_begin'] = $this->stripPreg($this->config['taglib_begin']); + $this->config['taglib_end'] = $this->stripPreg($this->config['taglib_end']); + $this->config['tpl_begin'] = $this->stripPreg($this->config['tpl_begin']); + $this->config['tpl_end'] = $this->stripPreg($this->config['tpl_end']); + + // 初始化模板编译存储器 + $type = $this->config['compile_type'] ? $this->config['compile_type'] : 'File'; + $class = false !== strpos($type, '\\') ? $type : '\\think\\template\\driver\\' . ucwords($type); + $this->storage = new $class(); + } + + /** + * 字符串替换 避免正则混淆 + * @access private + * @param string $str + * @return string + */ + private function stripPreg($str) + { + return str_replace( + ['{', '}', '(', ')', '|', '[', ']', '-', '+', '*', '.', '^', '?'], + ['\{', '\}', '\(', '\)', '\|', '\[', '\]', '\-', '\+', '\*', '\.', '\^', '\?'], + $str); + } + + /** + * 模板变量赋值 + * @access public + * @param mixed $name + * @param mixed $value + * @return void + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->data = array_merge($this->data, $name); + } else { + $this->data[$name] = $value; + } + } + + /** + * 模板引擎参数赋值 + * @access public + * @param mixed $name + * @param mixed $value + */ + public function __set($name, $value) + { + $this->config[$name] = $value; + } + + /** + * 模板引擎配置项 + * @access public + * @param array|string $config + * @return void|array + */ + public function config($config) + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } elseif (isset($this->config[$config])) { + return $this->config[$config]; + } else { + return; + } + } + + /** + * 模板变量获取 + * @access public + * @param string $name 变量名 + * @return mixed + */ + public function get($name = '') + { + if ('' == $name) { + return $this->data; + } else { + $data = $this->data; + foreach (explode('.', $name) as $key => $val) { + if (isset($data[$val])) { + $data = $data[$val]; + } else { + $data = null; + break; + } + } + return $data; + } + } + + /** + * 渲染模板文件 + * @access public + * @param string $template 模板文件 + * @param array $vars 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function fetch($template, $vars = [], $config = []) + { + if ($vars) { + $this->data = $vars; + } + if ($config) { + $this->config($config); + } + if (!empty($this->config['cache_id']) && $this->config['display_cache']) { + // 读取渲染缓存 + $cacheContent = Cache::get($this->config['cache_id']); + if (false !== $cacheContent) { + echo $cacheContent; + return; + } + } + $template = $this->parseTemplateFile($template); + if ($template) { + $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($template) . '.' . ltrim($this->config['cache_suffix'], '.'); + if (!$this->checkCache($cacheFile)) { + // 缓存无效 重新模板编译 + $content = file_get_contents($template); + $this->compiler($content, $cacheFile); + } + // 页面缓存 + ob_start(); + ob_implicit_flush(0); + // 读取编译存储 + $this->storage->read($cacheFile, $this->data); + // 获取并清空缓存 + $content = ob_get_clean(); + if (!empty($this->config['cache_id']) && $this->config['display_cache']) { + // 缓存页面输出 + Cache::set($this->config['cache_id'], $content, $this->config['cache_time']); + } + echo $content; + } + } + + /** + * 渲染模板内容 + * @access public + * @param string $content 模板内容 + * @param array $vars 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function display($content, $vars = [], $config = []) + { + if ($vars) { + $this->data = $vars; + } + if ($config) { + $this->config($config); + } + $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($content) . '.' . ltrim($this->config['cache_suffix'], '.'); + if (!$this->checkCache($cacheFile)) { + // 缓存无效 模板编译 + $this->compiler($content, $cacheFile); + } + // 读取编译存储 + $this->storage->read($cacheFile, $this->data); + } + + /** + * 设置布局 + * @access public + * @param mixed $name 布局模板名称 false 则关闭布局 + * @param string $replace 布局模板内容替换标识 + * @return object + */ + public function layout($name, $replace = '') + { + if (false === $name) { + // 关闭布局 + $this->config['layout_on'] = false; + } else { + // 开启布局 + $this->config['layout_on'] = true; + // 名称必须为字符串 + if (is_string($name)) { + $this->config['layout_name'] = $name; + } + if (!empty($replace)) { + $this->config['layout_item'] = $replace; + } + } + return $this; + } + + /** + * 检查编译缓存是否有效 + * 如果无效则需要重新编译 + * @access private + * @param string $cacheFile 缓存文件名 + * @return boolean + */ + private function checkCache($cacheFile) + { + // 未开启缓存功能 + if (!$this->config['tpl_cache']) { + return false; + } + // 缓存文件不存在 + if (!is_file($cacheFile)) { + return false; + } + // 读取缓存文件失败 + if (!$handle = @fopen($cacheFile, "r")) { + return false; + } + // 读取第一行 + preg_match('/\/\*(.+?)\*\//', fgets($handle), $matches); + if (!isset($matches[1])) { + return false; + } + $includeFile = unserialize($matches[1]); + if (!is_array($includeFile)) { + return false; + } + // 检查模板文件是否有更新 + foreach ($includeFile as $path => $time) { + if (is_file($path) && filemtime($path) > $time) { + // 模板文件如果有更新则缓存需要更新 + return false; + } + } + // 检查编译存储是否有效 + return $this->storage->check($cacheFile, $this->config['cache_time']); + } + + /** + * 检查编译缓存是否存在 + * @access public + * @param string $cacheId 缓存的id + * @return boolean + */ + public function isCache($cacheId) + { + if ($cacheId && $this->config['display_cache']) { + // 缓存页面输出 + return Cache::has($cacheId); + } + return false; + } + + /** + * 编译模板文件内容 + * @access private + * @param string $content 模板内容 + * @param string $cacheFile 缓存文件名 + * @return void + */ + private function compiler(&$content, $cacheFile) + { + // 判断是否启用布局 + if ($this->config['layout_on']) { + if (false !== strpos($content, '{__NOLAYOUT__}')) { + // 可以单独定义不使用布局 + $content = str_replace('{__NOLAYOUT__}', '', $content); + } else { + // 读取布局模板 + $layoutFile = $this->parseTemplateFile($this->config['layout_name']); + if ($layoutFile) { + // 替换布局的主体内容 + $content = str_replace($this->config['layout_item'], $content, file_get_contents($layoutFile)); + } + } + } else { + $content = str_replace('{__NOLAYOUT__}', '', $content); + } + + // 模板解析 + $this->parse($content); + if ($this->config['strip_space']) { + /* 去除html空格与换行 */ + $find = ['~>\s+<~', '~>(\s+\n|\r)~']; + $replace = ['><', '>']; + $content = preg_replace($find, $replace, $content); + } + // 优化生成的php代码 + $content = preg_replace('/\?>\s*<\?php\s(?!echo\b)/s', '', $content); + // 模板过滤输出 + $replace = $this->config['tpl_replace_string']; + $content = str_replace(array_keys($replace), array_values($replace), $content); + // 添加安全代码及模板引用记录 + $content = 'includeFile) . '*/ ?>' . "\n" . $content; + // 编译存储 + $this->storage->write($cacheFile, $content); + $this->includeFile = []; + return; + } + + /** + * 模板解析入口 + * 支持普通标签和TagLib解析 支持自定义标签库 + * @access public + * @param string $content 要解析的模板内容 + * @return void + */ + public function parse(&$content) + { + // 内容为空不解析 + if (empty($content)) { + return; + } + // 替换literal标签内容 + $this->parseLiteral($content); + // 解析继承 + $this->parseExtend($content); + // 解析布局 + $this->parseLayout($content); + // 检查include语法 + $this->parseInclude($content); + // 替换包含文件中literal标签内容 + $this->parseLiteral($content); + // 检查PHP语法 + $this->parsePhp($content); + + // 获取需要引入的标签库列表 + // 标签库只需要定义一次,允许引入多个一次 + // 一般放在文件的最前面 + // 格式: + // 当TAGLIB_LOAD配置为true时才会进行检测 + if ($this->config['taglib_load']) { + $tagLibs = $this->getIncludeTagLib($content); + if (!empty($tagLibs)) { + // 对导入的TagLib进行解析 + foreach ($tagLibs as $tagLibName) { + $this->parseTagLib($tagLibName, $content); + } + } + } + // 预先加载的标签库 无需在每个模板中使用taglib标签加载 但必须使用标签库XML前缀 + if ($this->config['taglib_pre_load']) { + $tagLibs = explode(',', $this->config['taglib_pre_load']); + foreach ($tagLibs as $tag) { + $this->parseTagLib($tag, $content); + } + } + // 内置标签库 无需使用taglib标签导入就可以使用 并且不需使用标签库XML前缀 + $tagLibs = explode(',', $this->config['taglib_build_in']); + foreach ($tagLibs as $tag) { + $this->parseTagLib($tag, $content, true); + } + // 解析普通模板标签 {$tagName} + $this->parseTag($content); + + // 还原被替换的Literal标签 + $this->parseLiteral($content, true); + return; + } + + /** + * 检查PHP语法 + * @access private + * @param string $content 要解析的模板内容 + * @return void + * @throws \think\Exception + */ + private function parsePhp(&$content) + { + // 短标签的情况要将' . "\n", $content); + // PHP语法检查 + if ($this->config['tpl_deny_php'] && false !== strpos($content, 'getRegex('layout'), $content, $matches)) { + // 替换Layout标签 + $content = str_replace($matches[0], '', $content); + // 解析Layout标签 + $array = $this->parseAttr($matches[0]); + if (!$this->config['layout_on'] || $this->config['layout_name'] != $array['name']) { + // 读取布局模板 + $layoutFile = $this->parseTemplateFile($array['name']); + if ($layoutFile) { + $replace = isset($array['replace']) ? $array['replace'] : $this->config['layout_item']; + // 替换布局的主体内容 + $content = str_replace($replace, $content, file_get_contents($layoutFile)); + } + } + } else { + $content = str_replace('{__NOLAYOUT__}', '', $content); + } + return; + } + + /** + * 解析模板中的include标签 + * @access private + * @param string $content 要解析的模板内容 + * @return void + */ + private function parseInclude(&$content) + { + $regex = $this->getRegex('include'); + $func = function ($template) use (&$func, &$regex, &$content) { + if (preg_match_all($regex, $template, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $array = $this->parseAttr($match[0]); + $file = $array['file']; + unset($array['file']); + // 分析模板文件名并读取内容 + $parseStr = $this->parseTemplateName($file); + foreach ($array as $k => $v) { + // 以$开头字符串转换成模板变量 + if (0 === strpos($v, '$')) { + $v = $this->get(substr($v, 1)); + } + $parseStr = str_replace('[' . $k . ']', $v, $parseStr); + } + $content = str_replace($match[0], $parseStr, $content); + // 再次对包含文件进行模板分析 + $func($parseStr); + } + unset($matches); + } + }; + // 替换模板中的include标签 + $func($content); + return; + } + + /** + * 解析模板中的extend标签 + * @access private + * @param string $content 要解析的模板内容 + * @return void + */ + private function parseExtend(&$content) + { + $regex = $this->getRegex('extend'); + $array = $blocks = $baseBlocks = []; + $extend = ''; + $func = function ($template) use (&$func, &$regex, &$array, &$extend, &$blocks, &$baseBlocks) { + if (preg_match($regex, $template, $matches)) { + if (!isset($array[$matches['name']])) { + $array[$matches['name']] = 1; + // 读取继承模板 + $extend = $this->parseTemplateName($matches['name']); + // 递归检查继承 + $func($extend); + // 取得block标签内容 + $blocks = array_merge($blocks, $this->parseBlock($template)); + return; + } + } else { + // 取得顶层模板block标签内容 + $baseBlocks = $this->parseBlock($template, true); + if (empty($extend)) { + // 无extend标签但有block标签的情况 + $extend = $template; + } + } + }; + + $func($content); + if (!empty($extend)) { + if ($baseBlocks) { + $children = []; + foreach ($baseBlocks as $name => $val) { + $replace = $val['content']; + if (!empty($children[$name])) { + // 如果包含有子block标签 + foreach ($children[$name] as $key) { + $replace = str_replace($baseBlocks[$key]['begin'] . $baseBlocks[$key]['content'] . $baseBlocks[$key]['end'], $blocks[$key]['content'], $replace); + } + } + if (isset($blocks[$name])) { + // 带有{__block__}表示与所继承模板的相应标签合并,而不是覆盖 + $replace = str_replace(['{__BLOCK__}', '{__block__}'], $replace, $blocks[$name]['content']); + if (!empty($val['parent'])) { + // 如果不是最顶层的block标签 + $parent = $val['parent']; + if (isset($blocks[$parent])) { + $blocks[$parent]['content'] = str_replace($blocks[$name]['begin'] . $blocks[$name]['content'] . $blocks[$name]['end'], $replace, $blocks[$parent]['content']); + } + $blocks[$name]['content'] = $replace; + $children[$parent][] = $name; + continue; + } + } elseif (!empty($val['parent'])) { + // 如果子标签没有被继承则用原值 + $children[$val['parent']][] = $name; + $blocks[$name] = $val; + } + if (!$val['parent']) { + // 替换模板中的顶级block标签 + $extend = str_replace($val['begin'] . $val['content'] . $val['end'], $replace, $extend); + } + } + } + $content = $extend; + unset($blocks, $baseBlocks); + } + return; + } + + /** + * 替换页面中的literal标签 + * @access private + * @param string $content 模板内容 + * @param boolean $restore 是否为还原 + * @return void + */ + private function parseLiteral(&$content, $restore = false) + { + $regex = $this->getRegex($restore ? 'restoreliteral' : 'literal'); + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { + if (!$restore) { + $count = count($this->literal); + // 替换literal标签 + foreach ($matches as $match) { + $this->literal[] = substr($match[0], strlen($match[1]), -strlen($match[2])); + $content = str_replace($match[0], "", $content); + $count++; + } + } else { + // 还原literal标签 + foreach ($matches as $match) { + $content = str_replace($match[0], $this->literal[$match[1]], $content); + } + // 清空literal记录 + $this->literal = []; + } + unset($matches); + } + return; + } + + /** + * 获取模板中的block标签 + * @access private + * @param string $content 模板内容 + * @param boolean $sort 是否排序 + * @return array + */ + private function parseBlock(&$content, $sort = false) + { + $regex = $this->getRegex('block'); + $result = []; + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + $right = $keys = []; + foreach ($matches as $match) { + if (empty($match['name'][0])) { + if (count($right) > 0) { + $tag = array_pop($right); + $start = $tag['offset'] + strlen($tag['tag']); + $length = $match[0][1] - $start; + $result[$tag['name']] = [ + 'begin' => $tag['tag'], + 'content' => substr($content, $start, $length), + 'end' => $match[0][0], + 'parent' => count($right) ? end($right)['name'] : '', + ]; + $keys[$tag['name']] = $match[0][1]; + } + } else { + // 标签头压入栈 + $right[] = [ + 'name' => $match[2][0], + 'offset' => $match[0][1], + 'tag' => $match[0][0], + ]; + } + } + unset($right, $matches); + if ($sort) { + // 按block标签结束符在模板中的位置排序 + array_multisort($keys, $result); + } + } + return $result; + } + + /** + * 搜索模板页面中包含的TagLib库 + * 并返回列表 + * @access private + * @param string $content 模板内容 + * @return array|null + */ + private function getIncludeTagLib(&$content) + { + // 搜索是否有TagLib标签 + if (preg_match($this->getRegex('taglib'), $content, $matches)) { + // 替换TagLib标签 + $content = str_replace($matches[0], '', $content); + return explode(',', $matches['name']); + } + return; + } + + /** + * TagLib库解析 + * @access public + * @param string $tagLib 要解析的标签库 + * @param string $content 要解析的模板内容 + * @param boolean $hide 是否隐藏标签库前缀 + * @return void + */ + public function parseTagLib($tagLib, &$content, $hide = false) + { + if (false !== strpos($tagLib, '\\')) { + // 支持指定标签库的命名空间 + $className = $tagLib; + $tagLib = substr($tagLib, strrpos($tagLib, '\\') + 1); + } else { + $className = '\\think\\template\\taglib\\' . ucwords($tagLib); + } + $tLib = new $className($this); + $tLib->parseTag($content, $hide ? '' : $tagLib); + return; + } + + /** + * 分析标签属性 + * @access public + * @param string $str 属性字符串 + * @param string $name 不为空时返回指定的属性名 + * @return array + */ + public function parseAttr($str, $name = null) + { + $regex = '/\s+(?>(?P[\w-]+)\s*)=(?>\s*)([\"\'])(?P(?:(?!\\2).)*)\\2/is'; + $array = []; + if (preg_match_all($regex, $str, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $array[$match['name']] = $match['value']; + } + unset($matches); + } + if (!empty($name) && isset($array[$name])) { + return $array[$name]; + } else { + return $array; + } + } + + /** + * 模板标签解析 + * 格式: {TagName:args [|content] } + * @access private + * @param string $content 要解析的模板内容 + * @return void + */ + private function parseTag(&$content) + { + $regex = $this->getRegex('tag'); + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $str = stripslashes($match[1]); + $flag = substr($str, 0, 1); + switch ($flag) { + case '$': + // 解析模板变量 格式 {$varName} + // 是否带有?号 + if (false !== $pos = strpos($str, '?')) { + $array = preg_split('/([!=]={1,2}|(?<]={0,1})/', substr($str, 0, $pos), 2, PREG_SPLIT_DELIM_CAPTURE); + $name = $array[0]; + $this->parseVar($name); + $this->parseVarFunction($name); + + $str = trim(substr($str, $pos + 1)); + $this->parseVar($str); + $first = substr($str, 0, 1); + if (strpos($name, ')')) { + // $name为对象或是自动识别,或者含有函数 + if (isset($array[1])) { + $this->parseVar($array[2]); + $name .= $array[1] . $array[2]; + } + switch ($first) { + case '?': + $str = ''; + break; + case '=': + $str = ''; + break; + default: + $str = ''; + } + } else { + if (isset($array[1])) { + $this->parseVar($array[2]); + $express = $name . $array[1] . $array[2]; + } else { + $express = false; + } + // $name为数组 + switch ($first) { + case '?': + // {$varname??'xxx'} $varname有定义则输出$varname,否则输出xxx + $str = ''; + break; + case '=': + // {$varname?='xxx'} $varname为真时才输出xxx + $str = ''; + break; + case ':': + // {$varname?:'xxx'} $varname为真时输出$varname,否则输出xxx + $str = ''; + break; + default: + $str = ''; + } + } + } else { + $this->parseVar($str); + $this->parseVarFunction($str); + $str = ''; + } + break; + case ':': + // 输出某个函数的结果 + $str = substr($str, 1); + $this->parseVar($str); + $str = ''; + break; + case '~': + // 执行某个函数 + $str = substr($str, 1); + $this->parseVar($str); + $str = ''; + break; + case '-': + case '+': + // 输出计算 + $this->parseVar($str); + $str = ''; + break; + case '/': + // 注释标签 + $flag2 = substr($str, 1, 1); + if ('/' == $flag2 || ('*' == $flag2 && substr(rtrim($str), -2) == '*/')) { + $str = ''; + } + break; + default: + // 未识别的标签直接返回 + $str = $this->config['tpl_begin'] . $str . $this->config['tpl_end']; + break; + } + $content = str_replace($match[0], $str, $content); + } + unset($matches); + } + return; + } + + /** + * 模板变量解析,支持使用函数 + * 格式: {$varname|function1|function2=arg1,arg2} + * @access public + * @param string $varStr 变量数据 + * @return void + */ + public function parseVar(&$varStr) + { + $varStr = trim($varStr); + if (preg_match_all('/\$[a-zA-Z_](?>\w*)(?:[:\.][0-9a-zA-Z_](?>\w*))+/', $varStr, $matches, PREG_OFFSET_CAPTURE)) { + static $_varParseList = []; + while ($matches[0]) { + $match = array_pop($matches[0]); + //如果已经解析过该变量字串,则直接返回变量值 + if (isset($_varParseList[$match[0]])) { + $parseStr = $_varParseList[$match[0]]; + } else { + if (strpos($match[0], '.')) { + $vars = explode('.', $match[0]); + $first = array_shift($vars); + if ('$Think' == $first) { + // 所有以Think.打头的以特殊变量对待 无需模板赋值就可以输出 + $parseStr = $this->parseThinkVar($vars); + } elseif ('$Request' == $first) { + // 获取Request请求对象参数 + $method = array_shift($vars); + if (!empty($vars)) { + $params = implode('.', $vars); + if ('true' != $params) { + $params = '\'' . $params . '\''; + } + } else { + $params = ''; + } + $parseStr = '\think\Request::instance()->' . $method . '(' . $params . ')'; + } else { + switch ($this->config['tpl_var_identify']) { + case 'array': // 识别为数组 + $parseStr = $first . '[\'' . implode('\'][\'', $vars) . '\']'; + break; + case 'obj': // 识别为对象 + $parseStr = $first . '->' . implode('->', $vars); + break; + default: // 自动判断数组或对象 + $parseStr = '(is_array(' . $first . ')?' . $first . '[\'' . implode('\'][\'', $vars) . '\']:' . $first . '->' . implode('->', $vars) . ')'; + } + } + } else { + $parseStr = str_replace(':', '->', $match[0]); + } + $_varParseList[$match[0]] = $parseStr; + } + $varStr = substr_replace($varStr, $parseStr, $match[1], strlen($match[0])); + } + unset($matches); + } + return; + } + + /** + * 对模板中使用了函数的变量进行解析 + * 格式 {$varname|function1|function2=arg1,arg2} + * @access public + * @param string $varStr 变量字符串 + * @return void + */ + public function parseVarFunction(&$varStr) + { + if (false == strpos($varStr, '|')) { + return; + } + static $_varFunctionList = []; + $_key = md5($varStr); + //如果已经解析过该变量字串,则直接返回变量值 + if (isset($_varFunctionList[$_key])) { + $varStr = $_varFunctionList[$_key]; + } else { + $varArray = explode('|', $varStr); + // 取得变量名称 + $name = array_shift($varArray); + // 对变量使用函数 + $length = count($varArray); + // 取得模板禁止使用函数列表 + $template_deny_funs = explode(',', $this->config['tpl_deny_func_list']); + for ($i = 0; $i < $length; $i++) { + $args = explode('=', $varArray[$i], 2); + // 模板函数过滤 + $fun = trim($args[0]); + switch ($fun) { + case 'default': // 特殊模板函数 + if (false === strpos($name, '(')) { + $name = '(isset(' . $name . ') && (' . $name . ' !== \'\')?' . $name . ':' . $args[1] . ')'; + } else { + $name = '(' . $name . ' ?: ' . $args[1] . ')'; + } + break; + default: // 通用模板函数 + if (!in_array($fun, $template_deny_funs)) { + if (isset($args[1])) { + if (strstr($args[1], '###')) { + $args[1] = str_replace('###', $name, $args[1]); + $name = "$fun($args[1])"; + } else { + $name = "$fun($name,$args[1])"; + } + } else { + if (!empty($args[0])) { + $name = "$fun($name)"; + } + } + } + } + } + $_varFunctionList[$_key] = $name; + $varStr = $name; + } + return; + } + + /** + * 特殊模板变量解析 + * 格式 以 $Think. 打头的变量属于特殊模板变量 + * @access public + * @param array $vars 变量数组 + * @return string + */ + public function parseThinkVar($vars) + { + $type = strtoupper(trim(array_shift($vars))); + $param = implode('.', $vars); + if ($vars) { + switch ($type) { + case 'SERVER': + $parseStr = '\\think\\Request::instance()->server(\'' . $param . '\')'; + break; + case 'GET': + $parseStr = '\\think\\Request::instance()->get(\'' . $param . '\')'; + break; + case 'POST': + $parseStr = '\\think\\Request::instance()->post(\'' . $param . '\')'; + break; + case 'COOKIE': + $parseStr = '\\think\\Cookie::get(\'' . $param . '\')'; + break; + case 'SESSION': + $parseStr = '\\think\\Session::get(\'' . $param . '\')'; + break; + case 'ENV': + $parseStr = '\\think\\Request::instance()->env(\'' . $param . '\')'; + break; + case 'REQUEST': + $parseStr = '\\think\\Request::instance()->request(\'' . $param . '\')'; + break; + case 'CONST': + $parseStr = strtoupper($param); + break; + case 'LANG': + $parseStr = '\\think\\Lang::get(\'' . $param . '\')'; + break; + case 'CONFIG': + $parseStr = '\\think\\Config::get(\'' . $param . '\')'; + break; + default: + $parseStr = '\'\''; + break; + } + } else { + switch ($type) { + case 'NOW': + $parseStr = "date('Y-m-d g:i a',time())"; + break; + case 'VERSION': + $parseStr = 'THINK_VERSION'; + break; + case 'LDELIM': + $parseStr = '\'' . ltrim($this->config['tpl_begin'], '\\') . '\''; + break; + case 'RDELIM': + $parseStr = '\'' . ltrim($this->config['tpl_end'], '\\') . '\''; + break; + default: + if (defined($type)) { + $parseStr = $type; + } else { + $parseStr = ''; + } + } + } + return $parseStr; + } + + /** + * 分析加载的模板文件并读取内容 支持多个模板文件读取 + * @access private + * @param string $templateName 模板文件名 + * @return string + */ + private function parseTemplateName($templateName) + { + $array = explode(',', $templateName); + $parseStr = ''; + foreach ($array as $templateName) { + if (empty($templateName)) { + continue; + } + if (0 === strpos($templateName, '$')) { + //支持加载变量文件名 + $templateName = $this->get(substr($templateName, 1)); + } + $template = $this->parseTemplateFile($templateName); + if ($template) { + // 获取模板文件内容 + $parseStr .= file_get_contents($template); + } + } + return $parseStr; + } + + /** + * 解析模板文件名 + * @access private + * @param string $template 文件名 + * @return string|false + */ + private function parseTemplateFile($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + if (strpos($template, '@')) { + list($module, $template) = explode('@', $template); + } + if (0 !== strpos($template, '/')) { + $template = str_replace(['/', ':'], $this->config['view_depr'], $template); + } else { + $template = str_replace(['/', ':'], $this->config['view_depr'], substr($template, 1)); + } + if ($this->config['view_base']) { + $module = isset($module) ? $module : Request::instance()->module(); + $path = $this->config['view_base'] . ($module ? $module . DS : ''); + } else { + $path = isset($module) ? APP_PATH . $module . DS . basename($this->config['view_path']) . DS : $this->config['view_path']; + } + $template = $path . $template . '.' . ltrim($this->config['view_suffix'], '.'); + } + + if (is_file($template)) { + // 记录模板文件的更新时间 + $this->includeFile[$template] = filemtime($template); + return $template; + } else { + throw new TemplateNotFoundException('template not exists:' . $template, $template); + } + } + + /** + * 按标签生成正则 + * @access private + * @param string $tagName 标签名 + * @return string + */ + private function getRegex($tagName) + { + $regex = ''; + if ('tag' == $tagName) { + $begin = $this->config['tpl_begin']; + $end = $this->config['tpl_end']; + if (strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1) { + $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>[^' . $end . ']*))' . $end; + } else { + $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>(?:(?!' . $end . ').)*))' . $end; + } + } else { + $begin = $this->config['taglib_begin']; + $end = $this->config['taglib_end']; + $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false; + switch ($tagName) { + case 'block': + if ($single) { + $regex = $begin . '(?:' . $tagName . '\b(?>(?:(?!name=).)*)\bname=([\'\"])(?P[\$\w\-\/\.]+)\\1(?>[^' . $end . ']*)|\/' . $tagName . ')' . $end; + } else { + $regex = $begin . '(?:' . $tagName . '\b(?>(?:(?!name=).)*)\bname=([\'\"])(?P[\$\w\-\/\.]+)\\1(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end; + } + break; + case 'literal': + if ($single) { + $regex = '(' . $begin . $tagName . '\b(?>[^' . $end . ']*)' . $end . ')'; + $regex .= '(?:(?>[^' . $begin . ']*)(?>(?!' . $begin . '(?>' . $tagName . '\b[^' . $end . ']*|\/' . $tagName . ')' . $end . ')' . $begin . '[^' . $begin . ']*)*)'; + $regex .= '(' . $begin . '\/' . $tagName . $end . ')'; + } else { + $regex = '(' . $begin . $tagName . '\b(?>(?:(?!' . $end . ').)*)' . $end . ')'; + $regex .= '(?:(?>(?:(?!' . $begin . ').)*)(?>(?!' . $begin . '(?>' . $tagName . '\b(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end . ')' . $begin . '(?>(?:(?!' . $begin . ').)*))*)'; + $regex .= '(' . $begin . '\/' . $tagName . $end . ')'; + } + break; + case 'restoreliteral': + $regex = ''; + break; + case 'include': + $name = 'file'; + case 'taglib': + case 'layout': + case 'extend': + if (empty($name)) { + $name = 'name'; + } + if ($single) { + $regex = $begin . $tagName . '\b(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P[\$\w\-\/\.\:@,\\\\]+)\\1(?>[^' . $end . ']*)' . $end; + } else { + $regex = $begin . $tagName . '\b(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P[\$\w\-\/\.\:@,\\\\]+)\\1(?>(?:(?!' . $end . ').)*)' . $end; + } + break; + } + } + return '/' . $regex . '/is'; + } +} diff --git a/thinkphp/library/think/Url.php b/thinkphp/library/think/Url.php new file mode 100644 index 0000000..3001dab --- /dev/null +++ b/thinkphp/library/think/Url.php @@ -0,0 +1,328 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Url +{ + // 生成URL地址的root + protected static $root; + protected static $bindCheck; + + /** + * URL生成 支持路由反射 + * @param string $url 路由地址 + * @param string|array $vars 参数(支持数组和字符串)a=val&b=val2... ['a'=>'val1', 'b'=>'val2'] + * @param string|bool $suffix 伪静态后缀,默认为true表示获取配置值 + * @param boolean|string $domain 是否显示域名 或者直接传入域名 + * @return string + */ + public static function build($url = '', $vars = '', $suffix = true, $domain = false) + { + if (false === $domain && Route::rules('domain')) { + $domain = true; + } + // 解析URL + if (0 === strpos($url, '[') && $pos = strpos($url, ']')) { + // [name] 表示使用路由命名标识生成URL + $name = substr($url, 1, $pos - 1); + $url = 'name' . substr($url, $pos + 1); + } + if (false === strpos($url, '://') && 0 !== strpos($url, '/')) { + $info = parse_url($url); + $url = !empty($info['path']) ? $info['path'] : ''; + if (isset($info['fragment'])) { + // 解析锚点 + $anchor = $info['fragment']; + if (false !== strpos($anchor, '?')) { + // 解析参数 + list($anchor, $info['query']) = explode('?', $anchor, 2); + } + if (false !== strpos($anchor, '@')) { + // 解析域名 + list($anchor, $domain) = explode('@', $anchor, 2); + } + } elseif (strpos($url, '@') && false === strpos($url, '\\')) { + // 解析域名 + list($url, $domain) = explode('@', $url, 2); + } + } + + // 解析参数 + if (is_string($vars)) { + // aaa=1&bbb=2 转换成数组 + parse_str($vars, $vars); + } + + if ($url) { + $rule = Route::name(isset($name) ? $name : $url . (isset($info['query']) ? '?' . $info['query'] : '')); + if (is_null($rule) && isset($info['query'])) { + $rule = Route::name($url); + // 解析地址里面参数 合并到vars + parse_str($info['query'], $params); + $vars = array_merge($params, $vars); + unset($info['query']); + } + } + if (!empty($rule) && $match = self::getRuleUrl($rule, $vars)) { + // 匹配路由命名标识 + $url = $match[0]; + // 替换可选分隔符 + $url = preg_replace(['/(\W)\?$/', '/(\W)\?/'], ['', '\1'], $url); + if (!empty($match[1])) { + $domain = $match[1]; + } + if (!is_null($match[2])) { + $suffix = $match[2]; + } + } elseif (!empty($rule) && isset($name)) { + throw new \InvalidArgumentException('route name not exists:' . $name); + } else { + // 检查别名路由 + $alias = Route::rules('alias'); + $matchAlias = false; + if ($alias) { + // 别名路由解析 + foreach ($alias as $key => $val) { + if (is_array($val)) { + $val = $val[0]; + } + if (0 === strpos($url, $val)) { + $url = $key . substr($url, strlen($val)); + $matchAlias = true; + break; + } + } + } + if (!$matchAlias) { + // 路由标识不存在 直接解析 + $url = self::parseUrl($url, $domain); + } + if (isset($info['query'])) { + // 解析地址里面参数 合并到vars + parse_str($info['query'], $params); + $vars = array_merge($params, $vars); + } + } + + // 检测URL绑定 + if (!self::$bindCheck) { + $type = Route::getBind('type'); + if ($type) { + $bind = Route::getBind($type); + if (0 === strpos($url, $bind)) { + $url = substr($url, strlen($bind) + 1); + } + } + } + // 还原URL分隔符 + $depr = Config::get('pathinfo_depr'); + $url = str_replace('/', $depr, $url); + + // URL后缀 + $suffix = in_array($url, ['/', '']) ? '' : self::parseSuffix($suffix); + // 锚点 + $anchor = !empty($anchor) ? '#' . $anchor : ''; + // 参数组装 + if (!empty($vars)) { + // 添加参数 + if (Config::get('url_common_param')) { + $vars = urldecode(http_build_query($vars)); + $url .= $suffix . '?' . $vars . $anchor; + } else { + $paramType = Config::get('url_param_type'); + foreach ($vars as $var => $val) { + if ('' !== trim($val)) { + if ($paramType) { + $url .= $depr . urlencode($val); + } else { + $url .= $depr . $var . $depr . urlencode($val); + } + } + } + $url .= $suffix . $anchor; + } + } else { + $url .= $suffix . $anchor; + } + // 检测域名 + $domain = self::parseDomain($url, $domain); + // URL组装 + $url = $domain . rtrim(self::$root ?: Request::instance()->root(), '/') . '/' . ltrim($url, '/'); + + self::$bindCheck = false; + return $url; + } + + // 直接解析URL地址 + protected static function parseUrl($url, &$domain) + { + $request = Request::instance(); + if (0 === strpos($url, '/')) { + // 直接作为路由地址解析 + $url = substr($url, 1); + } elseif (false !== strpos($url, '\\')) { + // 解析到类 + $url = ltrim(str_replace('\\', '/', $url), '/'); + } elseif (0 === strpos($url, '@')) { + // 解析到控制器 + $url = substr($url, 1); + } else { + // 解析到 模块/控制器/操作 + $module = $request->module(); + $domains = Route::rules('domain'); + if (true === $domain && 2 == substr_count($url, '/')) { + $current = $request->host(); + $match = []; + $pos = []; + foreach ($domains as $key => $item) { + if (isset($item['[bind]']) && 0 === strpos($url, $item['[bind]'][0])) { + $pos[$key] = strlen($item['[bind]'][0]) + 1; + $match[] = $key; + $module = ''; + } + } + if ($match) { + $domain = current($match); + foreach ($match as $item) { + if (0 === strpos($current, $item)) { + $domain = $item; + } + } + self::$bindCheck = true; + $url = substr($url, $pos[$domain]); + } + } elseif ($domain) { + if (isset($domains[$domain]['[bind]'][0])) { + $bindModule = $domains[$domain]['[bind]'][0]; + if ($bindModule && !in_array($bindModule[0], ['\\', '@'])) { + $module = ''; + } + } + } + $module = $module ? $module . '/' : ''; + + $controller = Loader::parseName($request->controller()); + if ('' == $url) { + // 空字符串输出当前的 模块/控制器/操作 + $url = $module . $controller . '/' . $request->action(); + } else { + $path = explode('/', $url); + $action = Config::get('url_convert') ? strtolower(array_pop($path)) : array_pop($path); + $controller = empty($path) ? $controller : (Config::get('url_convert') ? Loader::parseName(array_pop($path)) : array_pop($path)); + $module = empty($path) ? $module : array_pop($path) . '/'; + $url = $module . $controller . '/' . $action; + } + } + return $url; + } + + // 检测域名 + protected static function parseDomain(&$url, $domain) + { + if (!$domain) { + return ''; + } + $request = Request::instance(); + $rootDomain = Config::get('url_domain_root'); + if (true === $domain) { + // 自动判断域名 + $domain = Config::get('app_host') ?: $request->host(); + + $domains = Route::rules('domain'); + if ($domains) { + $route_domain = array_keys($domains); + foreach ($route_domain as $domain_prefix) { + if (0 === strpos($domain_prefix, '*.') && strpos($domain, ltrim($domain_prefix, '*.')) !== false) { + foreach ($domains as $key => $rule) { + $rule = is_array($rule) ? $rule[0] : $rule; + if (is_string($rule) && false === strpos($key, '*') && 0 === strpos($url, $rule)) { + $url = ltrim($url, $rule); + $domain = $key; + // 生成对应子域名 + if (!empty($rootDomain)) { + $domain .= $rootDomain; + } + break; + } elseif (false !== strpos($key, '*')) { + if (!empty($rootDomain)) { + $domain .= $rootDomain; + } + break; + } + } + } + } + } + + } else { + if (empty($rootDomain)) { + $host = Config::get('app_host') ?: $request->host(); + $rootDomain = substr_count($host, '.') > 1 ? substr(strstr($host, '.'), 1) : $host; + } + if (substr_count($domain, '.') < 2 && !strpos($domain, $rootDomain)) { + $domain .= '.' . $rootDomain; + } + } + if (false !== strpos($domain, '://')) { + $scheme = ''; + } else { + $scheme = $request->isSsl() || Config::get('is_https') ? 'https://' : 'http://'; + } + return $scheme . $domain; + } + + // 解析URL后缀 + protected static function parseSuffix($suffix) + { + if ($suffix) { + $suffix = true === $suffix ? Config::get('url_html_suffix') : $suffix; + if ($pos = strpos($suffix, '|')) { + $suffix = substr($suffix, 0, $pos); + } + } + return (empty($suffix) || 0 === strpos($suffix, '.')) ? $suffix : '.' . $suffix; + } + + // 匹配路由地址 + public static function getRuleUrl($rule, &$vars = []) + { + foreach ($rule as $item) { + list($url, $pattern, $domain, $suffix) = $item; + if (empty($pattern)) { + return [$url, $domain, $suffix]; + } + foreach ($pattern as $key => $val) { + if (isset($vars[$key])) { + $url = str_replace(['[:' . $key . ']', '<' . $key . '?>', ':' . $key . '', '<' . $key . '>'], urlencode($vars[$key]), $url); + unset($vars[$key]); + $result = [$url, $domain, $suffix]; + } elseif (2 == $val) { + $url = str_replace(['/[:' . $key . ']', '[:' . $key . ']', '<' . $key . '?>'], '', $url); + $result = [$url, $domain, $suffix]; + } else { + break; + } + } + if (isset($result)) { + return $result; + } + } + return false; + } + + // 指定当前生成URL地址的root + public static function root($root) + { + self::$root = $root; + Request::instance()->root($root); + } +} diff --git a/thinkphp/library/think/Validate.php b/thinkphp/library/think/Validate.php new file mode 100644 index 0000000..e5efbd2 --- /dev/null +++ b/thinkphp/library/think/Validate.php @@ -0,0 +1,1286 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; + +class Validate +{ + // 实例 + protected static $instance; + + // 自定义的验证类型 + protected static $type = []; + + // 验证类型别名 + protected $alias = [ + '>' => 'gt', '>=' => 'egt', '<' => 'lt', '<=' => 'elt', '=' => 'eq', 'same' => 'eq', + ]; + + // 当前验证的规则 + protected $rule = []; + + // 验证提示信息 + protected $message = []; + // 验证字段描述 + protected $field = []; + + // 验证规则默认提示信息 + protected static $typeMsg = [ + 'require' => ':attribute不能为空', + 'number' => ':attribute必须是数字', + 'integer' => ':attribute必须是整数', + 'float' => ':attribute必须是浮点数', + 'boolean' => ':attribute必须是布尔值', + 'email' => ':attribute格式不符', + 'array' => ':attribute必须是数组', + 'accepted' => ':attribute必须是yes、on或者1', + 'date' => ':attribute格式不符合', + 'file' => ':attribute不是有效的上传文件', + 'image' => ':attribute不是有效的图像文件', + 'alpha' => ':attribute只能是字母', + 'alphaNum' => ':attribute只能是字母和数字', + 'alphaDash' => ':attribute只能是字母、数字和下划线_及破折号-', + 'activeUrl' => ':attribute不是有效的域名或者IP', + 'chs' => ':attribute只能是汉字', + 'chsAlpha' => ':attribute只能是汉字、字母', + 'chsAlphaNum' => ':attribute只能是汉字、字母和数字', + 'chsDash' => ':attribute只能是汉字、字母、数字和下划线_及破折号-', + 'url' => ':attribute不是有效的URL地址', + 'ip' => ':attribute不是有效的IP地址', + 'dateFormat' => ':attribute必须使用日期格式 :rule', + 'in' => ':attribute必须在 :rule 范围内', + 'notIn' => ':attribute不能在 :rule 范围内', + 'between' => ':attribute只能在 :1 - :2 之间', + 'notBetween' => ':attribute不能在 :1 - :2 之间', + 'length' => ':attribute长度不符合要求 :rule', + 'max' => ':attribute长度不能超过 :rule', + 'min' => ':attribute长度不能小于 :rule', + 'after' => ':attribute日期不能小于 :rule', + 'before' => ':attribute日期不能超过 :rule', + 'expire' => '不在有效期内 :rule', + 'allowIp' => '不允许的IP访问', + 'denyIp' => '禁止的IP访问', + 'confirm' => ':attribute和确认字段:2不一致', + 'different' => ':attribute和比较字段:2不能相同', + 'egt' => ':attribute必须大于等于 :rule', + 'gt' => ':attribute必须大于 :rule', + 'elt' => ':attribute必须小于等于 :rule', + 'lt' => ':attribute必须小于 :rule', + 'eq' => ':attribute必须等于 :rule', + 'unique' => ':attribute已存在', + 'regex' => ':attribute不符合指定规则', + 'method' => '无效的请求类型', + 'token' => '令牌数据无效', + 'fileSize' => '上传文件大小不符', + 'fileExt' => '上传文件后缀不符', + 'fileMime' => '上传文件类型不符', + + ]; + + // 当前验证场景 + protected $currentScene = null; + + // 正则表达式 regex = ['zip'=>'\d{6}',...] + protected $regex = []; + + // 验证场景 scene = ['edit'=>'name1,name2,...'] + protected $scene = []; + + // 验证失败错误信息 + protected $error = []; + + // 批量验证 + protected $batch = false; + + /** + * 构造函数 + * @access public + * @param array $rules 验证规则 + * @param array $message 验证提示信息 + * @param array $field 验证字段描述信息 + */ + public function __construct(array $rules = [], $message = [], $field = []) + { + $this->rule = array_merge($this->rule, $rules); + $this->message = array_merge($this->message, $message); + $this->field = array_merge($this->field, $field); + } + + /** + * 实例化验证 + * @access public + * @param array $rules 验证规则 + * @param array $message 验证提示信息 + * @param array $field 验证字段描述信息 + * @return Validate + */ + public static function make($rules = [], $message = [], $field = []) + { + if (is_null(self::$instance)) { + self::$instance = new self($rules, $message, $field); + } + return self::$instance; + } + + /** + * 添加字段验证规则 + * @access protected + * @param string|array $name 字段名称或者规则数组 + * @param mixed $rule 验证规则 + * @return Validate + */ + public function rule($name, $rule = '') + { + if (is_array($name)) { + $this->rule = array_merge($this->rule, $name); + } else { + $this->rule[$name] = $rule; + } + return $this; + } + + /** + * 注册验证(类型)规则 + * @access public + * @param string $type 验证规则类型 + * @param mixed $callback callback方法(或闭包) + * @return void + */ + public static function extend($type, $callback = null) + { + if (is_array($type)) { + self::$type = array_merge(self::$type, $type); + } else { + self::$type[$type] = $callback; + } + } + + /** + * 设置验证规则的默认提示信息 + * @access protected + * @param string|array $type 验证规则类型名称或者数组 + * @param string $msg 验证提示信息 + * @return void + */ + public static function setTypeMsg($type, $msg = null) + { + if (is_array($type)) { + self::$typeMsg = array_merge(self::$typeMsg, $type); + } else { + self::$typeMsg[$type] = $msg; + } + } + + /** + * 设置提示信息 + * @access public + * @param string|array $name 字段名称 + * @param string $message 提示信息 + * @return Validate + */ + public function message($name, $message = '') + { + if (is_array($name)) { + $this->message = array_merge($this->message, $name); + } else { + $this->message[$name] = $message; + } + return $this; + } + + /** + * 设置验证场景 + * @access public + * @param string|array $name 场景名或者场景设置数组 + * @param mixed $fields 要验证的字段 + * @return Validate + */ + public function scene($name, $fields = null) + { + if (is_array($name)) { + $this->scene = array_merge($this->scene, $name); + }if (is_null($fields)) { + // 设置当前场景 + $this->currentScene = $name; + } else { + // 设置验证场景 + $this->scene[$name] = $fields; + } + return $this; + } + + /** + * 判断是否存在某个验证场景 + * @access public + * @param string $name 场景名 + * @return bool + */ + public function hasScene($name) + { + return isset($this->scene[$name]); + } + + /** + * 设置批量验证 + * @access public + * @param bool $batch 是否批量验证 + * @return Validate + */ + public function batch($batch = true) + { + $this->batch = $batch; + return $this; + } + + /** + * 数据自动验证 + * @access public + * @param array $data 数据 + * @param mixed $rules 验证规则 + * @param string $scene 验证场景 + * @return bool + */ + public function check($data, $rules = [], $scene = '') + { + $this->error = []; + + if (empty($rules)) { + // 读取验证规则 + $rules = $this->rule; + } + + // 分析验证规则 + $scene = $this->getScene($scene); + if (is_array($scene)) { + // 处理场景验证字段 + $change = []; + $array = []; + foreach ($scene as $k => $val) { + if (is_numeric($k)) { + $array[] = $val; + } else { + $array[] = $k; + $change[$k] = $val; + } + } + } + + foreach ($rules as $key => $item) { + // field => rule1|rule2... field=>['rule1','rule2',...] + if (is_numeric($key)) { + // [field,rule1|rule2,msg1|msg2] + $key = $item[0]; + $rule = $item[1]; + if (isset($item[2])) { + $msg = is_string($item[2]) ? explode('|', $item[2]) : $item[2]; + } else { + $msg = []; + } + } else { + $rule = $item; + $msg = []; + } + if (strpos($key, '|')) { + // 字段|描述 用于指定属性名称 + list($key, $title) = explode('|', $key); + } else { + $title = isset($this->field[$key]) ? $this->field[$key] : $key; + } + + // 场景检测 + if (!empty($scene)) { + if ($scene instanceof \Closure && !call_user_func_array($scene, [$key, $data])) { + continue; + } elseif (is_array($scene)) { + if (!in_array($key, $array)) { + continue; + } elseif (isset($change[$key])) { + // 重载某个验证规则 + $rule = $change[$key]; + } + } + } + + // 获取数据 支持二维数组 + $value = $this->getDataValue($data, $key); + + // 字段验证 + if ($rule instanceof \Closure) { + // 匿名函数验证 支持传入当前字段和所有字段两个数据 + $result = call_user_func_array($rule, [$value, $data]); + } else { + $result = $this->checkItem($key, $value, $rule, $data, $title, $msg); + } + + if (true !== $result) { + // 没有返回true 则表示验证失败 + if (!empty($this->batch)) { + // 批量验证 + if (is_array($result)) { + $this->error = array_merge($this->error, $result); + } else { + $this->error[$key] = $result; + } + } else { + $this->error = $result; + return false; + } + } + } + return !empty($this->error) ? false : true; + } + + /** + * 验证单个字段规则 + * @access protected + * @param string $field 字段名 + * @param mixed $value 字段值 + * @param mixed $rules 验证规则 + * @param array $data 数据 + * @param string $title 字段描述 + * @param array $msg 提示信息 + * @return mixed + */ + protected function checkItem($field, $value, $rules, $data, $title = '', $msg = []) + { + // 支持多规则验证 require|in:a,b,c|... 或者 ['require','in'=>'a,b,c',...] + if (is_string($rules)) { + $rules = explode('|', $rules); + } + $i = 0; + foreach ($rules as $key => $rule) { + if ($rule instanceof \Closure) { + $result = call_user_func_array($rule, [$value, $data]); + $info = is_numeric($key) ? '' : $key; + } else { + // 判断验证类型 + if (is_numeric($key)) { + if (strpos($rule, ':')) { + list($type, $rule) = explode(':', $rule, 2); + if (isset($this->alias[$type])) { + // 判断别名 + $type = $this->alias[$type]; + } + $info = $type; + } elseif (method_exists($this, $rule)) { + $type = $rule; + $info = $rule; + $rule = ''; + } else { + $type = 'is'; + $info = $rule; + } + } else { + $info = $type = $key; + } + + // 如果不是require 有数据才会行验证 + if (0 === strpos($info, 'require') || (!is_null($value) && '' !== $value)) { + // 验证类型 + $callback = isset(self::$type[$type]) ? self::$type[$type] : [$this, $type]; + // 验证数据 + $result = call_user_func_array($callback, [$value, $rule, $data, $field, $title]); + } else { + $result = true; + } + } + + if (false === $result) { + // 验证失败 返回错误信息 + if (isset($msg[$i])) { + $message = $msg[$i]; + if (is_string($message) && strpos($message, '{%') === 0) { + $message = Lang::get(substr($message, 2, -1)); + } + } else { + $message = $this->getRuleMsg($field, $title, $info, $rule); + } + return $message; + } elseif (true !== $result) { + // 返回自定义错误信息 + if (is_string($result) && false !== strpos($result, ':')) { + $result = str_replace([':attribute', ':rule'], [$title, (string) $rule], $result); + } + return $result; + } + $i++; + } + return $result; + } + + /** + * 验证是否和某个字段的值一致 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @param string $field 字段名 + * @return bool + */ + protected function confirm($value, $rule, $data, $field = '') + { + if ('' == $rule) { + if (strpos($field, '_confirm')) { + $rule = strstr($field, '_confirm', true); + } else { + $rule = $field . '_confirm'; + } + } + return $this->getDataValue($data, $rule) === $value; + } + + /** + * 验证是否和某个字段的值是否不同 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function different($value, $rule, $data) + { + return $this->getDataValue($data, $rule) != $value; + } + + /** + * 验证是否大于等于某个值 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function egt($value, $rule, $data) + { + $val = $this->getDataValue($data, $rule); + return !is_null($val) && $value >= $val; + } + + /** + * 验证是否大于某个值 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function gt($value, $rule, $data) + { + $val = $this->getDataValue($data, $rule); + return !is_null($val) && $value > $val; + } + + /** + * 验证是否小于等于某个值 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function elt($value, $rule, $data) + { + $val = $this->getDataValue($data, $rule); + return !is_null($val) && $value <= $val; + } + + /** + * 验证是否小于某个值 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function lt($value, $rule, $data) + { + $val = $this->getDataValue($data, $rule); + return !is_null($val) && $value < $val; + } + + /** + * 验证是否等于某个值 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function eq($value, $rule) + { + return $value == $rule; + } + + /** + * 验证字段值是否为有效格式 + * @access protected + * @param mixed $value 字段值 + * @param string $rule 验证规则 + * @param array $data 验证数据 + * @return bool + */ + protected function is($value, $rule, $data = []) + { + switch ($rule) { + case 'require': + // 必须 + $result = !empty($value) || '0' == $value; + break; + case 'accepted': + // 接受 + $result = in_array($value, ['1', 'on', 'yes']); + break; + case 'date': + // 是否是一个有效日期 + $result = false !== strtotime($value); + break; + case 'alpha': + // 只允许字母 + $result = $this->regex($value, '/^[A-Za-z]+$/'); + break; + case 'alphaNum': + // 只允许字母和数字 + $result = $this->regex($value, '/^[A-Za-z0-9]+$/'); + break; + case 'alphaDash': + // 只允许字母、数字和下划线 破折号 + $result = $this->regex($value, '/^[A-Za-z0-9\-\_]+$/'); + break; + case 'chs': + // 只允许汉字 + $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}]+$/u'); + break; + case 'chsAlpha': + // 只允许汉字、字母 + $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}a-zA-Z]+$/u'); + break; + case 'chsAlphaNum': + // 只允许汉字、字母和数字 + $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9]+$/u'); + break; + case 'chsDash': + // 只允许汉字、字母、数字和下划线_及破折号- + $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9\_\-]+$/u'); + break; + case 'activeUrl': + // 是否为有效的网址 + $result = checkdnsrr($value); + break; + case 'ip': + // 是否为IP地址 + $result = $this->filter($value, [FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6]); + break; + case 'url': + // 是否为一个URL地址 + $result = $this->filter($value, FILTER_VALIDATE_URL); + break; + case 'float': + // 是否为float + $result = $this->filter($value, FILTER_VALIDATE_FLOAT); + break; + case 'number': + $result = is_numeric($value); + break; + case 'integer': + // 是否为整型 + $result = $this->filter($value, FILTER_VALIDATE_INT); + break; + case 'email': + // 是否为邮箱地址 + $result = $this->filter($value, FILTER_VALIDATE_EMAIL); + break; + case 'boolean': + // 是否为布尔值 + $result = in_array($value, [true, false, 0, 1, '0', '1'], true); + break; + case 'array': + // 是否为数组 + $result = is_array($value); + break; + case 'file': + $result = $value instanceof File; + break; + case 'image': + $result = $value instanceof File && in_array($this->getImageType($value->getRealPath()), [1, 2, 3, 6]); + break; + case 'token': + $result = $this->token($value, '__token__', $data); + break; + default: + if (isset(self::$type[$rule])) { + // 注册的验证规则 + $result = call_user_func_array(self::$type[$rule], [$value]); + } else { + // 正则验证 + $result = $this->regex($value, $rule); + } + } + return $result; + } + + // 判断图像类型 + protected function getImageType($image) + { + if (function_exists('exif_imagetype')) { + return exif_imagetype($image); + } else { + $info = getimagesize($image); + return $info[2]; + } + } + + /** + * 验证是否为合格的域名或者IP 支持A,MX,NS,SOA,PTR,CNAME,AAAA,A6, SRV,NAPTR,TXT 或者 ANY类型 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function activeUrl($value, $rule) + { + if (!in_array($rule, ['A', 'MX', 'NS', 'SOA', 'PTR', 'CNAME', 'AAAA', 'A6', 'SRV', 'NAPTR', 'TXT', 'ANY'])) { + $rule = 'MX'; + } + return checkdnsrr($value, $rule); + } + + /** + * 验证是否有效IP + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 ipv4 ipv6 + * @return bool + */ + protected function ip($value, $rule) + { + if (!in_array($rule, ['ipv4', 'ipv6'])) { + $rule = 'ipv4'; + } + return $this->filter($value, [FILTER_VALIDATE_IP, 'ipv6' == $rule ? FILTER_FLAG_IPV6 : FILTER_FLAG_IPV4]); + } + + /** + * 验证上传文件后缀 + * @access protected + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function fileExt($file, $rule) + { + if (!($file instanceof File)) { + return false; + } + if (is_string($rule)) { + $rule = explode(',', $rule); + } + if (is_array($file)) { + foreach ($file as $item) { + if (!$item->checkExt($rule)) { + return false; + } + } + return true; + } else { + return $file->checkExt($rule); + } + } + + /** + * 验证上传文件类型 + * @access protected + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function fileMime($file, $rule) + { + if (!($file instanceof File)) { + return false; + } + if (is_string($rule)) { + $rule = explode(',', $rule); + } + if (is_array($file)) { + foreach ($file as $item) { + if (!$item->checkMime($rule)) { + return false; + } + } + return true; + } else { + return $file->checkMime($rule); + } + } + + /** + * 验证上传文件大小 + * @access protected + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function fileSize($file, $rule) + { + if (!($file instanceof File)) { + return false; + } + if (is_array($file)) { + foreach ($file as $item) { + if (!$item->checkSize($rule)) { + return false; + } + } + return true; + } else { + return $file->checkSize($rule); + } + } + + /** + * 验证图片的宽高及类型 + * @access protected + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function image($file, $rule) + { + if (!($file instanceof File)) { + return false; + } + if ($rule) { + $rule = explode(',', $rule); + list($width, $height, $type) = getimagesize($file->getRealPath()); + if (isset($rule[2])) { + $imageType = strtolower($rule[2]); + if ('jpeg' == $imageType) { + $imageType = 'jpg'; + } + if (image_type_to_extension($type, false) != $imageType) { + return false; + } + } + + list($w, $h) = $rule; + return $w == $width && $h == $height; + } else { + return in_array($this->getImageType($file->getRealPath()), [1, 2, 3, 6]); + } + } + + /** + * 验证请求类型 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function method($value, $rule) + { + $method = Request::instance()->method(); + return strtoupper($rule) == $method; + } + + /** + * 验证时间和日期是否符合指定格式 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function dateFormat($value, $rule) + { + $info = date_parse_from_format($rule, $value); + return 0 == $info['warning_count'] && 0 == $info['error_count']; + } + + /** + * 验证是否唯一 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 格式:数据表,字段名,排除ID,主键名 + * @param array $data 数据 + * @param string $field 验证字段名 + * @return bool + */ + protected function unique($value, $rule, $data, $field) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + if (false !== strpos($rule[0], '\\')) { + // 指定模型类 + $db = new $rule[0]; + } else { + try { + $db = Loader::model($rule[0]); + } catch (ClassNotFoundException $e) { + $db = Db::name($rule[0]); + } + } + $key = isset($rule[1]) ? $rule[1] : $field; + + if (strpos($key, '^')) { + // 支持多个字段验证 + $fields = explode('^', $key); + foreach ($fields as $key) { + $map[$key] = $data[$key]; + } + } elseif (strpos($key, '=')) { + parse_str($key, $map); + } else { + $map[$key] = $data[$field]; + } + + $pk = strval(isset($rule[3]) ? $rule[3] : $db->getPk()); + if (isset($rule[2])) { + $map[$pk] = ['neq', $rule[2]]; + } elseif (isset($data[$pk])) { + $map[$pk] = ['neq', $data[$pk]]; + } + + if ($db->where($map)->field($pk)->find()) { + return false; + } + return true; + } + + /** + * 使用行为类验证 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return mixed + */ + protected function behavior($value, $rule, $data) + { + return Hook::exec($rule, '', $data); + } + + /** + * 使用filter_var方式验证 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function filter($value, $rule) + { + if (is_string($rule) && strpos($rule, ',')) { + list($rule, $param) = explode(',', $rule); + } elseif (is_array($rule)) { + $param = isset($rule[1]) ? $rule[1] : null; + $rule = $rule[0]; + } else { + $param = null; + } + return false !== filter_var($value, is_int($rule) ? $rule : filter_id($rule), $param); + } + + /** + * 验证某个字段等于某个值的时候必须 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function requireIf($value, $rule, $data) + { + list($field, $val) = explode(',', $rule); + if ($this->getDataValue($data, $field) == $val) { + return !empty($value); + } else { + return true; + } + } + + /** + * 通过回调方法验证某个字段是否必须 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function requireCallback($value, $rule, $data) + { + $result = call_user_func_array($rule, [$value, $data]); + if ($result) { + return !empty($value); + } else { + return true; + } + } + + /** + * 验证某个字段有值的情况下必须 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function requireWith($value, $rule, $data) + { + $val = $this->getDataValue($data, $rule); + if (!empty($val)) { + return !empty($value); + } else { + return true; + } + } + + /** + * 验证是否在范围内 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function in($value, $rule) + { + return in_array($value, is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * 验证是否不在某个范围 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function notIn($value, $rule) + { + return !in_array($value, is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * between验证数据 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function between($value, $rule) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + list($min, $max) = $rule; + return $value >= $min && $value <= $max; + } + + /** + * 使用notbetween验证数据 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function notBetween($value, $rule) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + list($min, $max) = $rule; + return $value < $min || $value > $max; + } + + /** + * 验证数据长度 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function length($value, $rule) + { + if (is_array($value)) { + $length = count($value); + } elseif ($value instanceof File) { + $length = $value->getSize(); + } else { + $length = mb_strlen((string) $value); + } + + if (strpos($rule, ',')) { + // 长度区间 + list($min, $max) = explode(',', $rule); + return $length >= $min && $length <= $max; + } else { + // 指定长度 + return $length == $rule; + } + } + + /** + * 验证数据最大长度 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function max($value, $rule) + { + if (is_array($value)) { + $length = count($value); + } elseif ($value instanceof File) { + $length = $value->getSize(); + } else { + $length = mb_strlen((string) $value); + } + return $length <= $rule; + } + + /** + * 验证数据最小长度 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function min($value, $rule) + { + if (is_array($value)) { + $length = count($value); + } elseif ($value instanceof File) { + $length = $value->getSize(); + } else { + $length = mb_strlen((string) $value); + } + return $length >= $rule; + } + + /** + * 验证日期 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function after($value, $rule) + { + return strtotime($value) >= strtotime($rule); + } + + /** + * 验证日期 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function before($value, $rule) + { + return strtotime($value) <= strtotime($rule); + } + + /** + * 验证有效期 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function expire($value, $rule) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + list($start, $end) = $rule; + if (!is_numeric($start)) { + $start = strtotime($start); + } + + if (!is_numeric($end)) { + $end = strtotime($end); + } + return $_SERVER['REQUEST_TIME'] >= $start && $_SERVER['REQUEST_TIME'] <= $end; + } + + /** + * 验证IP许可 + * @access protected + * @param string $value 字段值 + * @param mixed $rule 验证规则 + * @return mixed + */ + protected function allowIp($value, $rule) + { + return in_array($_SERVER['REMOTE_ADDR'], is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * 验证IP禁用 + * @access protected + * @param string $value 字段值 + * @param mixed $rule 验证规则 + * @return mixed + */ + protected function denyIp($value, $rule) + { + return !in_array($_SERVER['REMOTE_ADDR'], is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * 使用正则验证数据 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 正则规则或者预定义正则名 + * @return mixed + */ + protected function regex($value, $rule) + { + if (isset($this->regex[$rule])) { + $rule = $this->regex[$rule]; + } + if (0 !== strpos($rule, '/') && !preg_match('/\/[imsU]{0,4}$/', $rule)) { + // 不是正则表达式则两端补上/ + $rule = '/^' . $rule . '$/'; + } + return 1 === preg_match($rule, (string) $value); + } + + /** + * 验证表单令牌 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function token($value, $rule, $data) + { + $rule = !empty($rule) ? $rule : '__token__'; + if (!isset($data[$rule]) || !Session::has($rule)) { + // 令牌数据无效 + return false; + } + + // 令牌验证 + if (isset($data[$rule]) && Session::get($rule) === $data[$rule]) { + // 防止重复提交 + Session::delete($rule); // 验证完成销毁session + return true; + } + // 开启TOKEN重置 + Session::delete($rule); + return false; + } + + // 获取错误信息 + public function getError() + { + return $this->error; + } + + /** + * 获取数据值 + * @access protected + * @param array $data 数据 + * @param string $key 数据标识 支持二维 + * @return mixed + */ + protected function getDataValue($data, $key) + { + if (is_numeric($key)) { + $value = $key; + } elseif (strpos($key, '.')) { + // 支持二维数组验证 + list($name1, $name2) = explode('.', $key); + $value = isset($data[$name1][$name2]) ? $data[$name1][$name2] : null; + } else { + $value = isset($data[$key]) ? $data[$key] : null; + } + return $value; + } + + /** + * 获取验证规则的错误提示信息 + * @access protected + * @param string $attribute 字段英文名 + * @param string $title 字段描述名 + * @param string $type 验证规则名称 + * @param mixed $rule 验证规则数据 + * @return string + */ + protected function getRuleMsg($attribute, $title, $type, $rule) + { + if (isset($this->message[$attribute . '.' . $type])) { + $msg = $this->message[$attribute . '.' . $type]; + } elseif (isset($this->message[$attribute][$type])) { + $msg = $this->message[$attribute][$type]; + } elseif (isset($this->message[$attribute])) { + $msg = $this->message[$attribute]; + } elseif (isset(self::$typeMsg[$type])) { + $msg = self::$typeMsg[$type]; + } else { + $msg = $title . '规则错误'; + } + + if (is_string($msg) && 0 === strpos($msg, '{%')) { + $msg = Lang::get(substr($msg, 2, -1)); + } + + if (is_string($msg) && is_scalar($rule) && false !== strpos($msg, ':')) { + // 变量替换 + if (is_string($rule) && strpos($rule, ',')) { + $array = array_pad(explode(',', $rule), 3, ''); + } else { + $array = array_pad([], 3, ''); + } + $msg = str_replace( + [':attribute', ':rule', ':1', ':2', ':3'], + [$title, (string) $rule, $array[0], $array[1], $array[2]], + $msg); + } + return $msg; + } + + /** + * 获取数据验证的场景 + * @access protected + * @param string $scene 验证场景 + * @return array + */ + protected function getScene($scene = '') + { + if (empty($scene)) { + // 读取指定场景 + $scene = $this->currentScene; + } + + if (!empty($scene) && isset($this->scene[$scene])) { + // 如果设置了验证适用场景 + $scene = $this->scene[$scene]; + if (is_string($scene)) { + $scene = explode(',', $scene); + } + } else { + $scene = []; + } + return $scene; + } + + public static function __callStatic($method, $params) + { + $class = self::make(); + if (method_exists($class, $method)) { + return call_user_func_array([$class, $method], $params); + } else { + throw new \BadMethodCallException('method not exists:' . __CLASS__ . '->' . $method); + } + } +} diff --git a/thinkphp/library/think/View.php b/thinkphp/library/think/View.php new file mode 100644 index 0000000..96ae56c --- /dev/null +++ b/thinkphp/library/think/View.php @@ -0,0 +1,236 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class View +{ + // 视图实例 + protected static $instance; + // 模板引擎实例 + public $engine; + // 模板变量 + protected $data = []; + // 用于静态赋值的模板变量 + protected static $var = []; + // 视图输出替换 + protected $replace = []; + + /** + * 构造函数 + * @access public + * @param array $engine 模板引擎参数 + * @param array $replace 字符串替换参数 + */ + public function __construct($engine = [], $replace = []) + { + // 初始化模板引擎 + $this->engine((array) $engine); + // 基础替换字符串 + $request = Request::instance(); + $base = $request->root(); + $root = strpos($base, '.') ? ltrim(dirname($base), DS) : $base; + if ('' != $root) { + $root = '/' . ltrim($root, '/'); + } + $baseReplace = [ + '__ROOT__' => $root, + '__URL__' => $base . '/' . $request->module() . '/' . Loader::parseName($request->controller()), + '__STATIC__' => $root . '/static', + '__CSS__' => $root . '/static/css', + '__JS__' => $root . '/static/js', + ]; + $this->replace = array_merge($baseReplace, (array) $replace); + } + + /** + * 初始化视图 + * @access public + * @param array $engine 模板引擎参数 + * @param array $replace 字符串替换参数 + * @return object + */ + public static function instance($engine = [], $replace = []) + { + if (is_null(self::$instance)) { + self::$instance = new self($engine, $replace); + } + return self::$instance; + } + + /** + * 模板变量静态赋值 + * @access public + * @param mixed $name 变量名 + * @param mixed $value 变量值 + * @return void + */ + public static function share($name, $value = '') + { + if (is_array($name)) { + self::$var = array_merge(self::$var, $name); + } else { + self::$var[$name] = $value; + } + } + + /** + * 模板变量赋值 + * @access public + * @param mixed $name 变量名 + * @param mixed $value 变量值 + * @return $this + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->data = array_merge($this->data, $name); + } else { + $this->data[$name] = $value; + } + return $this; + } + + /** + * 设置当前模板解析的引擎 + * @access public + * @param array|string $options 引擎参数 + * @return $this + */ + public function engine($options = []) + { + if (is_string($options)) { + $type = $options; + $options = []; + } else { + $type = !empty($options['type']) ? $options['type'] : 'Think'; + } + + $class = false !== strpos($type, '\\') ? $type : '\\think\\view\\driver\\' . ucfirst($type); + if (isset($options['type'])) { + unset($options['type']); + } + $this->engine = new $class($options); + return $this; + } + + /** + * 配置模板引擎 + * @access private + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return void + */ + public function config($name, $value = null) + { + $this->engine->config($name, $value); + return $this; + } + + /** + * 解析和获取模板内容 用于输出 + * @param string $template 模板文件名或者内容 + * @param array $vars 模板输出变量 + * @param array $replace 替换内容 + * @param array $config 模板参数 + * @param bool $renderContent 是否渲染内容 + * @return string + * @throws Exception + */ + public function fetch($template = '', $vars = [], $replace = [], $config = [], $renderContent = false) + { + // 模板变量 + $vars = array_merge(self::$var, $this->data, $vars); + + // 页面缓存 + ob_start(); + ob_implicit_flush(0); + + // 渲染输出 + $method = $renderContent ? 'display' : 'fetch'; + $this->engine->$method($template, $vars, $config); + + // 获取并清空缓存 + $content = ob_get_clean(); + // 内容过滤标签 + Hook::listen('view_filter', $content); + // 允许用户自定义模板的字符串替换 + $replace = array_merge($this->replace, $replace); + if (!empty($replace)) { + $content = strtr($content, $replace); + } + return $content; + } + + /** + * 视图内容替换 + * @access public + * @param string|array $content 被替换内容(支持批量替换) + * @param string $replace 替换内容 + * @return $this + */ + public function replace($content, $replace = '') + { + if (is_array($content)) { + $this->replace = array_merge($this->replace, $content); + } else { + $this->replace[$content] = $replace; + } + return $this; + } + + /** + * 渲染内容输出 + * @access public + * @param string $content 内容 + * @param array $vars 模板输出变量 + * @param array $replace 替换内容 + * @param array $config 模板参数 + * @return mixed + */ + public function display($content, $vars = [], $replace = [], $config = []) + { + return $this->fetch($content, $vars, $replace, $config, true); + } + + /** + * 模板变量赋值 + * @access public + * @param string $name 变量名 + * @param mixed $value 变量值 + */ + public function __set($name, $value) + { + $this->data[$name] = $value; + } + + /** + * 取得模板显示变量的值 + * @access protected + * @param string $name 模板变量 + * @return mixed + */ + public function __get($name) + { + return $this->data[$name]; + } + + /** + * 检测模板变量是否设置 + * @access public + * @param string $name 模板变量名 + * @return bool + */ + public function __isset($name) + { + return isset($this->data[$name]); + } +} diff --git a/thinkphp/library/think/cache/Driver.php b/thinkphp/library/think/cache/Driver.php new file mode 100644 index 0000000..ab48bdd --- /dev/null +++ b/thinkphp/library/think/cache/Driver.php @@ -0,0 +1,210 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache; + +/** + * 缓存基础类 + */ +abstract class Driver +{ + protected $handler = null; + protected $options = []; + protected $tag; + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + abstract public function has($name); + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + abstract public function get($name, $default = false); + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return boolean + */ + abstract public function set($name, $value, $expire = null); + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + abstract public function inc($name, $step = 1); + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + abstract public function dec($name, $step = 1); + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + abstract public function rm($name); + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + abstract public function clear($tag = null); + + /** + * 获取实际的缓存标识 + * @access public + * @param string $name 缓存名 + * @return string + */ + protected function getCacheKey($name) + { + return $this->options['prefix'] . $name; + } + + /** + * 读取缓存并删除 + * @access public + * @param string $name 缓存变量名 + * @return mixed + */ + public function pull($name) + { + $result = $this->get($name, false); + if ($result) { + $this->rm($name); + return $result; + } else { + return; + } + } + + /** + * 如果不存在则写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return mixed + */ + public function remember($name, $value, $expire = null) + { + if (!$this->has($name)) { + if ($value instanceof \Closure) { + $value = call_user_func($value); + } + $this->set($name, $value, $expire); + } else { + $value = $this->get($name); + } + return $value; + } + + /** + * 缓存标签 + * @access public + * @param string $name 标签名 + * @param string|array $keys 缓存标识 + * @param bool $overlay 是否覆盖 + * @return $this + */ + public function tag($name, $keys = null, $overlay = false) + { + if (is_null($keys)) { + $this->tag = $name; + } else { + $key = 'tag_' . md5($name); + if (is_string($keys)) { + $keys = explode(',', $keys); + } + $keys = array_map([$this, 'getCacheKey'], $keys); + if ($overlay) { + $value = $keys; + } else { + $value = array_unique(array_merge($this->getTagItem($name), $keys)); + } + $this->set($key, implode(',', $value)); + } + return $this; + } + + /** + * 更新标签 + * @access public + * @param string $name 缓存标识 + * @return void + */ + protected function setTagItem($name) + { + if ($this->tag) { + $key = 'tag_' . md5($this->tag); + $this->tag = null; + if ($this->has($key)) { + $value = explode(',', $this->get($key)); + $value[] = $name; + $value = implode(',', array_unique($value)); + } else { + $value = $name; + } + $this->set($key, $value); + } + } + + /** + * 获取标签包含的缓存标识 + * @access public + * @param string $tag 缓存标签 + * @return array + */ + protected function getTagItem($tag) + { + $key = 'tag_' . md5($tag); + $value = $this->get($key); + if ($value) { + return explode(',', $value); + } else { + return []; + } + } + + /** + * 返回句柄对象,可执行其它高级方法 + * + * @access public + * @return object + */ + public function handler() + { + return $this->handler; + } +} diff --git a/thinkphp/library/think/cache/driver/File.php b/thinkphp/library/think/cache/driver/File.php new file mode 100644 index 0000000..5aadb2b --- /dev/null +++ b/thinkphp/library/think/cache/driver/File.php @@ -0,0 +1,248 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * 文件类型缓存类 + * @author liu21st + */ +class File extends Driver +{ + protected $options = [ + 'expire' => 0, + 'cache_subdir' => true, + 'prefix' => '', + 'path' => CACHE_PATH, + 'data_compress' => false, + ]; + + /** + * 构造函数 + * @param array $options + */ + public function __construct($options = []) + { + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + if (substr($this->options['path'], -1) != DS) { + $this->options['path'] .= DS; + } + $this->init(); + } + + /** + * 初始化检查 + * @access private + * @return boolean + */ + private function init() + { + // 创建项目缓存目录 + if (!is_dir($this->options['path'])) { + if (mkdir($this->options['path'], 0755, true)) { + return true; + } + } + return false; + } + + /** + * 取得变量的存储文件名 + * @access protected + * @param string $name 缓存变量名 + * @return string + */ + protected function getCacheKey($name) + { + $name = md5($name); + if ($this->options['cache_subdir']) { + // 使用子目录 + $name = substr($name, 0, 2) . DS . substr($name, 2); + } + if ($this->options['prefix']) { + $name = $this->options['prefix'] . DS . $name; + } + $filename = $this->options['path'] . $name . '.php'; + $dir = dirname($filename); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + return $filename; + } + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + return $this->get($name) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $filename = $this->getCacheKey($name); + if (!is_file($filename)) { + return $default; + } + $content = file_get_contents($filename); + if (false !== $content) { + $expire = (int) substr($content, 8, 12); + if (0 != $expire && $_SERVER['REQUEST_TIME'] > filemtime($filename) + $expire) { + //缓存过期删除缓存文件 + $this->unlink($filename); + return $default; + } + $content = substr($content, 20, -3); + if ($this->options['data_compress'] && function_exists('gzcompress')) { + //启用数据压缩 + $content = gzuncompress($content); + } + $content = unserialize($content); + return $content; + } else { + return $default; + } + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return boolean + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + $filename = $this->getCacheKey($name); + if ($this->tag && !is_file($filename)) { + $first = true; + } + $data = serialize($value); + if ($this->options['data_compress'] && function_exists('gzcompress')) { + //数据压缩 + $data = gzcompress($data, 3); + } + $data = ""; + $result = file_put_contents($filename, $data); + if ($result) { + isset($first) && $this->setTagItem($filename); + clearstatcache(); + return true; + } else { + return false; + } + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) + $step; + } else { + $value = $step; + } + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) - $step; + } else { + $value = $step; + } + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + return $this->unlink($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + $this->unlink($key); + } + $this->rm('tag_' . md5($tag)); + return true; + } + $files = (array) glob($this->options['path'] . ($this->options['prefix'] ? $this->options['prefix'] . DS : '') . '*'); + foreach ($files as $path) { + if (is_dir($path)) { + array_map('unlink', glob($path . '/*.php')); + rmdir($path); + } else { + unlink($path); + } + } + return true; + } + + /** + * 判断文件是否存在后,删除 + * @param $path + * @return bool + * @author byron sampson + * @return boolean + */ + private function unlink($path) + { + return is_file($path) && unlink($path); + } + +} diff --git a/thinkphp/library/think/cache/driver/Lite.php b/thinkphp/library/think/cache/driver/Lite.php new file mode 100644 index 0000000..eeb96a1 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Lite.php @@ -0,0 +1,185 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * 文件类型缓存类 + * @author liu21st + */ +class Lite extends Driver +{ + protected $options = [ + 'prefix' => '', + 'path' => '', + 'expire' => 0, // 等于 10*365*24*3600(10年) + ]; + + /** + * 构造函数 + * @access public + * + * @param array $options + */ + public function __construct($options = []) + { + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + if (substr($this->options['path'], -1) != DS) { + $this->options['path'] .= DS; + } + + } + + /** + * 取得变量的存储文件名 + * @access protected + * @param string $name 缓存变量名 + * @return string + */ + protected function getCacheKey($name) + { + return $this->options['path'] . $this->options['prefix'] . md5($name) . '.php'; + } + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return mixed + */ + public function has($name) + { + return $this->get($name) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $filename = $this->getCacheKey($name); + if (is_file($filename)) { + // 判断是否过期 + $mtime = filemtime($filename); + if ($mtime < $_SERVER['REQUEST_TIME']) { + // 清除已经过期的文件 + unlink($filename); + return $default; + } + return include $filename; + } else { + return $default; + } + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return bool + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + // 模拟永久 + if (0 === $expire) { + $expire = 10 * 365 * 24 * 3600; + } + $filename = $this->getCacheKey($name); + if ($this->tag && !is_file($filename)) { + $first = true; + } + $ret = file_put_contents($filename, ("setTagItem($filename); + touch($filename, $_SERVER['REQUEST_TIME'] + $expire); + } + return $ret; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) + $step; + } else { + $value = $step; + } + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) - $step; + } else { + $value = $step; + } + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + return unlink($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return bool + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + unlink($key); + } + $this->rm('tag_' . md5($tag)); + return true; + } + array_map("unlink", glob($this->options['path'] . ($this->options['prefix'] ? $this->options['prefix'] . DS : '') . '*.php')); + } +} diff --git a/thinkphp/library/think/cache/driver/Memcache.php b/thinkphp/library/think/cache/driver/Memcache.php new file mode 100644 index 0000000..e464f38 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Memcache.php @@ -0,0 +1,174 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +class Memcache extends Driver +{ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'expire' => 0, + 'timeout' => 0, // 超时时间(单位:毫秒) + 'persistent' => true, + 'prefix' => '', + ]; + + /** + * 构造函数 + * @param array $options 缓存参数 + * @access public + * @throws \BadFunctionCallException + */ + public function __construct($options = []) + { + if (!extension_loaded('memcache')) { + throw new \BadFunctionCallException('not support: memcache'); + } + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + $this->handler = new \Memcache; + // 支持集群 + $hosts = explode(',', $this->options['host']); + $ports = explode(',', $this->options['port']); + if (empty($ports[0])) { + $ports[0] = 11211; + } + // 建立连接 + foreach ((array) $hosts as $i => $host) { + $port = isset($ports[$i]) ? $ports[$i] : $ports[0]; + $this->options['timeout'] > 0 ? + $this->handler->addServer($host, $port, $this->options['persistent'], 1, $this->options['timeout']) : + $this->handler->addServer($host, $port, $this->options['persistent'], 1); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + return $this->handler->get($key) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $result = $this->handler->get($this->getCacheKey($name)); + return false !== $result ? $result : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) + * @return bool + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + if ($this->tag && !$this->has($name)) { + $first = true; + } + $key = $this->getCacheKey($name); + if ($this->handler->set($key, $value, 0, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $key = $this->getCacheKey($name); + if ($this->handler->get($key)) { + return $this->handler->increment($key, $step); + } + return $this->handler->set($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $key = $this->getCacheKey($name); + $value = $this->handler->get($key) - $step; + $res = $this->handler->set($key, $value); + if (!$res) { + return false; + } else { + return $value; + } + } + + /** + * 删除缓存 + * @param string $name 缓存变量名 + * @param bool|false $ttl + * @return bool + */ + public function rm($name, $ttl = false) + { + $key = $this->getCacheKey($name); + return false === $ttl ? + $this->handler->delete($key) : + $this->handler->delete($key, $ttl); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return bool + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + $this->handler->delete($key); + } + $this->rm('tag_' . md5($tag)); + return true; + } + return $this->handler->flush(); + } +} diff --git a/thinkphp/library/think/cache/driver/Memcached.php b/thinkphp/library/think/cache/driver/Memcached.php new file mode 100644 index 0000000..4a9d898 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Memcached.php @@ -0,0 +1,184 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +class Memcached extends Driver +{ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'expire' => 0, + 'timeout' => 0, // 超时时间(单位:毫秒) + 'prefix' => '', + 'username' => '', //账号 + 'password' => '', //密码 + 'option' => [], + ]; + + /** + * 构造函数 + * @param array $options 缓存参数 + * @access public + */ + public function __construct($options = []) + { + if (!extension_loaded('memcached')) { + throw new \BadFunctionCallException('not support: memcached'); + } + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + $this->handler = new \Memcached; + if (!empty($this->options['option'])) { + $this->handler->setOptions($this->options['option']); + } + // 设置连接超时时间(单位:毫秒) + if ($this->options['timeout'] > 0) { + $this->handler->setOption(\Memcached::OPT_CONNECT_TIMEOUT, $this->options['timeout']); + } + // 支持集群 + $hosts = explode(',', $this->options['host']); + $ports = explode(',', $this->options['port']); + if (empty($ports[0])) { + $ports[0] = 11211; + } + // 建立连接 + $servers = []; + foreach ((array) $hosts as $i => $host) { + $servers[] = [$host, (isset($ports[$i]) ? $ports[$i] : $ports[0]), 1]; + } + $this->handler->addServers($servers); + if ('' != $this->options['username']) { + $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $this->handler->setSaslAuthData($this->options['username'], $this->options['password']); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + return $this->handler->get($key) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $result = $this->handler->get($this->getCacheKey($name)); + return false !== $result ? $result : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) + * @return bool + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + if ($this->tag && !$this->has($name)) { + $first = true; + } + $key = $this->getCacheKey($name); + $expire = 0 == $expire ? 0 : $_SERVER['REQUEST_TIME'] + $expire; + if ($this->handler->set($key, $value, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $key = $this->getCacheKey($name); + if ($this->handler->get($key)) { + return $this->handler->increment($key, $step); + } + return $this->handler->set($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $key = $this->getCacheKey($name); + $value = $this->handler->get($key) - $step; + $res = $this->handler->set($key, $value); + if (!$res) { + return false; + } else { + return $value; + } + } + + /** + * 删除缓存 + * @param string $name 缓存变量名 + * @param bool|false $ttl + * @return bool + */ + public function rm($name, $ttl = false) + { + $key = $this->getCacheKey($name); + return false === $ttl ? + $this->handler->delete($key) : + $this->handler->delete($key, $ttl); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return bool + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + $this->handler->deleteMulti($keys); + $this->rm('tag_' . md5($tag)); + return true; + } + return $this->handler->flush(); + } +} diff --git a/thinkphp/library/think/cache/driver/Redis.php b/thinkphp/library/think/cache/driver/Redis.php new file mode 100644 index 0000000..4f61878 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Redis.php @@ -0,0 +1,176 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Redis缓存驱动,适合单机部署、有前端代理实现高可用的场景,性能最好 + * 有需要在业务层实现读写分离、或者使用RedisCluster的需求,请使用Redisd驱动 + * + * 要求安装phpredis扩展:https://github.com/nicolasff/phpredis + * @author 尘缘 <130775@qq.com> + */ +class Redis extends Driver +{ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 6379, + 'password' => '', + 'select' => 0, + 'timeout' => 0, + 'expire' => 0, + 'persistent' => false, + 'prefix' => '', + ]; + + /** + * 构造函数 + * @param array $options 缓存参数 + * @access public + */ + public function __construct($options = []) + { + if (!extension_loaded('redis')) { + throw new \BadFunctionCallException('not support: redis'); + } + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + $func = $this->options['persistent'] ? 'pconnect' : 'connect'; + $this->handler = new \Redis; + $this->handler->$func($this->options['host'], $this->options['port'], $this->options['timeout']); + + if ('' != $this->options['password']) { + $this->handler->auth($this->options['password']); + } + + if (0 != $this->options['select']) { + $this->handler->select($this->options['select']); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + return $this->handler->get($this->getCacheKey($name)) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $value = $this->handler->get($this->getCacheKey($name)); + if (is_null($value)) { + return $default; + } + $jsonData = json_decode($value, true); + // 检测是否为JSON数据 true 返回JSON解析数组, false返回源数据 byron sampson + return (null === $jsonData) ? $value : $jsonData; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + if ($this->tag && !$this->has($name)) { + $first = true; + } + $key = $this->getCacheKey($name); + //对数组/对象数据进行缓存处理,保证数据完整性 byron sampson + $value = (is_object($value) || is_array($value)) ? json_encode($value) : $value; + if (is_int($expire) && $expire) { + $result = $this->handler->setex($key, $expire, $value); + } else { + $result = $this->handler->set($key, $value); + } + isset($first) && $this->setTagItem($key); + return $result; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $key = $this->getCacheKey($name); + return $this->handler->incrby($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $key = $this->getCacheKey($name); + return $this->handler->decrby($key, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + return $this->handler->delete($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + $this->handler->delete($key); + } + $this->rm('tag_' . md5($tag)); + return true; + } + return $this->handler->flushDB(); + } + +} diff --git a/thinkphp/library/think/cache/driver/Sqlite.php b/thinkphp/library/think/cache/driver/Sqlite.php new file mode 100644 index 0000000..305fd9e --- /dev/null +++ b/thinkphp/library/think/cache/driver/Sqlite.php @@ -0,0 +1,195 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Sqlite缓存驱动 + * @author liu21st + */ +class Sqlite extends Driver +{ + protected $options = [ + 'db' => ':memory:', + 'table' => 'sharedmemory', + 'prefix' => '', + 'expire' => 0, + 'persistent' => false, + ]; + + /** + * 构造函数 + * @param array $options 缓存参数 + * @throws \BadFunctionCallException + * @access public + */ + public function __construct($options = []) + { + if (!extension_loaded('sqlite')) { + throw new \BadFunctionCallException('not support: sqlite'); + } + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + $func = $this->options['persistent'] ? 'sqlite_popen' : 'sqlite_open'; + $this->handler = $func($this->options['db']); + } + + /** + * 获取实际的缓存标识 + * @access public + * @param string $name 缓存名 + * @return string + */ + protected function getCacheKey($name) + { + return $this->options['prefix'] . sqlite_escape_string($name); + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $name = $this->getCacheKey($name); + $sql = 'SELECT value FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\' AND (expire=0 OR expire >' . $_SERVER['REQUEST_TIME'] . ') LIMIT 1'; + $result = sqlite_query($this->handler, $sql); + return sqlite_num_rows($result); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $name = $this->getCacheKey($name); + $sql = 'SELECT value FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\' AND (expire=0 OR expire >' . $_SERVER['REQUEST_TIME'] . ') LIMIT 1'; + $result = sqlite_query($this->handler, $sql); + if (sqlite_num_rows($result)) { + $content = sqlite_fetch_single($result); + if (function_exists('gzcompress')) { + //启用数据压缩 + $content = gzuncompress($content); + } + return unserialize($content); + } + return $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + $name = $this->getCacheKey($name); + $value = sqlite_escape_string(serialize($value)); + if (is_null($expire)) { + $expire = $this->options['expire']; + } + $expire = (0 == $expire) ? 0 : ($_SERVER['REQUEST_TIME'] + $expire); //缓存有效期为0表示永久缓存 + if (function_exists('gzcompress')) { + //数据压缩 + $value = gzcompress($value, 3); + } + if ($this->tag) { + $tag = $this->tag; + $this->tag = null; + } else { + $tag = ''; + } + $sql = 'REPLACE INTO ' . $this->options['table'] . ' (var, value, expire, tag) VALUES (\'' . $name . '\', \'' . $value . '\', \'' . $expire . '\', \'' . $tag . '\')'; + if (sqlite_query($this->handler, $sql)) { + return true; + } + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) + $step; + } else { + $value = $step; + } + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) - $step; + } else { + $value = $step; + } + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $name = $this->getCacheKey($name); + $sql = 'DELETE FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\''; + sqlite_query($this->handler, $sql); + return true; + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + $name = sqlite_escape_string($tag); + $sql = 'DELETE FROM ' . $this->options['table'] . ' WHERE tag=\'' . $name . '\''; + sqlite_query($this->handler, $sql); + return true; + } + $sql = 'DELETE FROM ' . $this->options['table']; + sqlite_query($this->handler, $sql); + return true; + } +} diff --git a/thinkphp/library/think/cache/driver/Wincache.php b/thinkphp/library/think/cache/driver/Wincache.php new file mode 100644 index 0000000..3076fc1 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Wincache.php @@ -0,0 +1,149 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Wincache缓存驱动 + * @author liu21st + */ +class Wincache extends Driver +{ + protected $options = [ + 'prefix' => '', + 'expire' => 0, + ]; + + /** + * 构造函数 + * @param array $options 缓存参数 + * @throws \BadFunctionCallException + * @access public + */ + public function __construct($options = []) + { + if (!function_exists('wincache_ucache_info')) { + throw new \BadFunctionCallException('not support: WinCache'); + } + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + return wincache_ucache_exists($key); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $key = $this->getCacheKey($name); + return wincache_ucache_exists($key) ? wincache_ucache_get($key) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + $key = $this->getCacheKey($name); + if ($this->tag && !$this->has($name)) { + $first = true; + } + if (wincache_ucache_set($key, $value, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $key = $this->getCacheKey($name); + return wincache_ucache_inc($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $key = $this->getCacheKey($name); + return wincache_ucache_dec($key, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + return wincache_ucache_delete($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + wincache_ucache_delete($key); + } + $this->rm('tag_' . md5($tag)); + return true; + } else { + return wincache_ucache_clear(); + } + } + +} diff --git a/thinkphp/library/think/cache/driver/Xcache.php b/thinkphp/library/think/cache/driver/Xcache.php new file mode 100644 index 0000000..2a0e07a --- /dev/null +++ b/thinkphp/library/think/cache/driver/Xcache.php @@ -0,0 +1,152 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Xcache缓存驱动 + * @author liu21st + */ +class Xcache extends Driver +{ + protected $options = [ + 'prefix' => '', + 'expire' => 0, + ]; + + /** + * 构造函数 + * @param array $options 缓存参数 + * @access public + * @throws \BadFunctionCallException + */ + public function __construct($options = []) + { + if (!function_exists('xcache_info')) { + throw new \BadFunctionCallException('not support: Xcache'); + } + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + return xcache_isset($key); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $key = $this->getCacheKey($name); + return xcache_isset($key) ? xcache_get($key) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + if ($this->tag && !$this->has($name)) { + $first = true; + } + $key = $this->getCacheKey($name); + if (xcache_set($key, $value, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $key = $this->getCacheKey($name); + return xcache_inc($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $key = $this->getCacheKey($name); + return xcache_dec($key, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + return xcache_unset($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + xcache_unset($key); + } + $this->rm('tag_' . md5($tag)); + return true; + } + if (function_exists('xcache_unset_by_prefix')) { + return xcache_unset_by_prefix($this->options['prefix']); + } else { + return false; + } + } +} diff --git a/thinkphp/library/think/config/driver/Ini.php b/thinkphp/library/think/config/driver/Ini.php new file mode 100644 index 0000000..a223a57 --- /dev/null +++ b/thinkphp/library/think/config/driver/Ini.php @@ -0,0 +1,24 @@ + +// +---------------------------------------------------------------------- + +namespace think\config\driver; + +class Ini +{ + public function parse($config) + { + if (is_file($config)) { + return parse_ini_file($config, true); + } else { + return parse_ini_string($config, true); + } + } +} diff --git a/thinkphp/library/think/config/driver/Json.php b/thinkphp/library/think/config/driver/Json.php new file mode 100644 index 0000000..557f75f --- /dev/null +++ b/thinkphp/library/think/config/driver/Json.php @@ -0,0 +1,24 @@ + +// +---------------------------------------------------------------------- + +namespace think\config\driver; + +class Json +{ + public function parse($config) + { + if (is_file($config)) { + $config = file_get_contents($config); + } + $result = json_decode($config, true); + return $result; + } +} diff --git a/thinkphp/library/think/config/driver/Xml.php b/thinkphp/library/think/config/driver/Xml.php new file mode 100644 index 0000000..b573a56 --- /dev/null +++ b/thinkphp/library/think/config/driver/Xml.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- + +namespace think\config\driver; + +class Xml +{ + public function parse($config) + { + if (is_file($config)) { + $content = simplexml_load_file($config); + } else { + $content = simplexml_load_string($config); + } + $result = (array) $content; + foreach ($result as $key => $val) { + if (is_object($val)) { + $result[$key] = (array) $val; + } + } + return $result; + } +} diff --git a/thinkphp/library/think/console/Command.php b/thinkphp/library/think/console/Command.php new file mode 100644 index 0000000..d0caad2 --- /dev/null +++ b/thinkphp/library/think/console/Command.php @@ -0,0 +1,470 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use think\Console; +use think\console\input\Argument; +use think\console\input\Definition; +use think\console\input\Option; + +class Command +{ + + /** @var Console */ + private $console; + private $name; + private $aliases = []; + private $definition; + private $help; + private $description; + private $ignoreValidationErrors = false; + private $consoleDefinitionMerged = false; + private $consoleDefinitionMergedWithArgs = false; + private $code; + private $synopsis = []; + private $usages = []; + + /** @var Input */ + protected $input; + + /** @var Output */ + protected $output; + + /** + * 构造方法 + * @param string|null $name 命令名称,如果没有设置则比如在 configure() 里设置 + * @throws \LogicException + * @api + */ + public function __construct($name = null) + { + $this->definition = new Definition(); + + if (null !== $name) { + $this->setName($name); + } + + $this->configure(); + + if (!$this->name) { + throw new \LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this))); + } + } + + /** + * 忽略验证错误 + */ + public function ignoreValidationErrors() + { + $this->ignoreValidationErrors = true; + } + + /** + * 设置控制台 + * @param Console $console + */ + public function setConsole(Console $console = null) + { + $this->console = $console; + } + + /** + * 获取控制台 + * @return Console + * @api + */ + public function getConsole() + { + return $this->console; + } + + /** + * 是否有效 + * @return bool + */ + public function isEnabled() + { + return true; + } + + /** + * 配置指令 + */ + protected function configure() + { + } + + /** + * 执行指令 + * @param Input $input + * @param Output $output + * @return null|int + * @throws \LogicException + * @see setCode() + */ + protected function execute(Input $input, Output $output) + { + throw new \LogicException('You must override the execute() method in the concrete command class.'); + } + + /** + * 用户验证 + * @param Input $input + * @param Output $output + */ + protected function interact(Input $input, Output $output) + { + } + + /** + * 初始化 + * @param Input $input An InputInterface instance + * @param Output $output An OutputInterface instance + */ + protected function initialize(Input $input, Output $output) + { + } + + /** + * 执行 + * @param Input $input + * @param Output $output + * @return int + * @throws \Exception + * @see setCode() + * @see execute() + */ + public function run(Input $input, Output $output) + { + $this->input = $input; + $this->output = $output; + + $this->getSynopsis(true); + $this->getSynopsis(false); + + $this->mergeConsoleDefinition(); + + try { + $input->bind($this->definition); + } catch (\Exception $e) { + if (!$this->ignoreValidationErrors) { + throw $e; + } + } + + $this->initialize($input, $output); + + if ($input->isInteractive()) { + $this->interact($input, $output); + } + + $input->validate(); + + if ($this->code) { + $statusCode = call_user_func($this->code, $input, $output); + } else { + $statusCode = $this->execute($input, $output); + } + + return is_numeric($statusCode) ? (int) $statusCode : 0; + } + + /** + * 设置执行代码 + * @param callable $code callable(InputInterface $input, OutputInterface $output) + * @return Command + * @throws \InvalidArgumentException + * @see execute() + */ + public function setCode(callable $code) + { + if (!is_callable($code)) { + throw new \InvalidArgumentException('Invalid callable provided to Command::setCode.'); + } + + if (PHP_VERSION_ID >= 50400 && $code instanceof \Closure) { + $r = new \ReflectionFunction($code); + if (null === $r->getClosureThis()) { + $code = \Closure::bind($code, $this); + } + } + + $this->code = $code; + + return $this; + } + + /** + * 合并参数定义 + * @param bool $mergeArgs + */ + public function mergeConsoleDefinition($mergeArgs = true) + { + if (null === $this->console + || (true === $this->consoleDefinitionMerged + && ($this->consoleDefinitionMergedWithArgs || !$mergeArgs)) + ) { + return; + } + + if ($mergeArgs) { + $currentArguments = $this->definition->getArguments(); + $this->definition->setArguments($this->console->getDefinition()->getArguments()); + $this->definition->addArguments($currentArguments); + } + + $this->definition->addOptions($this->console->getDefinition()->getOptions()); + + $this->consoleDefinitionMerged = true; + if ($mergeArgs) { + $this->consoleDefinitionMergedWithArgs = true; + } + } + + /** + * 设置参数定义 + * @param array|Definition $definition + * @return Command + * @api + */ + public function setDefinition($definition) + { + if ($definition instanceof Definition) { + $this->definition = $definition; + } else { + $this->definition->setDefinition($definition); + } + + $this->consoleDefinitionMerged = false; + + return $this; + } + + /** + * 获取参数定义 + * @return Definition + * @api + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * 获取当前指令的参数定义 + * @return Definition + */ + public function getNativeDefinition() + { + return $this->getDefinition(); + } + + /** + * 添加参数 + * @param string $name 名称 + * @param int $mode 类型 + * @param string $description 描述 + * @param mixed $default 默认值 + * @return Command + */ + public function addArgument($name, $mode = null, $description = '', $default = null) + { + $this->definition->addArgument(new Argument($name, $mode, $description, $default)); + + return $this; + } + + /** + * 添加选项 + * @param string $name 选项名称 + * @param string $shortcut 别名 + * @param int $mode 类型 + * @param string $description 描述 + * @param mixed $default 默认值 + * @return Command + */ + public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + $this->definition->addOption(new Option($name, $shortcut, $mode, $description, $default)); + + return $this; + } + + /** + * 设置指令名称 + * @param string $name + * @return Command + * @throws \InvalidArgumentException + */ + public function setName($name) + { + $this->validateName($name); + + $this->name = $name; + + return $this; + } + + /** + * 获取指令名称 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 设置描述 + * @param string $description + * @return Command + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * 获取描述 + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * 设置帮助信息 + * @param string $help + * @return Command + */ + public function setHelp($help) + { + $this->help = $help; + + return $this; + } + + /** + * 获取帮助信息 + * @return string + */ + public function getHelp() + { + return $this->help; + } + + /** + * 描述信息 + * @return string + */ + public function getProcessedHelp() + { + $name = $this->name; + + $placeholders = [ + '%command.name%', + '%command.full_name%', + ]; + $replacements = [ + $name, + $_SERVER['PHP_SELF'] . ' ' . $name, + ]; + + return str_replace($placeholders, $replacements, $this->getHelp()); + } + + /** + * 设置别名 + * @param string[] $aliases + * @return Command + * @throws \InvalidArgumentException + */ + public function setAliases($aliases) + { + if (!is_array($aliases) && !$aliases instanceof \Traversable) { + throw new \InvalidArgumentException('$aliases must be an array or an instance of \Traversable'); + } + + foreach ($aliases as $alias) { + $this->validateName($alias); + } + + $this->aliases = $aliases; + + return $this; + } + + /** + * 获取别名 + * @return array + */ + public function getAliases() + { + return $this->aliases; + } + + /** + * 获取简介 + * @param bool $short 是否简单的 + * @return string + */ + public function getSynopsis($short = false) + { + $key = $short ? 'short' : 'long'; + + if (!isset($this->synopsis[$key])) { + $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short))); + } + + return $this->synopsis[$key]; + } + + /** + * 添加用法介绍 + * @param string $usage + * @return $this + */ + public function addUsage($usage) + { + if (0 !== strpos($usage, $this->name)) { + $usage = sprintf('%s %s', $this->name, $usage); + } + + $this->usages[] = $usage; + + return $this; + } + + /** + * 获取用法介绍 + * @return array + */ + public function getUsages() + { + return $this->usages; + } + + /** + * 验证指令名称 + * @param string $name + * @throws \InvalidArgumentException + */ + private function validateName($name) + { + if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) { + throw new \InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name)); + } + } +} diff --git a/thinkphp/library/think/console/Input.php b/thinkphp/library/think/console/Input.php new file mode 100644 index 0000000..2482dfd --- /dev/null +++ b/thinkphp/library/think/console/Input.php @@ -0,0 +1,464 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use think\console\input\Argument; +use think\console\input\Definition; +use think\console\input\Option; + +class Input +{ + + /** + * @var Definition + */ + protected $definition; + + /** + * @var Option[] + */ + protected $options = []; + + /** + * @var Argument[] + */ + protected $arguments = []; + + protected $interactive = true; + + private $tokens; + private $parsed; + + public function __construct($argv = null) + { + if (null === $argv) { + $argv = $_SERVER['argv']; + // 去除命令名 + array_shift($argv); + } + + $this->tokens = $argv; + + $this->definition = new Definition(); + } + + protected function setTokens(array $tokens) + { + $this->tokens = $tokens; + } + + /** + * 绑定实例 + * @param Definition $definition A InputDefinition instance + */ + public function bind(Definition $definition) + { + $this->arguments = []; + $this->options = []; + $this->definition = $definition; + + $this->parse(); + } + + /** + * 解析参数 + */ + protected function parse() + { + $parseOptions = true; + $this->parsed = $this->tokens; + while (null !== $token = array_shift($this->parsed)) { + if ($parseOptions && '' == $token) { + $this->parseArgument($token); + } elseif ($parseOptions && '--' == $token) { + $parseOptions = false; + } elseif ($parseOptions && 0 === strpos($token, '--')) { + $this->parseLongOption($token); + } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { + $this->parseShortOption($token); + } else { + $this->parseArgument($token); + } + } + } + + /** + * 解析短选项 + * @param string $token 当前的指令. + */ + private function parseShortOption($token) + { + $name = substr($token, 1); + + if (strlen($name) > 1) { + if ($this->definition->hasShortcut($name[0]) + && $this->definition->getOptionForShortcut($name[0])->acceptValue() + ) { + $this->addShortOption($name[0], substr($name, 1)); + } else { + $this->parseShortOptionSet($name); + } + } else { + $this->addShortOption($name, null); + } + } + + /** + * 解析短选项 + * @param string $name 当前指令 + * @throws \RuntimeException + */ + private function parseShortOptionSet($name) + { + $len = strlen($name); + for ($i = 0; $i < $len; ++$i) { + if (!$this->definition->hasShortcut($name[$i])) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $name[$i])); + } + + $option = $this->definition->getOptionForShortcut($name[$i]); + if ($option->acceptValue()) { + $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1)); + + break; + } else { + $this->addLongOption($option->getName(), null); + } + } + } + + /** + * 解析完整选项 + * @param string $token 当前指令 + */ + private function parseLongOption($token) + { + $name = substr($token, 2); + + if (false !== $pos = strpos($name, '=')) { + $this->addLongOption(substr($name, 0, $pos), substr($name, $pos + 1)); + } else { + $this->addLongOption($name, null); + } + } + + /** + * 解析参数 + * @param string $token 当前指令 + * @throws \RuntimeException + */ + private function parseArgument($token) + { + $c = count($this->arguments); + + if ($this->definition->hasArgument($c)) { + $arg = $this->definition->getArgument($c); + + $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; + + } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { + $arg = $this->definition->getArgument($c - 1); + + $this->arguments[$arg->getName()][] = $token; + } else { + throw new \RuntimeException('Too many arguments.'); + } + } + + /** + * 添加一个短选项的值 + * @param string $shortcut 短名称 + * @param mixed $value 值 + * @throws \RuntimeException + */ + private function addShortOption($shortcut, $value) + { + if (!$this->definition->hasShortcut($shortcut)) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); + } + + /** + * 添加一个完整选项的值 + * @param string $name 选项名 + * @param mixed $value 值 + * @throws \RuntimeException + */ + private function addLongOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \RuntimeException(sprintf('The "--%s" option does not exist.', $name)); + } + + $option = $this->definition->getOption($name); + + if (false === $value) { + $value = null; + } + + if (null !== $value && !$option->acceptValue()) { + throw new \RuntimeException(sprintf('The "--%s" option does not accept a value.', $name, $value)); + } + + if (null === $value && $option->acceptValue() && count($this->parsed)) { + $next = array_shift($this->parsed); + if (isset($next[0]) && '-' !== $next[0]) { + $value = $next; + } elseif (empty($next)) { + $value = ''; + } else { + array_unshift($this->parsed, $next); + } + } + + if (null === $value) { + if ($option->isValueRequired()) { + throw new \RuntimeException(sprintf('The "--%s" option requires a value.', $name)); + } + + if (!$option->isArray()) { + $value = $option->isValueOptional() ? $option->getDefault() : true; + } + } + + if ($option->isArray()) { + $this->options[$name][] = $value; + } else { + $this->options[$name] = $value; + } + } + + /** + * 获取第一个参数 + * @return string|null + */ + public function getFirstArgument() + { + foreach ($this->tokens as $token) { + if ($token && '-' === $token[0]) { + continue; + } + + return $token; + } + return; + } + + /** + * 检查原始参数是否包含某个值 + * @param string|array $values 需要检查的值 + * @return bool + */ + public function hasParameterOption($values) + { + $values = (array) $values; + + foreach ($this->tokens as $token) { + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + return true; + } + } + } + + return false; + } + + /** + * 获取原始选项的值 + * @param string|array $values 需要检查的值 + * @param mixed $default 默认值 + * @return mixed The option value + */ + public function getParameterOption($values, $default = false) + { + $values = (array) $values; + $tokens = $this->tokens; + + while (0 < count($tokens)) { + $token = array_shift($tokens); + + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + if (false !== $pos = strpos($token, '=')) { + return substr($token, $pos + 1); + } + + return array_shift($tokens); + } + } + } + + return $default; + } + + /** + * 验证输入 + * @throws \RuntimeException + */ + public function validate() + { + if (count($this->arguments) < $this->definition->getArgumentRequiredCount()) { + throw new \RuntimeException('Not enough arguments.'); + } + } + + /** + * 检查输入是否是交互的 + * @return bool + */ + public function isInteractive() + { + return $this->interactive; + } + + /** + * 设置输入的交互 + * @param bool + */ + public function setInteractive($interactive) + { + $this->interactive = (bool) $interactive; + } + + /** + * 获取所有的参数 + * @return Argument[] + */ + public function getArguments() + { + return array_merge($this->definition->getArgumentDefaults(), $this->arguments); + } + + /** + * 根据名称获取参数 + * @param string $name 参数名 + * @return mixed + * @throws \InvalidArgumentException + */ + public function getArgument($name) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + return isset($this->arguments[$name]) ? $this->arguments[$name] : $this->definition->getArgument($name) + ->getDefault(); + } + + /** + * 设置参数的值 + * @param string $name 参数名 + * @param string $value 值 + * @throws \InvalidArgumentException + */ + public function setArgument($name, $value) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $this->arguments[$name] = $value; + } + + /** + * 检查是否存在某个参数 + * @param string|int $name 参数名或位置 + * @return bool + */ + public function hasArgument($name) + { + return $this->definition->hasArgument($name); + } + + /** + * 获取所有的选项 + * @return Option[] + */ + public function getOptions() + { + return array_merge($this->definition->getOptionDefaults(), $this->options); + } + + /** + * 获取选项值 + * @param string $name 选项名称 + * @return mixed + * @throws \InvalidArgumentException + */ + public function getOption($name) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + return isset($this->options[$name]) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); + } + + /** + * 设置选项值 + * @param string $name 选项名 + * @param string|bool $value 值 + * @throws \InvalidArgumentException + */ + public function setOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + $this->options[$name] = $value; + } + + /** + * 是否有某个选项 + * @param string $name 选项名 + * @return bool + */ + public function hasOption($name) + { + return $this->definition->hasOption($name) && isset($this->options[$name]); + } + + /** + * 转义指令 + * @param string $token + * @return string + */ + public function escapeToken($token) + { + return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); + } + + /** + * 返回传递给命令的参数的字符串 + * @return string + */ + public function __toString() + { + $tokens = array_map(function ($token) { + if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { + return $match[1] . $this->escapeToken($match[2]); + } + + if ($token && '-' !== $token[0]) { + return $this->escapeToken($token); + } + + return $token; + }, $this->tokens); + + return implode(' ', $tokens); + } +} diff --git a/thinkphp/library/think/console/LICENSE b/thinkphp/library/think/console/LICENSE new file mode 100644 index 0000000..0abe056 --- /dev/null +++ b/thinkphp/library/think/console/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2016 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/thinkphp/library/think/console/Output.php b/thinkphp/library/think/console/Output.php new file mode 100644 index 0000000..65dc9fb --- /dev/null +++ b/thinkphp/library/think/console/Output.php @@ -0,0 +1,222 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use Exception; +use think\console\output\Ask; +use think\console\output\Descriptor; +use think\console\output\driver\Buffer; +use think\console\output\driver\Console; +use think\console\output\driver\Nothing; +use think\console\output\Question; +use think\console\output\question\Choice; +use think\console\output\question\Confirmation; + +/** + * Class Output + * @package think\console + * + * @see \think\console\output\driver\Console::setDecorated + * @method void setDecorated($decorated) + * + * @see \think\console\output\driver\Buffer::fetch + * @method string fetch() + * + * @method void info($message) + * @method void error($message) + * @method void comment($message) + * @method void warning($message) + * @method void highlight($message) + * @method void question($message) + */ +class Output +{ + const VERBOSITY_QUIET = 0; + const VERBOSITY_NORMAL = 1; + const VERBOSITY_VERBOSE = 2; + const VERBOSITY_VERY_VERBOSE = 3; + const VERBOSITY_DEBUG = 4; + + const OUTPUT_NORMAL = 0; + const OUTPUT_RAW = 1; + const OUTPUT_PLAIN = 2; + + private $verbosity = self::VERBOSITY_NORMAL; + + /** @var Buffer|Console|Nothing */ + private $handle = null; + + protected $styles = [ + 'info', + 'error', + 'comment', + 'question', + 'highlight', + 'warning' + ]; + + public function __construct($driver = 'console') + { + $class = '\\think\\console\\output\\driver\\' . ucwords($driver); + + $this->handle = new $class($this); + } + + public function ask(Input $input, $question, $default = null, $validator = null) + { + $question = new Question($question, $default); + $question->setValidator($validator); + + return $this->askQuestion($input, $question); + } + + public function askHidden(Input $input, $question, $validator = null) + { + $question = new Question($question); + + $question->setHidden(true); + $question->setValidator($validator); + + return $this->askQuestion($input, $question); + } + + public function confirm(Input $input, $question, $default = true) + { + return $this->askQuestion($input, new Confirmation($question, $default)); + } + + /** + * {@inheritdoc} + */ + public function choice(Input $input, $question, array $choices, $default = null) + { + if (null !== $default) { + $values = array_flip($choices); + $default = $values[$default]; + } + + return $this->askQuestion($input, new Choice($question, $choices, $default)); + } + + protected function askQuestion(Input $input, Question $question) + { + $ask = new Ask($input, $this, $question); + $answer = $ask->run(); + + if ($input->isInteractive()) { + $this->newLine(); + } + + return $answer; + } + + protected function block($style, $message) + { + $this->writeln("<{$style}>{$message}"); + } + + /** + * 输出空行 + * @param int $count + */ + public function newLine($count = 1) + { + $this->write(str_repeat(PHP_EOL, $count)); + } + + /** + * 输出信息并换行 + * @param string $messages + * @param int $type + */ + public function writeln($messages, $type = self::OUTPUT_NORMAL) + { + $this->write($messages, true, $type); + } + + /** + * 输出信息 + * @param string $messages + * @param bool $newline + * @param int $type + */ + public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) + { + $this->handle->write($messages, $newline, $type); + } + + public function renderException(\Exception $e) + { + $this->handle->renderException($e); + } + + /** + * {@inheritdoc} + */ + public function setVerbosity($level) + { + $this->verbosity = (int) $level; + } + + /** + * {@inheritdoc} + */ + public function getVerbosity() + { + return $this->verbosity; + } + + public function isQuiet() + { + return self::VERBOSITY_QUIET === $this->verbosity; + } + + public function isVerbose() + { + return self::VERBOSITY_VERBOSE <= $this->verbosity; + } + + public function isVeryVerbose() + { + return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; + } + + public function isDebug() + { + return self::VERBOSITY_DEBUG <= $this->verbosity; + } + + public function describe($object, array $options = []) + { + $descriptor = new Descriptor(); + $options = array_merge([ + 'raw_text' => false, + ], $options); + + $descriptor->describe($this, $object, $options); + } + + public function __call($method, $args) + { + if (in_array($method, $this->styles)) { + array_unshift($args, $method); + return call_user_func_array([$this, 'block'], $args); + } + + if ($this->handle && method_exists($this->handle, $method)) { + return call_user_func_array([$this->handle, $method], $args); + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } + +} diff --git a/thinkphp/library/think/console/bin/README.md b/thinkphp/library/think/console/bin/README.md new file mode 100644 index 0000000..9acc52f --- /dev/null +++ b/thinkphp/library/think/console/bin/README.md @@ -0,0 +1 @@ +console 工具使用 hiddeninput.exe 在 windows 上隐藏密码输入,该二进制文件由第三方提供,相关源码和其他细节可以在 [Hidden Input](https://github.com/Seldaek/hidden-input) 找到。 diff --git a/thinkphp/library/think/console/bin/hiddeninput.exe b/thinkphp/library/think/console/bin/hiddeninput.exe new file mode 100644 index 0000000..c8cf65e Binary files /dev/null and b/thinkphp/library/think/console/bin/hiddeninput.exe differ diff --git a/thinkphp/library/think/console/command/Build.php b/thinkphp/library/think/console/command/Build.php new file mode 100644 index 0000000..39806c3 --- /dev/null +++ b/thinkphp/library/think/console/command/Build.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; + +class Build extends Command +{ + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName('build') + ->setDefinition([ + new Option('config', null, Option::VALUE_OPTIONAL, "build.php path"), + new Option('module', null, Option::VALUE_OPTIONAL, "module name"), + ]) + ->setDescription('Build Application Dirs'); + } + + protected function execute(Input $input, Output $output) + { + if ($input->hasOption('module')) { + \think\Build::module($input->getOption('module')); + $output->writeln("Successed"); + return; + } + + if ($input->hasOption('config')) { + $build = include $input->getOption('config'); + } else { + $build = include APP_PATH . 'build.php'; + } + if (empty($build)) { + $output->writeln("Build Config Is Empty"); + return; + } + \think\Build::run($build); + $output->writeln("Successed"); + + } +} diff --git a/thinkphp/library/think/console/command/Clear.php b/thinkphp/library/think/console/command/Clear.php new file mode 100644 index 0000000..41019ce --- /dev/null +++ b/thinkphp/library/think/console/command/Clear.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; + +class Clear extends Command +{ + protected function configure() + { + // 指令配置 + $this + ->setName('clear') + ->addOption('path', 'd', Option::VALUE_OPTIONAL, 'path to clear', null) + ->setDescription('Clear runtime file'); + } + + protected function execute(Input $input, Output $output) + { + $path = $input->getOption('path') ?: RUNTIME_PATH; + + if (is_dir($path)) { + $this->clearPath($path); + } + + $output->writeln("Clear Successed"); + } + + protected function clearPath($path) + { + $path = realpath($path) . DS; + $files = scandir($path); + if ($files) { + foreach ($files as $file) { + if ('.' != $file && '..' != $file && is_dir($path . $file)) { + $this->clearPath($path . $file); + } elseif ('.gitignore' != $file && is_file($path . $file)) { + unlink($path . $file); + } + } + } + } +} diff --git a/thinkphp/library/think/console/command/Help.php b/thinkphp/library/think/console/command/Help.php new file mode 100644 index 0000000..bae2c65 --- /dev/null +++ b/thinkphp/library/think/console/command/Help.php @@ -0,0 +1,69 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Option as InputOption; +use think\console\Output; + +class Help extends Command +{ + + private $command; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->ignoreValidationErrors(); + + $this->setName('help')->setDefinition([ + new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), + ])->setDescription('Displays help for a command')->setHelp(<<%command.name% command displays help for a given command: + + php %command.full_name% list + +To display the list of available commands, please use the list command. +EOF + ); + } + + /** + * Sets the command. + * @param Command $command The command to set + */ + public function setCommand(Command $command) + { + $this->command = $command; + } + + /** + * {@inheritdoc} + */ + protected function execute(Input $input, Output $output) + { + if (null === $this->command) { + $this->command = $this->getConsole()->find($input->getArgument('command_name')); + } + + $output->describe($this->command, [ + 'raw_text' => $input->getOption('raw'), + ]); + + $this->command = null; + } +} diff --git a/thinkphp/library/think/console/command/Lists.php b/thinkphp/library/think/console/command/Lists.php new file mode 100644 index 0000000..084ddaa --- /dev/null +++ b/thinkphp/library/think/console/command/Lists.php @@ -0,0 +1,74 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\Output; +use think\console\input\Argument as InputArgument; +use think\console\input\Option as InputOption; +use think\console\input\Definition as InputDefinition; + +class Lists extends Command +{ + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName('list')->setDefinition($this->createDefinition())->setDescription('Lists commands')->setHelp(<<%command.name% command lists all commands: + + php %command.full_name% + +You can also display the commands for a specific namespace: + + php %command.full_name% test + +It's also possible to get raw list of commands (useful for embedding command runner): + + php %command.full_name% --raw +EOF + ); + } + + /** + * {@inheritdoc} + */ + public function getNativeDefinition() + { + return $this->createDefinition(); + } + + /** + * {@inheritdoc} + */ + protected function execute(Input $input, Output $output) + { + $output->describe($this->getConsole(), [ + 'raw_text' => $input->getOption('raw'), + 'namespace' => $input->getArgument('namespace'), + ]); + } + + /** + * {@inheritdoc} + */ + private function createDefinition() + { + return new InputDefinition([ + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list') + ]); + } +} diff --git a/thinkphp/library/think/console/command/Make.php b/thinkphp/library/think/console/command/Make.php new file mode 100644 index 0000000..d1daf34 --- /dev/null +++ b/thinkphp/library/think/console/command/Make.php @@ -0,0 +1,110 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\App; +use think\Config; +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\Output; + +abstract class Make extends Command +{ + + protected $type; + + abstract protected function getStub(); + + protected function configure() + { + $this->addArgument('name', Argument::REQUIRED, "The name of the class"); + } + + protected function execute(Input $input, Output $output) + { + + $name = trim($input->getArgument('name')); + + $classname = $this->getClassName($name); + + $pathname = $this->getPathName($classname); + + if (is_file($pathname)) { + $output->writeln('' . $this->type . ' already exists!'); + return false; + } + + if (!is_dir(dirname($pathname))) { + mkdir(strtolower(dirname($pathname)), 0755, true); + } + + file_put_contents($pathname, $this->buildClass($classname)); + + $output->writeln('' . $this->type . ' created successfully.'); + + } + + protected function buildClass($name) + { + $stub = file_get_contents($this->getStub()); + + $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\'); + + $class = str_replace($namespace . '\\', '', $name); + + return str_replace(['{%className%}', '{%namespace%}', '{%app_namespace%}'], [ + $class, + $namespace, + App::$namespace, + ], $stub); + + } + + protected function getPathName($name) + { + $name = str_replace(App::$namespace . '\\', '', $name); + + return APP_PATH . str_replace('\\', '/', $name) . '.php'; + } + + protected function getClassName($name) + { + $appNamespace = App::$namespace; + + if (strpos($name, $appNamespace . '\\') === 0) { + return $name; + } + + if (Config::get('app_multi_module')) { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name, 2); + } else { + $module = 'common'; + } + } else { + $module = null; + } + + if (strpos($name, '/') !== false) { + $name = str_replace('/', '\\', $name); + } + + return $this->getNamespace($appNamespace, $module) . '\\' . $name; + } + + protected function getNamespace($appNamespace, $module) + { + return $module ? ($appNamespace . '\\' . $module) : $appNamespace; + } + +} diff --git a/thinkphp/library/think/console/command/make/Controller.php b/thinkphp/library/think/console/command/make/Controller.php new file mode 100644 index 0000000..afa7be9 --- /dev/null +++ b/thinkphp/library/think/console/command/make/Controller.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\Config; +use think\console\command\Make; +use think\console\input\Option; + +class Controller extends Make +{ + + protected $type = "Controller"; + + protected function configure() + { + parent::configure(); + $this->setName('make:controller') + ->addOption('plain', null, Option::VALUE_NONE, 'Generate an empty controller class.') + ->setDescription('Create a new resource controller class'); + } + + protected function getStub() + { + if ($this->input->getOption('plain')) { + return __DIR__ . '/stubs/controller.plain.stub'; + } + + return __DIR__ . '/stubs/controller.stub'; + } + + protected function getClassName($name) + { + return parent::getClassName($name) . (Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : ''); + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\controller'; + } + +} diff --git a/thinkphp/library/think/console/command/make/Model.php b/thinkphp/library/think/console/command/make/Model.php new file mode 100644 index 0000000..d4e9b5d --- /dev/null +++ b/thinkphp/library/think/console/command/make/Model.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; + +class Model extends Make +{ + protected $type = "Model"; + + protected function configure() + { + parent::configure(); + $this->setName('make:model') + ->setDescription('Create a new model class'); + } + + protected function getStub() + { + return __DIR__ . '/stubs/model.stub'; + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\model'; + } +} diff --git a/thinkphp/library/think/console/command/make/stubs/controller.plain.stub b/thinkphp/library/think/console/command/make/stubs/controller.plain.stub new file mode 100644 index 0000000..b7539dc --- /dev/null +++ b/thinkphp/library/think/console/command/make/stubs/controller.plain.stub @@ -0,0 +1,10 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\App; +use think\Config; +use think\console\Command; +use think\console\Input; +use think\console\Output; + +class Autoload extends Command +{ + + protected function configure() + { + $this->setName('optimize:autoload') + ->setDescription('Optimizes PSR0 and PSR4 packages to be loaded with classmaps too, good for production.'); + } + + protected function execute(Input $input, Output $output) + { + + $classmapFile = << realpath(rtrim(APP_PATH)), + 'think\\' => LIB_PATH . 'think', + 'behavior\\' => LIB_PATH . 'behavior', + 'traits\\' => LIB_PATH . 'traits', + '' => realpath(rtrim(EXTEND_PATH)), + ]; + + $root_namespace = Config::get('root_namespace'); + foreach ($root_namespace as $namespace => $dir) { + $namespacesToScan[$namespace . '\\'] = realpath($dir); + } + + krsort($namespacesToScan); + $classMap = []; + foreach ($namespacesToScan as $namespace => $dir) { + + if (!is_dir($dir)) { + continue; + } + + $namespaceFilter = $namespace === '' ? null : $namespace; + $classMap = $this->addClassMapCode($dir, $namespaceFilter, $classMap); + } + + ksort($classMap); + foreach ($classMap as $class => $code) { + $classmapFile .= ' ' . var_export($class, true) . ' => ' . $code; + } + $classmapFile .= "];\n"; + + if (!is_dir(RUNTIME_PATH)) { + @mkdir(RUNTIME_PATH, 0755, true); + } + + file_put_contents(RUNTIME_PATH . 'classmap' . EXT, $classmapFile); + + $output->writeln('Succeed!'); + } + + protected function addClassMapCode($dir, $namespace, $classMap) + { + foreach ($this->createMap($dir, $namespace) as $class => $path) { + + $pathCode = $this->getPathCode($path) . ",\n"; + + if (!isset($classMap[$class])) { + $classMap[$class] = $pathCode; + } elseif ($classMap[$class] !== $pathCode && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($classMap[$class] . ' ' . $path, '\\', '/'))) { + $this->output->writeln( + 'Warning: Ambiguous class resolution, "' . $class . '"' . + ' was found in both "' . str_replace(["',\n"], [ + '', + ], $classMap[$class]) . '" and "' . $path . '", the first will be used.' + ); + } + } + return $classMap; + } + + protected function getPathCode($path) + { + + $baseDir = ''; + $libPath = $this->normalizePath(realpath(LIB_PATH)); + $appPath = $this->normalizePath(realpath(APP_PATH)); + $extendPath = $this->normalizePath(realpath(EXTEND_PATH)); + $rootPath = $this->normalizePath(realpath(ROOT_PATH)); + $path = $this->normalizePath($path); + + if ($libPath !== null && strpos($path, $libPath . '/') === 0) { + $path = substr($path, strlen(LIB_PATH)); + $baseDir = 'LIB_PATH'; + } elseif ($appPath !== null && strpos($path, $appPath . '/') === 0) { + $path = substr($path, strlen($appPath) + 1); + $baseDir = 'APP_PATH'; + } elseif ($extendPath !== null && strpos($path, $extendPath . '/') === 0) { + $path = substr($path, strlen($extendPath) + 1); + $baseDir = 'EXTEND_PATH'; + } elseif ($rootPath !== null && strpos($path, $rootPath . '/') === 0) { + $path = substr($path, strlen($rootPath) + 1); + $baseDir = 'ROOT_PATH'; + } + + if ($path !== false) { + $baseDir .= " . "; + } + + return $baseDir . (($path !== false) ? var_export($path, true) : ""); + } + + protected function normalizePath($path) + { + if ($path === false) { + return; + } + $parts = []; + $path = strtr($path, '\\', '/'); + $prefix = ''; + $absolute = false; + + if (preg_match('{^([0-9a-z]+:(?://(?:[a-z]:)?)?)}i', $path, $match)) { + $prefix = $match[1]; + $path = substr($path, strlen($prefix)); + } + + if (substr($path, 0, 1) === '/') { + $absolute = true; + $path = substr($path, 1); + } + + $up = false; + foreach (explode('/', $path) as $chunk) { + if ('..' === $chunk && ($absolute || $up)) { + array_pop($parts); + $up = !(empty($parts) || '..' === end($parts)); + } elseif ('.' !== $chunk && '' !== $chunk) { + $parts[] = $chunk; + $up = '..' !== $chunk; + } + } + + return $prefix . ($absolute ? '/' : '') . implode('/', $parts); + } + + protected function createMap($path, $namespace = null) + { + if (is_string($path)) { + if (is_file($path)) { + $path = [new \SplFileInfo($path)]; + } elseif (is_dir($path)) { + + $objects = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::SELF_FIRST); + + $path = []; + + /** @var \SplFileInfo $object */ + foreach ($objects as $object) { + if ($object->isFile() && $object->getExtension() == 'php') { + $path[] = $object; + } + } + } else { + throw new \RuntimeException( + 'Could not scan for classes inside "' . $path . + '" which does not appear to be a file nor a folder' + ); + } + } + + $map = []; + + /** @var \SplFileInfo $file */ + foreach ($path as $file) { + $filePath = $file->getRealPath(); + + if (pathinfo($filePath, PATHINFO_EXTENSION) != 'php') { + continue; + } + + $classes = $this->findClasses($filePath); + + foreach ($classes as $class) { + if (null !== $namespace && 0 !== strpos($class, $namespace)) { + continue; + } + + if (!isset($map[$class])) { + $map[$class] = $filePath; + } elseif ($map[$class] !== $filePath && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($map[$class] . ' ' . $filePath, '\\', '/'))) { + $this->output->writeln( + 'Warning: Ambiguous class resolution, "' . $class . '"' . + ' was found in both "' . $map[$class] . '" and "' . $filePath . '", the first will be used.' + ); + } + } + } + + return $map; + } + + protected function findClasses($path) + { + $extraTypes = '|trait'; + + $contents = @php_strip_whitespace($path); + if (!$contents) { + if (!file_exists($path)) { + $message = 'File at "%s" does not exist, check your classmap definitions'; + } elseif (!is_readable($path)) { + $message = 'File at "%s" is not readable, check its permissions'; + } elseif ('' === trim(file_get_contents($path))) { + return []; + } else { + $message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted'; + } + $error = error_get_last(); + if (isset($error['message'])) { + $message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message']; + } + throw new \RuntimeException(sprintf($message, $path)); + } + + if (!preg_match('{\b(?:class|interface' . $extraTypes . ')\s}i', $contents)) { + return []; + } + + // strip heredocs/nowdocs + $contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents); + // strip strings + $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents); + // strip leading non-php code if needed + if (substr($contents, 0, 2) !== '.+<\?}s', '?>'); + if (false !== $pos && false === strpos(substr($contents, $pos), '])(?Pclass|interface' . $extraTypes . ') \s++ (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) + | \b(?])(?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] + ) + }ix', $contents, $matches); + + $classes = []; + $namespace = ''; + + for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { + if (!empty($matches['ns'][$i])) { + $namespace = str_replace([' ', "\t", "\r", "\n"], '', $matches['nsname'][$i]) . '\\'; + } else { + $name = $matches['name'][$i]; + if ($name[0] === ':') { + $name = 'xhp' . substr(str_replace(['-', ':'], ['_', '__'], $name), 1); + } elseif ($matches['type'][$i] === 'enum') { + $name = rtrim($name, ':'); + } + $classes[] = ltrim($namespace . $name, '\\'); + } + } + + return $classes; + } + +} diff --git a/thinkphp/library/think/console/command/optimize/Config.php b/thinkphp/library/think/console/command/optimize/Config.php new file mode 100644 index 0000000..cadfe5e --- /dev/null +++ b/thinkphp/library/think/console/command/optimize/Config.php @@ -0,0 +1,93 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\Config as ThinkConfig; +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\Output; + +class Config extends Command +{ + /** @var Output */ + protected $output; + + protected function configure() + { + $this->setName('optimize:config') + ->addArgument('module', Argument::OPTIONAL, 'Build module config cache .') + ->setDescription('Build config and common file cache.'); + } + + protected function execute(Input $input, Output $output) + { + if ($input->hasArgument('module')) { + $module = $input->getArgument('module') . DS; + } else { + $module = ''; + } + + $content = 'buildCacheContent($module); + + if (!is_dir(RUNTIME_PATH . $module)) { + @mkdir(RUNTIME_PATH . $module, 0755, true); + } + + file_put_contents(RUNTIME_PATH . $module . 'init' . EXT, $content); + + $output->writeln('Succeed!'); + } + + protected function buildCacheContent($module) + { + $content = ''; + $path = realpath(APP_PATH . $module) . DS; + + if ($module) { + // 加载模块配置 + $config = ThinkConfig::load(CONF_PATH . $module . 'config' . CONF_EXT); + + // 读取数据库配置文件 + $filename = CONF_PATH . $module . 'database' . CONF_EXT; + ThinkConfig::load($filename, 'database'); + + // 加载应用状态配置 + if ($config['app_status']) { + $config = ThinkConfig::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT); + } + // 读取扩展配置文件 + if (is_dir(CONF_PATH . $module . 'extra')) { + $dir = CONF_PATH . $module . 'extra'; + $files = scandir($dir); + foreach ($files as $file) { + if (strpos($file, CONF_EXT)) { + $filename = $dir . DS . $file; + ThinkConfig::load($filename, pathinfo($file, PATHINFO_FILENAME)); + } + } + } + } + + // 加载行为扩展文件 + if (is_file(CONF_PATH . $module . 'tags' . EXT)) { + $content .= '\think\Hook::import(' . (var_export(include CONF_PATH . $module . 'tags' . EXT, true)) . ');' . PHP_EOL; + } + + // 加载公共文件 + if (is_file($path . 'common' . EXT)) { + $content .= substr(php_strip_whitespace($path . 'common' . EXT), 5) . PHP_EOL; + } + + $content .= '\think\Config::set(' . var_export(ThinkConfig::get(), true) . ');'; + return $content; + } +} diff --git a/thinkphp/library/think/console/command/optimize/Route.php b/thinkphp/library/think/console/command/optimize/Route.php new file mode 100644 index 0000000..911e4c1 --- /dev/null +++ b/thinkphp/library/think/console/command/optimize/Route.php @@ -0,0 +1,70 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\console\Command; +use think\console\Input; +use think\console\Output; + +class Route extends Command +{ + /** @var Output */ + protected $output; + + protected function configure() + { + $this->setName('optimize:route') + ->setDescription('Build route cache.'); + } + + protected function execute(Input $input, Output $output) + { + file_put_contents(RUNTIME_PATH . 'route.php', $this->buildRouteCache()); + $output->writeln('Succeed!'); + } + + protected function buildRouteCache() + { + $files = \think\Config::get('route_config_file'); + foreach ($files as $file) { + if (is_file(CONF_PATH . $file . CONF_EXT)) { + $config = include CONF_PATH . $file . CONF_EXT; + if (is_array($config)) { + \think\Route::import($config); + } + } + } + $rules = \think\Route::rules(true); + array_walk_recursive($rules, [$this, 'buildClosure']); + $content = 'getStartLine(); + $endLine = $reflection->getEndLine(); + $file = $reflection->getFileName(); + $item = file($file); + $content = ''; + for ($i = $startLine - 1; $i <= $endLine - 1; $i++) { + $content .= $item[$i]; + } + $start = strpos($content, 'function'); + $end = strrpos($content, '}'); + $value = '[__start__' . substr($content, $start, $end - $start + 1) . '__end__]'; + } + } +} diff --git a/thinkphp/library/think/console/command/optimize/Schema.php b/thinkphp/library/think/console/command/optimize/Schema.php new file mode 100644 index 0000000..27eb9db --- /dev/null +++ b/thinkphp/library/think/console/command/optimize/Schema.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\App; +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; +use think\Db; + +class Schema extends Command +{ + /** @var Output */ + protected $output; + + protected function configure() + { + $this->setName('optimize:schema') + ->addOption('config', null, Option::VALUE_REQUIRED, 'db config .') + ->addOption('db', null, Option::VALUE_REQUIRED, 'db name .') + ->addOption('table', null, Option::VALUE_REQUIRED, 'table name .') + ->addOption('module', null, Option::VALUE_REQUIRED, 'module name .') + ->setDescription('Build database schema cache.'); + } + + protected function execute(Input $input, Output $output) + { + if (!is_dir(RUNTIME_PATH . 'schema')) { + @mkdir(RUNTIME_PATH . 'schema', 0755, true); + } + $config = []; + if ($input->hasOption('config')) { + $config = $input->getOption('config'); + } + if ($input->hasOption('module')) { + $module = $input->getOption('module'); + // 读取模型 + $list = scandir(APP_PATH . $module . DS . 'model'); + $app = App::$namespace; + foreach ($list as $file) { + if (0 === strpos($file, '.')) { + continue; + } + $class = '\\' . $app . '\\' . $module . '\\model\\' . pathinfo($file, PATHINFO_FILENAME); + $this->buildModelSchema($class); + } + $output->writeln('Succeed!'); + return; + } elseif ($input->hasOption('table')) { + $table = $input->getOption('table'); + if (!strpos($table, '.')) { + $dbName = Db::connect($config)->getConfig('database'); + } + $tables[] = $table; + } elseif ($input->hasOption('db')) { + $dbName = $input->getOption('db'); + $tables = Db::connect($config)->getTables($dbName); + } elseif (!\think\Config::get('app_multi_module')) { + $app = App::$namespace; + $list = scandir(APP_PATH . 'model'); + foreach ($list as $file) { + if (0 === strpos($file, '.')) { + continue; + } + $class = '\\' . $app . '\\model\\' . pathinfo($file, PATHINFO_FILENAME); + $this->buildModelSchema($class); + } + $output->writeln('Succeed!'); + return; + } else { + $tables = Db::connect($config)->getTables(); + } + + $db = isset($dbName) ? $dbName . '.' : ''; + $this->buildDataBaseSchema($tables, $db, $config); + + $output->writeln('Succeed!'); + } + + protected function buildModelSchema($class) + { + $reflect = new \ReflectionClass($class); + if (!$reflect->isAbstract() && $reflect->isSubclassOf('\think\Model')) { + $table = $class::getTable(); + $dbName = $class::getConfig('database'); + $content = 'getFields($table); + $content .= var_export($info, true) . ';'; + file_put_contents(RUNTIME_PATH . 'schema' . DS . $dbName . '.' . $table . EXT, $content); + } + } + + protected function buildDataBaseSchema($tables, $db, $config) + { + if ('' == $db) { + $dbName = Db::connect($config)->getConfig('database') . '.'; + } else { + $dbName = $db; + } + foreach ($tables as $table) { + $content = 'getFields($db . $table); + $content .= var_export($info, true) . ';'; + file_put_contents(RUNTIME_PATH . 'schema' . DS . $dbName . $table . EXT, $content); + } + } +} diff --git a/thinkphp/library/think/console/input/Argument.php b/thinkphp/library/think/console/input/Argument.php new file mode 100644 index 0000000..16223bb --- /dev/null +++ b/thinkphp/library/think/console/input/Argument.php @@ -0,0 +1,115 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Argument +{ + + const REQUIRED = 1; + const OPTIONAL = 2; + const IS_ARRAY = 4; + + private $name; + private $mode; + private $default; + private $description; + + /** + * 构造方法 + * @param string $name 参数名 + * @param int $mode 参数类型: self::REQUIRED 或者 self::OPTIONAL + * @param string $description 描述 + * @param mixed $default 默认值 (仅 self::OPTIONAL 类型有效) + * @throws \InvalidArgumentException + */ + public function __construct($name, $mode = null, $description = '', $default = null) + { + if (null === $mode) { + $mode = self::OPTIONAL; + } elseif (!is_int($mode) || $mode > 7 || $mode < 1) { + throw new \InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->mode = $mode; + $this->description = $description; + + $this->setDefault($default); + } + + /** + * 获取参数名 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 是否必须 + * @return bool + */ + public function isRequired() + { + return self::REQUIRED === (self::REQUIRED & $this->mode); + } + + /** + * 该参数是否接受数组 + * @return bool + */ + public function isArray() + { + return self::IS_ARRAY === (self::IS_ARRAY & $this->mode); + } + + /** + * 设置默认值 + * @param mixed $default 默认值 + * @throws \LogicException + */ + public function setDefault($default = null) + { + if (self::REQUIRED === $this->mode && null !== $default) { + throw new \LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!is_array($default)) { + throw new \LogicException('A default value for an array argument must be an array.'); + } + } + + $this->default = $default; + } + + /** + * 获取默认值 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 获取描述 + * @return string + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/thinkphp/library/think/console/input/Definition.php b/thinkphp/library/think/console/input/Definition.php new file mode 100644 index 0000000..c71977e --- /dev/null +++ b/thinkphp/library/think/console/input/Definition.php @@ -0,0 +1,375 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Definition +{ + + /** + * @var Argument[] + */ + private $arguments; + + private $requiredCount; + private $hasAnArrayArgument = false; + private $hasOptional; + + /** + * @var Option[] + */ + private $options; + private $shortcuts; + + /** + * 构造方法 + * @param array $definition + * @api + */ + public function __construct(array $definition = []) + { + $this->setDefinition($definition); + } + + /** + * 设置指令的定义 + * @param array $definition 定义的数组 + */ + public function setDefinition(array $definition) + { + $arguments = []; + $options = []; + foreach ($definition as $item) { + if ($item instanceof Option) { + $options[] = $item; + } else { + $arguments[] = $item; + } + } + + $this->setArguments($arguments); + $this->setOptions($options); + } + + /** + * 设置参数 + * @param Argument[] $arguments 参数数组 + */ + public function setArguments($arguments = []) + { + $this->arguments = []; + $this->requiredCount = 0; + $this->hasOptional = false; + $this->hasAnArrayArgument = false; + $this->addArguments($arguments); + } + + /** + * 添加参数 + * @param Argument[] $arguments 参数数组 + * @api + */ + public function addArguments($arguments = []) + { + if (null !== $arguments) { + foreach ($arguments as $argument) { + $this->addArgument($argument); + } + } + } + + /** + * 添加一个参数 + * @param Argument $argument 参数 + * @throws \LogicException + */ + public function addArgument(Argument $argument) + { + if (isset($this->arguments[$argument->getName()])) { + throw new \LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName())); + } + + if ($this->hasAnArrayArgument) { + throw new \LogicException('Cannot add an argument after an array argument.'); + } + + if ($argument->isRequired() && $this->hasOptional) { + throw new \LogicException('Cannot add a required argument after an optional one.'); + } + + if ($argument->isArray()) { + $this->hasAnArrayArgument = true; + } + + if ($argument->isRequired()) { + ++$this->requiredCount; + } else { + $this->hasOptional = true; + } + + $this->arguments[$argument->getName()] = $argument; + } + + /** + * 根据名称或者位置获取参数 + * @param string|int $name 参数名或者位置 + * @return Argument 参数 + * @throws \InvalidArgumentException + */ + public function getArgument($name) + { + if (!$this->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; + + return $arguments[$name]; + } + + /** + * 根据名称或位置检查是否具有某个参数 + * @param string|int $name 参数名或者位置 + * @return bool + * @api + */ + public function hasArgument($name) + { + $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; + + return isset($arguments[$name]); + } + + /** + * 获取所有的参数 + * @return Argument[] 参数数组 + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * 获取参数数量 + * @return int + */ + public function getArgumentCount() + { + return $this->hasAnArrayArgument ? PHP_INT_MAX : count($this->arguments); + } + + /** + * 获取必填的参数的数量 + * @return int + */ + public function getArgumentRequiredCount() + { + return $this->requiredCount; + } + + /** + * 获取参数默认值 + * @return array + */ + public function getArgumentDefaults() + { + $values = []; + foreach ($this->arguments as $argument) { + $values[$argument->getName()] = $argument->getDefault(); + } + + return $values; + } + + /** + * 设置选项 + * @param Option[] $options 选项数组 + */ + public function setOptions($options = []) + { + $this->options = []; + $this->shortcuts = []; + $this->addOptions($options); + } + + /** + * 添加选项 + * @param Option[] $options 选项数组 + * @api + */ + public function addOptions($options = []) + { + foreach ($options as $option) { + $this->addOption($option); + } + } + + /** + * 添加一个选项 + * @param Option $option 选项 + * @throws \LogicException + * @api + */ + public function addOption(Option $option) + { + if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) { + throw new \LogicException(sprintf('An option named "%s" already exists.', $option->getName())); + } + + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + if (isset($this->shortcuts[$shortcut]) + && !$option->equals($this->options[$this->shortcuts[$shortcut]]) + ) { + throw new \LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut)); + } + } + } + + $this->options[$option->getName()] = $option; + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + $this->shortcuts[$shortcut] = $option->getName(); + } + } + } + + /** + * 根据名称获取选项 + * @param string $name 选项名 + * @return Option + * @throws \InvalidArgumentException + * @api + */ + public function getOption($name) + { + if (!$this->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); + } + + return $this->options[$name]; + } + + /** + * 根据名称检查是否有这个选项 + * @param string $name 选项名 + * @return bool + * @api + */ + public function hasOption($name) + { + return isset($this->options[$name]); + } + + /** + * 获取所有选项 + * @return Option[] + * @api + */ + public function getOptions() + { + return $this->options; + } + + /** + * 根据名称检查某个选项是否有短名称 + * @param string $name 短名称 + * @return bool + */ + public function hasShortcut($name) + { + return isset($this->shortcuts[$name]); + } + + /** + * 根据短名称获取选项 + * @param string $shortcut 短名称 + * @return Option + */ + public function getOptionForShortcut($shortcut) + { + return $this->getOption($this->shortcutToName($shortcut)); + } + + /** + * 获取所有选项的默认值 + * @return array + */ + public function getOptionDefaults() + { + $values = []; + foreach ($this->options as $option) { + $values[$option->getName()] = $option->getDefault(); + } + + return $values; + } + + /** + * 根据短名称获取选项名 + * @param string $shortcut 短名称 + * @return string + * @throws \InvalidArgumentException + */ + private function shortcutToName($shortcut) + { + if (!isset($this->shortcuts[$shortcut])) { + throw new \InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + return $this->shortcuts[$shortcut]; + } + + /** + * 获取该指令的介绍 + * @param bool $short 是否简洁介绍 + * @return string + */ + public function getSynopsis($short = false) + { + $elements = []; + + if ($short && $this->getOptions()) { + $elements[] = '[options]'; + } elseif (!$short) { + foreach ($this->getOptions() as $option) { + $value = ''; + if ($option->acceptValue()) { + $value = sprintf(' %s%s%s', $option->isValueOptional() ? '[' : '', strtoupper($option->getName()), $option->isValueOptional() ? ']' : ''); + } + + $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; + $elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value); + } + } + + if (count($elements) && $this->getArguments()) { + $elements[] = '[--]'; + } + + foreach ($this->getArguments() as $argument) { + $element = '<' . $argument->getName() . '>'; + if (!$argument->isRequired()) { + $element = '[' . $element . ']'; + } elseif ($argument->isArray()) { + $element .= ' (' . $element . ')'; + } + + if ($argument->isArray()) { + $element .= '...'; + } + + $elements[] = $element; + } + + return implode(' ', $elements); + } +} diff --git a/thinkphp/library/think/console/input/Option.php b/thinkphp/library/think/console/input/Option.php new file mode 100644 index 0000000..e5707c9 --- /dev/null +++ b/thinkphp/library/think/console/input/Option.php @@ -0,0 +1,190 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Option +{ + + const VALUE_NONE = 1; + const VALUE_REQUIRED = 2; + const VALUE_OPTIONAL = 4; + const VALUE_IS_ARRAY = 8; + + private $name; + private $shortcut; + private $mode; + private $default; + private $description; + + /** + * 构造方法 + * @param string $name 选项名 + * @param string|array $shortcut 短名称,多个用|隔开或者使用数组 + * @param int $mode 选项类型(可选类型为 self::VALUE_*) + * @param string $description 描述 + * @param mixed $default 默认值 (类型为 self::VALUE_REQUIRED 或者 self::VALUE_NONE 的时候必须为null) + * @throws \InvalidArgumentException + */ + public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + if (0 === strpos($name, '--')) { + $name = substr($name, 2); + } + + if (empty($name)) { + throw new \InvalidArgumentException('An option name cannot be empty.'); + } + + if (empty($shortcut)) { + $shortcut = null; + } + + if (null !== $shortcut) { + if (is_array($shortcut)) { + $shortcut = implode('|', $shortcut); + } + $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-')); + $shortcuts = array_filter($shortcuts); + $shortcut = implode('|', $shortcuts); + + if (empty($shortcut)) { + throw new \InvalidArgumentException('An option shortcut cannot be empty.'); + } + } + + if (null === $mode) { + $mode = self::VALUE_NONE; + } elseif (!is_int($mode) || $mode > 15 || $mode < 1) { + throw new \InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->shortcut = $shortcut; + $this->mode = $mode; + $this->description = $description; + + if ($this->isArray() && !$this->acceptValue()) { + throw new \InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); + } + + $this->setDefault($default); + } + + /** + * 获取短名称 + * @return string + */ + public function getShortcut() + { + return $this->shortcut; + } + + /** + * 获取选项名 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 是否可以设置值 + * @return bool 类型不是 self::VALUE_NONE 的时候返回true,其他均返回false + */ + public function acceptValue() + { + return $this->isValueRequired() || $this->isValueOptional(); + } + + /** + * 是否必须 + * @return bool 类型是 self::VALUE_REQUIRED 的时候返回true,其他均返回false + */ + public function isValueRequired() + { + return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode); + } + + /** + * 是否可选 + * @return bool 类型是 self::VALUE_OPTIONAL 的时候返回true,其他均返回false + */ + public function isValueOptional() + { + return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode); + } + + /** + * 选项值是否接受数组 + * @return bool 类型是 self::VALUE_IS_ARRAY 的时候返回true,其他均返回false + */ + public function isArray() + { + return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); + } + + /** + * 设置默认值 + * @param mixed $default 默认值 + * @throws \LogicException + */ + public function setDefault($default = null) + { + if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { + throw new \LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!is_array($default)) { + throw new \LogicException('A default value for an array option must be an array.'); + } + } + + $this->default = $this->acceptValue() ? $default : false; + } + + /** + * 获取默认值 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 获取描述文字 + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * 检查所给选项是否是当前这个 + * @param Option $option + * @return bool + */ + public function equals(Option $option) + { + return $option->getName() === $this->getName() + && $option->getShortcut() === $this->getShortcut() + && $option->getDefault() === $this->getDefault() + && $option->isArray() === $this->isArray() + && $option->isValueRequired() === $this->isValueRequired() + && $option->isValueOptional() === $this->isValueOptional(); + } +} diff --git a/thinkphp/library/think/console/output/Ask.php b/thinkphp/library/think/console/output/Ask.php new file mode 100644 index 0000000..3933eb2 --- /dev/null +++ b/thinkphp/library/think/console/output/Ask.php @@ -0,0 +1,340 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +use think\console\Input; +use think\console\Output; +use think\console\output\question\Choice; +use think\console\output\question\Confirmation; + +class Ask +{ + private static $stty; + + private static $shell; + + /** @var Input */ + protected $input; + + /** @var Output */ + protected $output; + + /** @var Question */ + protected $question; + + public function __construct(Input $input, Output $output, Question $question) + { + $this->input = $input; + $this->output = $output; + $this->question = $question; + } + + public function run() + { + if (!$this->input->isInteractive()) { + return $this->question->getDefault(); + } + + if (!$this->question->getValidator()) { + return $this->doAsk(); + } + + $that = $this; + + $interviewer = function () use ($that) { + return $that->doAsk(); + }; + + return $this->validateAttempts($interviewer); + } + + protected function doAsk() + { + $this->writePrompt(); + + $inputStream = STDIN; + $autocomplete = $this->question->getAutocompleterValues(); + + if (null === $autocomplete || !$this->hasSttyAvailable()) { + $ret = false; + if ($this->question->isHidden()) { + try { + $ret = trim($this->getHiddenResponse($inputStream)); + } catch (\RuntimeException $e) { + if (!$this->question->isHiddenFallback()) { + throw $e; + } + } + } + + if (false === $ret) { + $ret = fgets($inputStream, 4096); + if (false === $ret) { + throw new \RuntimeException('Aborted'); + } + $ret = trim($ret); + } + } else { + $ret = trim($this->autocomplete($inputStream)); + } + + $ret = strlen($ret) > 0 ? $ret : $this->question->getDefault(); + + if ($normalizer = $this->question->getNormalizer()) { + return $normalizer($ret); + } + + return $ret; + } + + private function autocomplete($inputStream) + { + $autocomplete = $this->question->getAutocompleterValues(); + $ret = ''; + + $i = 0; + $ofs = -1; + $matches = $autocomplete; + $numMatches = count($matches); + + $sttyMode = shell_exec('stty -g'); + + shell_exec('stty -icanon -echo'); + + while (!feof($inputStream)) { + $c = fread($inputStream, 1); + + if ("\177" === $c) { + if (0 === $numMatches && 0 !== $i) { + --$i; + $this->output->write("\033[1D"); + } + + if ($i === 0) { + $ofs = -1; + $matches = $autocomplete; + $numMatches = count($matches); + } else { + $numMatches = 0; + } + + $ret = substr($ret, 0, $i); + } elseif ("\033" === $c) { + $c .= fread($inputStream, 2); + + if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { + if ('A' === $c[2] && -1 === $ofs) { + $ofs = 0; + } + + if (0 === $numMatches) { + continue; + } + + $ofs += ('A' === $c[2]) ? -1 : 1; + $ofs = ($numMatches + $ofs) % $numMatches; + } + } elseif (ord($c) < 32) { + if ("\t" === $c || "\n" === $c) { + if ($numMatches > 0 && -1 !== $ofs) { + $ret = $matches[$ofs]; + $this->output->write(substr($ret, $i)); + $i = strlen($ret); + } + + if ("\n" === $c) { + $this->output->write($c); + break; + } + + $numMatches = 0; + } + + continue; + } else { + $this->output->write($c); + $ret .= $c; + ++$i; + + $numMatches = 0; + $ofs = 0; + + foreach ($autocomplete as $value) { + if (0 === strpos($value, $ret) && $i !== strlen($value)) { + $matches[$numMatches++] = $value; + } + } + } + + $this->output->write("\033[K"); + + if ($numMatches > 0 && -1 !== $ofs) { + $this->output->write("\0337"); + $this->output->highlight(substr($matches[$ofs], $i)); + $this->output->write("\0338"); + } + } + + shell_exec(sprintf('stty %s', $sttyMode)); + + return $ret; + } + + protected function getHiddenResponse($inputStream) + { + if ('\\' === DIRECTORY_SEPARATOR) { + $exe = __DIR__ . '/../bin/hiddeninput.exe'; + + $value = rtrim(shell_exec($exe)); + $this->output->writeln(''); + + if (isset($tmpExe)) { + unlink($tmpExe); + } + + return $value; + } + + if ($this->hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); + + shell_exec('stty -echo'); + $value = fgets($inputStream, 4096); + shell_exec(sprintf('stty %s', $sttyMode)); + + if (false === $value) { + throw new \RuntimeException('Aborted'); + } + + $value = trim($value); + $this->output->writeln(''); + + return $value; + } + + if (false !== $shell = $this->getShell()) { + $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword'; + $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); + $value = rtrim(shell_exec($command)); + $this->output->writeln(''); + + return $value; + } + + throw new \RuntimeException('Unable to hide the response.'); + } + + protected function validateAttempts($interviewer) + { + /** @var \Exception $error */ + $error = null; + $attempts = $this->question->getMaxAttempts(); + while (null === $attempts || $attempts--) { + if (null !== $error) { + $this->output->error($error->getMessage()); + } + + try { + return call_user_func($this->question->getValidator(), $interviewer()); + } catch (\Exception $error) { + } + } + + throw $error; + } + + /** + * 显示问题的提示信息 + */ + protected function writePrompt() + { + $text = $this->question->getQuestion(); + $default = $this->question->getDefault(); + + switch (true) { + case null === $default: + $text = sprintf(' %s:', $text); + + break; + + case $this->question instanceof Confirmation: + $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no'); + + break; + + case $this->question instanceof Choice && $this->question->isMultiselect(): + $choices = $this->question->getChoices(); + $default = explode(',', $default); + + foreach ($default as $key => $value) { + $default[$key] = $choices[trim($value)]; + } + + $text = sprintf(' %s [%s]:', $text, implode(', ', $default)); + + break; + + case $this->question instanceof Choice: + $choices = $this->question->getChoices(); + $text = sprintf(' %s [%s]:', $text, $choices[$default]); + + break; + + default: + $text = sprintf(' %s [%s]:', $text, $default); + } + + $this->output->writeln($text); + + if ($this->question instanceof Choice) { + $width = max(array_map('strlen', array_keys($this->question->getChoices()))); + + foreach ($this->question->getChoices() as $key => $value) { + $this->output->writeln(sprintf(" [%-${width}s] %s", $key, $value)); + } + } + + $this->output->write(' > '); + } + + private function getShell() + { + if (null !== self::$shell) { + return self::$shell; + } + + self::$shell = false; + + if (file_exists('/usr/bin/env')) { + $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null"; + foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) { + if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) { + self::$shell = $sh; + break; + } + } + } + + return self::$shell; + } + + private function hasSttyAvailable() + { + if (null !== self::$stty) { + return self::$stty; + } + + exec('stty 2>&1', $output, $exitcode); + + return self::$stty = $exitcode === 0; + } +} diff --git a/thinkphp/library/think/console/output/Descriptor.php b/thinkphp/library/think/console/output/Descriptor.php new file mode 100644 index 0000000..6d98d53 --- /dev/null +++ b/thinkphp/library/think/console/output/Descriptor.php @@ -0,0 +1,319 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +use think\Console; +use think\console\Command; +use think\console\input\Argument as InputArgument; +use think\console\input\Definition as InputDefinition; +use think\console\input\Option as InputOption; +use think\console\Output; +use think\console\output\descriptor\Console as ConsoleDescription; + +class Descriptor +{ + + /** + * @var Output + */ + protected $output; + + /** + * {@inheritdoc} + */ + public function describe(Output $output, $object, array $options = []) + { + $this->output = $output; + + switch (true) { + case $object instanceof InputArgument: + $this->describeInputArgument($object, $options); + break; + case $object instanceof InputOption: + $this->describeInputOption($object, $options); + break; + case $object instanceof InputDefinition: + $this->describeInputDefinition($object, $options); + break; + case $object instanceof Command: + $this->describeCommand($object, $options); + break; + case $object instanceof Console: + $this->describeConsole($object, $options); + break; + default: + throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object))); + } + } + + /** + * 输出内容 + * @param string $content + * @param bool $decorated + */ + protected function write($content, $decorated = false) + { + $this->output->write($content, false, $decorated ? Output::OUTPUT_NORMAL : Output::OUTPUT_RAW); + } + + /** + * 描述参数 + * @param InputArgument $argument + * @param array $options + * @return string|mixed + */ + protected function describeInputArgument(InputArgument $argument, array $options = []) + { + if (null !== $argument->getDefault() + && (!is_array($argument->getDefault()) + || count($argument->getDefault())) + ) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault())); + } else { + $default = ''; + } + + $totalWidth = isset($options['total_width']) ? $options['total_width'] : strlen($argument->getName()); + $spacingWidth = $totalWidth - strlen($argument->getName()) + 2; + + $this->writeText(sprintf(" %s%s%s%s", $argument->getName(), str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces + preg_replace('/\s*\R\s*/', PHP_EOL . str_repeat(' ', $totalWidth + 17), $argument->getDescription()), $default), $options); + } + + /** + * 描述选项 + * @param InputOption $option + * @param array $options + * @return string|mixed + */ + protected function describeInputOption(InputOption $option, array $options = []) + { + if ($option->acceptValue() && null !== $option->getDefault() + && (!is_array($option->getDefault()) + || count($option->getDefault())) + ) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault())); + } else { + $default = ''; + } + + $value = ''; + if ($option->acceptValue()) { + $value = '=' . strtoupper($option->getName()); + + if ($option->isValueOptional()) { + $value = '[' . $value . ']'; + } + } + + $totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]); + $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', sprintf('--%s%s', $option->getName(), $value)); + + $spacingWidth = $totalWidth - strlen($synopsis) + 2; + + $this->writeText(sprintf(" %s%s%s%s%s", $synopsis, str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces + preg_replace('/\s*\R\s*/', "\n" . str_repeat(' ', $totalWidth + 17), $option->getDescription()), $default, $option->isArray() ? ' (multiple values allowed)' : ''), $options); + } + + /** + * 描述输入 + * @param InputDefinition $definition + * @param array $options + * @return string|mixed + */ + protected function describeInputDefinition(InputDefinition $definition, array $options = []) + { + $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); + foreach ($definition->getArguments() as $argument) { + $totalWidth = max($totalWidth, strlen($argument->getName())); + } + + if ($definition->getArguments()) { + $this->writeText('Arguments:', $options); + $this->writeText("\n"); + foreach ($definition->getArguments() as $argument) { + $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth])); + $this->writeText("\n"); + } + } + + if ($definition->getArguments() && $definition->getOptions()) { + $this->writeText("\n"); + } + + if ($definition->getOptions()) { + $laterOptions = []; + + $this->writeText('Options:', $options); + foreach ($definition->getOptions() as $option) { + if (strlen($option->getShortcut()) > 1) { + $laterOptions[] = $option; + continue; + } + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + foreach ($laterOptions as $option) { + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + } + } + + /** + * 描述指令 + * @param Command $command + * @param array $options + * @return string|mixed + */ + protected function describeCommand(Command $command, array $options = []) + { + $command->getSynopsis(true); + $command->getSynopsis(false); + $command->mergeConsoleDefinition(false); + + $this->writeText('Usage:', $options); + foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) { + $this->writeText("\n"); + $this->writeText(' ' . $usage, $options); + } + $this->writeText("\n"); + + $definition = $command->getNativeDefinition(); + if ($definition->getOptions() || $definition->getArguments()) { + $this->writeText("\n"); + $this->describeInputDefinition($definition, $options); + $this->writeText("\n"); + } + + if ($help = $command->getProcessedHelp()) { + $this->writeText("\n"); + $this->writeText('Help:', $options); + $this->writeText("\n"); + $this->writeText(' ' . str_replace("\n", "\n ", $help), $options); + $this->writeText("\n"); + } + } + + /** + * 描述控制台 + * @param Console $console + * @param array $options + * @return string|mixed + */ + protected function describeConsole(Console $console, array $options = []) + { + $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; + $description = new ConsoleDescription($console, $describedNamespace); + + if (isset($options['raw_text']) && $options['raw_text']) { + $width = $this->getColumnWidth($description->getCommands()); + + foreach ($description->getCommands() as $command) { + $this->writeText(sprintf("%-${width}s %s", $command->getName(), $command->getDescription()), $options); + $this->writeText("\n"); + } + } else { + if ('' != $help = $console->getHelp()) { + $this->writeText("$help\n\n", $options); + } + + $this->writeText("Usage:\n", $options); + $this->writeText(" command [options] [arguments]\n\n", $options); + + $this->describeInputDefinition(new InputDefinition($console->getDefinition()->getOptions()), $options); + + $this->writeText("\n"); + $this->writeText("\n"); + + $width = $this->getColumnWidth($description->getCommands()); + + if ($describedNamespace) { + $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options); + } else { + $this->writeText('Available commands:', $options); + } + + // add commands by namespace + foreach ($description->getNamespaces() as $namespace) { + if (!$describedNamespace && ConsoleDescription::GLOBAL_NAMESPACE !== $namespace['id']) { + $this->writeText("\n"); + $this->writeText(' ' . $namespace['id'] . '', $options); + } + + foreach ($namespace['commands'] as $name) { + $this->writeText("\n"); + $spacingWidth = $width - strlen($name); + $this->writeText(sprintf(" %s%s%s", $name, str_repeat(' ', $spacingWidth), $description->getCommand($name) + ->getDescription()), $options); + } + } + + $this->writeText("\n"); + } + } + + /** + * {@inheritdoc} + */ + private function writeText($content, array $options = []) + { + $this->write(isset($options['raw_text']) + && $options['raw_text'] ? strip_tags($content) : $content, isset($options['raw_output']) ? !$options['raw_output'] : true); + } + + /** + * 格式化 + * @param mixed $default + * @return string + */ + private function formatDefaultValue($default) + { + return json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + + /** + * @param Command[] $commands + * @return int + */ + private function getColumnWidth(array $commands) + { + $width = 0; + foreach ($commands as $command) { + $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width; + } + + return $width + 2; + } + + /** + * @param InputOption[] $options + * @return int + */ + private function calculateTotalWidthForOptions($options) + { + $totalWidth = 0; + foreach ($options as $option) { + $nameLength = 4 + strlen($option->getName()) + 2; // - + shortcut + , + whitespace + name + -- + + if ($option->acceptValue()) { + $valueLength = 1 + strlen($option->getName()); // = + value + $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ] + + $nameLength += $valueLength; + } + $totalWidth = max($totalWidth, $nameLength); + } + + return $totalWidth; + } +} diff --git a/thinkphp/library/think/console/output/Formatter.php b/thinkphp/library/think/console/output/Formatter.php new file mode 100644 index 0000000..f8bee55 --- /dev/null +++ b/thinkphp/library/think/console/output/Formatter.php @@ -0,0 +1,198 @@ + +// +---------------------------------------------------------------------- +namespace think\console\output; + +use think\console\output\formatter\Stack as StyleStack; +use think\console\output\formatter\Style; + +class Formatter +{ + + private $decorated = false; + private $styles = []; + private $styleStack; + + /** + * 转义 + * @param string $text + * @return string + */ + public static function escape($text) + { + return preg_replace('/([^\\\\]?)setStyle('error', new Style('white', 'red')); + $this->setStyle('info', new Style('green')); + $this->setStyle('comment', new Style('yellow')); + $this->setStyle('question', new Style('black', 'cyan')); + $this->setStyle('highlight', new Style('red')); + $this->setStyle('warning', new Style('black', 'yellow')); + + $this->styleStack = new StyleStack(); + } + + /** + * 设置外观标识 + * @param bool $decorated 是否美化文字 + */ + public function setDecorated($decorated) + { + $this->decorated = (bool) $decorated; + } + + /** + * 获取外观标识 + * @return bool + */ + public function isDecorated() + { + return $this->decorated; + } + + /** + * 添加一个新样式 + * @param string $name 样式名 + * @param Style $style 样式实例 + */ + public function setStyle($name, Style $style) + { + $this->styles[strtolower($name)] = $style; + } + + /** + * 是否有这个样式 + * @param string $name + * @return bool + */ + public function hasStyle($name) + { + return isset($this->styles[strtolower($name)]); + } + + /** + * 获取样式 + * @param string $name + * @return Style + * @throws \InvalidArgumentException + */ + public function getStyle($name) + { + if (!$this->hasStyle($name)) { + throw new \InvalidArgumentException(sprintf('Undefined style: %s', $name)); + } + + return $this->styles[strtolower($name)]; + } + + /** + * 使用所给的样式格式化文字 + * @param string $message 文字 + * @return string + */ + public function format($message) + { + $offset = 0; + $output = ''; + $tagRegex = '[a-z][a-z0-9_=;-]*'; + preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#isx", $message, $matches, PREG_OFFSET_CAPTURE); + foreach ($matches[0] as $i => $match) { + $pos = $match[1]; + $text = $match[0]; + + if (0 != $pos && '\\' == $message[$pos - 1]) { + continue; + } + + $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset)); + $offset = $pos + strlen($text); + + if ($open = '/' != $text[1]) { + $tag = $matches[1][$i][0]; + } else { + $tag = isset($matches[3][$i][0]) ? $matches[3][$i][0] : ''; + } + + if (!$open && !$tag) { + // + $this->styleStack->pop(); + } elseif (false === $style = $this->createStyleFromString(strtolower($tag))) { + $output .= $this->applyCurrentStyle($text); + } elseif ($open) { + $this->styleStack->push($style); + } else { + $this->styleStack->pop($style); + } + } + + $output .= $this->applyCurrentStyle(substr($message, $offset)); + + return str_replace('\\<', '<', $output); + } + + /** + * @return StyleStack + */ + public function getStyleStack() + { + return $this->styleStack; + } + + /** + * 根据字符串创建新的样式实例 + * @param string $string + * @return Style|bool + */ + private function createStyleFromString($string) + { + if (isset($this->styles[$string])) { + return $this->styles[$string]; + } + + if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', strtolower($string), $matches, PREG_SET_ORDER)) { + return false; + } + + $style = new Style(); + foreach ($matches as $match) { + array_shift($match); + + if ('fg' == $match[0]) { + $style->setForeground($match[1]); + } elseif ('bg' == $match[0]) { + $style->setBackground($match[1]); + } else { + try { + $style->setOption($match[1]); + } catch (\InvalidArgumentException $e) { + return false; + } + } + } + + return $style; + } + + /** + * 从堆栈应用样式到文字 + * @param string $text 文字 + * @return string + */ + private function applyCurrentStyle($text) + { + return $this->isDecorated() && strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text; + } +} diff --git a/thinkphp/library/think/console/output/Question.php b/thinkphp/library/think/console/output/Question.php new file mode 100644 index 0000000..03975f2 --- /dev/null +++ b/thinkphp/library/think/console/output/Question.php @@ -0,0 +1,211 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +class Question +{ + + private $question; + private $attempts; + private $hidden = false; + private $hiddenFallback = true; + private $autocompleterValues; + private $validator; + private $default; + private $normalizer; + + /** + * 构造方法 + * @param string $question 问题 + * @param mixed $default 默认答案 + */ + public function __construct($question, $default = null) + { + $this->question = $question; + $this->default = $default; + } + + /** + * 获取问题 + * @return string + */ + public function getQuestion() + { + return $this->question; + } + + /** + * 获取默认答案 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 是否隐藏答案 + * @return bool + */ + public function isHidden() + { + return $this->hidden; + } + + /** + * 隐藏答案 + * @param bool $hidden + * @return Question + */ + public function setHidden($hidden) + { + if ($this->autocompleterValues) { + throw new \LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->hidden = (bool) $hidden; + + return $this; + } + + /** + * 不能被隐藏是否撤销 + * @return bool + */ + public function isHiddenFallback() + { + return $this->hiddenFallback; + } + + /** + * 设置不能被隐藏的时候的操作 + * @param bool $fallback + * @return Question + */ + public function setHiddenFallback($fallback) + { + $this->hiddenFallback = (bool) $fallback; + + return $this; + } + + /** + * 获取自动完成 + * @return null|array|\Traversable + */ + public function getAutocompleterValues() + { + return $this->autocompleterValues; + } + + /** + * 设置自动完成的值 + * @param null|array|\Traversable $values + * @return Question + * @throws \InvalidArgumentException + * @throws \LogicException + */ + public function setAutocompleterValues($values) + { + if (is_array($values) && $this->isAssoc($values)) { + $values = array_merge(array_keys($values), array_values($values)); + } + + if (null !== $values && !is_array($values)) { + if (!$values instanceof \Traversable || $values instanceof \Countable) { + throw new \InvalidArgumentException('Autocompleter values can be either an array, `null` or an object implementing both `Countable` and `Traversable` interfaces.'); + } + } + + if ($this->hidden) { + throw new \LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->autocompleterValues = $values; + + return $this; + } + + /** + * 设置答案的验证器 + * @param null|callable $validator + * @return Question The current instance + */ + public function setValidator($validator) + { + $this->validator = $validator; + + return $this; + } + + /** + * 获取验证器 + * @return null|callable + */ + public function getValidator() + { + return $this->validator; + } + + /** + * 设置最大重试次数 + * @param null|int $attempts + * @return Question + * @throws \InvalidArgumentException + */ + public function setMaxAttempts($attempts) + { + if (null !== $attempts && $attempts < 1) { + throw new \InvalidArgumentException('Maximum number of attempts must be a positive value.'); + } + + $this->attempts = $attempts; + + return $this; + } + + /** + * 获取最大重试次数 + * @return null|int + */ + public function getMaxAttempts() + { + return $this->attempts; + } + + /** + * 设置响应的回调 + * @param string|\Closure $normalizer + * @return Question + */ + public function setNormalizer($normalizer) + { + $this->normalizer = $normalizer; + + return $this; + } + + /** + * 获取响应回调 + * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. + * @return string|\Closure + */ + public function getNormalizer() + { + return $this->normalizer; + } + + protected function isAssoc($array) + { + return (bool) count(array_filter(array_keys($array), 'is_string')); + } +} diff --git a/thinkphp/library/think/console/output/descriptor/Console.php b/thinkphp/library/think/console/output/descriptor/Console.php new file mode 100644 index 0000000..4648b68 --- /dev/null +++ b/thinkphp/library/think/console/output/descriptor/Console.php @@ -0,0 +1,149 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\descriptor; + +use think\Console as ThinkConsole; +use think\console\Command; + +class Console +{ + + const GLOBAL_NAMESPACE = '_global'; + + /** + * @var ThinkConsole + */ + private $console; + + /** + * @var null|string + */ + private $namespace; + + /** + * @var array + */ + private $namespaces; + + /** + * @var Command[] + */ + private $commands; + + /** + * @var Command[] + */ + private $aliases; + + /** + * 构造方法 + * @param ThinkConsole $console + * @param string|null $namespace + */ + public function __construct(ThinkConsole $console, $namespace = null) + { + $this->console = $console; + $this->namespace = $namespace; + } + + /** + * @return array + */ + public function getNamespaces() + { + if (null === $this->namespaces) { + $this->inspectConsole(); + } + + return $this->namespaces; + } + + /** + * @return Command[] + */ + public function getCommands() + { + if (null === $this->commands) { + $this->inspectConsole(); + } + + return $this->commands; + } + + /** + * @param string $name + * @return Command + * @throws \InvalidArgumentException + */ + public function getCommand($name) + { + if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) { + throw new \InvalidArgumentException(sprintf('Command %s does not exist.', $name)); + } + + return isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name]; + } + + private function inspectConsole() + { + $this->commands = []; + $this->namespaces = []; + + $all = $this->console->all($this->namespace ? $this->console->findNamespace($this->namespace) : null); + foreach ($this->sortCommands($all) as $namespace => $commands) { + $names = []; + + /** @var Command $command */ + foreach ($commands as $name => $command) { + if (!$command->getName()) { + continue; + } + + if ($command->getName() === $name) { + $this->commands[$name] = $command; + } else { + $this->aliases[$name] = $command; + } + + $names[] = $name; + } + + $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names]; + } + } + + /** + * @param array $commands + * @return array + */ + private function sortCommands(array $commands) + { + $namespacedCommands = []; + foreach ($commands as $name => $command) { + $key = $this->console->extractNamespace($name, 1); + if (!$key) { + $key = self::GLOBAL_NAMESPACE; + } + + $namespacedCommands[$key][$name] = $command; + } + ksort($namespacedCommands); + + foreach ($namespacedCommands as &$commandsSet) { + ksort($commandsSet); + } + // unset reference to keep scope clear + unset($commandsSet); + + return $namespacedCommands; + } +} diff --git a/thinkphp/library/think/console/output/driver/Buffer.php b/thinkphp/library/think/console/output/driver/Buffer.php new file mode 100644 index 0000000..c77a2ec --- /dev/null +++ b/thinkphp/library/think/console/output/driver/Buffer.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; + +class Buffer +{ + /** + * @var string + */ + private $buffer = ''; + + public function __construct(Output $output) + { + // do nothing + } + + public function fetch() + { + $content = $this->buffer; + $this->buffer = ''; + return $content; + } + + public function write($messages, $newline = false, $options = Output::OUTPUT_NORMAL) + { + $messages = (array) $messages; + + foreach ($messages as $message) { + $this->buffer .= $message; + } + if ($newline) { + $this->buffer .= "\n"; + } + } + + public function renderException(\Exception $e) + { + // do nothing + } + +} diff --git a/thinkphp/library/think/console/output/driver/Console.php b/thinkphp/library/think/console/output/driver/Console.php new file mode 100644 index 0000000..8f29fd0 --- /dev/null +++ b/thinkphp/library/think/console/output/driver/Console.php @@ -0,0 +1,373 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; +use think\console\output\Formatter; + +class Console +{ + + /** @var Resource */ + private $stdout; + + /** @var Formatter */ + private $formatter; + + private $terminalDimensions; + + /** @var Output */ + private $output; + + public function __construct(Output $output) + { + $this->output = $output; + $this->formatter = new Formatter(); + $this->stdout = $this->openOutputStream(); + $decorated = $this->hasColorSupport($this->stdout); + $this->formatter->setDecorated($decorated); + } + + public function getFormatter() + { + return $this->formatter; + } + + public function setDecorated($decorated) + { + $this->formatter->setDecorated($decorated); + } + + public function write($messages, $newline = false, $type = Output::OUTPUT_NORMAL, $stream = null) + { + if (Output::VERBOSITY_QUIET === $this->output->getVerbosity()) { + return; + } + + $messages = (array) $messages; + + foreach ($messages as $message) { + switch ($type) { + case Output::OUTPUT_NORMAL: + $message = $this->formatter->format($message); + break; + case Output::OUTPUT_RAW: + break; + case Output::OUTPUT_PLAIN: + $message = strip_tags($this->formatter->format($message)); + break; + default: + throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type)); + } + + $this->doWrite($message, $newline, $stream); + } + } + + public function renderException(\Exception $e) + { + $stderr = $this->openErrorStream(); + $decorated = $this->hasColorSupport($stderr); + $this->formatter->setDecorated($decorated); + + do { + $title = sprintf(' [%s] ', get_class($e)); + + $len = $this->stringWidth($title); + + $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX; + + if (defined('HHVM_VERSION') && $width > 1 << 31) { + $width = 1 << 31; + } + $lines = []; + foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) { + foreach ($this->splitStringByWidth($line, $width - 4) as $line) { + + $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $line)) + 4; + $lines[] = [$line, $lineLength]; + + $len = max($lineLength, $len); + } + } + + $messages = ['', '']; + $messages[] = $emptyLine = sprintf('%s', str_repeat(' ', $len)); + $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title)))); + foreach ($lines as $line) { + $messages[] = sprintf(' %s %s', $line[0], str_repeat(' ', $len - $line[1])); + } + $messages[] = $emptyLine; + $messages[] = ''; + $messages[] = ''; + + $this->write($messages, true, Output::OUTPUT_NORMAL, $stderr); + + if (Output::VERBOSITY_VERBOSE <= $this->output->getVerbosity()) { + $this->write('Exception trace:', true, Output::OUTPUT_NORMAL, $stderr); + + // exception related properties + $trace = $e->getTrace(); + array_unshift($trace, [ + 'function' => '', + 'file' => $e->getFile() !== null ? $e->getFile() : 'n/a', + 'line' => $e->getLine() !== null ? $e->getLine() : 'n/a', + 'args' => [], + ]); + + for ($i = 0, $count = count($trace); $i < $count; ++$i) { + $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : ''; + $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : ''; + $function = $trace[$i]['function']; + $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a'; + $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a'; + + $this->write(sprintf(' %s%s%s() at %s:%s', $class, $type, $function, $file, $line), true, Output::OUTPUT_NORMAL, $stderr); + } + + $this->write('', true, Output::OUTPUT_NORMAL, $stderr); + $this->write('', true, Output::OUTPUT_NORMAL, $stderr); + } + } while ($e = $e->getPrevious()); + + } + + /** + * 获取终端宽度 + * @return int|null + */ + protected function getTerminalWidth() + { + $dimensions = $this->getTerminalDimensions(); + + return $dimensions[0]; + } + + /** + * 获取终端高度 + * @return int|null + */ + protected function getTerminalHeight() + { + $dimensions = $this->getTerminalDimensions(); + + return $dimensions[1]; + } + + /** + * 获取当前终端的尺寸 + * @return array + */ + public function getTerminalDimensions() + { + if ($this->terminalDimensions) { + return $this->terminalDimensions; + } + + if ('\\' === DS) { + if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) { + return [(int) $matches[1], (int) $matches[2]]; + } + if (preg_match('/^(\d+)x(\d+)$/', $this->getMode(), $matches)) { + return [(int) $matches[1], (int) $matches[2]]; + } + } + + if ($sttyString = $this->getSttyColumns()) { + if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { + return [(int) $matches[2], (int) $matches[1]]; + } + if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { + return [(int) $matches[2], (int) $matches[1]]; + } + } + + return [null, null]; + } + + /** + * 获取stty列数 + * @return string + */ + private function getSttyColumns() + { + if (!function_exists('proc_open')) { + return; + } + + $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + return $info; + } + return; + } + + /** + * 获取终端模式 + * @return string x 或 null + */ + private function getMode() + { + if (!function_exists('proc_open')) { + return; + } + + $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $process = proc_open('mode CON', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { + return $matches[2] . 'x' . $matches[1]; + } + } + return; + } + + private function stringWidth($string) + { + if (!function_exists('mb_strwidth')) { + return strlen($string); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return strlen($string); + } + + return mb_strwidth($string, $encoding); + } + + private function splitStringByWidth($string, $width) + { + if (!function_exists('mb_strwidth')) { + return str_split($string, $width); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return str_split($string, $width); + } + + $utf8String = mb_convert_encoding($string, 'utf8', $encoding); + $lines = []; + $line = ''; + foreach (preg_split('//u', $utf8String) as $char) { + if (mb_strwidth($line . $char, 'utf8') <= $width) { + $line .= $char; + continue; + } + $lines[] = str_pad($line, $width); + $line = $char; + } + if (strlen($line)) { + $lines[] = count($lines) ? str_pad($line, $width) : $line; + } + + mb_convert_variables($encoding, 'utf8', $lines); + + return $lines; + } + + private function isRunningOS400() + { + $checks = [ + function_exists('php_uname') ? php_uname('s') : '', + getenv('OSTYPE'), + PHP_OS, + ]; + return false !== stripos(implode(';', $checks), 'OS400'); + } + + /** + * 当前环境是否支持写入控制台输出到stdout. + * + * @return bool + */ + protected function hasStdoutSupport() + { + return false === $this->isRunningOS400(); + } + + /** + * 当前环境是否支持写入控制台输出到stderr. + * + * @return bool + */ + protected function hasStderrSupport() + { + return false === $this->isRunningOS400(); + } + + /** + * @return resource + */ + private function openOutputStream() + { + if (!$this->hasStdoutSupport()) { + return fopen('php://output', 'w'); + } + return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w'); + } + + /** + * @return resource + */ + private function openErrorStream() + { + return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w'); + } + + /** + * 将消息写入到输出。 + * @param string $message 消息 + * @param bool $newline 是否另起一行 + * @param null $stream + */ + protected function doWrite($message, $newline, $stream = null) + { + if (null === $stream) { + $stream = $this->stdout; + } + if (false === @fwrite($stream, $message . ($newline ? PHP_EOL : ''))) { + throw new \RuntimeException('Unable to write output.'); + } + + fflush($stream); + } + + /** + * 是否支持着色 + * @param $stream + * @return bool + */ + protected function hasColorSupport($stream) + { + if (DIRECTORY_SEPARATOR === '\\') { + return + '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD + || false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + || 'xterm' === getenv('TERM'); + } + + return function_exists('posix_isatty') && @posix_isatty($stream); + } + +} diff --git a/thinkphp/library/think/console/output/driver/Nothing.php b/thinkphp/library/think/console/output/driver/Nothing.php new file mode 100644 index 0000000..9a55f77 --- /dev/null +++ b/thinkphp/library/think/console/output/driver/Nothing.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; + +class Nothing +{ + + public function __construct(Output $output) + { + // do nothing + } + + public function write($messages, $newline = false, $options = Output::OUTPUT_NORMAL) + { + // do nothing + } + + public function renderException(\Exception $e) + { + // do nothing + } +} diff --git a/thinkphp/library/think/console/output/formatter/Stack.php b/thinkphp/library/think/console/output/formatter/Stack.php new file mode 100644 index 0000000..4864a3f --- /dev/null +++ b/thinkphp/library/think/console/output/formatter/Stack.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\formatter; + +class Stack +{ + + /** + * @var Style[] + */ + private $styles; + + /** + * @var Style + */ + private $emptyStyle; + + /** + * 构造方法 + * @param Style|null $emptyStyle + */ + public function __construct(Style $emptyStyle = null) + { + $this->emptyStyle = $emptyStyle ?: new Style(); + $this->reset(); + } + + /** + * 重置堆栈 + */ + public function reset() + { + $this->styles = []; + } + + /** + * 推一个样式进入堆栈 + * @param Style $style + */ + public function push(Style $style) + { + $this->styles[] = $style; + } + + /** + * 从堆栈中弹出一个样式 + * @param Style|null $style + * @return Style + * @throws \InvalidArgumentException + */ + public function pop(Style $style = null) + { + if (empty($this->styles)) { + return $this->emptyStyle; + } + + if (null === $style) { + return array_pop($this->styles); + } + + /** + * @var int $index + * @var Style $stackedStyle + */ + foreach (array_reverse($this->styles, true) as $index => $stackedStyle) { + if ($style->apply('') === $stackedStyle->apply('')) { + $this->styles = array_slice($this->styles, 0, $index); + + return $stackedStyle; + } + } + + throw new \InvalidArgumentException('Incorrectly nested style tag found.'); + } + + /** + * 计算堆栈的当前样式。 + * @return Style + */ + public function getCurrent() + { + if (empty($this->styles)) { + return $this->emptyStyle; + } + + return $this->styles[count($this->styles) - 1]; + } + + /** + * @param Style $emptyStyle + * @return Stack + */ + public function setEmptyStyle(Style $emptyStyle) + { + $this->emptyStyle = $emptyStyle; + + return $this; + } + + /** + * @return Style + */ + public function getEmptyStyle() + { + return $this->emptyStyle; + } +} diff --git a/thinkphp/library/think/console/output/formatter/Style.php b/thinkphp/library/think/console/output/formatter/Style.php new file mode 100644 index 0000000..d9b0999 --- /dev/null +++ b/thinkphp/library/think/console/output/formatter/Style.php @@ -0,0 +1,189 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\formatter; + +class Style +{ + + private static $availableForegroundColors = [ + 'black' => ['set' => 30, 'unset' => 39], + 'red' => ['set' => 31, 'unset' => 39], + 'green' => ['set' => 32, 'unset' => 39], + 'yellow' => ['set' => 33, 'unset' => 39], + 'blue' => ['set' => 34, 'unset' => 39], + 'magenta' => ['set' => 35, 'unset' => 39], + 'cyan' => ['set' => 36, 'unset' => 39], + 'white' => ['set' => 37, 'unset' => 39], + ]; + private static $availableBackgroundColors = [ + 'black' => ['set' => 40, 'unset' => 49], + 'red' => ['set' => 41, 'unset' => 49], + 'green' => ['set' => 42, 'unset' => 49], + 'yellow' => ['set' => 43, 'unset' => 49], + 'blue' => ['set' => 44, 'unset' => 49], + 'magenta' => ['set' => 45, 'unset' => 49], + 'cyan' => ['set' => 46, 'unset' => 49], + 'white' => ['set' => 47, 'unset' => 49], + ]; + private static $availableOptions = [ + 'bold' => ['set' => 1, 'unset' => 22], + 'underscore' => ['set' => 4, 'unset' => 24], + 'blink' => ['set' => 5, 'unset' => 25], + 'reverse' => ['set' => 7, 'unset' => 27], + 'conceal' => ['set' => 8, 'unset' => 28], + ]; + + private $foreground; + private $background; + private $options = []; + + /** + * 初始化输出的样式 + * @param string|null $foreground 字体颜色 + * @param string|null $background 背景色 + * @param array $options 格式 + * @api + */ + public function __construct($foreground = null, $background = null, array $options = []) + { + if (null !== $foreground) { + $this->setForeground($foreground); + } + if (null !== $background) { + $this->setBackground($background); + } + if (count($options)) { + $this->setOptions($options); + } + } + + /** + * 设置字体颜色 + * @param string|null $color 颜色名 + * @throws \InvalidArgumentException + * @api + */ + public function setForeground($color = null) + { + if (null === $color) { + $this->foreground = null; + + return; + } + + if (!isset(static::$availableForegroundColors[$color])) { + throw new \InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableForegroundColors)))); + } + + $this->foreground = static::$availableForegroundColors[$color]; + } + + /** + * 设置背景色 + * @param string|null $color 颜色名 + * @throws \InvalidArgumentException + * @api + */ + public function setBackground($color = null) + { + if (null === $color) { + $this->background = null; + + return; + } + + if (!isset(static::$availableBackgroundColors[$color])) { + throw new \InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableBackgroundColors)))); + } + + $this->background = static::$availableBackgroundColors[$color]; + } + + /** + * 设置字体格式 + * @param string $option 格式名 + * @throws \InvalidArgumentException When the option name isn't defined + * @api + */ + public function setOption($option) + { + if (!isset(static::$availableOptions[$option])) { + throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)))); + } + + if (!in_array(static::$availableOptions[$option], $this->options)) { + $this->options[] = static::$availableOptions[$option]; + } + } + + /** + * 重置字体格式 + * @param string $option 格式名 + * @throws \InvalidArgumentException + */ + public function unsetOption($option) + { + if (!isset(static::$availableOptions[$option])) { + throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)))); + } + + $pos = array_search(static::$availableOptions[$option], $this->options); + if (false !== $pos) { + unset($this->options[$pos]); + } + } + + /** + * 批量设置字体格式 + * @param array $options + */ + public function setOptions(array $options) + { + $this->options = []; + + foreach ($options as $option) { + $this->setOption($option); + } + } + + /** + * 应用样式到文字 + * @param string $text 文字 + * @return string + */ + public function apply($text) + { + $setCodes = []; + $unsetCodes = []; + + if (null !== $this->foreground) { + $setCodes[] = $this->foreground['set']; + $unsetCodes[] = $this->foreground['unset']; + } + if (null !== $this->background) { + $setCodes[] = $this->background['set']; + $unsetCodes[] = $this->background['unset']; + } + if (count($this->options)) { + foreach ($this->options as $option) { + $setCodes[] = $option['set']; + $unsetCodes[] = $option['unset']; + } + } + + if (0 === count($setCodes)) { + return $text; + } + + return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes)); + } +} diff --git a/thinkphp/library/think/console/output/question/Choice.php b/thinkphp/library/think/console/output/question/Choice.php new file mode 100644 index 0000000..f6760e5 --- /dev/null +++ b/thinkphp/library/think/console/output/question/Choice.php @@ -0,0 +1,163 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\question; + +use think\console\output\Question; + +class Choice extends Question +{ + + private $choices; + private $multiselect = false; + private $prompt = ' > '; + private $errorMessage = 'Value "%s" is invalid'; + + /** + * 构造方法 + * @param string $question 问题 + * @param array $choices 选项 + * @param mixed $default 默认答案 + */ + public function __construct($question, array $choices, $default = null) + { + parent::__construct($question, $default); + + $this->choices = $choices; + $this->setValidator($this->getDefaultValidator()); + $this->setAutocompleterValues($choices); + } + + /** + * 可选项 + * @return array + */ + public function getChoices() + { + return $this->choices; + } + + /** + * 设置可否多选 + * @param bool $multiselect + * @return self + */ + public function setMultiselect($multiselect) + { + $this->multiselect = $multiselect; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + public function isMultiselect() + { + return $this->multiselect; + } + + /** + * 获取提示 + * @return string + */ + public function getPrompt() + { + return $this->prompt; + } + + /** + * 设置提示 + * @param string $prompt + * @return self + */ + public function setPrompt($prompt) + { + $this->prompt = $prompt; + + return $this; + } + + /** + * 设置错误提示信息 + * @param string $errorMessage + * @return self + */ + public function setErrorMessage($errorMessage) + { + $this->errorMessage = $errorMessage; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + /** + * 获取默认的验证方法 + * @return callable + */ + private function getDefaultValidator() + { + $choices = $this->choices; + $errorMessage = $this->errorMessage; + $multiselect = $this->multiselect; + $isAssoc = $this->isAssoc($choices); + + return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) { + // Collapse all spaces. + $selectedChoices = str_replace(' ', '', $selected); + + if ($multiselect) { + // Check for a separated comma values + if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) { + throw new \InvalidArgumentException(sprintf($errorMessage, $selected)); + } + $selectedChoices = explode(',', $selectedChoices); + } else { + $selectedChoices = [$selected]; + } + + $multiselectChoices = []; + foreach ($selectedChoices as $value) { + $results = []; + foreach ($choices as $key => $choice) { + if ($choice === $value) { + $results[] = $key; + } + } + + if (count($results) > 1) { + throw new \InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of %s.', implode(' or ', $results))); + } + + $result = array_search($value, $choices); + + if (!$isAssoc) { + if (!empty($result)) { + $result = $choices[$result]; + } elseif (isset($choices[$value])) { + $result = $choices[$value]; + } + } elseif (empty($result) && array_key_exists($value, $choices)) { + $result = $value; + } + + if (empty($result)) { + throw new \InvalidArgumentException(sprintf($errorMessage, $value)); + } + array_push($multiselectChoices, $result); + } + + if ($multiselect) { + return $multiselectChoices; + } + + return current($multiselectChoices); + }; + } +} diff --git a/thinkphp/library/think/console/output/question/Confirmation.php b/thinkphp/library/think/console/output/question/Confirmation.php new file mode 100644 index 0000000..6598f9b --- /dev/null +++ b/thinkphp/library/think/console/output/question/Confirmation.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\question; + +use think\console\output\Question; + +class Confirmation extends Question +{ + + private $trueAnswerRegex; + + /** + * 构造方法 + * @param string $question 问题 + * @param bool $default 默认答案 + * @param string $trueAnswerRegex 验证正则 + */ + public function __construct($question, $default = true, $trueAnswerRegex = '/^y/i') + { + parent::__construct($question, (bool) $default); + + $this->trueAnswerRegex = $trueAnswerRegex; + $this->setNormalizer($this->getDefaultNormalizer()); + } + + /** + * 获取默认的答案回调 + * @return callable + */ + private function getDefaultNormalizer() + { + $default = $this->getDefault(); + $regex = $this->trueAnswerRegex; + + return function ($answer) use ($default, $regex) { + if (is_bool($answer)) { + return $answer; + } + + $answerIsTrue = (bool) preg_match($regex, $answer); + if (false === $default) { + return $answer && $answerIsTrue; + } + + return !$answer || $answerIsTrue; + }; + } +} diff --git a/thinkphp/library/think/controller/Rest.php b/thinkphp/library/think/controller/Rest.php new file mode 100644 index 0000000..8c5911d --- /dev/null +++ b/thinkphp/library/think/controller/Rest.php @@ -0,0 +1,99 @@ + +// +---------------------------------------------------------------------- + +namespace think\controller; + +use think\App; +use think\Request; +use think\Response; + +abstract class Rest +{ + + protected $method; // 当前请求类型 + protected $type; // 当前资源类型 + // 输出类型 + protected $restMethodList = 'get|post|put|delete'; + protected $restDefaultMethod = 'get'; + protected $restTypeList = 'html|xml|json|rss'; + protected $restDefaultType = 'html'; + protected $restOutputType = [ // REST允许输出的资源类型列表 + 'xml' => 'application/xml', + 'json' => 'application/json', + 'html' => 'text/html', + ]; + + /** + * 构造函数 取得模板对象实例 + * @access public + */ + public function __construct() + { + // 资源类型检测 + $request = Request::instance(); + $ext = $request->ext(); + if ('' == $ext) { + // 自动检测资源类型 + $this->type = $request->type(); + } elseif (!preg_match('/\(' . $this->restTypeList . '\)$/i', $ext)) { + // 资源类型非法 则用默认资源类型访问 + $this->type = $this->restDefaultType; + } else { + $this->type = $ext; + } + // 请求方式检测 + $method = strtolower($request->method()); + if (false === stripos($this->restMethodList, $method)) { + // 请求方式非法 则用默认请求方法 + $method = $this->restDefaultMethod; + } + $this->method = $method; + } + + /** + * REST 调用 + * @access public + * @param string $method 方法名 + * @return mixed + * @throws \Exception + */ + public function _empty($method) + { + if (method_exists($this, $method . '_' . $this->method . '_' . $this->type)) { + // RESTFul方法支持 + $fun = $method . '_' . $this->method . '_' . $this->type; + } elseif ($this->method == $this->restDefaultMethod && method_exists($this, $method . '_' . $this->type)) { + $fun = $method . '_' . $this->type; + } elseif ($this->type == $this->restDefaultType && method_exists($this, $method . '_' . $this->method)) { + $fun = $method . '_' . $this->method; + } + if (isset($fun)) { + return App::invokeMethod([$this, $fun]); + } else { + // 抛出异常 + throw new \Exception('error action :' . $method); + } + } + + /** + * 输出返回数据 + * @access protected + * @param mixed $data 要返回的数据 + * @param String $type 返回类型 JSON XML + * @param integer $code HTTP状态码 + * @return Response + */ + protected function response($data, $type = 'json', $code = 200) + { + return Response::create($data, $type)->code($code); + } + +} diff --git a/thinkphp/library/think/controller/Yar.php b/thinkphp/library/think/controller/Yar.php new file mode 100644 index 0000000..af4e9a1 --- /dev/null +++ b/thinkphp/library/think/controller/Yar.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- + +namespace think\controller; + +/** + * ThinkPHP Yar控制器类 + */ +abstract class Yar +{ + + /** + * 构造函数 + * @access public + */ + public function __construct() + { + //控制器初始化 + if (method_exists($this, '_initialize')) { + $this->_initialize(); + } + + //判断扩展是否存在 + if (!extension_loaded('yar')) { + throw new \Exception('not support yar'); + } + + //实例化Yar_Server + $server = new \Yar_Server($this); + // 启动server + $server->handle(); + } + + /** + * 魔术方法 有不存在的操作的时候执行 + * @access public + * @param string $method 方法名 + * @param array $args 参数 + * @return mixed + */ + public function __call($method, $args) + {} +} diff --git a/thinkphp/library/think/db/Builder.php b/thinkphp/library/think/db/Builder.php new file mode 100644 index 0000000..99f6031 --- /dev/null +++ b/thinkphp/library/think/db/Builder.php @@ -0,0 +1,848 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use PDO; +use think\Exception; + +abstract class Builder +{ + // connection对象实例 + protected $connection; + // 查询对象实例 + protected $query; + + // 数据库表达式 + protected $exp = ['eq' => '=', 'neq' => '<>', 'gt' => '>', 'egt' => '>=', 'lt' => '<', 'elt' => '<=', 'notlike' => 'NOT LIKE', 'not like' => 'NOT LIKE', 'like' => 'LIKE', 'in' => 'IN', 'exp' => 'EXP', 'notin' => 'NOT IN', 'not in' => 'NOT IN', 'between' => 'BETWEEN', 'not between' => 'NOT BETWEEN', 'notbetween' => 'NOT BETWEEN', 'exists' => 'EXISTS', 'notexists' => 'NOT EXISTS', 'not exists' => 'NOT EXISTS', 'null' => 'NULL', 'notnull' => 'NOT NULL', 'not null' => 'NOT NULL', '> time' => '> TIME', '< time' => '< TIME', '>= time' => '>= TIME', '<= time' => '<= TIME', 'between time' => 'BETWEEN TIME', 'not between time' => 'NOT BETWEEN TIME', 'notbetween time' => 'NOT BETWEEN TIME']; + + // SQL表达式 + protected $selectSql = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%LOCK%%COMMENT%'; + protected $insertSql = '%INSERT% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%'; + protected $insertAllSql = '%INSERT% INTO %TABLE% (%FIELD%) %DATA% %COMMENT%'; + protected $updateSql = 'UPDATE %TABLE% SET %SET% %JOIN% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; + protected $deleteSql = 'DELETE FROM %TABLE% %USING% %JOIN% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; + + /** + * 构造函数 + * @access public + * @param Connection $connection 数据库连接对象实例 + * @param Query $query 数据库查询对象实例 + */ + public function __construct(Connection $connection, Query $query) + { + $this->connection = $connection; + $this->query = $query; + } + + /** + * 获取当前的连接对象实例 + * @access public + * @return void + */ + public function getConnection() + { + return $this->connection; + } + + /** + * 获取当前的Query对象实例 + * @access public + * @return void + */ + public function getQuery() + { + return $this->query; + } + + /** + * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写) + * @access protected + * @param string $sql sql语句 + * @return string + */ + protected function parseSqlTable($sql) + { + return $this->query->parseSqlTable($sql); + } + + /** + * 数据分析 + * @access protected + * @param array $data 数据 + * @param array $options 查询参数 + * @return array + */ + protected function parseData($data, $options) + { + if (empty($data)) { + return []; + } + + // 获取绑定信息 + $bind = $this->query->getFieldsBind($options['table']); + if ('*' == $options['field']) { + $fields = array_keys($bind); + } else { + $fields = $options['field']; + } + + $result = []; + foreach ($data as $key => $val) { + $item = $this->parseKey($key, $options); + if (is_object($val) && method_exists($val, '__toString')) { + // 对象数据写入 + $val = $val->__toString(); + } + if (false === strpos($key, '.') && !in_array($key, $fields, true)) { + if ($options['strict']) { + throw new Exception('fields not exists:[' . $key . ']'); + } + } elseif (is_null($val)) { + $result[$item] = 'NULL'; + } elseif (isset($val[0]) && 'exp' == $val[0]) { + $result[$item] = $val[1]; + } elseif (is_scalar($val)) { + // 过滤非标量数据 + if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) { + $result[$item] = $val; + } else { + $key = str_replace('.', '_', $key); + $this->query->bind('__data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR); + $result[$item] = ':__data__' . $key; + } + } + } + return $result; + } + + /** + * 字段名分析 + * @access protected + * @param string $key + * @param array $options + * @return string + */ + protected function parseKey($key, $options = []) + { + return $key; + } + + /** + * value分析 + * @access protected + * @param mixed $value + * @param string $field + * @return string|array + */ + protected function parseValue($value, $field = '') + { + if (is_string($value)) { + $value = strpos($value, ':') === 0 && $this->query->isBind(substr($value, 1)) ? $value : $this->connection->quote($value); + } elseif (is_array($value)) { + $value = array_map([$this, 'parseValue'], $value); + } elseif (is_bool($value)) { + $value = $value ? '1' : '0'; + } elseif (is_null($value)) { + $value = 'null'; + } + return $value; + } + + /** + * field分析 + * @access protected + * @param mixed $fields + * @param array $options + * @return string + */ + protected function parseField($fields, $options = []) + { + if ('*' == $fields || empty($fields)) { + $fieldsStr = '*'; + } elseif (is_array($fields)) { + // 支持 'field1'=>'field2' 这样的字段别名定义 + $array = []; + foreach ($fields as $key => $field) { + if (!is_numeric($key)) { + $array[] = $this->parseKey($key, $options) . ' AS ' . $this->parseKey($field, $options); + } else { + $array[] = $this->parseKey($field, $options); + } + } + $fieldsStr = implode(',', $array); + } + return $fieldsStr; + } + + /** + * table分析 + * @access protected + * @param mixed $tables + * @param array $options + * @return string + */ + protected function parseTable($tables, $options = []) + { + $item = []; + foreach ((array) $tables as $key => $table) { + if (!is_numeric($key)) { + if (strpos($key, '@think')) { + $key = strstr($key, '@think', true); + } + $key = $this->parseSqlTable($key); + $item[] = $this->parseKey($key) . ' ' . (isset($options['alias'][$table]) ? $this->parseKey($options['alias'][$table]) : $this->parseKey($table)); + } else { + $table = $this->parseSqlTable($table); + if (isset($options['alias'][$table])) { + $item[] = $this->parseKey($table) . ' ' . $this->parseKey($options['alias'][$table]); + } else { + $item[] = $this->parseKey($table); + } + } + } + return implode(',', $item); + } + + /** + * where分析 + * @access protected + * @param mixed $where 查询条件 + * @param array $options 查询参数 + * @return string + */ + protected function parseWhere($where, $options) + { + $whereStr = $this->buildWhere($where, $options); + if (!empty($options['soft_delete'])) { + // 附加软删除条件 + list($field, $condition) = $options['soft_delete']; + + $binds = $this->query->getFieldsBind($options['table']); + $whereStr = $whereStr ? '( ' . $whereStr . ' ) AND ' : ''; + $whereStr = $whereStr . $this->parseWhereItem($field, $condition, '', $options, $binds); + } + return empty($whereStr) ? '' : ' WHERE ' . $whereStr; + } + + /** + * 生成查询条件SQL + * @access public + * @param mixed $where + * @param array $options + * @return string + */ + public function buildWhere($where, $options) + { + if (empty($where)) { + $where = []; + } + + if ($where instanceof Query) { + return $this->buildWhere($where->getOptions('where'), $options); + } + + $whereStr = ''; + $binds = $this->query->getFieldsBind($options['table']); + foreach ($where as $key => $val) { + $str = []; + foreach ($val as $field => $value) { + if ($value instanceof \Closure) { + // 使用闭包查询 + $query = new Query($this->connection); + call_user_func_array($value, [ & $query]); + $whereClause = $this->buildWhere($query->getOptions('where'), $options); + if (!empty($whereClause)) { + $str[] = ' ' . $key . ' ( ' . $whereClause . ' )'; + } + } elseif (strpos($field, '|')) { + // 不同字段使用相同查询条件(OR) + $array = explode('|', $field); + $item = []; + foreach ($array as $k) { + $item[] = $this->parseWhereItem($k, $value, '', $options, $binds); + } + $str[] = ' ' . $key . ' ( ' . implode(' OR ', $item) . ' )'; + } elseif (strpos($field, '&')) { + // 不同字段使用相同查询条件(AND) + $array = explode('&', $field); + $item = []; + foreach ($array as $k) { + $item[] = $this->parseWhereItem($k, $value, '', $options, $binds); + } + $str[] = ' ' . $key . ' ( ' . implode(' AND ', $item) . ' )'; + } else { + // 对字段使用表达式查询 + $field = is_string($field) ? $field : ''; + $str[] = ' ' . $key . ' ' . $this->parseWhereItem($field, $value, $key, $options, $binds); + } + } + + $whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($key) + 1) : implode(' ', $str); + } + + return $whereStr; + } + + // where子单元分析 + protected function parseWhereItem($field, $val, $rule = '', $options = [], $binds = [], $bindName = null) + { + // 字段分析 + $key = $field ? $this->parseKey($field, $options) : ''; + + // 查询规则和条件 + if (!is_array($val)) { + $val = ['=', $val]; + } + list($exp, $value) = $val; + + // 对一个字段使用多个查询条件 + if (is_array($exp)) { + $item = array_pop($val); + // 传入 or 或者 and + if (is_string($item) && in_array($item, ['AND', 'and', 'OR', 'or'])) { + $rule = $item; + } else { + array_push($val, $item); + } + foreach ($val as $k => $item) { + $bindName = 'where_' . str_replace('.', '_', $field) . '_' . $k; + $str[] = $this->parseWhereItem($field, $item, $rule, $options, $binds, $bindName); + } + return '( ' . implode(' ' . $rule . ' ', $str) . ' )'; + } + + // 检测操作符 + if (!in_array($exp, $this->exp)) { + $exp = strtolower($exp); + if (isset($this->exp[$exp])) { + $exp = $this->exp[$exp]; + } else { + throw new Exception('where express error:' . $exp); + } + } + $bindName = $bindName ?: 'where_' . str_replace(['.', '-'], '_', $field); + if (preg_match('/\W/', $bindName)) { + // 处理带非单词字符的字段名 + $bindName = md5($bindName); + } + + $bindType = isset($binds[$field]) ? $binds[$field] : PDO::PARAM_STR; + if (is_scalar($value) && array_key_exists($field, $binds) && !in_array($exp, ['EXP', 'NOT NULL', 'NULL', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN']) && strpos($exp, 'TIME') === false) { + if (strpos($value, ':') !== 0 || !$this->query->isBind(substr($value, 1))) { + if ($this->query->isBind($bindName)) { + $bindName .= '_' . str_replace('.', '_', uniqid('', true)); + } + $this->query->bind($bindName, $value, $bindType); + $value = ':' . $bindName; + } + } + + $whereStr = ''; + if (in_array($exp, ['=', '<>', '>', '>=', '<', '<='])) { + // 比较运算 + if ($value instanceof \Closure) { + $whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value); + } else { + $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value, $field); + } + } elseif ('LIKE' == $exp || 'NOT LIKE' == $exp) { + // 模糊匹配 + if (is_array($value)) { + foreach ($value as $item) { + $array[] = $key . ' ' . $exp . ' ' . $this->parseValue($item, $field); + } + $logic = isset($val[2]) ? $val[2] : 'AND'; + $whereStr .= '(' . implode($array, ' ' . strtoupper($logic) . ' ') . ')'; + } else { + $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value, $field); + } + } elseif ('EXP' == $exp) { + // 表达式查询 + $whereStr .= '( ' . $key . ' ' . $value . ' )'; + } elseif (in_array($exp, ['NOT NULL', 'NULL'])) { + // NULL 查询 + $whereStr .= $key . ' IS ' . $exp; + } elseif (in_array($exp, ['NOT IN', 'IN'])) { + // IN 查询 + if ($value instanceof \Closure) { + $whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value); + } else { + $value = array_unique(is_array($value) ? $value : explode(',', $value)); + if (array_key_exists($field, $binds)) { + $bind = []; + $array = []; + $i = 0; + foreach ($value as $v) { + $i++; + if ($this->query->isBind($bindName . '_in_' . $i)) { + $bindKey = $bindName . '_in_' . uniqid() . '_' . $i; + } else { + $bindKey = $bindName . '_in_' . $i; + } + $bind[$bindKey] = [$v, $bindType]; + $array[] = ':' . $bindKey; + } + $this->query->bind($bind); + $zone = implode(',', $array); + } else { + $zone = implode(',', $this->parseValue($value, $field)); + } + $whereStr .= $key . ' ' . $exp . ' (' . (empty($zone) ? "''" : $zone) . ')'; + } + } elseif (in_array($exp, ['NOT BETWEEN', 'BETWEEN'])) { + // BETWEEN 查询 + $data = is_array($value) ? $value : explode(',', $value); + if (array_key_exists($field, $binds)) { + if ($this->query->isBind($bindName . '_between_1')) { + $bindKey1 = $bindName . '_between_1' . uniqid(); + $bindKey2 = $bindName . '_between_2' . uniqid(); + } else { + $bindKey1 = $bindName . '_between_1'; + $bindKey2 = $bindName . '_between_2'; + } + $bind = [ + $bindKey1 => [$data[0], $bindType], + $bindKey2 => [$data[1], $bindType], + ]; + $this->query->bind($bind); + $between = ':' . $bindKey1 . ' AND :' . $bindKey2; + } else { + $between = $this->parseValue($data[0], $field) . ' AND ' . $this->parseValue($data[1], $field); + } + $whereStr .= $key . ' ' . $exp . ' ' . $between; + } elseif (in_array($exp, ['NOT EXISTS', 'EXISTS'])) { + // EXISTS 查询 + if ($value instanceof \Closure) { + $whereStr .= $exp . ' ' . $this->parseClosure($value); + } else { + $whereStr .= $exp . ' (' . $value . ')'; + } + } elseif (in_array($exp, ['< TIME', '> TIME', '<= TIME', '>= TIME'])) { + $whereStr .= $key . ' ' . substr($exp, 0, 2) . ' ' . $this->parseDateTime($value, $field, $options, $bindName, $bindType); + } elseif (in_array($exp, ['BETWEEN TIME', 'NOT BETWEEN TIME'])) { + if (is_string($value)) { + $value = explode(',', $value); + } + + $whereStr .= $key . ' ' . substr($exp, 0, -4) . $this->parseDateTime($value[0], $field, $options, $bindName . '_between_1', $bindType) . ' AND ' . $this->parseDateTime($value[1], $field, $options, $bindName . '_between_2', $bindType); + } + return $whereStr; + } + + // 执行闭包子查询 + protected function parseClosure($call, $show = true) + { + $query = new Query($this->connection); + call_user_func_array($call, [ & $query]); + return $query->buildSql($show); + } + + /** + * 日期时间条件解析 + * @access protected + * @param string $value + * @param string $key + * @param array $options + * @param string $bindName + * @param integer $bindType + * @return string + */ + protected function parseDateTime($value, $key, $options = [], $bindName = null, $bindType = null) + { + // 获取时间字段类型 + if (strpos($key, '.')) { + list($table, $key) = explode('.', $key); + if (isset($options['alias']) && $pos = array_search($table, $options['alias'])) { + $table = $pos; + } + } else { + $table = $options['table']; + } + $type = $this->query->getTableInfo($table, 'type'); + if (isset($type[$key])) { + $info = $type[$key]; + } + if (isset($info)) { + if (is_string($value)) { + $value = strtotime($value) ?: $value; + } + + if (preg_match('/(datetime|timestamp)/is', $info)) { + // 日期及时间戳类型 + $value = date('Y-m-d H:i:s', $value); + } elseif (preg_match('/(date)/is', $info)) { + // 日期及时间戳类型 + $value = date('Y-m-d', $value); + } + } + $bindName = $bindName ?: $key; + $this->query->bind($bindName, $value, $bindType); + return ':' . $bindName; + } + + /** + * limit分析 + * @access protected + * @param mixed $lmit + * @return string + */ + protected function parseLimit($limit) + { + return (!empty($limit) && false === strpos($limit, '(')) ? ' LIMIT ' . $limit . ' ' : ''; + } + + /** + * join分析 + * @access protected + * @param array $join + * @param array $options 查询条件 + * @return string + */ + protected function parseJoin($join, $options = []) + { + $joinStr = ''; + if (!empty($join)) { + foreach ($join as $item) { + list($table, $type, $on) = $item; + $condition = []; + foreach ((array) $on as $val) { + if (strpos($val, '=')) { + list($val1, $val2) = explode('=', $val, 2); + $condition[] = $this->parseKey($val1, $options) . '=' . $this->parseKey($val2, $options); + } else { + $condition[] = $val; + } + } + + $table = $this->parseTable($table, $options); + $joinStr .= ' ' . $type . ' JOIN ' . $table . ' ON ' . implode(' AND ', $condition); + } + } + return $joinStr; + } + + /** + * order分析 + * @access protected + * @param mixed $order + * @param array $options 查询条件 + * @return string + */ + protected function parseOrder($order, $options = []) + { + if (is_array($order)) { + $array = []; + foreach ($order as $key => $val) { + if (is_numeric($key)) { + if ('[rand]' == $val) { + $array[] = $this->parseRand(); + } elseif (false === strpos($val, '(')) { + $array[] = $this->parseKey($val, $options); + } else { + $array[] = $val; + } + } else { + $sort = in_array(strtolower(trim($val)), ['asc', 'desc']) ? ' ' . $val : ''; + $array[] = $this->parseKey($key, $options) . ' ' . $sort; + } + } + $order = implode(',', $array); + } + return !empty($order) ? ' ORDER BY ' . $order : ''; + } + + /** + * group分析 + * @access protected + * @param mixed $group + * @return string + */ + protected function parseGroup($group) + { + return !empty($group) ? ' GROUP BY ' . $group : ''; + } + + /** + * having分析 + * @access protected + * @param string $having + * @return string + */ + protected function parseHaving($having) + { + return !empty($having) ? ' HAVING ' . $having : ''; + } + + /** + * comment分析 + * @access protected + * @param string $comment + * @return string + */ + protected function parseComment($comment) + { + return !empty($comment) ? ' /* ' . $comment . ' */' : ''; + } + + /** + * distinct分析 + * @access protected + * @param mixed $distinct + * @return string + */ + protected function parseDistinct($distinct) + { + return !empty($distinct) ? ' DISTINCT ' : ''; + } + + /** + * union分析 + * @access protected + * @param mixed $union + * @return string + */ + protected function parseUnion($union) + { + if (empty($union)) { + return ''; + } + $type = $union['type']; + unset($union['type']); + foreach ($union as $u) { + if ($u instanceof \Closure) { + $sql[] = $type . ' ' . $this->parseClosure($u, false); + } elseif (is_string($u)) { + $sql[] = $type . ' ' . $this->parseSqlTable($u); + } + } + return implode(' ', $sql); + } + + /** + * index分析,可在操作链中指定需要强制使用的索引 + * @access protected + * @param mixed $index + * @return string + */ + protected function parseForce($index) + { + if (empty($index)) { + return ''; + } + + if (is_array($index)) { + $index = join(",", $index); + } + + return sprintf(" FORCE INDEX ( %s ) ", $index); + } + + /** + * 设置锁机制 + * @access protected + * @param bool $locl + * @return string + */ + protected function parseLock($lock = false) + { + return $lock ? ' FOR UPDATE ' : ''; + } + + /** + * 生成查询SQL + * @access public + * @param array $options 表达式 + * @return string + */ + public function select($options = []) + { + $sql = str_replace( + ['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'], + [ + $this->parseTable($options['table'], $options), + $this->parseDistinct($options['distinct']), + $this->parseField($options['field'], $options), + $this->parseJoin($options['join'], $options), + $this->parseWhere($options['where'], $options), + $this->parseGroup($options['group']), + $this->parseHaving($options['having']), + $this->parseOrder($options['order'], $options), + $this->parseLimit($options['limit']), + $this->parseUnion($options['union']), + $this->parseLock($options['lock']), + $this->parseComment($options['comment']), + $this->parseForce($options['force']), + ], $this->selectSql); + return $sql; + } + + /** + * 生成insert SQL + * @access public + * @param array $data 数据 + * @param array $options 表达式 + * @param bool $replace 是否replace + * @return string + */ + public function insert(array $data, $options = [], $replace = false) + { + // 分析并处理数据 + $data = $this->parseData($data, $options); + if (empty($data)) { + return 0; + } + $fields = array_keys($data); + $values = array_values($data); + + $sql = str_replace( + ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], + [ + $replace ? 'REPLACE' : 'INSERT', + $this->parseTable($options['table'], $options), + implode(' , ', $fields), + implode(' , ', $values), + $this->parseComment($options['comment']), + ], $this->insertSql); + + return $sql; + } + + /** + * 生成insertall SQL + * @access public + * @param array $dataSet 数据集 + * @param array $options 表达式 + * @param bool $replace 是否replace + * @return string + */ + public function insertAll($dataSet, $options = [], $replace = false) + { + // 获取合法的字段 + if ('*' == $options['field']) { + $fields = array_keys($this->query->getFieldsType($options['table'])); + } else { + $fields = $options['field']; + } + + foreach ($dataSet as &$data) { + foreach ($data as $key => $val) { + if (!in_array($key, $fields, true)) { + if ($options['strict']) { + throw new Exception('fields not exists:[' . $key . ']'); + } + unset($data[$key]); + } elseif (is_null($val)) { + $data[$key] = 'NULL'; + } elseif (is_scalar($val)) { + $data[$key] = $this->parseValue($val, $key); + } elseif (is_object($val) && method_exists($val, '__toString')) { + // 对象数据写入 + $data[$key] = $val->__toString(); + } else { + // 过滤掉非标量数据 + unset($data[$key]); + } + } + $value = array_values($data); + $values[] = 'SELECT ' . implode(',', $value); + } + $fields = array_map([$this, 'parseKey'], array_keys(reset($dataSet))); + $sql = str_replace( + ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], + [ + $replace ? 'REPLACE' : 'INSERT', + $this->parseTable($options['table'], $options), + implode(' , ', $fields), + implode(' UNION ALL ', $values), + $this->parseComment($options['comment']), + ], $this->insertAllSql); + + return $sql; + } + + /** + * 生成slectinsert SQL + * @access public + * @param array $fields 数据 + * @param string $table 数据表 + * @param array $options 表达式 + * @return string + */ + public function selectInsert($fields, $table, $options) + { + if (is_string($fields)) { + $fields = explode(',', $fields); + } + + $fields = array_map([$this, 'parseKey'], $fields); + $sql = 'INSERT INTO ' . $this->parseTable($table, $options) . ' (' . implode(',', $fields) . ') ' . $this->select($options); + return $sql; + } + + /** + * 生成update SQL + * @access public + * @param array $fields 数据 + * @param array $options 表达式 + * @return string + */ + public function update($data, $options) + { + $table = $this->parseTable($options['table'], $options); + $data = $this->parseData($data, $options); + if (empty($data)) { + return ''; + } + foreach ($data as $key => $val) { + $set[] = $key . '=' . $val; + } + + $sql = str_replace( + ['%TABLE%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], + [ + $this->parseTable($options['table'], $options), + implode(',', $set), + $this->parseJoin($options['join'], $options), + $this->parseWhere($options['where'], $options), + $this->parseOrder($options['order'], $options), + $this->parseLimit($options['limit']), + $this->parseLock($options['lock']), + $this->parseComment($options['comment']), + ], $this->updateSql); + + return $sql; + } + + /** + * 生成delete SQL + * @access public + * @param array $options 表达式 + * @return string + */ + public function delete($options) + { + $sql = str_replace( + ['%TABLE%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], + [ + $this->parseTable($options['table'], $options), + !empty($options['using']) ? ' USING ' . $this->parseTable($options['using'], $options) . ' ' : '', + $this->parseJoin($options['join'], $options), + $this->parseWhere($options['where'], $options), + $this->parseOrder($options['order'], $options), + $this->parseLimit($options['limit']), + $this->parseLock($options['lock']), + $this->parseComment($options['comment']), + ], $this->deleteSql); + + return $sql; + } +} diff --git a/thinkphp/library/think/db/Connection.php b/thinkphp/library/think/db/Connection.php new file mode 100644 index 0000000..dee8749 --- /dev/null +++ b/thinkphp/library/think/db/Connection.php @@ -0,0 +1,1032 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use PDO; +use PDOStatement; +use think\Db; +use think\db\exception\BindParamException; +use think\Debug; +use think\Exception; +use think\exception\PDOException; +use think\Log; + +/** + * Class Connection + * @package think + * @method Query table(string $table) 指定数据表(含前缀) + * @method Query name(string $name) 指定数据表(不含前缀) + * + */ +abstract class Connection +{ + + /** @var PDOStatement PDO操作实例 */ + protected $PDOStatement; + + /** @var string 当前SQL指令 */ + protected $queryStr = ''; + // 返回或者影响记录数 + protected $numRows = 0; + // 事务指令数 + protected $transTimes = 0; + // 错误信息 + protected $error = ''; + + /** @var PDO[] 数据库连接ID 支持多个连接 */ + protected $links = []; + + /** @var PDO 当前连接ID */ + protected $linkID; + protected $linkRead; + protected $linkWrite; + + // 查询结果类型 + protected $fetchType = PDO::FETCH_ASSOC; + // 字段属性大小写 + protected $attrCase = PDO::CASE_LOWER; + // 监听回调 + protected static $event = []; + // 使用Builder类 + protected $builder; + // 数据库连接参数配置 + protected $config = [ + // 数据库类型 + 'type' => '', + // 服务器地址 + 'hostname' => '', + // 数据库名 + 'database' => '', + // 用户名 + 'username' => '', + // 密码 + 'password' => '', + // 端口 + 'hostport' => '', + // 连接dsn + 'dsn' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => '', + // 数据库调试模式 + 'debug' => false, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 数据返回类型 + 'result_type' => PDO::FETCH_ASSOC, + // 数据集返回类型 + 'resultset_type' => 'array', + // 自动写入时间戳字段 + 'auto_timestamp' => false, + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + // 是否需要进行SQL性能分析 + 'sql_explain' => false, + // Builder类 + 'builder' => '', + // Query类 + 'query' => '\\think\\db\\Query', + // 是否需要断线重连 + 'break_reconnect' => false, + ]; + + // PDO连接参数 + protected $params = [ + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + // 绑定参数 + protected $bind = []; + + /** + * 构造函数 读取数据库配置信息 + * @access public + * @param array $config 数据库配置数组 + */ + public function __construct(array $config = []) + { + if (!empty($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * 获取新的查询对象 + * @access protected + * @return Query + */ + protected function getQuery() + { + $class = $this->config['query']; + return new $class($this); + } + + /** + * 获取当前连接器类对应的Builder类 + * @access public + * @return string + */ + public function getBuilder() + { + if (!empty($this->builder)) { + return $this->builder; + } else { + return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type')); + } + } + + /** + * 调用Query类的查询方法 + * @access public + * @param string $method 方法名称 + * @param array $args 调用参数 + * @return mixed + */ + public function __call($method, $args) + { + return call_user_func_array([$this->getQuery(), $method], $args); + } + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + abstract protected function parseDsn($config); + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + abstract public function getFields($tableName); + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + abstract public function getTables($dbName); + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + abstract protected function getExplain($sql); + + /** + * 对返数据表字段信息进行大小写转换出来 + * @access public + * @param array $info 字段信息 + * @return array + */ + public function fieldCase($info) + { + // 字段大小写转换 + switch ($this->attrCase) { + case PDO::CASE_LOWER: + $info = array_change_key_case($info); + break; + case PDO::CASE_UPPER: + $info = array_change_key_case($info, CASE_UPPER); + break; + case PDO::CASE_NATURAL: + default: + // 不做转换 + } + return $info; + } + + /** + * 获取数据库的配置参数 + * @access public + * @param string $config 配置名称 + * @return mixed + */ + public function getConfig($config = '') + { + return $config ? $this->config[$config] : $this->config; + } + + /** + * 设置数据库的配置参数 + * @access public + * @param string|array $config 配置名称 + * @param mixed $value 配置值 + * @return void + */ + public function setConfig($config, $value = '') + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } else { + $this->config[$config] = $value; + } + } + + /** + * 连接数据库方法 + * @access public + * @param array $config 连接参数 + * @param integer $linkNum 连接序号 + * @param array|bool $autoConnection 是否自动连接主数据库(用于分布式) + * @return PDO + * @throws Exception + */ + public function connect(array $config = [], $linkNum = 0, $autoConnection = false) + { + if (!isset($this->links[$linkNum])) { + if (!$config) { + $config = $this->config; + } else { + $config = array_merge($this->config, $config); + } + // 连接参数 + if (isset($config['params']) && is_array($config['params'])) { + $params = $config['params'] + $this->params; + } else { + $params = $this->params; + } + // 记录当前字段属性大小写设置 + $this->attrCase = $params[PDO::ATTR_CASE]; + + // 数据返回类型 + if (isset($config['result_type'])) { + $this->fetchType = $config['result_type']; + } + try { + if (empty($config['dsn'])) { + $config['dsn'] = $this->parseDsn($config); + } + if ($config['debug']) { + $startTime = microtime(true); + } + $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params); + if ($config['debug']) { + // 记录数据库连接信息 + Log::record('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn'], 'sql'); + } + } catch (\PDOException $e) { + if ($autoConnection) { + Log::record($e->getMessage(), 'error'); + return $this->connect($autoConnection, $linkNum); + } else { + throw $e; + } + } + } + return $this->links[$linkNum]; + } + + /** + * 释放查询结果 + * @access public + */ + public function free() + { + $this->PDOStatement = null; + } + + /** + * 获取PDO对象 + * @access public + * @return \PDO|false + */ + public function getPdo() + { + if (!$this->linkID) { + return false; + } else { + return $this->linkID; + } + } + + /** + * 执行查询 返回数据集 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param bool $master 是否在主服务器读操作 + * @param bool $pdo 是否返回PDO对象 + * @return mixed + * @throws BindParamException + * @throws PDOException + */ + public function query($sql, $bind = [], $master = false, $pdo = false) + { + $this->initConnect($master); + if (!$this->linkID) { + return false; + } + + // 记录SQL语句 + $this->queryStr = $sql; + if ($bind) { + $this->bind = $bind; + } + + // 释放前次的查询结果 + if (!empty($this->PDOStatement)) { + $this->free(); + } + + Db::$queryTimes++; + try { + // 调试开始 + $this->debug(true); + // 预处理 + if (empty($this->PDOStatement)) { + $this->PDOStatement = $this->linkID->prepare($sql); + } + // 是否为存储过程调用 + $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); + // 参数绑定 + if ($procedure) { + $this->bindParam($bind); + } else { + $this->bindValue($bind); + } + // 执行查询 + $this->PDOStatement->execute(); + // 调试结束 + $this->debug(false); + // 返回结果集 + return $this->getResult($pdo, $procedure); + } catch (\PDOException $e) { + if ($this->isBreak($e)) { + return $this->close()->query($sql, $bind, $master, $pdo); + } + throw new PDOException($e, $this->config, $this->getLastsql()); + } catch (\Exception $e) { + if ($this->isBreak($e)) { + return $this->close()->query($sql, $bind, $master, $pdo); + } + throw $e; + } + } + + /** + * 执行语句 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @return int + * @throws BindParamException + * @throws PDOException + */ + public function execute($sql, $bind = []) + { + $this->initConnect(true); + if (!$this->linkID) { + return false; + } + + // 记录SQL语句 + $this->queryStr = $sql; + if ($bind) { + $this->bind = $bind; + } + + //释放前次的查询结果 + if (!empty($this->PDOStatement) && $this->PDOStatement->queryString != $sql) { + $this->free(); + } + + Db::$executeTimes++; + try { + // 调试开始 + $this->debug(true); + // 预处理 + if (empty($this->PDOStatement)) { + $this->PDOStatement = $this->linkID->prepare($sql); + } + // 是否为存储过程调用 + $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); + // 参数绑定 + if ($procedure) { + $this->bindParam($bind); + } else { + $this->bindValue($bind); + } + // 执行语句 + $this->PDOStatement->execute(); + // 调试结束 + $this->debug(false); + + $this->numRows = $this->PDOStatement->rowCount(); + return $this->numRows; + } catch (\PDOException $e) { + if ($this->isBreak($e)) { + return $this->close()->execute($sql, $bind); + } + throw new PDOException($e, $this->config, $this->getLastsql()); + } catch (\Exception $e) { + if ($this->isBreak($e)) { + return $this->close()->execute($sql, $bind); + } + throw $e; + } + } + + /** + * 根据参数绑定组装最终的SQL语句 便于调试 + * @access public + * @param string $sql 带参数绑定的sql语句 + * @param array $bind 参数绑定列表 + * @return string + */ + public function getRealSql($sql, array $bind = []) + { + foreach ($bind as $key => $val) { + $value = is_array($val) ? $val[0] : $val; + $type = is_array($val) ? $val[1] : PDO::PARAM_STR; + if (PDO::PARAM_STR == $type) { + $value = $this->quote($value); + } elseif (PDO::PARAM_INT == $type) { + $value = (float) $value; + } + // 判断占位符 + $sql = is_numeric($key) ? + substr_replace($sql, $value, strpos($sql, '?'), 1) : + str_replace( + [':' . $key . ')', ':' . $key . ',', ':' . $key . ' '], + [$value . ')', $value . ',', $value . ' '], + $sql . ' '); + } + return rtrim($sql); + } + + /** + * 参数绑定 + * 支持 ['name'=>'value','id'=>123] 对应命名占位符 + * 或者 ['value',123] 对应问号占位符 + * @access public + * @param array $bind 要绑定的参数列表 + * @return void + * @throws BindParamException + */ + protected function bindValue(array $bind = []) + { + foreach ($bind as $key => $val) { + // 占位符 + $param = is_numeric($key) ? $key + 1 : ':' . $key; + if (is_array($val)) { + if (PDO::PARAM_INT == $val[1] && '' === $val[0]) { + $val[0] = 0; + } + $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]); + } else { + $result = $this->PDOStatement->bindValue($param, $val); + } + if (!$result) { + throw new BindParamException( + "Error occurred when binding parameters '{$param}'", + $this->config, + $this->getLastsql(), + $bind + ); + } + } + } + + /** + * 存储过程的输入输出参数绑定 + * @access public + * @param array $bind 要绑定的参数列表 + * @return void + * @throws BindParamException + */ + protected function bindParam($bind) + { + foreach ($bind as $key => $val) { + $param = is_numeric($key) ? $key + 1 : ':' . $key; + if (is_array($val)) { + array_unshift($val, $param); + $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val); + } else { + $result = $this->PDOStatement->bindValue($param, $val); + } + if (!$result) { + $param = array_shift($val); + throw new BindParamException( + "Error occurred when binding parameters '{$param}'", + $this->config, + $this->getLastsql(), + $bind + ); + } + } + } + + /** + * 获得数据集数组 + * @access protected + * @param bool $pdo 是否返回PDOStatement + * @param bool $procedure 是否存储过程 + * @return array + */ + protected function getResult($pdo = false, $procedure = false) + { + if ($pdo) { + // 返回PDOStatement对象处理 + return $this->PDOStatement; + } + if ($procedure) { + // 存储过程返回结果 + return $this->procedure(); + } + $result = $this->PDOStatement->fetchAll($this->fetchType); + $this->numRows = count($result); + return $result; + } + + /** + * 获得存储过程数据集 + * @access protected + * @return array + */ + protected function procedure() + { + $item = []; + do { + $result = $this->getResult(); + if ($result) { + $item[] = $result; + } + } while ($this->PDOStatement->nextRowset()); + $this->numRows = count($item); + return $item; + } + + /** + * 执行数据库事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @return mixed + * @throws PDOException + * @throws \Exception + * @throws \Throwable + */ + public function transaction($callback) + { + $this->startTrans(); + try { + $result = null; + if (is_callable($callback)) { + $result = call_user_func_array($callback, [$this]); + } + $this->commit(); + return $result; + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } catch (\Throwable $e) { + $this->rollback(); + throw $e; + } + } + + /** + * 启动事务 + * @access public + * @return void + */ + public function startTrans() + { + $this->initConnect(true); + if (!$this->linkID) { + return false; + } + + ++$this->transTimes; + try { + if (1 == $this->transTimes) { + $this->linkID->beginTransaction(); + } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { + $this->linkID->exec( + $this->parseSavepoint('trans' . $this->transTimes) + ); + } + + } catch (\PDOException $e) { + if ($this->isBreak($e)) { + return $this->close()->startTrans(); + } + throw $e; + } catch (\Exception $e) { + if ($this->isBreak($e)) { + return $this->close()->startTrans(); + } + throw $e; + } + } + + /** + * 用于非自动提交状态下面的查询提交 + * @access public + * @return void + * @throws PDOException + */ + public function commit() + { + $this->initConnect(true); + + if (1 == $this->transTimes) { + $this->linkID->commit(); + } + + --$this->transTimes; + } + + /** + * 事务回滚 + * @access public + * @return void + * @throws PDOException + */ + public function rollback() + { + $this->initConnect(true); + + if (1 == $this->transTimes) { + $this->linkID->rollBack(); + } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { + $this->linkID->exec( + $this->parseSavepointRollBack('trans' . $this->transTimes) + ); + } + + $this->transTimes = max(0, $this->transTimes - 1); + } + + /** + * 是否支持事务嵌套 + * @return bool + */ + protected function supportSavepoint() + { + return false; + } + + /** + * 生成定义保存点的SQL + * @param $name + * @return string + */ + protected function parseSavepoint($name) + { + return 'SAVEPOINT ' . $name; + } + + /** + * 生成回滚到保存点的SQL + * @param $name + * @return string + */ + protected function parseSavepointRollBack($name) + { + return 'ROLLBACK TO SAVEPOINT ' . $name; + } + + /** + * 批处理执行SQL语句 + * 批处理的指令都认为是execute操作 + * @access public + * @param array $sqlArray SQL批处理指令 + * @return boolean + */ + public function batchQuery($sqlArray = []) + { + if (!is_array($sqlArray)) { + return false; + } + // 自动启动事务支持 + $this->startTrans(); + try { + foreach ($sqlArray as $sql) { + $this->execute($sql); + } + // 提交事务 + $this->commit(); + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } + return true; + } + + /** + * 获得查询次数 + * @access public + * @param boolean $execute 是否包含所有查询 + * @return integer + */ + public function getQueryTimes($execute = false) + { + return $execute ? Db::$queryTimes + Db::$executeTimes : Db::$queryTimes; + } + + /** + * 获得执行次数 + * @access public + * @return integer + */ + public function getExecuteTimes() + { + return Db::$executeTimes; + } + + /** + * 关闭数据库(或者重新连接) + * @access public + * @return $this + */ + public function close() + { + $this->linkID = null; + $this->linkWrite = null; + $this->linkRead = null; + $this->links = []; + return $this; + } + + /** + * 是否断线 + * @access protected + * @param \PDOException $e 异常对象 + * @return bool + */ + protected function isBreak($e) + { + if (!$this->config['break_reconnect']) { + return false; + } + + $info = [ + 'server has gone away', + 'no connection to the server', + 'Lost connection', + 'is dead or not enabled', + 'Error while sending', + 'decryption failed or bad record mac', + 'server closed the connection unexpectedly', + 'SSL connection has been closed unexpectedly', + 'Error writing data to the connection', + 'Resource deadlock avoided', + ]; + + $error = $e->getMessage(); + + foreach ($info as $msg) { + if (false !== stripos($error, $msg)) { + return true; + } + } + return false; + } + + /** + * 获取最近一次查询的sql语句 + * @access public + * @return string + */ + public function getLastSql() + { + return $this->getRealSql($this->queryStr, $this->bind); + } + + /** + * 获取最近插入的ID + * @access public + * @param string $sequence 自增序列名 + * @return string + */ + public function getLastInsID($sequence = null) + { + return $this->linkID->lastInsertId($sequence); + } + + /** + * 获取返回或者影响的记录数 + * @access public + * @return integer + */ + public function getNumRows() + { + return $this->numRows; + } + + /** + * 获取最近的错误信息 + * @access public + * @return string + */ + public function getError() + { + if ($this->PDOStatement) { + $error = $this->PDOStatement->errorInfo(); + $error = $error[1] . ':' . $error[2]; + } else { + $error = ''; + } + if ('' != $this->queryStr) { + $error .= "\n [ SQL语句 ] : " . $this->getLastsql(); + } + return $error; + } + + /** + * SQL指令安全过滤 + * @access public + * @param string $str SQL字符串 + * @param bool $master 是否主库查询 + * @return string + */ + public function quote($str, $master = true) + { + $this->initConnect($master); + return $this->linkID ? $this->linkID->quote($str) : $str; + } + + /** + * 数据库调试 记录当前SQL及分析性能 + * @access protected + * @param boolean $start 调试开始标记 true 开始 false 结束 + * @param string $sql 执行的SQL语句 留空自动获取 + * @return void + */ + protected function debug($start, $sql = '') + { + if (!empty($this->config['debug'])) { + // 开启数据库调试模式 + if ($start) { + Debug::remark('queryStartTime', 'time'); + } else { + // 记录操作结束时间 + Debug::remark('queryEndTime', 'time'); + $runtime = Debug::getRangeTime('queryStartTime', 'queryEndTime'); + $sql = $sql ?: $this->getLastsql(); + $result = []; + // SQL性能分析 + if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) { + $result = $this->getExplain($sql); + } + // SQL监听 + $this->trigger($sql, $runtime, $result); + } + } + } + + /** + * 监听SQL执行 + * @access public + * @param callable $callback 回调方法 + * @return void + */ + public function listen($callback) + { + self::$event[] = $callback; + } + + /** + * 触发SQL事件 + * @access protected + * @param string $sql SQL语句 + * @param float $runtime SQL运行时间 + * @param mixed $explain SQL分析 + * @return bool + */ + protected function trigger($sql, $runtime, $explain = []) + { + if (!empty(self::$event)) { + foreach (self::$event as $callback) { + if (is_callable($callback)) { + call_user_func_array($callback, [$sql, $runtime, $explain]); + } + } + } else { + // 未注册监听则记录到日志中 + Log::record('[ SQL ] ' . $sql . ' [ RunTime:' . $runtime . 's ]', 'sql'); + if (!empty($explain)) { + Log::record('[ EXPLAIN : ' . var_export($explain, true) . ' ]', 'sql'); + } + } + } + + /** + * 初始化数据库连接 + * @access protected + * @param boolean $master 是否主服务器 + * @return void + */ + protected function initConnect($master = true) + { + if (!empty($this->config['deploy'])) { + // 采用分布式数据库 + if ($master || $this->transTimes) { + if (!$this->linkWrite) { + $this->linkWrite = $this->multiConnect(true); + } + $this->linkID = $this->linkWrite; + } else { + if (!$this->linkRead) { + $this->linkRead = $this->multiConnect(false); + } + $this->linkID = $this->linkRead; + } + } elseif (!$this->linkID) { + // 默认单数据库 + $this->linkID = $this->connect(); + } + } + + /** + * 连接分布式服务器 + * @access protected + * @param boolean $master 主服务器 + * @return PDO + */ + protected function multiConnect($master = false) + { + $_config = []; + // 分布式数据库配置解析 + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $_config[$name] = explode(',', $this->config[$name]); + } + + // 主服务器序号 + $m = floor(mt_rand(0, $this->config['master_num'] - 1)); + + if ($this->config['rw_separate']) { + // 主从式采用读写分离 + if ($master) // 主服务器写入 + { + $r = $m; + } elseif (is_numeric($this->config['slave_no'])) { + // 指定服务器读 + $r = $this->config['slave_no']; + } else { + // 读操作连接从服务器 每次随机连接的数据库 + $r = floor(mt_rand($this->config['master_num'], count($_config['hostname']) - 1)); + } + } else { + // 读写操作不区分服务器 每次随机连接的数据库 + $r = floor(mt_rand(0, count($_config['hostname']) - 1)); + } + $dbMaster = false; + if ($m != $r) { + $dbMaster = []; + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $dbMaster[$name] = isset($_config[$name][$m]) ? $_config[$name][$m] : $_config[$name][0]; + } + } + $dbConfig = []; + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $dbConfig[$name] = isset($_config[$name][$r]) ? $_config[$name][$r] : $_config[$name][0]; + } + return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster); + } + + /** + * 析构方法 + * @access public + */ + public function __destruct() + { + // 释放查询 + if ($this->PDOStatement) { + $this->free(); + } + // 关闭连接 + $this->close(); + } +} diff --git a/thinkphp/library/think/db/Query.php b/thinkphp/library/think/db/Query.php new file mode 100644 index 0000000..f18f38b --- /dev/null +++ b/thinkphp/library/think/db/Query.php @@ -0,0 +1,2814 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use PDO; +use think\Cache; +use think\Collection; +use think\Config; +use think\Db; +use think\db\exception\BindParamException; +use think\db\exception\DataNotFoundException; +use think\db\exception\ModelNotFoundException; +use think\Exception; +use think\exception\DbException; +use think\exception\PDOException; +use think\Loader; +use think\Model; +use think\model\Relation; +use think\model\relation\OneToOne; +use think\Paginator; + +class Query +{ + // 数据库Connection对象实例 + protected $connection; + // 数据库Builder对象实例 + protected $builder; + // 当前模型类名称 + protected $model; + // 当前数据表名称(含前缀) + protected $table = ''; + // 当前数据表名称(不含前缀) + protected $name = ''; + // 当前数据表主键 + protected $pk; + // 当前数据表前缀 + protected $prefix = ''; + // 查询参数 + protected $options = []; + // 参数绑定 + protected $bind = []; + // 数据表信息 + protected static $info = []; + // 回调事件 + private static $event = []; + + /** + * 构造函数 + * @access public + * @param Connection $connection 数据库对象实例 + * @param string $model 模型名 + */ + public function __construct(Connection $connection = null, $model = '') + { + $this->connection = $connection ?: Db::connect([], true); + $this->prefix = $this->connection->getConfig('prefix'); + $this->model = $model; + // 设置当前连接的Builder对象 + $this->setBuilder(); + } + + /** + * 利用__call方法实现一些特殊的Model方法 + * @access public + * @param string $method 方法名称 + * @param array $args 调用参数 + * @return mixed + * @throws DbException + * @throws Exception + */ + public function __call($method, $args) + { + if (strtolower(substr($method, 0, 5)) == 'getby') { + // 根据某个字段获取记录 + $field = Loader::parseName(substr($method, 5)); + $where[$field] = $args[0]; + return $this->where($where)->find(); + } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') { + // 根据某个字段获取记录的某个值 + $name = Loader::parseName(substr($method, 10)); + $where[$name] = $args[0]; + return $this->where($where)->value($args[1]); + } else { + throw new Exception('method not exist:' . __CLASS__ . '->' . $method); + } + } + + /** + * 获取当前的数据库Connection对象 + * @access public + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * 切换当前的数据库连接 + * @access public + * @param mixed $config + * @return $this + */ + public function connect($config) + { + $this->connection = Db::connect($config); + $this->setBuilder(); + return $this; + } + + /** + * 设置当前的数据库Builder对象 + * @access protected + * @return void + */ + protected function setBuilder() + { + $class = $this->connection->getBuilder(); + $this->builder = new $class($this->connection, $this); + } + + /** + * 获取当前的模型对象名 + * @access public + * @return string + */ + public function getModel() + { + return $this->model; + } + + /** + * 获取当前的builder实例对象 + * @access public + * @return Builder + */ + public function getBuilder() + { + return $this->builder; + } + + /** + * 指定默认的数据表名(不含前缀) + * @access public + * @param string $name + * @return $this + */ + public function name($name) + { + $this->name = $name; + return $this; + } + + /** + * 指定默认数据表名(含前缀) + * @access public + * @param string $table 表名 + * @return $this + */ + public function setTable($table) + { + $this->table = $table; + return $this; + } + + /** + * 得到当前或者指定名称的数据表 + * @access public + * @param string $name + * @return string + */ + public function getTable($name = '') + { + if ($name || empty($this->table)) { + $name = $name ?: $this->name; + $tableName = $this->prefix; + if ($name) { + $tableName .= Loader::parseName($name); + } + } else { + $tableName = $this->table; + } + return $tableName; + } + + /** + * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写) + * @access public + * @param string $sql sql语句 + * @return string + */ + public function parseSqlTable($sql) + { + if (false !== strpos($sql, '__')) { + $prefix = $this->prefix; + $sql = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) use ($prefix) { + return $prefix . strtolower($match[1]); + }, $sql); + } + return $sql; + } + + /** + * 执行查询 返回数据集 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param boolean $master 是否在主服务器读操作 + * @param bool|string $class 指定返回的数据集对象 + * @return mixed + * @throws BindParamException + * @throws PDOException + */ + public function query($sql, $bind = [], $master = false, $class = false) + { + return $this->connection->query($sql, $bind, $master, $class); + } + + /** + * 执行语句 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @return int + * @throws BindParamException + * @throws PDOException + */ + public function execute($sql, $bind = []) + { + return $this->connection->execute($sql, $bind); + } + + /** + * 获取最近插入的ID + * @access public + * @param string $sequence 自增序列名 + * @return string + */ + public function getLastInsID($sequence = null) + { + return $this->connection->getLastInsID($sequence); + } + + /** + * 获取最近一次查询的sql语句 + * @access public + * @return string + */ + public function getLastSql() + { + return $this->connection->getLastSql(); + } + + /** + * 执行数据库事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @return mixed + */ + public function transaction($callback) + { + return $this->connection->transaction($callback); + } + + /** + * 启动事务 + * @access public + * @return void + */ + public function startTrans() + { + $this->connection->startTrans(); + } + + /** + * 用于非自动提交状态下面的查询提交 + * @access public + * @return void + * @throws PDOException + */ + public function commit() + { + $this->connection->commit(); + } + + /** + * 事务回滚 + * @access public + * @return void + * @throws PDOException + */ + public function rollback() + { + $this->connection->rollback(); + } + + /** + * 批处理执行SQL语句 + * 批处理的指令都认为是execute操作 + * @access public + * @param array $sql SQL批处理指令 + * @return boolean + */ + public function batchQuery($sql = []) + { + return $this->connection->batchQuery($sql); + } + + /** + * 获取数据库的配置参数 + * @access public + * @param string $name 参数名称 + * @return boolean + */ + public function getConfig($name = '') + { + return $this->connection->getConfig($name); + } + + /** + * 得到分表的的数据表名 + * @access public + * @param array $data 操作的数据 + * @param string $field 分表依据的字段 + * @param array $rule 分表规则 + * @return string + */ + public function getPartitionTableName($data, $field, $rule = []) + { + // 对数据表进行分区 + if ($field && isset($data[$field])) { + $value = $data[$field]; + $type = $rule['type']; + switch ($type) { + case 'id': + // 按照id范围分表 + $step = $rule['expr']; + $seq = floor($value / $step) + 1; + break; + case 'year': + // 按照年份分表 + if (!is_numeric($value)) { + $value = strtotime($value); + } + $seq = date('Y', $value) - $rule['expr'] + 1; + break; + case 'mod': + // 按照id的模数分表 + $seq = ($value % $rule['num']) + 1; + break; + case 'md5': + // 按照md5的序列分表 + $seq = (ord(substr(md5($value), 0, 1)) % $rule['num']) + 1; + break; + default: + if (function_exists($type)) { + // 支持指定函数哈希 + $seq = (ord(substr($type($value), 0, 1)) % $rule['num']) + 1; + } else { + // 按照字段的首字母的值分表 + $seq = (ord($value{0}) % $rule['num']) + 1; + } + } + return $this->getTable() . '_' . $seq; + } else { + // 当设置的分表字段不在查询条件或者数据中 + // 进行联合查询,必须设定 partition['num'] + $tableName = []; + for ($i = 0; $i < $rule['num']; $i++) { + $tableName[] = 'SELECT * FROM ' . $this->getTable() . '_' . ($i + 1); + } + + $tableName = '( ' . implode(" UNION ", $tableName) . ') AS ' . $this->name; + return $tableName; + } + } + + /** + * 得到某个字段的值 + * @access public + * @param string $field 字段名 + * @param mixed $default 默认值 + * @param bool $force 强制转为数字类型 + * @return mixed + */ + public function value($field, $default = null, $force = false) + { + $result = false; + if (empty($this->options['fetch_sql']) && !empty($this->options['cache'])) { + // 判断查询缓存 + $cache = $this->options['cache']; + if (empty($this->options['table'])) { + $this->options['table'] = $this->getTable(); + } + $key = is_string($cache['key']) ? $cache['key'] : md5($field . serialize($this->options) . serialize($this->bind)); + $result = Cache::get($key); + } + if (false === $result) { + if (isset($this->options['field'])) { + unset($this->options['field']); + } + $pdo = $this->field($field)->limit(1)->getPdo(); + if (is_string($pdo)) { + // 返回SQL语句 + return $pdo; + } + $result = $pdo->fetchColumn(); + if ($force) { + $result += 0; + } + + if (isset($cache)) { + // 缓存数据 + $this->cacheData($key, $result, $cache); + } + } else { + // 清空查询条件 + $this->options = []; + } + return false !== $result ? $result : $default; + } + + /** + * 得到某个列的数组 + * @access public + * @param string $field 字段名 多个字段用逗号分隔 + * @param string $key 索引 + * @return array + */ + public function column($field, $key = '') + { + $result = false; + if (empty($this->options['fetch_sql']) && !empty($this->options['cache'])) { + // 判断查询缓存 + $cache = $this->options['cache']; + if (empty($this->options['table'])) { + $this->options['table'] = $this->getTable(); + } + $guid = is_string($cache['key']) ? $cache['key'] : md5($field . serialize($this->options) . serialize($this->bind)); + $result = Cache::get($guid); + } + if (false === $result) { + if (isset($this->options['field'])) { + unset($this->options['field']); + } + if (is_null($field)) { + $field = '*'; + } elseif ($key && '*' != $field) { + $field = $key . ',' . $field; + } + $pdo = $this->field($field)->getPdo(); + if (is_string($pdo)) { + // 返回SQL语句 + return $pdo; + } + if (1 == $pdo->columnCount()) { + $result = $pdo->fetchAll(PDO::FETCH_COLUMN); + } else { + $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC); + if ($resultSet) { + $fields = array_keys($resultSet[0]); + $count = count($fields); + $key1 = array_shift($fields); + $key2 = $fields ? array_shift($fields) : ''; + $key = $key ?: $key1; + if (strpos($key, '.')) { + list($alias, $key) = explode('.', $key); + } + foreach ($resultSet as $val) { + if ($count > 2) { + $result[$val[$key]] = $val; + } elseif (2 == $count) { + $result[$val[$key]] = $val[$key2]; + } elseif (1 == $count) { + $result[$val[$key]] = $val[$key1]; + } + } + } else { + $result = []; + } + } + if (isset($cache) && isset($guid)) { + // 缓存数据 + $this->cacheData($guid, $result, $cache); + } + } else { + // 清空查询条件 + $this->options = []; + } + return $result; + } + + /** + * COUNT查询 + * @access public + * @param string $field 字段名 + * @return integer|string + */ + public function count($field = '*') + { + if (isset($this->options['group'])) { + // 支持GROUP + $options = $this->getOptions(); + $subSql = $this->options($options)->field('count(' . $field . ')')->bind($this->bind)->buildSql(); + return $this->table([$subSql => '_group_count_'])->value('COUNT(*) AS tp_count', 0, true); + } + + return $this->value('COUNT(' . $field . ') AS tp_count', 0, true); + } + + /** + * SUM查询 + * @access public + * @param string $field 字段名 + * @return float|int + */ + public function sum($field) + { + return $this->value('SUM(' . $field . ') AS tp_sum', 0, true); + } + + /** + * MIN查询 + * @access public + * @param string $field 字段名 + * @return mixed + */ + public function min($field) + { + return $this->value('MIN(' . $field . ') AS tp_min', 0, true); + } + + /** + * MAX查询 + * @access public + * @param string $field 字段名 + * @return mixed + */ + public function max($field) + { + return $this->value('MAX(' . $field . ') AS tp_max', 0, true); + } + + /** + * AVG查询 + * @access public + * @param string $field 字段名 + * @return float|int + */ + public function avg($field) + { + return $this->value('AVG(' . $field . ') AS tp_avg', 0, true); + } + + /** + * 设置记录的某个字段值 + * 支持使用数据库字段和方法 + * @access public + * @param string|array $field 字段名 + * @param mixed $value 字段值 + * @return integer + */ + public function setField($field, $value = '') + { + if (is_array($field)) { + $data = $field; + } else { + $data[$field] = $value; + } + return $this->update($data); + } + + /** + * 字段值(延迟)增长 + * @access public + * @param string $field 字段名 + * @param integer $step 增长值 + * @param integer $lazyTime 延时时间(s) + * @return integer|true + * @throws Exception + */ + public function setInc($field, $step = 1, $lazyTime = 0) + { + $condition = !empty($this->options['where']) ? $this->options['where'] : []; + if (empty($condition)) { + // 没有条件不做任何更新 + throw new Exception('no data to update'); + } + if ($lazyTime > 0) { + // 延迟写入 + $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition) . serialize($this->bind)); + $step = $this->lazyWrite('inc', $guid, $step, $lazyTime); + if (false === $step) { + // 清空查询条件 + $this->options = []; + return true; + } + } + return $this->setField($field, ['exp', $field . '+' . $step]); + } + + /** + * 字段值(延迟)减少 + * @access public + * @param string $field 字段名 + * @param integer $step 减少值 + * @param integer $lazyTime 延时时间(s) + * @return integer|true + * @throws Exception + */ + public function setDec($field, $step = 1, $lazyTime = 0) + { + $condition = !empty($this->options['where']) ? $this->options['where'] : []; + if (empty($condition)) { + // 没有条件不做任何更新 + throw new Exception('no data to update'); + } + if ($lazyTime > 0) { + // 延迟写入 + $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition) . serialize($this->bind)); + $step = $this->lazyWrite('dec', $guid, $step, $lazyTime); + if (false === $step) { + // 清空查询条件 + $this->options = []; + return true; + } + } + return $this->setField($field, ['exp', $field . '-' . $step]); + } + + /** + * 延时更新检查 返回false表示需要延时 + * 否则返回实际写入的数值 + * @access protected + * @param string $type 自增或者自减 + * @param string $guid 写入标识 + * @param integer $step 写入步进值 + * @param integer $lazyTime 延时时间(s) + * @return false|integer + */ + protected function lazyWrite($type, $guid, $step, $lazyTime) + { + if (!Cache::has($guid . '_time')) { + // 计时开始 + Cache::set($guid . '_time', $_SERVER['REQUEST_TIME'], 0); + Cache::$type($guid, $step); + } elseif ($_SERVER['REQUEST_TIME'] > Cache::get($guid . '_time') + $lazyTime) { + // 删除缓存 + $value = Cache::$type($guid, $step); + Cache::rm($guid); + Cache::rm($guid . '_time'); + return 0 === $value ? false : $value; + } else { + // 更新缓存 + Cache::$type($guid, $step); + } + return false; + } + + /** + * 查询SQL组装 join + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param string $type JOIN类型 + * @return $this + */ + public function join($join, $condition = null, $type = 'INNER') + { + if (empty($condition)) { + // 如果为组数,则循环调用join + foreach ($join as $key => $value) { + if (is_array($value) && 2 <= count($value)) { + $this->join($value[0], $value[1], isset($value[2]) ? $value[2] : $type); + } + } + } else { + $table = $this->getJoinTable($join); + + $this->options['join'][] = [$table, strtoupper($type), $condition]; + } + return $this; + } + + /** + * 获取Join表名及别名 支持 + * ['prefix_table或者子查询'=>'alias'] 'prefix_table alias' 'table alias' + * @access public + * @param array|string $join + * @return array|string + */ + protected function getJoinTable($join, &$alias = null) + { + // 传入的表名为数组 + if (is_array($join)) { + list($table, $alias) = each($join); + } else { + $join = trim($join); + if (false !== strpos($join, '(')) { + // 使用子查询 + $table = $join; + } else { + $prefix = $this->prefix; + if (strpos($join, ' ')) { + // 使用别名 + list($table, $alias) = explode(' ', $join); + } else { + $table = $join; + if (false === strpos($join, '.') && 0 !== strpos($join, '__')) { + $alias = $join; + } + } + if ($prefix && false === strpos($table, '.') && 0 !== strpos($table, $prefix) && 0 !== strpos($table, '__')) { + $table = $this->getTable($table); + } + } + } + if (isset($alias)) { + if (isset($this->options['alias'][$table])) { + $table = $table . '@think' . uniqid(); + } + $table = [$table => $alias]; + $this->alias($table); + } + return $table; + } + + /** + * 查询SQL组装 union + * @access public + * @param mixed $union + * @param boolean $all + * @return $this + */ + public function union($union, $all = false) + { + $this->options['union']['type'] = $all ? 'UNION ALL' : 'UNION'; + + if (is_array($union)) { + $this->options['union'] = array_merge($this->options['union'], $union); + } else { + $this->options['union'][] = $union; + } + return $this; + } + + /** + * 指定查询字段 支持字段排除和指定数据表 + * @access public + * @param mixed $field + * @param boolean $except 是否排除 + * @param string $tableName 数据表名 + * @param string $prefix 字段前缀 + * @param string $alias 别名前缀 + * @return $this + */ + public function field($field, $except = false, $tableName = '', $prefix = '', $alias = '') + { + if (empty($field)) { + return $this; + } + if (is_string($field)) { + $field = array_map('trim', explode(',', $field)); + } + if (true === $field) { + // 获取全部字段 + $fields = $this->getTableInfo($tableName ?: (isset($this->options['table']) ? $this->options['table'] : ''), 'fields'); + $field = $fields ?: ['*']; + } elseif ($except) { + // 字段排除 + $fields = $this->getTableInfo($tableName ?: (isset($this->options['table']) ? $this->options['table'] : ''), 'fields'); + $field = $fields ? array_diff($fields, $field) : $field; + } + if ($tableName) { + // 添加统一的前缀 + $prefix = $prefix ?: $tableName; + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $val = $prefix . '.' . $val . ($alias ? ' AS ' . $alias . $val : ''); + } + $field[$key] = $val; + } + } + + if (isset($this->options['field'])) { + $field = array_merge($this->options['field'], $field); + } + $this->options['field'] = array_unique($field); + return $this; + } + + /** + * 设置数据 + * @access public + * @param mixed $field 字段名或者数据 + * @param mixed $value 字段值 + * @return $this + */ + public function data($field, $value = null) + { + if (is_array($field)) { + $this->options['data'] = isset($this->options['data']) ? array_merge($this->options['data'], $field) : $field; + } else { + $this->options['data'][$field] = $value; + } + return $this; + } + + /** + * 字段值增长 + * @access public + * @param string|array $field 字段名 + * @param integer $step 增长值 + * @return $this + */ + public function inc($field, $step = 1) + { + $fields = is_string($field) ? explode(',', $field) : $field; + foreach ($fields as $field) { + $this->data($field, ['exp', $field . '+' . $step]); + } + return $this; + } + + /** + * 字段值减少 + * @access public + * @param string|array $field 字段名 + * @param integer $step 增长值 + * @return $this + */ + public function dec($field, $step = 1) + { + $fields = is_string($field) ? explode(',', $field) : $field; + foreach ($fields as $field) { + $this->data($field, ['exp', $field . '-' . $step]); + } + return $this; + } + + /** + * 使用表达式设置数据 + * @access public + * @param string $field 字段名 + * @param string $value 字段值 + * @return $this + */ + public function exp($field, $value) + { + $this->data($field, ['exp', $value]); + return $this; + } + + /** + * 指定JOIN查询字段 + * @access public + * @param string|array $table 数据表 + * @param string|array $field 查询字段 + * @param string|array $on JOIN条件 + * @param string $type JOIN类型 + * @return $this + */ + public function view($join, $field = true, $on = null, $type = 'INNER') + { + $this->options['view'] = true; + if (is_array($join) && key($join) !== 0) { + foreach ($join as $key => $val) { + $this->view($key, $val[0], isset($val[1]) ? $val[1] : null, isset($val[2]) ? $val[2] : 'INNER'); + } + } else { + $fields = []; + $table = $this->getJoinTable($join, $alias); + + if (true === $field) { + $fields = $alias . '.*'; + } else { + if (is_string($field)) { + $field = explode(',', $field); + } + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $fields[] = $alias . '.' . $val; + $this->options['map'][$val] = $alias . '.' . $val; + } else { + if (preg_match('/[,=\.\'\"\(\s]/', $key)) { + $name = $key; + } else { + $name = $alias . '.' . $key; + } + $fields[$name] = $val; + $this->options['map'][$val] = $name; + } + } + } + $this->field($fields); + if ($on) { + $this->join($table, $on, $type); + } else { + $this->table($table); + } + } + return $this; + } + + /** + * 设置分表规则 + * @access public + * @param array $data 操作的数据 + * @param string $field 分表依据的字段 + * @param array $rule 分表规则 + * @return $this + */ + public function partition($data, $field, $rule = []) + { + $this->options['table'] = $this->getPartitionTableName($data, $field, $rule); + return $this; + } + + /** + * 指定AND查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return $this + */ + public function where($field, $op = null, $condition = null) + { + $param = func_get_args(); + array_shift($param); + $this->parseWhereExp('AND', $field, $op, $condition, $param); + return $this; + } + + /** + * 指定OR查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return $this + */ + public function whereOr($field, $op = null, $condition = null) + { + $param = func_get_args(); + array_shift($param); + $this->parseWhereExp('OR', $field, $op, $condition, $param); + return $this; + } + + /** + * 指定XOR查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return $this + */ + public function whereXor($field, $op = null, $condition = null) + { + $param = func_get_args(); + array_shift($param); + $this->parseWhereExp('XOR', $field, $op, $condition, $param); + return $this; + } + + /** + * 指定Null查询条件 + * @access public + * @param mixed $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNull($field, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'null', null); + return $this; + } + + /** + * 指定NotNull查询条件 + * @access public + * @param mixed $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotNull($field, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'notnull', null); + return $this; + } + + /** + * 指定Exists查询条件 + * @access public + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereExists($condition, $logic = 'AND') + { + $this->options['where'][strtoupper($logic)][] = ['exists', $condition]; + return $this; + } + + /** + * 指定NotExists查询条件 + * @access public + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotExists($condition, $logic = 'AND') + { + $this->options['where'][strtoupper($logic)][] = ['not exists', $condition]; + return $this; + } + + /** + * 指定In查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereIn($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'in', $condition); + return $this; + } + + /** + * 指定NotIn查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotIn($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'not in', $condition); + return $this; + } + + /** + * 指定Like查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereLike($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'like', $condition); + return $this; + } + + /** + * 指定NotLike查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotLike($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'not like', $condition); + return $this; + } + + /** + * 指定Between查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereBetween($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'between', $condition); + return $this; + } + + /** + * 指定NotBetween查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotBetween($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'not between', $condition); + return $this; + } + + /** + * 指定Exp查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereExp($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'exp', $condition); + return $this; + } + + /** + * 设置软删除字段及条件 + * @access public + * @param false|string $field 查询字段 + * @param mixed $condition 查询条件 + * @return $this + */ + public function useSoftDelete($field, $condition = null) + { + if ($field) { + $this->options['soft_delete'] = [$field, $condition ?: ['null', '']]; + } + return $this; + } + + /** + * 分析查询表达式 + * @access public + * @param string $logic 查询逻辑 and or xor + * @param string|array|\Closure $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @param array $param 查询参数 + * @return void + */ + protected function parseWhereExp($logic, $field, $op, $condition, $param = []) + { + $logic = strtoupper($logic); + if ($field instanceof \Closure) { + $this->options['where'][$logic][] = is_string($op) ? [$op, $field] : $field; + return; + } + + if (is_string($field) && !empty($this->options['via']) && !strpos($field, '.')) { + $field = $this->options['via'] . '.' . $field; + } + if (is_string($field) && preg_match('/[,=\>\<\'\"\(\s]/', $field)) { + $where[] = ['exp', $field]; + if (is_array($op)) { + // 参数绑定 + $this->bind($op); + } + } elseif (is_null($op) && is_null($condition)) { + if (is_array($field)) { + // 数组批量查询 + $where = $field; + foreach ($where as $k => $val) { + $this->options['multi'][$logic][$k][] = $val; + } + } elseif ($field && is_string($field)) { + // 字符串查询 + $where[$field] = ['null', '']; + $this->options['multi'][$logic][$field][] = $where[$field]; + } + } elseif (is_array($op)) { + $where[$field] = $param; + } elseif (in_array(strtolower($op), ['null', 'notnull', 'not null'])) { + // null查询 + $where[$field] = [$op, '']; + $this->options['multi'][$logic][$field][] = $where[$field]; + } elseif (is_null($condition)) { + // 字段相等查询 + $where[$field] = ['eq', $op]; + if ('AND' != $logic) { + $this->options['multi'][$logic][$field][] = $where[$field]; + } + } else { + $where[$field] = [$op, $condition, isset($param[2]) ? $param[2] : null]; + if ('exp' == strtolower($op) && isset($param[2]) && is_array($param[2])) { + // 参数绑定 + $this->bind($param[2]); + } + // 记录一个字段多次查询条件 + $this->options['multi'][$logic][$field][] = $where[$field]; + } + if (!empty($where)) { + if (!isset($this->options['where'][$logic])) { + $this->options['where'][$logic] = []; + } + if (is_string($field) && $this->checkMultiField($field, $logic)) { + $where[$field] = $this->options['multi'][$logic][$field]; + } elseif (is_array($field)) { + foreach ($field as $key => $val) { + if ($this->checkMultiField($key, $logic)) { + $where[$key] = $this->options['multi'][$logic][$key]; + } + } + } + $this->options['where'][$logic] = array_merge($this->options['where'][$logic], $where); + } + } + + /** + * 检查是否存在一个字段多次查询条件 + * @access public + * @param string $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return bool + */ + private function checkMultiField($field, $logic) + { + return isset($this->options['multi'][$logic][$field]) && count($this->options['multi'][$logic][$field]) > 1; + } + + /** + * 去除某个查询条件 + * @access public + * @param string $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function removeWhereField($field, $logic = 'AND') + { + $logic = strtoupper($logic); + if (isset($this->options['where'][$logic][$field])) { + unset($this->options['where'][$logic][$field]); + } + return $this; + } + + /** + * 去除查询参数 + * @access public + * @param string|bool $option 参数名 true 表示去除所有参数 + * @return $this + */ + public function removeOption($option = true) + { + if (true === $option) { + $this->options = []; + } elseif (is_string($option) && isset($this->options[$option])) { + unset($this->options[$option]); + } + return $this; + } + + /** + * 指定查询数量 + * @access public + * @param mixed $offset 起始位置 + * @param mixed $length 查询数量 + * @return $this + */ + public function limit($offset, $length = null) + { + if (is_null($length) && strpos($offset, ',')) { + list($offset, $length) = explode(',', $offset); + } + $this->options['limit'] = intval($offset) . ($length ? ',' . intval($length) : ''); + return $this; + } + + /** + * 指定分页 + * @access public + * @param mixed $page 页数 + * @param mixed $listRows 每页数量 + * @return $this + */ + public function page($page, $listRows = null) + { + if (is_null($listRows) && strpos($page, ',')) { + list($page, $listRows) = explode(',', $page); + } + $this->options['page'] = [intval($page), intval($listRows)]; + return $this; + } + + /** + * 分页查询 + * @param int|array $listRows 每页数量 数组表示配置参数 + * @param int|bool $simple 是否简洁模式或者总记录数 + * @param array $config 配置参数 + * page:当前页, + * path:url路径, + * query:url额外参数, + * fragment:url锚点, + * var_page:分页变量, + * list_rows:每页数量 + * type:分页类名 + * @return \think\Paginator + * @throws DbException + */ + public function paginate($listRows = null, $simple = false, $config = []) + { + if (is_int($simple)) { + $total = $simple; + $simple = false; + } + if (is_array($listRows)) { + $config = array_merge(Config::get('paginate'), $listRows); + $listRows = $config['list_rows']; + } else { + $config = array_merge(Config::get('paginate'), $config); + $listRows = $listRows ?: $config['list_rows']; + } + + /** @var Paginator $class */ + $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\paginator\\driver\\' . ucwords($config['type']); + $page = isset($config['page']) ? (int) $config['page'] : call_user_func([ + $class, + 'getCurrentPage', + ], $config['var_page']); + + $page = $page < 1 ? 1 : $page; + + $config['path'] = isset($config['path']) ? $config['path'] : call_user_func([$class, 'getCurrentPath']); + + if (!isset($total) && !$simple) { + $options = $this->getOptions(); + + unset($this->options['order'], $this->options['limit'], $this->options['page'], $this->options['field']); + + $bind = $this->bind; + $total = $this->count(); + $results = $this->options($options)->bind($bind)->page($page, $listRows)->select(); + } elseif ($simple) { + $results = $this->limit(($page - 1) * $listRows, $listRows + 1)->select(); + $total = null; + } else { + $results = $this->page($page, $listRows)->select(); + } + return $class::make($results, $listRows, $page, $total, $simple, $config); + } + + /** + * 指定当前操作的数据表 + * @access public + * @param mixed $table 表名 + * @return $this + */ + public function table($table) + { + if (is_string($table)) { + if (strpos($table, ')')) { + // 子查询 + } elseif (strpos($table, ',')) { + $tables = explode(',', $table); + $table = []; + foreach ($tables as $item) { + list($item, $alias) = explode(' ', trim($item)); + if ($alias) { + $this->alias([$item => $alias]); + $table[$item] = $alias; + } else { + $table[] = $item; + } + } + } elseif (strpos($table, ' ')) { + list($table, $alias) = explode(' ', $table); + + $table = [$table => $alias]; + $this->alias($table); + } + } else { + $tables = $table; + $table = []; + foreach ($tables as $key => $val) { + if (is_numeric($key)) { + $table[] = $val; + } else { + $this->alias([$key => $val]); + $table[$key] = $val; + } + } + } + $this->options['table'] = $table; + return $this; + } + + /** + * USING支持 用于多表删除 + * @access public + * @param mixed $using + * @return $this + */ + public function using($using) + { + $this->options['using'] = $using; + return $this; + } + + /** + * 指定排序 order('id','desc') 或者 order(['id'=>'desc','create_time'=>'desc']) + * @access public + * @param string|array $field 排序字段 + * @param string $order 排序 + * @return $this + */ + public function order($field, $order = null) + { + if (!empty($field)) { + if (is_string($field)) { + if (!empty($this->options['via'])) { + $field = $this->options['via'] . '.' . $field; + } + $field = empty($order) ? $field : [$field => $order]; + } elseif (!empty($this->options['via'])) { + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $field[$key] = $this->options['via'] . '.' . $val; + } else { + $field[$this->options['via'] . '.' . $key] = $val; + unset($field[$key]); + } + } + } + if (!isset($this->options['order'])) { + $this->options['order'] = []; + } + if (is_array($field)) { + $this->options['order'] = array_merge($this->options['order'], $field); + } else { + $this->options['order'][] = $field; + } + } + return $this; + } + + /** + * 查询缓存 + * @access public + * @param mixed $key 缓存key + * @param integer $expire 缓存有效期 + * @param string $tag 缓存标签 + * @return $this + */ + public function cache($key = true, $expire = null, $tag = null) + { + // 增加快捷调用方式 cache(10) 等同于 cache(true, 10) + if (is_numeric($key) && is_null($expire)) { + $expire = $key; + $key = true; + } + if (false !== $key) { + $this->options['cache'] = ['key' => $key, 'expire' => $expire, 'tag' => $tag]; + } + return $this; + } + + /** + * 指定group查询 + * @access public + * @param string $group GROUP + * @return $this + */ + public function group($group) + { + $this->options['group'] = $group; + return $this; + } + + /** + * 指定having查询 + * @access public + * @param string $having having + * @return $this + */ + public function having($having) + { + $this->options['having'] = $having; + return $this; + } + + /** + * 指定查询lock + * @access public + * @param boolean $lock 是否lock + * @return $this + */ + public function lock($lock = false) + { + $this->options['lock'] = $lock; + $this->options['master'] = true; + return $this; + } + + /** + * 指定distinct查询 + * @access public + * @param string $distinct 是否唯一 + * @return $this + */ + public function distinct($distinct) + { + $this->options['distinct'] = $distinct; + return $this; + } + + /** + * 指定数据表别名 + * @access public + * @param mixed $alias 数据表别名 + * @return $this + */ + public function alias($alias) + { + if (is_array($alias)) { + foreach ($alias as $key => $val) { + $this->options['alias'][$key] = $val; + } + } else { + if (isset($this->options['table'])) { + $table = is_array($this->options['table']) ? key($this->options['table']) : $this->options['table']; + if (false !== strpos($table, '__')) { + $table = $this->parseSqlTable($table); + } + } else { + $table = $this->getTable(); + } + + $this->options['alias'][$table] = $alias; + } + return $this; + } + + /** + * 指定强制索引 + * @access public + * @param string $force 索引名称 + * @return $this + */ + public function force($force) + { + $this->options['force'] = $force; + return $this; + } + + /** + * 查询注释 + * @access public + * @param string $comment 注释 + * @return $this + */ + public function comment($comment) + { + $this->options['comment'] = $comment; + return $this; + } + + /** + * 获取执行的SQL语句 + * @access public + * @param boolean $fetch 是否返回sql + * @return $this + */ + public function fetchSql($fetch = true) + { + $this->options['fetch_sql'] = $fetch; + return $this; + } + + /** + * 不主动获取数据集 + * @access public + * @param bool $pdo 是否返回 PDOStatement 对象 + * @return $this + */ + public function fetchPdo($pdo = true) + { + $this->options['fetch_pdo'] = $pdo; + return $this; + } + + /** + * 设置从主服务器读取数据 + * @access public + * @return $this + */ + public function master() + { + $this->options['master'] = true; + return $this; + } + + /** + * 设置是否严格检查字段名 + * @access public + * @param bool $strict 是否严格检查字段 + * @return $this + */ + public function strict($strict = true) + { + $this->options['strict'] = $strict; + return $this; + } + + /** + * 设置查询数据不存在是否抛出异常 + * @access public + * @param bool $fail 数据不存在是否抛出异常 + * @return $this + */ + public function failException($fail = true) + { + $this->options['fail'] = $fail; + return $this; + } + + /** + * 设置自增序列名 + * @access public + * @param string $sequence 自增序列名 + * @return $this + */ + public function sequence($sequence = null) + { + $this->options['sequence'] = $sequence; + return $this; + } + + /** + * 指定数据表主键 + * @access public + * @param string $pk 主键 + * @return $this + */ + public function pk($pk) + { + $this->pk = $pk; + return $this; + } + + /** + * 查询日期或者时间 + * @access public + * @param string $field 日期字段名 + * @param string $op 比较运算符或者表达式 + * @param string|array $range 比较范围 + * @return $this + */ + public function whereTime($field, $op, $range = null) + { + if (is_null($range)) { + // 使用日期表达式 + $date = getdate(); + switch (strtolower($op)) { + case 'today': + case 'd': + $range = ['today', 'tomorrow']; + break; + case 'week': + case 'w': + $range = 'this week 00:00:00'; + break; + case 'month': + case 'm': + $range = mktime(0, 0, 0, $date['mon'], 1, $date['year']); + break; + case 'year': + case 'y': + $range = mktime(0, 0, 0, 1, 1, $date['year']); + break; + case 'yesterday': + $range = ['yesterday', 'today']; + break; + case 'last week': + $range = ['last week 00:00:00', 'this week 00:00:00']; + break; + case 'last month': + $range = [date('y-m-01', strtotime('-1 month')), mktime(0, 0, 0, $date['mon'], 1, $date['year'])]; + break; + case 'last year': + $range = [mktime(0, 0, 0, 1, 1, $date['year'] - 1), mktime(0, 0, 0, 1, 1, $date['year'])]; + break; + default: + $range = $op; + } + $op = is_array($range) ? 'between' : '>'; + } + $this->where($field, strtolower($op) . ' time', $range); + return $this; + } + + /** + * 获取数据表信息 + * @access public + * @param mixed $tableName 数据表名 留空自动获取 + * @param string $fetch 获取信息类型 包括 fields type bind pk + * @return mixed + */ + public function getTableInfo($tableName = '', $fetch = '') + { + if (!$tableName) { + $tableName = $this->getTable(); + } + if (is_array($tableName)) { + $tableName = key($tableName) ?: current($tableName); + } + + if (strpos($tableName, ',')) { + // 多表不获取字段信息 + return false; + } else { + $tableName = $this->parseSqlTable($tableName); + } + + // 修正子查询作为表名的问题 + if (strpos($tableName, ')')) { + return []; + } + + list($guid) = explode(' ', $tableName); + $db = $this->getConfig('database'); + if (!isset(self::$info[$db . '.' . $guid])) { + if (!strpos($guid, '.')) { + $schema = $db . '.' . $guid; + } else { + $schema = $guid; + } + // 读取缓存 + if (is_file(RUNTIME_PATH . 'schema/' . $schema . '.php')) { + $info = include RUNTIME_PATH . 'schema/' . $schema . '.php'; + } else { + $info = $this->connection->getFields($guid); + } + $fields = array_keys($info); + $bind = $type = []; + foreach ($info as $key => $val) { + // 记录字段类型 + $type[$key] = $val['type']; + $bind[$key] = $this->getFieldBindType($val['type']); + if (!empty($val['primary'])) { + $pk[] = $key; + } + } + if (isset($pk)) { + // 设置主键 + $pk = count($pk) > 1 ? $pk : $pk[0]; + } else { + $pk = null; + } + self::$info[$db . '.' . $guid] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk]; + } + return $fetch ? self::$info[$db . '.' . $guid][$fetch] : self::$info[$db . '.' . $guid]; + } + + /** + * 获取当前数据表的主键 + * @access public + * @param string|array $options 数据表名或者查询参数 + * @return string|array + */ + public function getPk($options = '') + { + if (!empty($this->pk)) { + $pk = $this->pk; + } else { + $pk = $this->getTableInfo(is_array($options) ? $options['table'] : $options, 'pk'); + } + return $pk; + } + + // 获取当前数据表字段信息 + public function getTableFields($table = '') + { + return $this->getTableInfo($table ?: $this->getOptions('table'), 'fields'); + } + + // 获取当前数据表字段类型 + public function getFieldsType($table = '') + { + return $this->getTableInfo($table ?: $this->getOptions('table'), 'type'); + } + + // 获取当前数据表绑定信息 + public function getFieldsBind($table = '') + { + $types = $this->getFieldsType($table); + $bind = []; + if ($types) { + foreach ($types as $key => $type) { + $bind[$key] = $this->getFieldBindType($type); + } + } + return $bind; + } + + /** + * 获取字段绑定类型 + * @access public + * @param string $type 字段类型 + * @return integer + */ + protected function getFieldBindType($type) + { + if (preg_match('/(int|double|float|decimal|real|numeric|serial|bit)/is', $type)) { + $bind = PDO::PARAM_INT; + } elseif (preg_match('/bool/is', $type)) { + $bind = PDO::PARAM_BOOL; + } else { + $bind = PDO::PARAM_STR; + } + return $bind; + } + + /** + * 参数绑定 + * @access public + * @param mixed $key 参数名 + * @param mixed $value 绑定变量值 + * @param integer $type 绑定类型 + * @return $this + */ + public function bind($key, $value = false, $type = PDO::PARAM_STR) + { + if (is_array($key)) { + $this->bind = array_merge($this->bind, $key); + } else { + $this->bind[$key] = [$value, $type]; + } + return $this; + } + + /** + * 检测参数是否已经绑定 + * @access public + * @param string $key 参数名 + * @return bool + */ + public function isBind($key) + { + return isset($this->bind[$key]); + } + + /** + * 查询参数赋值 + * @access protected + * @param array $options 表达式参数 + * @return $this + */ + protected function options(array $options) + { + $this->options = $options; + return $this; + } + + /** + * 获取当前的查询参数 + * @access public + * @param string $name 参数名 + * @return mixed + */ + public function getOptions($name = '') + { + if ('' === $name) { + return $this->options; + } else { + return isset($this->options[$name]) ? $this->options[$name] : null; + } + } + + /** + * 设置关联查询JOIN预查询 + * @access public + * @param string|array $with 关联方法名称 + * @return $this + */ + public function with($with) + { + if (empty($with)) { + return $this; + } + + if (is_string($with)) { + $with = explode(',', $with); + } + + $first = true; + $currentModel = $this->model; + + /** @var Model $class */ + $class = new $currentModel; + foreach ($with as $key => $relation) { + $subRelation = ''; + $closure = false; + if ($relation instanceof \Closure) { + // 支持闭包查询过滤关联条件 + $closure = $relation; + $relation = $key; + $with[$key] = $key; + } elseif (is_array($relation)) { + $subRelation = $relation; + $relation = $key; + } elseif (is_string($relation) && strpos($relation, '.')) { + $with[$key] = $relation; + list($relation, $subRelation) = explode('.', $relation, 2); + } + + /** @var Relation $model */ + $relation = Loader::parseName($relation, 1, false); + $model = $class->$relation(); + if ($model instanceof OneToOne && 0 == $model->getEagerlyType()) { + $model->eagerly($this, $relation, $subRelation, $closure, $first); + $first = false; + } elseif ($closure) { + $with[$key] = $closure; + } + } + $this->via(); + if (isset($this->options['with'])) { + $this->options['with'] = array_merge($this->options['with'], $with); + } else { + $this->options['with'] = $with; + } + return $this; + } + + /** + * 关联统计 + * @access public + * @param string|array $relation 关联方法名 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withCount($relation, $subQuery = true) + { + if (!$subQuery) { + $this->options['with_count'] = $relation; + } else { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + if (!isset($this->options['field'])) { + $this->field('*'); + } + foreach ($relations as $key => $relation) { + $closure = false; + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } + $relation = Loader::parseName($relation, 1, false); + $count = '(' . (new $this->model)->$relation()->getRelationCountQuery($closure) . ')'; + $this->field([$count => Loader::parseName($relation) . '_count']); + } + } + return $this; + } + + /** + * 关联预加载中 获取关联指定字段值 + * example: + * Model::with(['relation' => function($query){ + * $query->withField("id,name"); + * }]) + * + * @param string | array $field 指定获取的字段 + * @return $this + */ + public function withField($field) + { + $this->options['with_field'] = $field; + return $this; + } + + /** + * 设置当前字段添加的表别名 + * @access public + * @param string $via + * @return $this + */ + public function via($via = '') + { + $this->options['via'] = $via; + return $this; + } + + /** + * 设置关联查询 + * @access public + * @param string|array $relation 关联名称 + * @return $this + */ + public function relation($relation) + { + if (empty($relation)) { + return $this; + } + if (is_string($relation)) { + $relation = explode(',', $relation); + } + if (isset($this->options['relation'])) { + $this->options['relation'] = array_merge($this->options['relation'], $relation); + } else { + $this->options['relation'] = $relation; + } + return $this; + } + + /** + * 把主键值转换为查询条件 支持复合主键 + * @access public + * @param array|string $data 主键数据 + * @param mixed $options 表达式参数 + * @return void + * @throws Exception + */ + protected function parsePkWhere($data, &$options) + { + $pk = $this->getPk($options); + // 获取当前数据表 + $table = is_array($options['table']) ? key($options['table']) : $options['table']; + if (!empty($options['alias'][$table])) { + $alias = $options['alias'][$table]; + } + if (is_string($pk)) { + $key = isset($alias) ? $alias . '.' . $pk : $pk; + // 根据主键查询 + if (is_array($data)) { + $where[$key] = isset($data[$pk]) ? $data[$pk] : ['in', $data]; + } else { + $where[$key] = strpos($data, ',') ? ['IN', $data] : $data; + } + } elseif (is_array($pk) && is_array($data) && !empty($data)) { + // 根据复合主键查询 + foreach ($pk as $key) { + if (isset($data[$key])) { + $attr = isset($alias) ? $alias . '.' . $key : $key; + $where[$attr] = $data[$key]; + } else { + throw new Exception('miss complex primary data'); + } + } + } + + if (!empty($where)) { + if (isset($options['where']['AND'])) { + $options['where']['AND'] = array_merge($options['where']['AND'], $where); + } else { + $options['where']['AND'] = $where; + } + } + return; + } + + /** + * 插入记录 + * @access public + * @param mixed $data 数据 + * @param boolean $replace 是否replace + * @param boolean $getLastInsID 返回自增主键 + * @param string $sequence 自增序列名 + * @return integer|string + */ + public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null) + { + // 分析查询表达式 + $options = $this->parseExpress(); + $data = array_merge($options['data'], $data); + // 生成SQL语句 + $sql = $this->builder->insert($data, $options, $replace); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } + + // 执行操作 + $result = 0 === $sql ? 0 : $this->execute($sql, $bind); + if ($result) { + $sequence = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null); + $lastInsId = $this->getLastInsID($sequence); + if ($lastInsId) { + $pk = $this->getPk($options); + if (is_string($pk)) { + $data[$pk] = $lastInsId; + } + } + $options['data'] = $data; + $this->trigger('after_insert', $options); + + if ($getLastInsID) { + return $lastInsId; + } + } + return $result; + } + + /** + * 插入记录并获取自增ID + * @access public + * @param mixed $data 数据 + * @param boolean $replace 是否replace + * @param string $sequence 自增序列名 + * @return integer|string + */ + public function insertGetId(array $data, $replace = false, $sequence = null) + { + return $this->insert($data, $replace, true, $sequence); + } + + /** + * 批量插入记录 + * @access public + * @param mixed $dataSet 数据集 + * @param boolean $replace 是否replace + * @return integer|string + */ + public function insertAll(array $dataSet, $replace = false) + { + // 分析查询表达式 + $options = $this->parseExpress(); + if (!is_array(reset($dataSet))) { + return false; + } + // 生成SQL语句 + $sql = $this->builder->insertAll($dataSet, $options, $replace); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } else { + // 执行操作 + return $this->execute($sql, $bind); + } + } + + /** + * 通过Select方式插入记录 + * @access public + * @param string $fields 要插入的数据表字段名 + * @param string $table 要插入的数据表名 + * @return integer|string + * @throws PDOException + */ + public function selectInsert($fields, $table) + { + // 分析查询表达式 + $options = $this->parseExpress(); + // 生成SQL语句 + $table = $this->parseSqlTable($table); + $sql = $this->builder->selectInsert($fields, $table, $options); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } else { + // 执行操作 + return $this->execute($sql, $bind); + } + } + + /** + * 更新记录 + * @access public + * @param mixed $data 数据 + * @return integer|string + * @throws Exception + * @throws PDOException + */ + public function update(array $data = []) + { + $options = $this->parseExpress(); + $data = array_merge($options['data'], $data); + $pk = $this->getPk($options); + if (isset($options['cache']) && is_string($options['cache']['key'])) { + $key = $options['cache']['key']; + } + + if (empty($options['where'])) { + // 如果存在主键数据 则自动作为更新条件 + if (is_string($pk) && isset($data[$pk])) { + $where[$pk] = $data[$pk]; + if (!isset($key)) { + $key = 'think:' . $options['table'] . '|' . $data[$pk]; + } + unset($data[$pk]); + } elseif (is_array($pk)) { + // 增加复合主键支持 + foreach ($pk as $field) { + if (isset($data[$field])) { + $where[$field] = $data[$field]; + } else { + // 如果缺少复合主键数据则不执行 + throw new Exception('miss complex primary data'); + } + unset($data[$field]); + } + } + if (!isset($where)) { + // 如果没有任何更新条件则不执行 + throw new Exception('miss update condition'); + } else { + $options['where']['AND'] = $where; + } + } elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'][$pk])) { + $key = $this->getCacheKey($options['where']['AND'][$pk], $options, $this->bind); + } + + // 生成UPDATE SQL语句 + $sql = $this->builder->update($data, $options); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } else { + // 检测缓存 + if (isset($key) && Cache::get($key)) { + // 删除缓存 + Cache::rm($key); + } elseif (!empty($options['cache']['tag'])) { + Cache::clear($options['cache']['tag']); + } + // 执行操作 + $result = '' == $sql ? 0 : $this->execute($sql, $bind); + if ($result) { + if (is_string($pk) && isset($where[$pk])) { + $data[$pk] = $where[$pk]; + } elseif (is_string($pk) && isset($key) && strpos($key, '|')) { + list($a, $val) = explode('|', $key); + $data[$pk] = $val; + } + $options['data'] = $data; + $this->trigger('after_update', $options); + } + return $result; + } + } + + /** + * 执行查询但只返回PDOStatement对象 + * @access public + * @return \PDOStatement|string + */ + public function getPdo() + { + // 分析查询表达式 + $options = $this->parseExpress(); + // 生成查询SQL + $sql = $this->builder->select($options); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } + // 执行查询操作 + return $this->query($sql, $bind, $options['master'], true); + } + + /** + * 查找记录 + * @access public + * @param array|string|Query|\Closure $data + * @return Collection|false|\PDOStatement|string + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function select($data = null) + { + if ($data instanceof Query) { + return $data->select(); + } elseif ($data instanceof \Closure) { + call_user_func_array($data, [ & $this]); + $data = null; + } + // 分析查询表达式 + $options = $this->parseExpress(); + + if (false === $data) { + // 用于子查询 不查询只返回SQL + $options['fetch_sql'] = true; + } elseif (!is_null($data)) { + // 主键条件分析 + $this->parsePkWhere($data, $options); + } + + $resultSet = false; + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + // 判断查询缓存 + $cache = $options['cache']; + unset($options['cache']); + $key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options) . serialize($this->bind)); + $resultSet = Cache::get($key); + } + if (false === $resultSet) { + // 生成查询SQL + $sql = $this->builder->select($options); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } + + $options['data'] = $data; + if ($resultSet = $this->trigger('before_select', $options)) { + } else { + // 执行查询操作 + $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']); + + if ($resultSet instanceof \PDOStatement) { + // 返回PDOStatement对象 + return $resultSet; + } + } + + if (isset($cache) && false !== $resultSet) { + // 缓存数据集 + $this->cacheData($key, $resultSet, $cache); + } + } + + // 数据列表读取后的处理 + if (!empty($this->model)) { + // 生成模型对象 + $modelName = $this->model; + if (count($resultSet) > 0) { + foreach ($resultSet as $key => $result) { + /** @var Model $result */ + $model = new $modelName($result); + $model->isUpdate(true); + + // 关联查询 + if (!empty($options['relation'])) { + $model->relationQuery($options['relation']); + } + // 关联统计 + if (!empty($options['with_count'])) { + $model->relationCount($model, $options['with_count']); + } + $resultSet[$key] = $model; + } + if (!empty($options['with'])) { + // 预载入 + $model->eagerlyResultSet($resultSet, $options['with']); + } + // 模型数据集转换 + $resultSet = $model->toCollection($resultSet); + } else { + $resultSet = (new $modelName)->toCollection($resultSet); + } + } elseif ('collection' == $this->connection->getConfig('resultset_type')) { + // 返回Collection对象 + $resultSet = new Collection($resultSet); + } + // 返回结果处理 + if (!empty($options['fail']) && count($resultSet) == 0) { + $this->throwNotFound($options); + } + return $resultSet; + } + + /** + * 缓存数据 + * @access public + * @param string $key 缓存标识 + * @param mixed $data 缓存数据 + * @param array $config 缓存参数 + */ + protected function cacheData($key, $data, $config = []) + { + if (isset($config['tag'])) { + Cache::tag($config['tag'])->set($key, $data, $config['expire']); + } else { + Cache::set($key, $data, $config['expire']); + } + } + + /** + * 生成缓存标识 + * @access public + * @param mixed $value 缓存数据 + * @param array $options 缓存参数 + * @param array $bind 绑定参数 + */ + protected function getCacheKey($value, $options, $bind = []) + { + if (is_scalar($value)) { + $data = $value; + } elseif (is_array($value) && 'eq' == strtolower($value[0])) { + $data = $value[1]; + } + if (isset($data)) { + return 'think:' . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data; + } else { + return md5(serialize($options) . serialize($bind)); + } + } + + /** + * 查找单条记录 + * @access public + * @param array|string|Query|\Closure $data + * @return array|false|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function find($data = null) + { + if ($data instanceof Query) { + return $data->find(); + } elseif ($data instanceof \Closure) { + call_user_func_array($data, [ & $this]); + $data = null; + } + // 分析查询表达式 + $options = $this->parseExpress(); + $pk = $this->getPk($options); + if (!is_null($data)) { + // AR模式分析主键条件 + $this->parsePkWhere($data, $options); + } elseif (!empty($options['cache']) && true === $options['cache']['key'] && is_string($pk) && isset($options['where']['AND'][$pk])) { + $key = $this->getCacheKey($options['where']['AND'][$pk], $options, $this->bind); + } + + $options['limit'] = 1; + $result = false; + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + // 判断查询缓存 + $cache = $options['cache']; + if (true === $cache['key'] && !is_null($data) && !is_array($data)) { + $key = 'think:' . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data; + } elseif (is_string($cache['key'])) { + $key = $cache['key']; + } elseif (!isset($key)) { + $key = md5(serialize($options) . serialize($this->bind)); + } + $result = Cache::get($key); + } + if (false === $result) { + // 生成查询SQL + $sql = $this->builder->select($options); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } + if (is_string($pk)) { + if (!is_array($data)) { + if (isset($key) && strpos($key, '|')) { + list($a, $val) = explode('|', $key); + $item[$pk] = $val; + } else { + $item[$pk] = $data; + } + $data = $item; + } + } + $options['data'] = $data; + // 事件回调 + if ($result = $this->trigger('before_find', $options)) { + } else { + // 执行查询 + $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']); + + if ($resultSet instanceof \PDOStatement) { + // 返回PDOStatement对象 + return $resultSet; + } + $result = isset($resultSet[0]) ? $resultSet[0] : null; + } + + if (isset($cache) && false !== $result) { + // 缓存数据 + $this->cacheData($key, $result, $cache); + } + } + + // 数据处理 + if (!empty($result)) { + if (!empty($this->model)) { + // 返回模型对象 + $model = $this->model; + $result = new $model($result); + $result->isUpdate(true, isset($options['where']['AND']) ? $options['where']['AND'] : null); + // 关联查询 + if (!empty($options['relation'])) { + $result->relationQuery($options['relation']); + } + // 预载入查询 + if (!empty($options['with'])) { + $result->eagerlyResult($result, $options['with']); + } + // 关联统计 + if (!empty($options['with_count'])) { + $result->relationCount($result, $options['with_count']); + } + } + } elseif (!empty($options['fail'])) { + $this->throwNotFound($options); + } + return $result; + } + + /** + * 查询失败 抛出异常 + * @access public + * @param array $options 查询参数 + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + protected function throwNotFound($options = []) + { + if (!empty($this->model)) { + throw new ModelNotFoundException('model data Not Found:' . $this->model, $this->model, $options); + } else { + $table = is_array($options['table']) ? key($options['table']) : $options['table']; + throw new DataNotFoundException('table data not Found:' . $table, $table, $options); + } + } + + /** + * 查找多条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function selectOrFail($data = null) + { + return $this->failException(true)->select($data); + } + + /** + * 查找单条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function findOrFail($data = null) + { + return $this->failException(true)->find($data); + } + + /** + * 分批数据返回处理 + * @access public + * @param integer $count 每次处理的数据数量 + * @param callable $callback 处理回调方法 + * @param string $column 分批处理的字段名 + * @return boolean + */ + public function chunk($count, $callback, $column = null) + { + $options = $this->getOptions(); + if (isset($options['table'])) { + $table = is_array($options['table']) ? key($options['table']) : $options['table']; + } else { + $table = ''; + } + $column = $column ?: $this->getPk($table); + $bind = $this->bind; + $resultSet = $this->limit($count)->order($column, 'asc')->select(); + if (strpos($column, '.')) { + list($alias, $key) = explode('.', $column); + } else { + $key = $column; + } + if ($resultSet instanceof Collection) { + $resultSet = $resultSet->all(); + } + + while (!empty($resultSet)) { + if (false === call_user_func($callback, $resultSet)) { + return false; + } + $end = end($resultSet); + $lastId = is_array($end) ? $end[$key] : $end->$key; + $resultSet = $this->options($options) + ->limit($count) + ->bind($bind) + ->where($column, '>', $lastId) + ->order($column, 'asc') + ->select(); + if ($resultSet instanceof Collection) { + $resultSet = $resultSet->all(); + } + } + return true; + } + + /** + * 获取绑定的参数 并清空 + * @access public + * @return array + */ + public function getBind() + { + $bind = $this->bind; + $this->bind = []; + return $bind; + } + + /** + * 创建子查询SQL + * @access public + * @param bool $sub + * @return string + * @throws DbException + */ + public function buildSql($sub = true) + { + return $sub ? '( ' . $this->select(false) . ' )' : $this->select(false); + } + + /** + * 删除记录 + * @access public + * @param mixed $data 表达式 true 表示强制删除 + * @return int + * @throws Exception + * @throws PDOException + */ + public function delete($data = null) + { + // 分析查询表达式 + $options = $this->parseExpress(); + $pk = $this->getPk($options); + if (isset($options['cache']) && is_string($options['cache']['key'])) { + $key = $options['cache']['key']; + } + + if (!is_null($data) && true !== $data) { + if (!isset($key) && !is_array($data)) { + // 缓存标识 + $key = 'think:' . $options['table'] . '|' . $data; + } + // AR模式分析主键条件 + $this->parsePkWhere($data, $options); + } elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'][$pk])) { + $key = $this->getCacheKey($options['where']['AND'][$pk], $options, $this->bind); + } + + if (true !== $data && empty($options['where'])) { + // 如果条件为空 不进行删除操作 除非设置 1=1 + throw new Exception('delete without condition'); + } + // 生成删除SQL语句 + $sql = $this->builder->delete($options); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } + + // 检测缓存 + if (isset($key) && Cache::get($key)) { + // 删除缓存 + Cache::rm($key); + } elseif (!empty($options['cache']['tag'])) { + Cache::clear($options['cache']['tag']); + } + // 执行操作 + $result = $this->execute($sql, $bind); + if ($result) { + if (!is_array($data) && is_string($pk) && isset($key) && strpos($key, '|')) { + list($a, $val) = explode('|', $key); + $item[$pk] = $val; + $data = $item; + } + $options['data'] = $data; + $this->trigger('after_delete', $options); + } + return $result; + } + + /** + * 分析表达式(可用于查询或者写入操作) + * @access protected + * @return array + */ + protected function parseExpress() + { + $options = $this->options; + + // 获取数据表 + if (empty($options['table'])) { + $options['table'] = $this->getTable(); + } + + if (!isset($options['where'])) { + $options['where'] = []; + } elseif (isset($options['view'])) { + // 视图查询条件处理 + foreach (['AND', 'OR'] as $logic) { + if (isset($options['where'][$logic])) { + foreach ($options['where'][$logic] as $key => $val) { + if (array_key_exists($key, $options['map'])) { + $options['where'][$logic][$options['map'][$key]] = $val; + unset($options['where'][$logic][$key]); + } + } + } + } + + if (isset($options['order'])) { + // 视图查询排序处理 + if (is_string($options['order'])) { + $options['order'] = explode(',', $options['order']); + } + foreach ($options['order'] as $key => $val) { + if (is_numeric($key)) { + if (strpos($val, ' ')) { + list($field, $sort) = explode(' ', $val); + if (array_key_exists($field, $options['map'])) { + $options['order'][$options['map'][$field]] = $sort; + unset($options['order'][$key]); + } + } elseif (array_key_exists($val, $options['map'])) { + $options['order'][$options['map'][$val]] = 'asc'; + unset($options['order'][$key]); + } + } elseif (array_key_exists($key, $options['map'])) { + $options['order'][$options['map'][$key]] = $val; + unset($options['order'][$key]); + } + } + } + } + + if (!isset($options['field'])) { + $options['field'] = '*'; + } + + if (!isset($options['data'])) { + $options['data'] = []; + } + + if (!isset($options['strict'])) { + $options['strict'] = $this->getConfig('fields_strict'); + } + + foreach (['master', 'lock', 'fetch_pdo', 'fetch_sql', 'distinct'] as $name) { + if (!isset($options[$name])) { + $options[$name] = false; + } + } + + foreach (['join', 'union', 'group', 'having', 'limit', 'order', 'force', 'comment'] as $name) { + if (!isset($options[$name])) { + $options[$name] = ''; + } + } + + if (isset($options['page'])) { + // 根据页数计算limit + list($page, $listRows) = $options['page']; + $page = $page > 0 ? $page : 1; + $listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20); + $offset = $listRows * ($page - 1); + $options['limit'] = $offset . ',' . $listRows; + } + + $this->options = []; + return $options; + } + + /** + * 注册回调方法 + * @access public + * @param string $event 事件名 + * @param callable $callback 回调方法 + * @return void + */ + public static function event($event, $callback) + { + self::$event[$event] = $callback; + } + + /** + * 触发事件 + * @access protected + * @param string $event 事件名 + * @param mixed $params 额外参数 + * @return bool + */ + protected function trigger($event, $params = []) + { + $result = false; + if (isset(self::$event[$event])) { + $callback = self::$event[$event]; + $result = call_user_func_array($callback, [$params, $this]); + } + return $result; + } +} diff --git a/thinkphp/library/think/db/builder/Mysql.php b/thinkphp/library/think/db/builder/Mysql.php new file mode 100644 index 0000000..5bc9d03 --- /dev/null +++ b/thinkphp/library/think/db/builder/Mysql.php @@ -0,0 +1,68 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; + +/** + * mysql数据库驱动 + */ +class Mysql extends Builder +{ + protected $updateSql = 'UPDATE %TABLE% %JOIN% SET %SET% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; + + /** + * 字段和表名处理 + * @access protected + * @param string $key + * @param array $options + * @return string + */ + protected function parseKey($key, $options = []) + { + $key = trim($key); + if (strpos($key, '$.') && false === strpos($key, '(')) { + // JSON字段支持 + list($field, $name) = explode('$.', $key); + $key = 'json_extract(' . $field . ', \'$.' . $name . '\')'; + } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) { + list($table, $key) = explode('.', $key, 2); + if ('__TABLE__' == $table) { + $table = $this->query->getTable(); + } + if (isset($options['alias'][$table])) { + $table = $options['alias'][$table]; + } + } + if (!preg_match('/[,\'\"\*\(\)`.\s]/', $key)) { + $key = '`' . $key . '`'; + } + if (isset($table)) { + if (strpos($table, '.')) { + $table = str_replace('.', '`.`', $table); + } + $key = '`' . $table . '`.' . $key; + } + return $key; + } + + /** + * 随机排序 + * @access protected + * @return string + */ + protected function parseRand() + { + return 'rand()'; + } + +} diff --git a/thinkphp/library/think/db/builder/Pgsql.php b/thinkphp/library/think/db/builder/Pgsql.php new file mode 100644 index 0000000..b690401 --- /dev/null +++ b/thinkphp/library/think/db/builder/Pgsql.php @@ -0,0 +1,83 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; + +/** + * Pgsql数据库驱动 + */ +class Pgsql extends Builder +{ + protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%'; + protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%'; + + /** + * limit分析 + * @access protected + * @param mixed $limit + * @return string + */ + public function parseLimit($limit) + { + $limitStr = ''; + if (!empty($limit)) { + $limit = explode(',', $limit); + if (count($limit) > 1) { + $limitStr .= ' LIMIT ' . $limit[1] . ' OFFSET ' . $limit[0] . ' '; + } else { + $limitStr .= ' LIMIT ' . $limit[0] . ' '; + } + } + return $limitStr; + } + + /** + * 字段和表名处理 + * @access protected + * @param string $key + * @param array $options + * @return string + */ + protected function parseKey($key, $options = []) + { + $key = trim($key); + if (strpos($key, '$.') && false === strpos($key, '(')) { + // JSON字段支持 + list($field, $name) = explode('$.', $key); + $key = $field . '->>\'' . $name . '\''; + } elseif (strpos($key, '.')) { + list($table, $key) = explode('.', $key, 2); + if ('__TABLE__' == $table) { + $table = $this->query->getTable(); + } + if (isset($options['alias'][$table])) { + $table = $options['alias'][$table]; + } + } + if (isset($table)) { + $key = $table . '.' . $key; + } + return $key; + } + + /** + * 随机排序 + * @access protected + * @return string + */ + protected function parseRand() + { + return 'RANDOM()'; + } + +} diff --git a/thinkphp/library/think/db/builder/Sqlite.php b/thinkphp/library/think/db/builder/Sqlite.php new file mode 100644 index 0000000..55d3abc --- /dev/null +++ b/thinkphp/library/think/db/builder/Sqlite.php @@ -0,0 +1,75 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; + +/** + * Sqlite数据库驱动 + */ +class Sqlite extends Builder +{ + + /** + * limit + * @access public + * @return string + */ + public function parseLimit($limit) + { + $limitStr = ''; + if (!empty($limit)) { + $limit = explode(',', $limit); + if (count($limit) > 1) { + $limitStr .= ' LIMIT ' . $limit[1] . ' OFFSET ' . $limit[0] . ' '; + } else { + $limitStr .= ' LIMIT ' . $limit[0] . ' '; + } + } + return $limitStr; + } + + /** + * 随机排序 + * @access protected + * @return string + */ + protected function parseRand() + { + return 'RANDOM()'; + } + + /** + * 字段和表名处理 + * @access protected + * @param string $key + * @param array $options + * @return string + */ + protected function parseKey($key, $options = []) + { + $key = trim($key); + if (strpos($key, '.')) { + list($table, $key) = explode('.', $key, 2); + if ('__TABLE__' == $table) { + $table = $this->query->getTable(); + } + if (isset($options['alias'][$table])) { + $table = $options['alias'][$table]; + } + } + if (isset($table)) { + $key = $table . '.' . $key; + } + return $key; + } +} diff --git a/thinkphp/library/think/db/builder/Sqlsrv.php b/thinkphp/library/think/db/builder/Sqlsrv.php new file mode 100644 index 0000000..7537845 --- /dev/null +++ b/thinkphp/library/think/db/builder/Sqlsrv.php @@ -0,0 +1,121 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; + +/** + * Sqlsrv数据库驱动 + */ +class Sqlsrv extends Builder +{ + protected $selectSql = 'SELECT T1.* FROM (SELECT thinkphp.*, ROW_NUMBER() OVER (%ORDER%) AS ROW_NUMBER FROM (SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%) AS thinkphp) AS T1 %LIMIT%%COMMENT%'; + protected $selectInsertSql = 'SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%'; + protected $updateSql = 'UPDATE %TABLE% SET %SET% FROM %TABLE% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%'; + protected $deleteSql = 'DELETE FROM %TABLE% %USING% FROM %TABLE% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%'; + protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%'; + protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%'; + + /** + * order分析 + * @access protected + * @param mixed $order + * @param array $options + * @return string + */ + protected function parseOrder($order, $options = []) + { + if (is_array($order)) { + $array = []; + foreach ($order as $key => $val) { + if (is_numeric($key)) { + if (false === strpos($val, '(')) { + $array[] = $this->parseKey($val, $options); + } elseif ('[rand]' == $val) { + $array[] = $this->parseRand(); + } + } else { + $sort = in_array(strtolower(trim($val)), ['asc', 'desc']) ? ' ' . $val : ''; + $array[] = $this->parseKey($key, $options) . ' ' . $sort; + } + } + $order = implode(',', $array); + } + return !empty($order) ? ' ORDER BY ' . $order : ' ORDER BY rand()'; + } + + /** + * 随机排序 + * @access protected + * @return string + */ + protected function parseRand() + { + return 'rand()'; + } + + /** + * 字段和表名处理 + * @access protected + * @param string $key + * @param array $options + * @return string + */ + protected function parseKey($key, $options = []) + { + $key = trim($key); + if (strpos($key, '.') && !preg_match('/[,\'\"\(\)\[\s]/', $key)) { + list($table, $key) = explode('.', $key, 2); + if ('__TABLE__' == $table) { + $table = $this->query->getTable(); + } + if (isset($options['alias'][$table])) { + $table = $options['alias'][$table]; + } + } + if (!is_numeric($key) && !preg_match('/[,\'\"\*\(\)\[.\s]/', $key)) { + $key = '[' . $key . ']'; + } + if (isset($table)) { + $key = '[' . $table . '].' . $key; + } + return $key; + } + + /** + * limit + * @access protected + * @param mixed $limit + * @return string + */ + protected function parseLimit($limit) + { + if (empty($limit)) { + return ''; + } + + $limit = explode(',', $limit); + if (count($limit) > 1) { + $limitStr = '(T1.ROW_NUMBER BETWEEN ' . $limit[0] . ' + 1 AND ' . $limit[0] . ' + ' . $limit[1] . ')'; + } else { + $limitStr = '(T1.ROW_NUMBER BETWEEN 1 AND ' . $limit[0] . ")"; + } + return 'WHERE ' . $limitStr; + } + + public function selectInsert($fields, $table, $options) + { + $this->selectSql = $this->selectInsertSql; + return parent::selectInsert($fields, $table, $options); + } + +} diff --git a/thinkphp/library/think/db/connector/Mysql.php b/thinkphp/library/think/db/connector/Mysql.php new file mode 100644 index 0000000..536125f --- /dev/null +++ b/thinkphp/library/think/db/connector/Mysql.php @@ -0,0 +1,123 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; +use think\Log; + +/** + * mysql数据库驱动 + */ +class Mysql extends Connection +{ + + protected $builder = '\\think\\db\\builder\\Mysql'; + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'mysql:dbname=' . $config['database'] . ';host=' . $config['hostname']; + if (!empty($config['hostport'])) { + $dsn .= ';port=' . $config['hostport']; + } elseif (!empty($config['socket'])) { + $dsn .= ';unix_socket=' . $config['socket']; + } + if (!empty($config['charset'])) { + $dsn .= ';charset=' . $config['charset']; + } + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + list($tableName) = explode(' ', $tableName); + if (false === strpos($tableName, '`')) { + if (strpos($tableName, '.')) { + $tableName = str_replace('.', '`.`', $tableName); + } + $tableName = '`' . $tableName . '`'; + } + $sql = 'SHOW COLUMNS FROM ' . $tableName; + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['field']] = [ + 'name' => $val['field'], + 'type' => $val['type'], + 'notnull' => (bool) ('' === $val['null']), // not null is empty, null is yes + 'default' => $val['default'], + 'primary' => (strtolower($val['key']) == 'pri'), + 'autoinc' => (strtolower($val['extra']) == 'auto_increment'), + ]; + } + } + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $sql = !empty($dbName) ? 'SHOW TABLES FROM ' . $dbName : 'SHOW TABLES '; + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + $pdo = $this->linkID->query("EXPLAIN " . $sql); + $result = $pdo->fetch(PDO::FETCH_ASSOC); + $result = array_change_key_case($result); + if (isset($result['extra'])) { + if (strpos($result['extra'], 'filesort') || strpos($result['extra'], 'temporary')) { + Log::record('SQL:' . $this->queryStr . '[' . $result['extra'] . ']', 'warn'); + } + } + return $result; + } + + protected function supportSavepoint() + { + return true; + } + +} diff --git a/thinkphp/library/think/db/connector/Pgsql.php b/thinkphp/library/think/db/connector/Pgsql.php new file mode 100644 index 0000000..761fbac --- /dev/null +++ b/thinkphp/library/think/db/connector/Pgsql.php @@ -0,0 +1,103 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; + +/** + * Pgsql数据库驱动 + */ +class Pgsql extends Connection +{ + protected $builder = '\\think\\db\\builder\\Pgsql'; + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'pgsql:dbname=' . $config['database'] . ';host=' . $config['hostname']; + if (!empty($config['hostport'])) { + $dsn .= ';port=' . $config['hostport']; + } + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + + list($tableName) = explode(' ', $tableName); + $sql = 'select fields_name as "field",fields_type as "type",fields_not_null as "null",fields_key_name as "key",fields_default as "default",fields_default as "extra" from table_msg(\'' . $tableName . '\');'; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['field']] = [ + 'name' => $val['field'], + 'type' => $val['type'], + 'notnull' => (bool) ('' !== $val['null']), + 'default' => $val['default'], + 'primary' => !empty($val['key']), + 'autoinc' => (0 === strpos($val['extra'], 'nextval(')), + ]; + } + } + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $sql = "select tablename as Tables_in_test from pg_tables where schemaname ='public'"; + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + return []; + } + + protected function supportSavepoint() + { + return true; + } +} diff --git a/thinkphp/library/think/db/connector/Sqlite.php b/thinkphp/library/think/db/connector/Sqlite.php new file mode 100644 index 0000000..fd7f02b --- /dev/null +++ b/thinkphp/library/think/db/connector/Sqlite.php @@ -0,0 +1,104 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; + +/** + * Sqlite数据库驱动 + */ +class Sqlite extends Connection +{ + + protected $builder = '\\think\\db\\builder\\Sqlite'; + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'sqlite:' . $config['database']; + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + list($tableName) = explode(' ', $tableName); + $sql = 'PRAGMA table_info( ' . $tableName . ' )'; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['name']] = [ + 'name' => $val['name'], + 'type' => $val['type'], + 'notnull' => 1 === $val['notnull'], + 'default' => $val['dflt_value'], + 'primary' => '1' == $val['pk'], + 'autoinc' => '1' == $val['pk'], + ]; + } + } + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + + $sql = "SELECT name FROM sqlite_master WHERE type='table' " + . "UNION ALL SELECT name FROM sqlite_temp_master " + . "WHERE type='table' ORDER BY name"; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + return []; + } + + protected function supportSavepoint() + { + return true; + } +} diff --git a/thinkphp/library/think/db/connector/Sqlsrv.php b/thinkphp/library/think/db/connector/Sqlsrv.php new file mode 100644 index 0000000..08ad45e --- /dev/null +++ b/thinkphp/library/think/db/connector/Sqlsrv.php @@ -0,0 +1,122 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; + +/** + * Sqlsrv数据库驱动 + */ +class Sqlsrv extends Connection +{ + // PDO连接参数 + protected $params = [ + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_STRINGIFY_FETCHES => false, + ]; + protected $builder = '\\think\\db\\builder\\Sqlsrv'; + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'sqlsrv:Database=' . $config['database'] . ';Server=' . $config['hostname']; + if (!empty($config['hostport'])) { + $dsn .= ',' . $config['hostport']; + } + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + list($tableName) = explode(' ', $tableName); + $sql = "SELECT column_name, data_type, column_default, is_nullable + FROM information_schema.tables AS t + JOIN information_schema.columns AS c + ON t.table_catalog = c.table_catalog + AND t.table_schema = c.table_schema + AND t.table_name = c.table_name + WHERE t.table_name = '$tableName'"; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['column_name']] = [ + 'name' => $val['column_name'], + 'type' => $val['data_type'], + 'notnull' => (bool) ('' === $val['is_nullable']), // not null is empty, null is yes + 'default' => $val['column_default'], + 'primary' => false, + 'autoinc' => false, + ]; + } + } + $sql = "SELECT column_name FROM information_schema.key_column_usage WHERE table_name='$tableName'"; + // 调试开始 + $this->debug(true); + $pdo = $this->linkID->query($sql); + // 调试结束 + $this->debug(false, $sql); + $result = $pdo->fetch(PDO::FETCH_ASSOC); + if ($result) { + $info[$result['column_name']]['primary'] = true; + } + return $this->fieldCase($info); + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $sql = "SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_TYPE = 'BASE TABLE' + "; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + return []; + } +} diff --git a/thinkphp/library/think/db/connector/pgsql.sql b/thinkphp/library/think/db/connector/pgsql.sql new file mode 100644 index 0000000..e1a09a3 --- /dev/null +++ b/thinkphp/library/think/db/connector/pgsql.sql @@ -0,0 +1,117 @@ +CREATE OR REPLACE FUNCTION pgsql_type(a_type varchar) RETURNS varchar AS +$BODY$ +DECLARE + v_type varchar; +BEGIN + IF a_type='int8' THEN + v_type:='bigint'; + ELSIF a_type='int4' THEN + v_type:='integer'; + ELSIF a_type='int2' THEN + v_type:='smallint'; + ELSIF a_type='bpchar' THEN + v_type:='char'; + ELSE + v_type:=a_type; + END IF; + RETURN v_type; +END; +$BODY$ +LANGUAGE PLPGSQL; + +CREATE TYPE "public"."tablestruct" AS ( + "fields_key_name" varchar(100), + "fields_name" VARCHAR(200), + "fields_type" VARCHAR(20), + "fields_length" BIGINT, + "fields_not_null" VARCHAR(10), + "fields_default" VARCHAR(500), + "fields_comment" VARCHAR(1000) +); + +CREATE OR REPLACE FUNCTION "public"."table_msg" (a_schema_name varchar, a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS +$body$ +DECLARE + v_ret tablestruct; + v_oid oid; + v_sql varchar; + v_rec RECORD; + v_key varchar; +BEGIN + SELECT + pg_class.oid INTO v_oid + FROM + pg_class + INNER JOIN pg_namespace ON (pg_class.relnamespace = pg_namespace.oid AND lower(pg_namespace.nspname) = a_schema_name) + WHERE + pg_class.relname=a_table_name; + IF NOT FOUND THEN + RETURN; + END IF; + + v_sql=' + SELECT + pg_attribute.attname AS fields_name, + pg_attribute.attnum AS fields_index, + pgsql_type(pg_type.typname::varchar) AS fields_type, + pg_attribute.atttypmod-4 as fields_length, + CASE WHEN pg_attribute.attnotnull THEN ''not null'' + ELSE '''' + END AS fields_not_null, + pg_attrdef.adsrc AS fields_default, + pg_description.description AS fields_comment + FROM + pg_attribute + INNER JOIN pg_class ON pg_attribute.attrelid = pg_class.oid + INNER JOIN pg_type ON pg_attribute.atttypid = pg_type.oid + LEFT OUTER JOIN pg_attrdef ON pg_attrdef.adrelid = pg_class.oid AND pg_attrdef.adnum = pg_attribute.attnum + LEFT OUTER JOIN pg_description ON pg_description.objoid = pg_class.oid AND pg_description.objsubid = pg_attribute.attnum + WHERE + pg_attribute.attnum > 0 + AND attisdropped <> ''t'' + AND pg_class.oid = ' || v_oid || ' + ORDER BY pg_attribute.attnum' ; + + FOR v_rec IN EXECUTE v_sql LOOP + v_ret.fields_name=v_rec.fields_name; + v_ret.fields_type=v_rec.fields_type; + IF v_rec.fields_length > 0 THEN + v_ret.fields_length:=v_rec.fields_length; + ELSE + v_ret.fields_length:=NULL; + END IF; + v_ret.fields_not_null=v_rec.fields_not_null; + v_ret.fields_default=v_rec.fields_default; + v_ret.fields_comment=v_rec.fields_comment; + SELECT constraint_name INTO v_key FROM information_schema.key_column_usage WHERE table_schema=a_schema_name AND table_name=a_table_name AND column_name=v_rec.fields_name; + IF FOUND THEN + v_ret.fields_key_name=v_key; + ELSE + v_ret.fields_key_name=''; + END IF; + RETURN NEXT v_ret; + END LOOP; + RETURN ; +END; +$body$ +LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER; + +COMMENT ON FUNCTION "public"."table_msg"(a_schema_name varchar, a_table_name varchar) +IS '获得表信息'; + +---重载一个函数 +CREATE OR REPLACE FUNCTION "public"."table_msg" (a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS +$body$ +DECLARE + v_ret tablestruct; +BEGIN + FOR v_ret IN SELECT * FROM table_msg('public',a_table_name) LOOP + RETURN NEXT v_ret; + END LOOP; + RETURN; +END; +$body$ +LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER; + +COMMENT ON FUNCTION "public"."table_msg"(a_table_name varchar) +IS '获得表信息'; \ No newline at end of file diff --git a/thinkphp/library/think/db/exception/BindParamException.php b/thinkphp/library/think/db/exception/BindParamException.php new file mode 100644 index 0000000..d0e2387 --- /dev/null +++ b/thinkphp/library/think/db/exception/BindParamException.php @@ -0,0 +1,35 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +/** + * PDO参数绑定异常 + */ +class BindParamException extends DbException +{ + + /** + * BindParamException constructor. + * @param string $message + * @param array $config + * @param string $sql + * @param array $bind + * @param int $code + */ + public function __construct($message, $config, $sql, $bind, $code = 10502) + { + $this->setData('Bind Param', $bind); + parent::__construct($message, $config, $sql, $code); + } +} diff --git a/thinkphp/library/think/db/exception/DataNotFoundException.php b/thinkphp/library/think/db/exception/DataNotFoundException.php new file mode 100644 index 0000000..e399b06 --- /dev/null +++ b/thinkphp/library/think/db/exception/DataNotFoundException.php @@ -0,0 +1,43 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +class DataNotFoundException extends DbException +{ + protected $table; + + /** + * DbException constructor. + * @param string $message + * @param string $table + * @param array $config + */ + public function __construct($message, $table = '', array $config = []) + { + $this->message = $message; + $this->table = $table; + + $this->setData('Database Config', $config); + } + + /** + * 获取数据表名 + * @access public + * @return string + */ + public function getTable() + { + return $this->table; + } +} diff --git a/thinkphp/library/think/db/exception/ModelNotFoundException.php b/thinkphp/library/think/db/exception/ModelNotFoundException.php new file mode 100644 index 0000000..2180ab0 --- /dev/null +++ b/thinkphp/library/think/db/exception/ModelNotFoundException.php @@ -0,0 +1,43 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +class ModelNotFoundException extends DbException +{ + protected $model; + + /** + * 构造方法 + * @param string $message + * @param string $model + */ + public function __construct($message, $model = '', array $config = []) + { + $this->message = $message; + $this->model = $model; + + $this->setData('Database Config', $config); + } + + /** + * 获取模型类名 + * @access public + * @return string + */ + public function getModel() + { + return $this->model; + } + +} diff --git a/thinkphp/library/think/debug/Console.php b/thinkphp/library/think/debug/Console.php new file mode 100644 index 0000000..8a232c8 --- /dev/null +++ b/thinkphp/library/think/debug/Console.php @@ -0,0 +1,160 @@ + +// +---------------------------------------------------------------------- + +namespace think\debug; + +use think\Cache; +use think\Config; +use think\Db; +use think\Debug; +use think\Request; +use think\Response; + +/** + * 浏览器调试输出 + */ +class Console +{ + protected $config = [ + 'trace_tabs' => ['base' => '基本', 'file' => '文件', 'info' => '流程', 'notice|error' => '错误', 'sql' => 'SQL', 'debug|log' => '调试'], + ]; + + // 实例化并传入参数 + public function __construct($config = []) + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * 调试输出接口 + * @access public + * @param Response $response Response对象 + * @param array $log 日志信息 + * @return bool + */ + public function output(Response $response, array $log = []) + { + $request = Request::instance(); + $contentType = $response->getHeader('Content-Type'); + $accept = $request->header('accept'); + if (strpos($accept, 'application/json') === 0 || $request->isAjax()) { + return false; + } elseif (!empty($contentType) && strpos($contentType, 'html') === false) { + return false; + } + // 获取基本信息 + $runtime = number_format(microtime(true) - THINK_START_TIME, 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $mem = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + + if (isset($_SERVER['HTTP_HOST'])) { + $uri = $_SERVER['SERVER_PROTOCOL'] . ' ' . $_SERVER['REQUEST_METHOD'] . ' : ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + } else { + $uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + + // 页面Trace信息 + $base = [ + '请求信息' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']) . ' ' . $uri, + '运行时间' => number_format($runtime, 6) . 's [ 吞吐率:' . $reqs . 'req/s ] 内存消耗:' . $mem . 'kb 文件加载:' . count(get_included_files()), + '查询信息' => Db::$queryTimes . ' queries ' . Db::$executeTimes . ' writes ', + '缓存信息' => Cache::$readTimes . ' reads,' . Cache::$writeTimes . ' writes', + '配置加载' => count(Config::get()), + ]; + + if (session_id()) { + $base['会话信息'] = 'SESSION_ID=' . session_id(); + } + + $info = Debug::getFile(true); + + // 页面Trace信息 + $trace = []; + foreach ($this->config['trace_tabs'] as $name => $title) { + $name = strtolower($name); + switch ($name) { + case 'base': // 基本信息 + $trace[$title] = $base; + break; + case 'file': // 文件信息 + $trace[$title] = $info; + break; + default: // 调试信息 + if (strpos($name, '|')) { + // 多组信息 + $names = explode('|', $name); + $result = []; + foreach ($names as $name) { + $result = array_merge($result, isset($log[$name]) ? $log[$name] : []); + } + $trace[$title] = $result; + } else { + $trace[$title] = isset($log[$name]) ? $log[$name] : ''; + } + } + } + + //输出到控制台 + $lines = ''; + foreach ($trace as $type => $msg) { + $lines .= $this->console($type, $msg); + } + $js = << +{$lines} + +JS; + return $js; + } + + protected function console($type, $msg) + { + $type = strtolower($type); + $trace_tabs = array_values($this->config['trace_tabs']); + $line[] = ($type == $trace_tabs[0] || '调试' == $type || '错误' == $type) + ? "console.group('{$type}');" + : "console.groupCollapsed('{$type}');"; + + foreach ((array) $msg as $key => $m) { + switch ($type) { + case '调试': + $var_type = gettype($m); + if (in_array($var_type, ['array', 'string'])) { + $line[] = "console.log(" . json_encode($m) . ");"; + } else { + $line[] = "console.log(" . json_encode(var_export($m, 1)) . ");"; + } + break; + case '错误': + $msg = str_replace("\n", '\n', $m); + $style = 'color:#F4006B;font-size:14px;'; + $line[] = "console.error(\"%c{$msg}\", \"{$style}\");"; + break; + case 'sql': + $msg = str_replace("\n", '\n', $m); + $style = "color:#009bb4;"; + $line[] = "console.log(\"%c{$msg}\", \"{$style}\");"; + break; + default: + $m = is_string($key) ? $key . ' ' . $m : $key + 1 . ' ' . $m; + $msg = json_encode($m); + $line[] = "console.log({$msg});"; + break; + } + } + $line[] = "console.groupEnd();"; + return implode(PHP_EOL, $line); + } + +} diff --git a/thinkphp/library/think/debug/Html.php b/thinkphp/library/think/debug/Html.php new file mode 100644 index 0000000..f8651aa --- /dev/null +++ b/thinkphp/library/think/debug/Html.php @@ -0,0 +1,111 @@ + +// +---------------------------------------------------------------------- + +namespace think\debug; + +use think\Cache; +use think\Config; +use think\Db; +use think\Debug; +use think\Request; +use think\Response; + +/** + * 页面Trace调试 + */ +class Html +{ + protected $config = [ + 'trace_file' => '', + 'trace_tabs' => ['base' => '基本', 'file' => '文件', 'info' => '流程', 'notice|error' => '错误', 'sql' => 'SQL', 'debug|log' => '调试'], + ]; + + // 实例化并传入参数 + public function __construct(array $config = []) + { + $this->config['trace_file'] = THINK_PATH . 'tpl/page_trace.tpl'; + $this->config = array_merge($this->config, $config); + } + + /** + * 调试输出接口 + * @access public + * @param Response $response Response对象 + * @param array $log 日志信息 + * @return bool + */ + public function output(Response $response, array $log = []) + { + $request = Request::instance(); + $contentType = $response->getHeader('Content-Type'); + $accept = $request->header('accept'); + if (strpos($accept, 'application/json') === 0 || $request->isAjax()) { + return false; + } elseif (!empty($contentType) && strpos($contentType, 'html') === false) { + return false; + } + // 获取基本信息 + $runtime = number_format(microtime(true) - THINK_START_TIME, 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $mem = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + + // 页面Trace信息 + if (isset($_SERVER['HTTP_HOST'])) { + $uri = $_SERVER['SERVER_PROTOCOL'] . ' ' . $_SERVER['REQUEST_METHOD'] . ' : ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + } else { + $uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + $base = [ + '请求信息' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']) . ' ' . $uri, + '运行时间' => number_format($runtime, 6) . 's [ 吞吐率:' . $reqs . 'req/s ] 内存消耗:' . $mem . 'kb 文件加载:' . count(get_included_files()), + '查询信息' => Db::$queryTimes . ' queries ' . Db::$executeTimes . ' writes ', + '缓存信息' => Cache::$readTimes . ' reads,' . Cache::$writeTimes . ' writes', + '配置加载' => count(Config::get()), + ]; + + if (session_id()) { + $base['会话信息'] = 'SESSION_ID=' . session_id(); + } + + $info = Debug::getFile(true); + + // 页面Trace信息 + $trace = []; + foreach ($this->config['trace_tabs'] as $name => $title) { + $name = strtolower($name); + switch ($name) { + case 'base': // 基本信息 + $trace[$title] = $base; + break; + case 'file': // 文件信息 + $trace[$title] = $info; + break; + default: // 调试信息 + if (strpos($name, '|')) { + // 多组信息 + $names = explode('|', $name); + $result = []; + foreach ($names as $name) { + $result = array_merge($result, isset($log[$name]) ? $log[$name] : []); + } + $trace[$title] = $result; + } else { + $trace[$title] = isset($log[$name]) ? $log[$name] : ''; + } + } + } + // 调用Trace页面模板 + ob_start(); + include $this->config['trace_file']; + return ob_get_clean(); + } + +} diff --git a/thinkphp/library/think/exception/ClassNotFoundException.php b/thinkphp/library/think/exception/ClassNotFoundException.php new file mode 100644 index 0000000..eb22e73 --- /dev/null +++ b/thinkphp/library/think/exception/ClassNotFoundException.php @@ -0,0 +1,32 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ClassNotFoundException extends \RuntimeException +{ + protected $class; + public function __construct($message, $class = '') + { + $this->message = $message; + $this->class = $class; + } + + /** + * 获取类名 + * @access public + * @return string + */ + public function getClass() + { + return $this->class; + } +} diff --git a/thinkphp/library/think/exception/DbException.php b/thinkphp/library/think/exception/DbException.php new file mode 100644 index 0000000..532af5e --- /dev/null +++ b/thinkphp/library/think/exception/DbException.php @@ -0,0 +1,43 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\Exception; + +/** + * Database相关异常处理类 + */ +class DbException extends Exception +{ + /** + * DbException constructor. + * @param string $message + * @param array $config + * @param string $sql + * @param int $code + */ + public function __construct($message, array $config, $sql, $code = 10500) + { + $this->message = $message; + $this->code = $code; + + $this->setData('Database Status', [ + 'Error Code' => $code, + 'Error Message' => $message, + 'Error SQL' => $sql, + ]); + + unset($config['username'], $config['password']); + $this->setData('Database Config', $config); + } + +} diff --git a/thinkphp/library/think/exception/ErrorException.php b/thinkphp/library/think/exception/ErrorException.php new file mode 100644 index 0000000..e3f1837 --- /dev/null +++ b/thinkphp/library/think/exception/ErrorException.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\Exception; + +/** + * ThinkPHP错误异常 + * 主要用于封装 set_error_handler 和 register_shutdown_function 得到的错误 + * 除开从 think\Exception 继承的功能 + * 其他和PHP系统\ErrorException功能基本一样 + */ +class ErrorException extends Exception +{ + /** + * 用于保存错误级别 + * @var integer + */ + protected $severity; + + /** + * 错误异常构造函数 + * @param integer $severity 错误级别 + * @param string $message 错误详细信息 + * @param string $file 出错文件路径 + * @param integer $line 出错行号 + * @param array $context 错误上下文,会包含错误触发处作用域内所有变量的数组 + */ + public function __construct($severity, $message, $file, $line, array $context = []) + { + $this->severity = $severity; + $this->message = $message; + $this->file = $file; + $this->line = $line; + $this->code = 0; + + empty($context) || $this->setData('Error Context', $context); + } + + /** + * 获取错误级别 + * @return integer 错误级别 + */ + final public function getSeverity() + { + return $this->severity; + } +} diff --git a/thinkphp/library/think/exception/Handle.php b/thinkphp/library/think/exception/Handle.php new file mode 100644 index 0000000..d975690 --- /dev/null +++ b/thinkphp/library/think/exception/Handle.php @@ -0,0 +1,270 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use Exception; +use think\App; +use think\Config; +use think\console\Output; +use think\Lang; +use think\Log; +use think\Response; + +class Handle +{ + + protected $ignoreReport = [ + '\\think\\exception\\HttpException', + ]; + + /** + * Report or log an exception. + * + * @param \Exception $exception + * @return void + */ + public function report(Exception $exception) + { + if (!$this->isIgnoreReport($exception)) { + // 收集异常数据 + if (App::$debug) { + $data = [ + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'message' => $this->getMessage($exception), + 'code' => $this->getCode($exception), + ]; + $log = "[{$data['code']}]{$data['message']}[{$data['file']}:{$data['line']}]"; + } else { + $data = [ + 'code' => $this->getCode($exception), + 'message' => $this->getMessage($exception), + ]; + $log = "[{$data['code']}]{$data['message']}"; + } + + if (Config::get('record_trace')) { + $log .= "\r\n" . $exception->getTraceAsString(); + } + + Log::record($log, 'error'); + } + } + + protected function isIgnoreReport(Exception $exception) + { + foreach ($this->ignoreReport as $class) { + if ($exception instanceof $class) { + return true; + } + } + return false; + } + + /** + * Render an exception into an HTTP response. + * + * @param \Exception $e + * @return Response + */ + public function render(Exception $e) + { + if ($e instanceof HttpException) { + return $this->renderHttpException($e); + } else { + return $this->convertExceptionToResponse($e); + } + } + + /** + * @param Output $output + * @param Exception $e + */ + public function renderForConsole(Output $output, Exception $e) + { + if (App::$debug) { + $output->setVerbosity(Output::VERBOSITY_DEBUG); + } + $output->renderException($e); + } + + /** + * @param HttpException $e + * @return Response + */ + protected function renderHttpException(HttpException $e) + { + $status = $e->getStatusCode(); + $template = Config::get('http_exception_template'); + if (!App::$debug && !empty($template[$status])) { + return Response::create($template[$status], 'view', $status)->assign(['e' => $e]); + } else { + return $this->convertExceptionToResponse($e); + } + } + + /** + * @param Exception $exception + * @return Response + */ + protected function convertExceptionToResponse(Exception $exception) + { + // 收集异常数据 + if (App::$debug) { + // 调试模式,获取详细的错误信息 + $data = [ + 'name' => get_class($exception), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'message' => $this->getMessage($exception), + 'trace' => $exception->getTrace(), + 'code' => $this->getCode($exception), + 'source' => $this->getSourceCode($exception), + 'datas' => $this->getExtendData($exception), + 'tables' => [ + 'GET Data' => $_GET, + 'POST Data' => $_POST, + 'Files' => $_FILES, + 'Cookies' => $_COOKIE, + 'Session' => isset($_SESSION) ? $_SESSION : [], + 'Server/Request Data' => $_SERVER, + 'Environment Variables' => $_ENV, + 'ThinkPHP Constants' => $this->getConst(), + ], + ]; + } else { + // 部署模式仅显示 Code 和 Message + $data = [ + 'code' => $this->getCode($exception), + 'message' => $this->getMessage($exception), + ]; + + if (!Config::get('show_error_msg')) { + // 不显示详细错误信息 + $data['message'] = Config::get('error_message'); + } + } + + //保留一层 + while (ob_get_level() > 1) { + ob_end_clean(); + } + + $data['echo'] = ob_get_clean(); + + ob_start(); + extract($data); + include Config::get('exception_tmpl'); + // 获取并清空缓存 + $content = ob_get_clean(); + $response = new Response($content, 'html'); + + if ($exception instanceof HttpException) { + $statusCode = $exception->getStatusCode(); + $response->header($exception->getHeaders()); + } + + if (!isset($statusCode)) { + $statusCode = 500; + } + $response->code($statusCode); + return $response; + } + + /** + * 获取错误编码 + * ErrorException则使用错误级别作为错误编码 + * @param \Exception $exception + * @return integer 错误编码 + */ + protected function getCode(Exception $exception) + { + $code = $exception->getCode(); + if (!$code && $exception instanceof ErrorException) { + $code = $exception->getSeverity(); + } + return $code; + } + + /** + * 获取错误信息 + * ErrorException则使用错误级别作为错误编码 + * @param \Exception $exception + * @return string 错误信息 + */ + protected function getMessage(Exception $exception) + { + $message = $exception->getMessage(); + if (IS_CLI) { + return $message; + } + + if (strpos($message, ':')) { + $name = strstr($message, ':', true); + $message = Lang::has($name) ? Lang::get($name) . strstr($message, ':') : $message; + } elseif (strpos($message, ',')) { + $name = strstr($message, ',', true); + $message = Lang::has($name) ? Lang::get($name) . ':' . substr(strstr($message, ','), 1) : $message; + } elseif (Lang::has($message)) { + $message = Lang::get($message); + } + return $message; + } + + /** + * 获取出错文件内容 + * 获取错误的前9行和后9行 + * @param \Exception $exception + * @return array 错误文件内容 + */ + protected function getSourceCode(Exception $exception) + { + // 读取前9行和后9行 + $line = $exception->getLine(); + $first = ($line - 9 > 0) ? $line - 9 : 1; + + try { + $contents = file($exception->getFile()); + $source = [ + 'first' => $first, + 'source' => array_slice($contents, $first - 1, 19), + ]; + } catch (Exception $e) { + $source = []; + } + return $source; + } + + /** + * 获取异常扩展信息 + * 用于非调试模式html返回类型显示 + * @param \Exception $exception + * @return array 异常类定义的扩展数据 + */ + protected function getExtendData(Exception $exception) + { + $data = []; + if ($exception instanceof \think\Exception) { + $data = $exception->getData(); + } + return $data; + } + + /** + * 获取常量列表 + * @return array 常量列表 + */ + private static function getConst() + { + return get_defined_constants(true)['user']; + } +} diff --git a/thinkphp/library/think/exception/HttpException.php b/thinkphp/library/think/exception/HttpException.php new file mode 100644 index 0000000..01a27fc --- /dev/null +++ b/thinkphp/library/think/exception/HttpException.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class HttpException extends \RuntimeException +{ + private $statusCode; + private $headers; + + public function __construct($statusCode, $message = null, \Exception $previous = null, array $headers = [], $code = 0) + { + $this->statusCode = $statusCode; + $this->headers = $headers; + + parent::__construct($message, $code, $previous); + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function getHeaders() + { + return $this->headers; + } +} diff --git a/thinkphp/library/think/exception/HttpResponseException.php b/thinkphp/library/think/exception/HttpResponseException.php new file mode 100644 index 0000000..5297286 --- /dev/null +++ b/thinkphp/library/think/exception/HttpResponseException.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\Response; + +class HttpResponseException extends \RuntimeException +{ + /** + * @var Response + */ + protected $response; + + public function __construct(Response $response) + { + $this->response = $response; + } + + public function getResponse() + { + return $this->response; + } + +} diff --git a/thinkphp/library/think/exception/PDOException.php b/thinkphp/library/think/exception/PDOException.php new file mode 100644 index 0000000..ebd53df --- /dev/null +++ b/thinkphp/library/think/exception/PDOException.php @@ -0,0 +1,39 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +/** + * PDO异常处理类 + * 重新封装了系统的\PDOException类 + */ +class PDOException extends DbException +{ + /** + * PDOException constructor. + * @param \PDOException $exception + * @param array $config + * @param string $sql + * @param int $code + */ + public function __construct(\PDOException $exception, array $config, $sql, $code = 10501) + { + $error = $exception->errorInfo; + + $this->setData('PDO Error Info', [ + 'SQLSTATE' => $error[0], + 'Driver Error Code' => isset($error[1]) ? $error[1] : 0, + 'Driver Error Message' => isset($error[2]) ? $error[2] : '', + ]); + + parent::__construct($exception->getMessage(), $config, $sql, $code); + } +} diff --git a/thinkphp/library/think/exception/RouteNotFoundException.php b/thinkphp/library/think/exception/RouteNotFoundException.php new file mode 100644 index 0000000..d22e3a6 --- /dev/null +++ b/thinkphp/library/think/exception/RouteNotFoundException.php @@ -0,0 +1,22 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class RouteNotFoundException extends HttpException +{ + + public function __construct() + { + parent::__construct(404, 'Route Not Found'); + } + +} diff --git a/thinkphp/library/think/exception/TemplateNotFoundException.php b/thinkphp/library/think/exception/TemplateNotFoundException.php new file mode 100644 index 0000000..4202069 --- /dev/null +++ b/thinkphp/library/think/exception/TemplateNotFoundException.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class TemplateNotFoundException extends \RuntimeException +{ + protected $template; + + public function __construct($message, $template = '') + { + $this->message = $message; + $this->template = $template; + } + + /** + * 获取模板文件 + * @access public + * @return string + */ + public function getTemplate() + { + return $this->template; + } +} diff --git a/thinkphp/library/think/exception/ThrowableError.php b/thinkphp/library/think/exception/ThrowableError.php new file mode 100644 index 0000000..87b6b9d --- /dev/null +++ b/thinkphp/library/think/exception/ThrowableError.php @@ -0,0 +1,47 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ThrowableError extends \ErrorException +{ + public function __construct(\Throwable $e) + { + + if ($e instanceof \ParseError) { + $message = 'Parse error: ' . $e->getMessage(); + $severity = E_PARSE; + } elseif ($e instanceof \TypeError) { + $message = 'Type error: ' . $e->getMessage(); + $severity = E_RECOVERABLE_ERROR; + } else { + $message = 'Fatal error: ' . $e->getMessage(); + $severity = E_ERROR; + } + + parent::__construct( + $message, + $e->getCode(), + $severity, + $e->getFile(), + $e->getLine() + ); + + $this->setTrace($e->getTrace()); + } + + protected function setTrace($trace) + { + $traceReflector = new \ReflectionProperty('Exception', 'trace'); + $traceReflector->setAccessible(true); + $traceReflector->setValue($this, $trace); + } +} diff --git a/thinkphp/library/think/exception/ValidateException.php b/thinkphp/library/think/exception/ValidateException.php new file mode 100644 index 0000000..b368416 --- /dev/null +++ b/thinkphp/library/think/exception/ValidateException.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ValidateException extends \RuntimeException +{ + protected $error; + + public function __construct($error) + { + $this->error = $error; + $this->message = is_array($error) ? implode("\n\r", $error) : $error; + } + + /** + * 获取验证错误信息 + * @access public + * @return array|string + */ + public function getError() + { + return $this->error; + } +} diff --git a/thinkphp/library/think/log/driver/File.php b/thinkphp/library/think/log/driver/File.php new file mode 100644 index 0000000..d82a524 --- /dev/null +++ b/thinkphp/library/think/log/driver/File.php @@ -0,0 +1,119 @@ + +// +---------------------------------------------------------------------- + +namespace think\log\driver; + +use think\App; + +/** + * 本地化调试输出到文件 + */ +class File +{ + protected $config = [ + 'time_format' => ' c ', + 'file_size' => 2097152, + 'path' => LOG_PATH, + 'apart_level' => [], + ]; + + protected $writed = []; + + // 实例化并传入参数 + public function __construct($config = []) + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * 日志写入接口 + * @access public + * @param array $log 日志信息 + * @return bool + */ + public function save(array $log = []) + { + $cli = IS_CLI ? '_cli' : ''; + $destination = $this->config['path'] . date('Ym') . DS . date('d') . $cli . '.log'; + + $path = dirname($destination); + !is_dir($path) && mkdir($path, 0755, true); + + $info = ''; + foreach ($log as $type => $val) { + $level = ''; + foreach ($val as $msg) { + if (!is_string($msg)) { + $msg = var_export($msg, true); + } + $level .= '[ ' . $type . ' ] ' . $msg . "\r\n"; + } + if (in_array($type, $this->config['apart_level'])) { + // 独立记录的日志级别 + $filename = $path . DS . date('d') . '_' . $type . $cli . '.log'; + $this->write($level, $filename, true); + } else { + $info .= $level; + } + } + if ($info) { + return $this->write($info, $destination); + } + return true; + } + + protected function write($message, $destination, $apart = false) + { + //检测日志文件大小,超过配置大小则备份日志文件重新生成 + if (is_file($destination) && floor($this->config['file_size']) <= filesize($destination)) { + rename($destination, dirname($destination) . DS . time() . '-' . basename($destination)); + $this->writed[$destination] = false; + } + + if (empty($this->writed[$destination]) && !IS_CLI) { + if (App::$debug && !$apart) { + // 获取基本信息 + if (isset($_SERVER['HTTP_HOST'])) { + $current_uri = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + } else { + $current_uri = "cmd:" . implode(' ', $_SERVER['argv']); + } + + $runtime = round(microtime(true) - THINK_START_TIME, 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $time_str = ' [运行时间:' . number_format($runtime, 6) . 's][吞吐率:' . $reqs . 'req/s]'; + $memory_use = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + $memory_str = ' [内存消耗:' . $memory_use . 'kb]'; + $file_load = ' [文件加载:' . count(get_included_files()) . ']'; + + $message = '[ info ] ' . $current_uri . $time_str . $memory_str . $file_load . "\r\n" . $message; + } + $now = date($this->config['time_format']); + $server = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '0.0.0.0'; + $remote = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; + $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'CLI'; + $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; + $message = "---------------------------------------------------------------\r\n[{$now}] {$server} {$remote} {$method} {$uri}\r\n" . $message; + + $this->writed[$destination] = true; + } + + if (IS_CLI) { + $now = date($this->config['time_format']); + $message = "[{$now}]" . $message; + } + + return error_log($message, 3, $destination); + } + +} diff --git a/thinkphp/library/think/log/driver/Socket.php b/thinkphp/library/think/log/driver/Socket.php new file mode 100644 index 0000000..d30bba3 --- /dev/null +++ b/thinkphp/library/think/log/driver/Socket.php @@ -0,0 +1,250 @@ + +// +---------------------------------------------------------------------- + +namespace think\log\driver; + +use think\App; + +/** + * github: https://github.com/luofei614/SocketLog + * @author luofei614 + */ +class Socket +{ + public $port = 1116; //SocketLog 服务的http的端口号 + + protected $config = [ + // socket服务器地址 + 'host' => 'localhost', + // 是否显示加载的文件列表 + 'show_included_files' => false, + // 日志强制记录到配置的client_id + 'force_client_ids' => [], + // 限制允许读取日志的client_id + 'allow_client_ids' => [], + ]; + + protected $css = [ + 'sql' => 'color:#009bb4;', + 'sql_warn' => 'color:#009bb4;font-size:14px;', + 'error' => 'color:#f4006b;font-size:14px;', + 'page' => 'color:#40e2ff;background:#171717;', + 'big' => 'font-size:20px;color:red;', + ]; + + protected $allowForceClientIds = []; //配置强制推送且被授权的client_id + + /** + * 构造函数 + * @param array $config 缓存参数 + * @access public + */ + public function __construct(array $config = []) + { + if (!empty($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * 调试输出接口 + * @access public + * @param array $log 日志信息 + * @return bool + */ + public function save(array $log = []) + { + if (!$this->check()) { + return false; + } + $trace = []; + if (App::$debug) { + $runtime = round(microtime(true) - THINK_START_TIME, 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $time_str = ' [运行时间:' . number_format($runtime, 6) . 's][吞吐率:' . $reqs . 'req/s]'; + $memory_use = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + $memory_str = ' [内存消耗:' . $memory_use . 'kb]'; + $file_load = ' [文件加载:' . count(get_included_files()) . ']'; + + if (isset($_SERVER['HTTP_HOST'])) { + $current_uri = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + } else { + $current_uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + // 基本信息 + $trace[] = [ + 'type' => 'group', + 'msg' => $current_uri . $time_str . $memory_str . $file_load, + 'css' => $this->css['page'], + ]; + } + + foreach ($log as $type => $val) { + $trace[] = [ + 'type' => 'groupCollapsed', + 'msg' => '[ ' . $type . ' ]', + 'css' => isset($this->css[$type]) ? $this->css[$type] : '', + ]; + foreach ($val as $msg) { + if (!is_string($msg)) { + $msg = var_export($msg, true); + } + $trace[] = [ + 'type' => 'log', + 'msg' => $msg, + 'css' => '', + ]; + } + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + } + + if ($this->config['show_included_files']) { + $trace[] = [ + 'type' => 'groupCollapsed', + 'msg' => '[ file ]', + 'css' => '', + ]; + $trace[] = [ + 'type' => 'log', + 'msg' => implode("\n", get_included_files()), + 'css' => '', + ]; + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + } + + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + + $tabid = $this->getClientArg('tabid'); + if (!$client_id = $this->getClientArg('client_id')) { + $client_id = ''; + } + + if (!empty($this->allowForceClientIds)) { + //强制推送到多个client_id + foreach ($this->allowForceClientIds as $force_client_id) { + $client_id = $force_client_id; + $this->sendToClient($tabid, $client_id, $trace, $force_client_id); + } + } else { + $this->sendToClient($tabid, $client_id, $trace, ''); + } + return true; + } + + /** + * 发送给指定客户端 + * @author Zjmainstay + * @param $tabid + * @param $client_id + * @param $logs + * @param $force_client_id + */ + protected function sendToClient($tabid, $client_id, $logs, $force_client_id) + { + $logs = [ + 'tabid' => $tabid, + 'client_id' => $client_id, + 'logs' => $logs, + 'force_client_id' => $force_client_id, + ]; + $msg = @json_encode($logs); + $address = '/' . $client_id; //将client_id作为地址, server端通过地址判断将日志发布给谁 + $this->send($this->config['host'], $msg, $address); + } + + protected function check() + { + $tabid = $this->getClientArg('tabid'); + //是否记录日志的检查 + if (!$tabid && !$this->config['force_client_ids']) { + return false; + } + //用户认证 + $allow_client_ids = $this->config['allow_client_ids']; + if (!empty($allow_client_ids)) { + //通过数组交集得出授权强制推送的client_id + $this->allowForceClientIds = array_intersect($allow_client_ids, $this->config['force_client_ids']); + if (!$tabid && count($this->allowForceClientIds)) { + return true; + } + + $client_id = $this->getClientArg('client_id'); + if (!in_array($client_id, $allow_client_ids)) { + return false; + } + } else { + $this->allowForceClientIds = $this->config['force_client_ids']; + } + return true; + } + + protected function getClientArg($name) + { + static $args = []; + + $key = 'HTTP_USER_AGENT'; + + if (isset($_SERVER['HTTP_SOCKETLOG'])) { + $key = 'HTTP_SOCKETLOG'; + } + + if (!isset($_SERVER[$key])) { + return; + } + if (empty($args)) { + if (!preg_match('/SocketLog\((.*?)\)/', $_SERVER[$key], $match)) { + $args = ['tabid' => null]; + return; + } + parse_str($match[1], $args); + } + if (isset($args[$name])) { + return $args[$name]; + } + return; + } + + /** + * @param string $host - $host of socket server + * @param string $message - 发送的消息 + * @param string $address - 地址 + * @return bool + */ + protected function send($host, $message = '', $address = '/') + { + $url = 'http://' . $host . ':' . $this->port . $address; + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $message); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + $headers = [ + "Content-Type: application/json;charset=UTF-8", + ]; + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); //设置header + return curl_exec($ch); + } + +} diff --git a/thinkphp/library/think/log/driver/Test.php b/thinkphp/library/think/log/driver/Test.php new file mode 100644 index 0000000..7f66338 --- /dev/null +++ b/thinkphp/library/think/log/driver/Test.php @@ -0,0 +1,30 @@ + +// +---------------------------------------------------------------------- + +namespace think\log\driver; + +/** + * 模拟测试输出 + */ +class Test +{ + /** + * 日志写入接口 + * @access public + * @param array $log 日志信息 + * @return bool + */ + public function save(array $log = []) + { + return true; + } + +} diff --git a/thinkphp/library/think/model/Collection.php b/thinkphp/library/think/model/Collection.php new file mode 100644 index 0000000..4e4bb4d --- /dev/null +++ b/thinkphp/library/think/model/Collection.php @@ -0,0 +1,93 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\Collection as BaseCollection; +use think\Model; + +class Collection extends BaseCollection +{ + /** + * 返回数组中指定的一列 + * @param string $column_key + * @param string|null $index_key + * @return array + */ + public function column($column_key, $index_key = null) + { + if (function_exists('array_column')) { + return array_column($this->toArray(), $column_key, $index_key); + } + return parent::column($column_key, $index_key); + } + + /** + * 延迟预载入关联查询 + * @access public + * @param mixed $relation 关联 + * @return $this + */ + public function load($relation) + { + $item = current($this->items); + $item->eagerlyResultSet($this->items, $relation); + return $this; + } + + /** + * 设置需要隐藏的输出属性 + * @access public + * @param array $hidden 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function hidden($hidden = [], $override = false) + { + $this->each(function ($model) use ($hidden, $override) { + /** @var Model $model */ + $model->hidden($hidden, $override); + }); + return $this; + } + + /** + * 设置需要输出的属性 + * @param array $visible + * @param bool $override 是否覆盖 + * @return $this + */ + public function visible($visible = [], $override = false) + { + $this->each(function ($model) use ($visible, $override) { + /** @var Model $model */ + $model->visible($visible, $override); + }); + return $this; + } + + /** + * 设置需要追加的输出属性 + * @access public + * @param array $append 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function append($append = [], $override = false) + { + $this->each(function ($model) use ($append, $override) { + /** @var Model $model */ + $model->append($append, $override); + }); + return $this; + } + +} diff --git a/thinkphp/library/think/model/Merge.php b/thinkphp/library/think/model/Merge.php new file mode 100644 index 0000000..d944979 --- /dev/null +++ b/thinkphp/library/think/model/Merge.php @@ -0,0 +1,322 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\Db; +use think\db\Query; +use think\Model; + +class Merge extends Model +{ + + protected $relationModel = []; // HAS ONE 关联的模型列表 + protected $fk = ''; // 外键名 默认为主表名_id + protected $mapFields = []; // 需要处理的模型映射字段,避免混淆 array( id => 'user.id' ) + + /** + * 构造函数 + * @access public + * @param array|object $data 数据 + */ + public function __construct($data = []) + { + parent::__construct($data); + + // 设置默认外键名 仅支持单一外键 + if (empty($this->fk)) { + $this->fk = strtolower($this->name) . '_id'; + } + } + + /** + * 查找单条记录 + * @access public + * @param mixed $data 主键值或者查询条件(闭包) + * @param string|array $with 关联预查询 + * @param bool $cache 是否缓存 + * @return \think\Model + */ + public static function get($data = null, $with = [], $cache = false) + { + $query = self::parseQuery($data, $with, $cache); + $query = self::attachQuery($query); + return $query->find($data); + } + + /** + * 附加查询表达式 + * @access protected + * @param \think\db\Query $query 查询对象 + * @return \think\db\Query + */ + protected static function attachQuery($query) + { + $class = new static(); + $master = $class->name; + $fields = self::getModelField($query, $master, '', $class->mapFields, $class->field); + $query->alias($master)->field($fields); + + foreach ($class->relationModel as $key => $model) { + $name = is_int($key) ? $model : $key; + $table = is_int($key) ? $query->getTable($name) : $model; + $query->join($table . ' ' . $name, $name . '.' . $class->fk . '=' . $master . '.' . $class->getPk()); + $fields = self::getModelField($query, $name, $table, $class->mapFields, $class->field); + $query->field($fields); + } + return $query; + } + + /** + * 获取关联模型的字段 并解决混淆 + * @access protected + * @param \think\db\Query $query 查询对象 + * @param string $name 模型名称 + * @param string $table 关联表名称 + * @param array $map 字段映射 + * @param array $fields 查询字段 + * @return array + */ + protected static function getModelField($query, $name, $table = '', $map = [], $fields = []) + { + // 获取模型的字段信息 + $fields = $fields ?: $query->getTableInfo($table, 'fields'); + $array = []; + foreach ($fields as $field) { + if ($key = array_search($name . '.' . $field, $map)) { + // 需要处理映射字段 + $array[] = $name . '.' . $field . ' AS ' . $key; + } else { + $array[] = $field; + } + } + return $array; + } + + /** + * 查找所有记录 + * @access public + * @param mixed $data 主键列表或者查询条件(闭包) + * @param array|string $with 关联预查询 + * @param bool $cache + * @return array|false|string + */ + public static function all($data = null, $with = [], $cache = false) + { + $query = self::parseQuery($data, $with, $cache); + $query = self::attachQuery($query); + return $query->select($data); + } + + /** + * 处理写入的模型数据 + * @access public + * @param string $model 模型名称 + * @param array $data 数据 + * @return array + */ + protected function parseData($model, $data) + { + $item = []; + foreach ($data as $key => $val) { + if ($this->fk != $key && array_key_exists($key, $this->mapFields)) { + list($name, $key) = explode('.', $this->mapFields[$key]); + if ($model == $name) { + $item[$key] = $val; + } + } else { + $item[$key] = $val; + } + } + return $item; + } + + /** + * 保存模型数据 以及关联数据 + * @access public + * @param mixed $data 数据 + * @param array $where 更新条件 + * @param string $sequence 自增序列名 + * @return false|int + * @throws \Exception + */ + public function save($data = [], $where = [], $sequence = null) + { + if (!empty($data)) { + // 数据自动验证 + if (!$this->validateData($data)) { + return false; + } + // 数据对象赋值 + foreach ($data as $key => $value) { + $this->setAttr($key, $value, $data); + } + if (!empty($where)) { + $this->isUpdate = true; + } + } + + // 数据自动完成 + $this->autoCompleteData($this->auto); + + // 自动写入更新时间 + if ($this->autoWriteTimestamp && $this->updateTime && !isset($this->data[$this->updateTime])) { + $this->setAttr($this->updateTime, null); + } + + // 事件回调 + if (false === $this->trigger('before_write', $this)) { + return false; + } + + $db = $this->db(); + $db->startTrans(); + $pk = $this->getPk(); + try { + if ($this->isUpdate) { + // 自动写入 + $this->autoCompleteData($this->update); + + if (false === $this->trigger('before_update', $this)) { + return false; + } + + if (empty($where) && !empty($this->updateWhere)) { + $where = $this->updateWhere; + } + + // 获取有更新的数据 + $data = $this->getChangedData(); + // 保留主键数据 + foreach ($this->data as $key => $val) { + if ($this->isPk($key)) { + $data[$key] = $val; + } + } + // 处理模型数据 + $data = $this->parseData($this->name, $data); + if (is_string($pk) && isset($data[$pk])) { + if (!isset($where[$pk])) { + unset($where); + $where[$pk] = $data[$pk]; + } + unset($data[$pk]); + } + // 写入主表数据 + $result = $db->strict(false)->where($where)->update($data); + + // 写入附表数据 + foreach ($this->relationModel as $key => $model) { + $name = is_int($key) ? $model : $key; + $table = is_int($key) ? $db->getTable($model) : $model; + // 处理关联模型数据 + $data = $this->parseData($name, $data); + if (Db::table($table)->strict(false)->where($this->fk, $this->data[$this->getPk()])->update($data)) { + $result = 1; + } + } + + // 新增回调 + $this->trigger('after_update', $this); + } else { + // 自动写入 + $this->autoCompleteData($this->insert); + + // 自动写入创建时间 + if ($this->autoWriteTimestamp && $this->createTime && !isset($this->data[$this->createTime])) { + $this->setAttr($this->createTime, null); + } + + if (false === $this->trigger('before_insert', $this)) { + return false; + } + + // 处理模型数据 + $data = $this->parseData($this->name, $this->data); + // 写入主表数据 + $result = $db->name($this->name)->strict(false)->insert($data); + if ($result) { + $insertId = $db->getLastInsID($sequence); + // 写入外键数据 + if ($insertId) { + if (is_string($pk)) { + $this->data[$pk] = $insertId; + } + $this->data[$this->fk] = $insertId; + } + + // 写入附表数据 + $source = $this->data; + if ($insertId && is_string($pk) && isset($source[$pk]) && $this->fk != $pk) { + unset($source[$pk]); + } + foreach ($this->relationModel as $key => $model) { + $name = is_int($key) ? $model : $key; + $table = is_int($key) ? $db->getTable($model) : $model; + // 处理关联模型数据 + $data = $this->parseData($name, $source); + Db::table($table)->strict(false)->insert($data); + } + } + // 标记为更新 + $this->isUpdate = true; + // 新增回调 + $this->trigger('after_insert', $this); + } + $db->commit(); + // 写入回调 + $this->trigger('after_write', $this); + + $this->origin = $this->data; + return $result; + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + + /** + * 删除当前的记录 并删除关联数据 + * @access public + * @return int + * @throws \Exception + */ + public function delete() + { + if (false === $this->trigger('before_delete', $this)) { + return false; + } + + $db = $this->db(); + $db->startTrans(); + try { + $result = $db->delete($this->data); + if ($result) { + // 获取主键数据 + $pk = $this->data[$this->getPk()]; + + // 删除关联数据 + foreach ($this->relationModel as $key => $model) { + $table = is_int($key) ? $db->getTable($model) : $model; + $query = new Query; + $query->table($table)->where($this->fk, $pk)->delete(); + } + } + $this->trigger('after_delete', $this); + $db->commit(); + return $result; + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + +} diff --git a/thinkphp/library/think/model/Pivot.php b/thinkphp/library/think/model/Pivot.php new file mode 100644 index 0000000..06c6b18 --- /dev/null +++ b/thinkphp/library/think/model/Pivot.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\Model; + +class Pivot extends Model +{ + + /** @var Model */ + public $parent; + + protected $autoWriteTimestamp = false; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型 + * @param array|object $data 数据 + * @param string $table 中间数据表名 + */ + public function __construct(Model $parent, $data = [], $table = '') + { + $this->parent = $parent; + + if (is_null($this->name)) { + $this->name = $table; + } + + parent::__construct($data); + + $this->class = $this->name; + } + +} diff --git a/thinkphp/library/think/model/Relation.php b/thinkphp/library/think/model/Relation.php new file mode 100644 index 0000000..4869e52 --- /dev/null +++ b/thinkphp/library/think/model/Relation.php @@ -0,0 +1,130 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\db\Query; +use think\Exception; +use think\Model; + +/** + * Class Relation + * @package think\model + * + * @mixin Query + */ +abstract class Relation +{ + // 父模型对象 + protected $parent; + /** @var Model 当前关联的模型类 */ + protected $model; + /** @var Query 关联模型查询对象 */ + protected $query; + // 关联表外键 + protected $foreignKey; + // 关联表主键 + protected $localKey; + // 基础查询 + protected $baseQuery; + + /** + * 获取关联的所属模型 + * @access public + * @return Model + */ + public function getParent() + { + return $this->parent; + } + + /** + * 获取当前的关联模型类 + * @access public + * @return string + */ + public function getModel() + { + return $this->model; + } + + /** + * 获取关联的查询对象 + * @access public + * @return Query + */ + public function getQuery() + { + return $this->query; + } + + /** + * 封装关联数据集 + * @access public + * @param array $resultSet 数据集 + * @return mixed + */ + protected function resultSetBuild($resultSet) + { + return (new $this->model)->toCollection($resultSet); + } + + protected function getQueryFields($model) + { + $fields = $this->query->getOptions('field'); + return $this->getRelationQueryFields($fields, $model); + } + + protected function getRelationQueryFields($fields, $model) + { + if ($fields) { + + if (is_string($fields)) { + $fields = explode(',', $fields); + } + + foreach ($fields as &$field) { + if (false === strpos($field, '.')) { + $field = $model . '.' . $field; + } + } + } else { + $fields = $model . '.*'; + } + + return $fields; + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + abstract protected function baseQuery(); + + public function __call($method, $args) + { + if ($this->query) { + // 执行基础查询 + $this->baseQuery(); + + $result = call_user_func_array([$this->query, $method], $args); + if ($result instanceof Query) { + return $this; + } else { + $this->baseQuery = false; + return $result; + } + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } +} diff --git a/thinkphp/library/think/model/relation/BelongsTo.php b/thinkphp/library/think/model/relation/BelongsTo.php new file mode 100644 index 0000000..a32cc12 --- /dev/null +++ b/thinkphp/library/think/model/relation/BelongsTo.php @@ -0,0 +1,218 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\Loader; +use think\Model; + +class BelongsTo extends OneToOne +{ + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 关联主键 + * @param string $joinType JOIN类型 + * @param string $relation 关联名 + */ + public function __construct(Model $parent, $model, $foreignKey, $localKey, $joinType = 'INNER', $relation = null) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->joinType = $joinType; + $this->query = (new $model)->db(); + $this->relation = $relation; + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @access public + * @return array|false|\PDOStatement|string|Model + */ + public function getRelation($subRelation = '', $closure = null) + { + $foreignKey = $this->foreignKey; + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $relationModel = $this->query + ->where($this->localKey, $this->parent->$foreignKey) + ->relation($subRelation) + ->find(); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @return Query + */ + public function hasWhere($where = []) + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + if (is_array($where)) { + foreach ($where as $key => $val) { + if (false === strpos($key, '.')) { + $where[$relation . '.' . $key] = $val; + unset($where[$key]); + } + } + } + return $this->parent->db()->alias($model) + ->field($model . '.*') + ->join($table . ' ' . $relation, $model . '.' . $this->foreignKey . '=' . $relation . '.' . $this->localKey, $this->joinType) + ->where($where); + } + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$foreignKey)) { + $range[] = $result->$foreignKey; + } + } + + if (!empty($range)) { + $data = $this->eagerlyWhere($this, [ + $localKey => [ + 'in', + $range, + ], + ], $localKey, $relation, $subRelation, $closure); + // 关联属性名 + $attr = Loader::parseName($relation); + // 关联数据封装 + foreach ($resultSet as $result) { + // 关联模型 + if (!isset($data[$result->$foreignKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$foreignKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result, $this->bindAttr); + } + + // 设置关联属性 + $result->setRelation($attr, $relationModel); + } + } + } + + /** + * 预载入关联查询(数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlyOne(&$result, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + $data = $this->eagerlyWhere($this, [$localKey => $result->$foreignKey], $localKey, $relation, $subRelation, $closure); + // 关联模型 + if (!isset($data[$result->$foreignKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$foreignKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result, $this->bindAttr); + } + // 设置关联属性 + $result->setRelation(Loader::parseName($relation), $relationModel); + } + + /** + * 添加关联数据 + * @access public + * @param Model $model 关联模型对象 + * @return Model + */ + public function associate($model) + { + $foreignKey = $this->foreignKey; + $pk = $model->getPk(); + + $this->parent->setAttr($foreignKey, $model->$pk); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, $model); + } + + /** + * 注销关联数据 + * @access public + * @return Model + */ + public function dissociate() + { + $foreignKey = $this->foreignKey; + + $this->parent->setAttr($foreignKey, null); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, null); + } +} diff --git a/thinkphp/library/think/model/relation/BelongsToMany.php b/thinkphp/library/think/model/relation/BelongsToMany.php new file mode 100644 index 0000000..ef08abb --- /dev/null +++ b/thinkphp/library/think/model/relation/BelongsToMany.php @@ -0,0 +1,573 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\Collection; +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Pivot; +use think\model\Relation; +use think\Paginator; + +class BelongsToMany extends Relation +{ + // 中间表表名 + protected $middle; + // 中间表模型名称 + protected $pivotName; + // 中间表模型对象 + protected $pivot; + + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $table 中间表名 + * @param string $foreignKey 关联模型外键 + * @param string $localKey 当前模型关联键 + */ + public function __construct(Model $parent, $model, $table, $foreignKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + if (false !== strpos($table, '\\')) { + $this->pivotName = $table; + $this->middle = basename(str_replace('\\', '/', $table)); + } else { + $this->middle = $table; + } + $this->query = (new $model)->db(); + $this->pivot = $this->newPivot(); + } + + /** + * 设置中间表模型 + * @param $pivot + * @return $this + */ + public function pivot($pivot) + { + $this->pivotName = $pivot; + return $this; + } + + /** + * 实例化中间表模型 + * @param $data + * @return mixed + */ + protected function newPivot($data = []) + { + $pivot = $this->pivotName ?: '\\think\\model\\Pivot'; + return new $pivot($this->parent, $data, $this->middle); + } + + /** + * 合成中间表模型 + * @param array|Collection|Paginator $models + */ + protected function hydratePivot($models) + { + foreach ($models as $model) { + $pivot = []; + foreach ($model->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + if ('pivot' == $name) { + $pivot[$attr] = $val; + unset($model->$key); + } + } + } + $model->setRelation('pivot', $this->newPivot($pivot)); + } + } + + /** + * 创建关联查询Query对象 + * @return Query + */ + protected function buildQuery() + { + $foreignKey = $this->foreignKey; + $localKey = $this->localKey; + $pk = $this->parent->getPk(); + // 关联查询 + $condition['pivot.' . $localKey] = $this->parent->$pk; + return $this->belongsToManyQuery($foreignKey, $localKey, $condition); + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return false|\PDOStatement|string|\think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $result = $this->buildQuery()->relation($subRelation)->select(); + $this->hydratePivot($result); + return $result; + } + + /** + * 重载select方法 + * @param null $data + * @return false|\PDOStatement|string|Collection + */ + public function select($data = null) + { + $result = $this->buildQuery()->select($data); + $this->hydratePivot($result); + return $result; + } + + /** + * 重载paginate方法 + * @param null $listRows + * @param bool $simple + * @param array $config + * @return Paginator + */ + public function paginate($listRows = null, $simple = false, $config = []) + { + $result = $this->buildQuery()->paginate($listRows, $simple, $config); + $this->hydratePivot($result); + return $result; + } + + /** + * 重载find方法 + * @param null $data + * @return array|false|\PDOStatement|string|Model + */ + public function find($data = null) + { + $result = $this->buildQuery()->find($data); + if ($result) { + $this->hydratePivot([$result]); + } + return $result; + } + + /** + * 查找多条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + */ + public function selectOrFail($data = null) + { + return $this->failException(true)->select($data); + } + + /** + * 查找单条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + */ + public function findOrFail($data = null) + { + return $this->failException(true)->find($data); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @return Query + * @throws Exception + */ + public function hasWhere($where = []) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 设置中间表的查询条件 + * @param $field + * @param null $op + * @param null $condition + * @return $this + */ + public function wherePivot($field, $op = null, $condition = null) + { + $field = 'pivot.' . $field; + $this->query->where($field, $op, $condition); + return $this; + } + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $pk = $resultSet[0]->getPk(); + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + // 查询关联数据 + $data = $this->eagerlyManyToMany([ + 'pivot.' . $localKey => [ + 'in', + $range, + ], + ], $relation, $subRelation); + // 关联属性名 + $attr = Loader::parseName($relation); + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + + $result->setRelation($attr, $this->resultSetBuild($data[$result->$pk])); + } + } + } + + /** + * 预载入关联查询(单个数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $pk = $result->getPk(); + if (isset($result->$pk)) { + $pk = $result->$pk; + // 查询管理数据 + $data = $this->eagerlyManyToMany(['pivot.' . $this->localKey => $pk], $relation, $subRelation); + + // 关联数据封装 + if (!isset($data[$pk])) { + $data[$pk] = []; + } + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$pk])); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @return integer + */ + public function relationCount($result, $closure) + { + $pk = $result->getPk(); + $count = 0; + if (isset($result->$pk)) { + $pk = $result->$pk; + $count = $this->belongsToManyQuery($this->foreignKey, $this->localKey, ['pivot.' . $this->localKey => $pk])->count(); + } + return $count; + } + + /** + * 获取关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @return string + */ + public function getRelationCountQuery($closure) + { + return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [ + 'pivot.' . $this->localKey => [ + 'exp', + '=' . $this->parent->getTable() . '.' . $this->parent->getPk(), + ], + ])->fetchSql()->count(); + } + + /** + * 多对多 关联模型预查询 + * @access public + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @return array + */ + protected function eagerlyManyToMany($where, $relation, $subRelation = '') + { + // 预载入关联查询 支持嵌套预载入 + $list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where)->with($subRelation)->select(); + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $pivot = []; + foreach ($set->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + if ('pivot' == $name) { + $pivot[$attr] = $val; + unset($set->$key); + } + } + } + $set->setRelation('pivot', $this->newPivot($pivot)); + $data[$pivot[$this->localKey]][] = $set; + } + return $data; + } + + /** + * BELONGS TO MANY 关联查询 + * @access public + * @param string $foreignKey 关联模型关联键 + * @param string $localKey 当前模型关联键 + * @param array $condition 关联查询条件 + * @return Query + */ + protected function belongsToManyQuery($foreignKey, $localKey, $condition = []) + { + // 关联查询封装 + $tableName = $this->query->getTable(); + $table = $this->pivot->getTable(); + $fields = $this->getQueryFields($tableName); + + $query = $this->query->field($fields) + ->field(true, false, $table, 'pivot', 'pivot__'); + + if (empty($this->baseQuery)) { + $relationFk = $this->query->getPk(); + $query->join($table . ' pivot', 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk) + ->where($condition); + } + return $query; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @param array $pivot 中间表额外数据 + * @return integer + */ + public function save($data, array $pivot = []) + { + // 保存关联表/中间表数据 + return $this->attach($data, $pivot); + } + + /** + * 批量保存当前关联数据对象 + * @access public + * @param array $dataSet 数据集 + * @param array $pivot 中间表额外数据 + * @param bool $samePivot 额外数据是否相同 + * @return integer + */ + public function saveAll(array $dataSet, array $pivot = [], $samePivot = false) + { + $result = false; + foreach ($dataSet as $key => $data) { + if (!$samePivot) { + $pivotData = isset($pivot[$key]) ? $pivot[$key] : []; + } else { + $pivotData = $pivot; + } + $result = $this->attach($data, $pivotData); + } + return $result; + } + + /** + * 附加关联的一个中间表数据 + * @access public + * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键 + * @param array $pivot 中间表额外数据 + * @return array|Pivot + * @throws Exception + */ + public function attach($data, $pivot = []) + { + if (is_array($data)) { + if (key($data) === 0) { + $id = $data; + } else { + // 保存关联表数据 + $model = new $this->model; + $model->save($data); + $id = $model->getLastInsID(); + } + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $relationFk = $data->getPk(); + $id = $data->$relationFk; + } + + if ($id) { + // 保存中间表数据 + $pk = $this->parent->getPk(); + $pivot[$this->localKey] = $this->parent->$pk; + $ids = (array) $id; + foreach ($ids as $id) { + $pivot[$this->foreignKey] = $id; + $this->pivot->insert($pivot, true); + $result[] = $this->newPivot($pivot); + } + if (count($result) == 1) { + // 返回中间表模型对象 + $result = $result[0]; + } + return $result; + } else { + throw new Exception('miss relation data'); + } + } + + /** + * 解除关联的一个中间表数据 + * @access public + * @param integer|array $data 数据 可以使用关联对象的主键 + * @param bool $relationDel 是否同时删除关联表数据 + * @return integer + */ + public function detach($data = null, $relationDel = false) + { + if (is_array($data)) { + $id = $data; + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $relationFk = $data->getPk(); + $id = $data->$relationFk; + } + // 删除中间表数据 + $pk = $this->parent->getPk(); + $pivot[$this->localKey] = $this->parent->$pk; + if (isset($id)) { + $pivot[$this->foreignKey] = is_array($id) ? ['in', $id] : $id; + } + $this->pivot->where($pivot)->delete(); + // 删除关联表数据 + if (isset($id) && $relationDel) { + $model = $this->model; + $model::destroy($id); + } + } + + /** + * 数据同步 + * @param array $ids + * @param bool $detaching + * @return array + */ + public function sync($ids, $detaching = true) + { + $changes = [ + 'attached' => [], + 'detached' => [], + 'updated' => [], + ]; + $pk = $this->parent->getPk(); + $current = $this->pivot->where($this->localKey, $this->parent->$pk) + ->column($this->foreignKey); + $records = []; + + foreach ($ids as $key => $value) { + if (!is_array($value)) { + $records[$value] = []; + } else { + $records[$key] = $value; + } + } + + $detach = array_diff($current, array_keys($records)); + + if ($detaching && count($detach) > 0) { + $this->detach($detach); + + $changes['detached'] = $detach; + } + + foreach ($records as $id => $attributes) { + if (!in_array($id, $current)) { + $this->attach($id, $attributes); + $changes['attached'][] = $id; + } elseif (count($attributes) > 0 && + $this->attach($id, $attributes) + ) { + $changes['updated'][] = $id; + } + } + + return $changes; + + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $pk = $this->parent->getPk(); + $table = $this->pivot->getTable(); + $this->query->join($table . ' pivot', 'pivot.' . $this->foreignKey . '=' . $this->query->getTable() . '.' . $this->query->getPk())->where('pivot.' . $this->localKey, $this->parent->$pk); + $this->baseQuery = true; + } + } + +} diff --git a/thinkphp/library/think/model/relation/HasMany.php b/thinkphp/library/think/model/relation/HasMany.php new file mode 100644 index 0000000..ba307ec --- /dev/null +++ b/thinkphp/library/think/model/relation/HasMany.php @@ -0,0 +1,289 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\db\Query; +use think\Loader; +use think\Model; +use think\model\Relation; + +class HasMany extends Relation +{ + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型主键 + */ + public function __construct(Model $parent, $model, $foreignKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return false|\PDOStatement|string|\think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $list = $this->relation($subRelation)->select(); + $parent = clone $this->parent; + + foreach ($list as &$model) { + $model->setParent($parent); + } + + return $list; + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $data = $this->eagerlyOneToMany($this, [ + $this->foreignKey => [ + 'in', + $range, + ], + ], $relation, $subRelation, $closure); + // 关联属性名 + $attr = Loader::parseName($relation); + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$localKey])) { + $data[$result->$localKey] = []; + } + + foreach ($data[$result->$localKey] as &$relationModel) { + $relationModel->setParent(clone $result); + } + + $result->setRelation($attr, $this->resultSetBuild($data[$result->$localKey])); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + + if (isset($result->$localKey)) { + $data = $this->eagerlyOneToMany($this, [$this->foreignKey => $result->$localKey], $relation, $subRelation, $closure); + // 关联数据封装 + if (!isset($data[$result->$localKey])) { + $data[$result->$localKey] = []; + } + + foreach ($data[$result->$localKey] as &$relationModel) { + $relationModel->setParent(clone $result); + } + + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$result->$localKey])); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @return integer + */ + public function relationCount($result, $closure) + { + $localKey = $this->localKey; + $count = 0; + if (isset($result->$localKey)) { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $count = $this->query->where([$this->foreignKey => $result->$localKey])->count(); + } + return $count; + } + + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @return string + */ + public function getRelationCountQuery($closure) + { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + + return $this->query->where([ + $this->foreignKey => [ + 'exp', + '=' . $this->parent->getTable() . '.' . $this->parent->getPk(), + ], + ])->fetchSql()->count(); + } + + /** + * 一对多 关联模型预查询 + * @access public + * @param object $model 关联模型对象 + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param bool $closure + * @return array + */ + protected function eagerlyOneToMany($model, $where, $relation, $subRelation = '', $closure = false) + { + $foreignKey = $this->foreignKey; + // 预载入关联查询 支持嵌套预载入 + if ($closure) { + call_user_func_array($closure, [ & $model]); + } + $list = $model->where($where)->with($subRelation)->select(); + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $data[$set->$foreignKey][] = $set; + } + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @return Model|false + */ + public function save($data) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + // 保存关联表数据 + $model = new $this->model; + $data[$this->foreignKey] = $this->parent->{$this->localKey}; + return $model->save($data) ? $model : false; + } + + /** + * 批量保存当前关联数据对象 + * @access public + * @param array $dataSet 数据集 + * @return integer + */ + public function saveAll(array $dataSet) + { + $result = false; + foreach ($dataSet as $key => $data) { + $result = $this->save($data); + } + return $result; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + $table = $this->query->getTable(); + return $this->parent->db()->alias('a') + ->join($table . ' b', 'a.' . $this->localKey . '=b.' . $this->foreignKey, $joinType) + ->group('b.' . $this->foreignKey) + ->having('count(' . $id . ')' . $operator . $count); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @return Query + */ + public function hasWhere($where = []) + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + if (is_array($where)) { + foreach ($where as $key => $val) { + if (false === strpos($key, '.')) { + $where[$relation . '.' . $key] = $val; + unset($where[$key]); + } + } + } + return $this->parent->db()->alias($model) + ->field($model . '.*') + ->join($table . ' ' . $relation, $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey) + ->where($where); + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery)) { + if (isset($this->parent->{$this->localKey})) { + // 关联查询带入关联条件 + $this->query->where($this->foreignKey, $this->parent->{$this->localKey}); + } + $this->baseQuery = true; + } + } + +} diff --git a/thinkphp/library/think/model/relation/HasManyThrough.php b/thinkphp/library/think/model/relation/HasManyThrough.php new file mode 100644 index 0000000..1573fc6 --- /dev/null +++ b/thinkphp/library/think/model/relation/HasManyThrough.php @@ -0,0 +1,146 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class HasManyThrough extends Relation +{ + // 中间关联表外键 + protected $throughKey; + // 中间表模型 + protected $through; + + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $through 中间模型名 + * @param string $foreignKey 关联外键 + * @param string $throughKey 关联外键 + * @param string $localKey 关联主键 + */ + public function __construct(Model $parent, $model, $through, $foreignKey, $throughKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->through = $through; + $this->foreignKey = $foreignKey; + $this->throughKey = $throughKey; + $this->localKey = $localKey; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return false|\PDOStatement|string|\think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + + return $this->relation($subRelation)->select(); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @return Query + */ + public function hasWhere($where = []) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @param string $class 数据集对象名 为空表示数组 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure, $class) + {} + + /** + * 预载入关联查询 返回模型对象 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @param string $class 数据集对象名 为空表示数组 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure, $class) + {} + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @return integer + */ + public function relationCount($result, $closure) + {} + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $through = $this->through; + $alias = Loader::parseName(basename(str_replace('\\', '/', $this->model))); + $throughTable = $through::getTable(); + $pk = (new $through)->getPk(); + $throughKey = $this->throughKey; + $modelTable = $this->parent->getTable(); + $this->query->field($alias . '.*')->alias($alias) + ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey) + ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey) + ->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey}); + $this->baseQuery = true; + } + } + +} diff --git a/thinkphp/library/think/model/relation/HasOne.php b/thinkphp/library/think/model/relation/HasOne.php new file mode 100644 index 0000000..bb95ef1 --- /dev/null +++ b/thinkphp/library/think/model/relation/HasOne.php @@ -0,0 +1,185 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\db\Query; +use think\Loader; +use think\Model; + +class HasOne extends OneToOne +{ + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型主键 + * @param string $joinType JOIN类型 + */ + public function __construct(Model $parent, $model, $foreignKey, $localKey, $joinType = 'INNER') + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->joinType = $joinType; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return array|false|\PDOStatement|string|Model + */ + public function getRelation($subRelation = '', $closure = null) + { + // 执行关联定义方法 + $localKey = $this->localKey; + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + // 判断关联类型执行查询 + $relationModel = $this->query->where($this->foreignKey, $this->parent->$localKey)->relation($subRelation)->find(); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @return Query + */ + public function has() + { + $table = $this->query->getTable(); + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + return $this->parent->db()->alias('a') + ->whereExists(function ($query) use ($table, $localKey, $foreignKey) { + $query->table([$table => 'b'])->field('b.' . $foreignKey)->whereExp('a.' . $localKey, '=b.' . $foreignKey); + }); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @return Query + */ + public function hasWhere($where = []) + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + if (is_array($where)) { + foreach ($where as $key => $val) { + if (false === strpos($key, '.')) { + $where[$relation . '.' . $key] = $val; + unset($where[$key]); + } + } + } + return $this->parent->db()->alias($model) + ->field($model . '.*') + ->join($table . ' ' . $relation, $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $this->joinType) + ->where($where); + } + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $data = $this->eagerlyWhere($this, [ + $foreignKey => [ + 'in', + $range, + ], + ], $foreignKey, $relation, $subRelation, $closure); + // 关联属性名 + $attr = Loader::parseName($relation); + // 关联数据封装 + foreach ($resultSet as $result) { + // 关联模型 + if (!isset($data[$result->$localKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$localKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result, $this->bindAttr); + } + // 设置关联属性 + $result->setRelation($attr, $relationModel); + } + } + } + + /** + * 预载入关联查询(数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlyOne(&$result, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + $data = $this->eagerlyWhere($this, [$foreignKey => $result->$localKey], $foreignKey, $relation, $subRelation, $closure); + + // 关联模型 + if (!isset($data[$result->$localKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$localKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result, $this->bindAttr); + } + + $result->setRelation(Loader::parseName($relation), $relationModel); + } + +} diff --git a/thinkphp/library/think/model/relation/MorphMany.php b/thinkphp/library/think/model/relation/MorphMany.php new file mode 100644 index 0000000..87c2d66 --- /dev/null +++ b/thinkphp/library/think/model/relation/MorphMany.php @@ -0,0 +1,285 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class MorphMany extends Relation +{ + // 多态字段 + protected $morphKey; + protected $morphType; + // 多态类型 + protected $type; + + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $morphKey 关联外键 + * @param string $morphType 多态字段名 + * @param string $type 多态类型 + */ + public function __construct(Model $parent, $model, $morphKey, $morphType, $type) + { + $this->parent = $parent; + $this->model = $model; + $this->type = $type; + $this->morphKey = $morphKey; + $this->morphType = $morphType; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return false|\PDOStatement|string|\think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $list = $this->relation($subRelation)->select(); + $parent = clone $this->parent; + + foreach ($list as &$model) { + $model->setParent($parent); + } + + return $list; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + throw new Exception('relation not support: has'); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @return Query + */ + public function hasWhere($where = []) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $morphType = $this->morphType; + $morphKey = $this->morphKey; + $type = $this->type; + $range = []; + foreach ($resultSet as $result) { + $pk = $result->getPk(); + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + $data = $this->eagerlyMorphToMany([ + $morphKey => ['in', $range], + $morphType => $type, + ], $relation, $subRelation, $closure); + // 关联属性名 + $attr = Loader::parseName($relation); + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + foreach ($data[$result->$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + $result->setRelation($attr, $this->resultSetBuild($data[$result->$pk])); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $pk = $result->getPk(); + if (isset($result->$pk)) { + $data = $this->eagerlyMorphToMany([ + $this->morphKey => $result->$pk, + $this->morphType => $this->type, + ], $relation, $subRelation, $closure); + + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + + foreach ($data[$result->$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$result->$pk])); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @return integer + */ + public function relationCount($result, $closure) + { + $pk = $result->getPk(); + $count = 0; + if (isset($result->$pk)) { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $count = $this->query->where([$this->morphKey => $result->$pk, $this->morphType => $this->type])->count(); + } + return $count; + } + + /** + * 获取关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @return string + */ + public function getRelationCountQuery($closure) + { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + + return $this->query->where([ + $this->morphKey => [ + 'exp', + '=' . $this->parent->getTable() . '.' . $this->parent->getPk(), + ], + $this->morphType => $this->type, + ])->fetchSql()->count(); + } + + /** + * 多态一对多 关联模型预查询 + * @access public + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param bool|\Closure $closure 闭包 + * @return array + */ + protected function eagerlyMorphToMany($where, $relation, $subRelation = '', $closure = false) + { + // 预载入关联查询 支持嵌套预载入 + if ($closure) { + call_user_func_array($closure, [ & $this]); + } + $list = $this->query->where($where)->with($subRelation)->select(); + $morphKey = $this->morphKey; + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $data[$set->$morphKey][] = $set; + } + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @return Model|false + */ + public function save($data) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $model = new $this->model; + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + return $model->save($data) ? $model : false; + } + + /** + * 批量保存当前关联数据对象 + * @access public + * @param array $dataSet 数据集 + * @return integer + */ + public function saveAll(array $dataSet) + { + $result = false; + foreach ($dataSet as $key => $data) { + $result = $this->save($data); + } + return $result; + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $pk = $this->parent->getPk(); + $map[$this->morphKey] = $this->parent->$pk; + $map[$this->morphType] = $this->type; + $this->query->where($map); + $this->baseQuery = true; + } + } + +} diff --git a/thinkphp/library/think/model/relation/MorphOne.php b/thinkphp/library/think/model/relation/MorphOne.php new file mode 100644 index 0000000..4dfd1c0 --- /dev/null +++ b/thinkphp/library/think/model/relation/MorphOne.php @@ -0,0 +1,229 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class MorphOne extends Relation +{ + // 多态字段 + protected $morphKey; + protected $morphType; + // 多态类型 + protected $type; + + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $morphKey 关联外键 + * @param string $morphType 多态字段名 + * @param string $type 多态类型 + */ + public function __construct(Model $parent, $model, $morphKey, $morphType, $type) + { + $this->parent = $parent; + $this->model = $model; + $this->type = $type; + $this->morphKey = $morphKey; + $this->morphType = $morphType; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return false|\PDOStatement|string|\think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $relationModel = $this->relation($subRelation)->find(); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @return Query + */ + public function hasWhere($where = []) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $morphType = $this->morphType; + $morphKey = $this->morphKey; + $type = $this->type; + $range = []; + foreach ($resultSet as $result) { + $pk = $result->getPk(); + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + $data = $this->eagerlyMorphToOne([ + $morphKey => ['in', $range], + $morphType => $type, + ], $relation, $subRelation, $closure); + // 关联属性名 + $attr = Loader::parseName($relation); + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$pk]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + $result->setRelation($attr, $relationModel); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $pk = $result->getPk(); + if (isset($result->$pk)) { + $pk = $result->$pk; + $data = $this->eagerlyMorphToOne([ + $this->morphKey => $pk, + $this->morphType => $this->type, + ], $relation, $subRelation, $closure); + + if (isset($data[$pk])) { + $relationModel = $data[$pk]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } else { + $relationModel = null; + } + + $result->setRelation(Loader::parseName($relation), $relationModel); + } + } + + /** + * 多态一对一 关联模型预查询 + * @access public + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param bool|\Closure $closure 闭包 + * @return array + */ + protected function eagerlyMorphToOne($where, $relation, $subRelation = '', $closure = false) + { + // 预载入关联查询 支持嵌套预载入 + if ($closure) { + call_user_func_array($closure, [ & $this]); + } + $list = $this->query->where($where)->with($subRelation)->find(); + $morphKey = $this->morphKey; + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $data[$set->$morphKey][] = $set; + } + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @return Model|false + */ + public function save($data) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $model = new $this->model; + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + return $model->save($data) ? $model : false; + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $pk = $this->parent->getPk(); + $map[$this->morphKey] = $this->parent->$pk; + $map[$this->morphType] = $this->type; + $this->query->where($map); + $this->baseQuery = true; + } + } + +} diff --git a/thinkphp/library/think/model/relation/MorphTo.php b/thinkphp/library/think/model/relation/MorphTo.php new file mode 100644 index 0000000..df104a9 --- /dev/null +++ b/thinkphp/library/think/model/relation/MorphTo.php @@ -0,0 +1,281 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class MorphTo extends Relation +{ + // 多态字段 + protected $morphKey; + protected $morphType; + // 多态别名 + protected $alias; + protected $relation; + + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $morphType 多态字段名 + * @param string $morphKey 外键名 + * @param array $alias 多态别名定义 + * @param string $relation 关联名 + */ + public function __construct(Model $parent, $morphType, $morphKey, $alias = [], $relation = null) + { + $this->parent = $parent; + $this->morphType = $morphType; + $this->morphKey = $morphKey; + $this->alias = $alias; + $this->relation = $relation; + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return mixed + */ + public function getRelation($subRelation = '', $closure = null) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + // 多态模型 + $model = $this->parseModel($this->parent->$morphType); + // 主键数据 + $pk = $this->parent->$morphKey; + $relationModel = (new $model)->relation($subRelation)->find($pk); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @return Query + */ + public function hasWhere($where = []) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 解析模型的完整命名空间 + * @access public + * @param string $model 模型名(或者完整类名) + * @return string + */ + protected function parseModel($model) + { + if (isset($this->alias[$model])) { + $model = $this->alias[$model]; + } + if (false === strpos($model, '\\')) { + $path = explode('\\', get_class($this->parent)); + array_pop($path); + array_push($path, Loader::parseName($model, 1)); + $model = implode('\\', $path); + } + return $model; + } + + /** + * 设置多态别名 + * @access public + * @param array $alias 别名定义 + * @return $this + */ + public function setAlias($alias) + { + $this->alias = $alias; + return $this; + } + + /** + * 移除关联查询参数 + * @access public + * @return $this + */ + public function removeOption() + { + return $this; + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + * @throws Exception + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (!empty($result->$morphKey)) { + $range[$result->$morphType][] = $result->$morphKey; + } + } + + if (!empty($range)) { + // 关联属性名 + $attr = Loader::parseName($relation); + foreach ($range as $key => $val) { + // 多态类型映射 + $model = $this->parseModel($key); + $obj = new $model; + $pk = $obj->getPk(); + $list = $obj->all($val, $subRelation); + $data = []; + foreach ($list as $k => $vo) { + $data[$vo->$pk] = $vo; + } + foreach ($resultSet as $result) { + if ($key == $result->$morphType) { + // 关联模型 + if (!isset($data[$result->$morphKey])) { + throw new Exception('relation data not exists :' . $this->model); + } else { + $relationModel = $data[$result->$morphKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + + $result->setRelation($attr, $relationModel); + } + } + } + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + // 多态类型映射 + $model = $this->parseModel($result->{$this->morphType}); + $this->eagerlyMorphToOne($model, $relation, $result, $subRelation); + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @return integer + */ + public function relationCount($result, $closure) + { + } + + /** + * 多态MorphTo 关联模型预查询 + * @access public + * @param object $model 关联模型对象 + * @param string $relation 关联名 + * @param $result + * @param string $subRelation 子关联 + * @return void + */ + protected function eagerlyMorphToOne($model, $relation, &$result, $subRelation = '') + { + // 预载入关联查询 支持嵌套预载入 + $pk = $this->parent->{$this->morphKey}; + $data = (new $model)->with($subRelation)->find($pk); + if ($data) { + $data->setParent(clone $result); + $data->isUpdate(true); + } + $result->setRelation(Loader::parseName($relation), $data ?: null); + } + + /** + * 添加关联数据 + * @access public + * @param Model $model 关联模型对象 + * @return Model + */ + public function associate($model) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + $pk = $model->getPk(); + + $this->parent->setAttr($morphKey, $model->$pk); + $this->parent->setAttr($morphType, get_class($model)); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, $model); + } + + /** + * 注销关联数据 + * @access public + * @return Model + */ + public function dissociate() + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + + $this->parent->setAttr($morphKey, null); + $this->parent->setAttr($morphType, null); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, null); + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + {} +} diff --git a/thinkphp/library/think/model/relation/OneToOne.php b/thinkphp/library/think/model/relation/OneToOne.php new file mode 100644 index 0000000..7877eaa --- /dev/null +++ b/thinkphp/library/think/model/relation/OneToOne.php @@ -0,0 +1,321 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +/** + * Class OneToOne + * @package think\model\relation + * + */ +abstract class OneToOne extends Relation +{ + // 预载入方式 0 -JOIN 1 -IN + protected $eagerlyType = 1; + // 当前关联的JOIN类型 + protected $joinType; + // 要绑定的属性 + protected $bindAttr = []; + // 关联方法名 + protected $relation; + + /** + * 设置join类型 + * @access public + * @param string $type JOIN类型 + * @return $this + */ + public function joinType($type) + { + $this->joinType = $type; + return $this; + } + + /** + * 预载入关联查询(JOIN方式) + * @access public + * @param Query $query 查询对象 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param \Closure $closure 闭包条件 + * @param bool $first + * @return void + */ + public function eagerly(Query $query, $relation, $subRelation, $closure, $first) + { + $name = Loader::parseName(basename(str_replace('\\', '/', $query->getModel()))); + $alias = $name; + if ($first) { + $table = $query->getTable(); + $query->table([$table => $alias]); + if ($query->getOptions('field')) { + $field = $query->getOptions('field'); + $query->removeOption('field'); + } else { + $field = true; + } + $query->field($field, false, $table, $alias); + $field = null; + } + + // 预载入封装 + $joinTable = $this->query->getTable(); + $joinAlias = $relation; + $query->via($joinAlias); + + if ($this instanceof BelongsTo) { + $query->join($joinTable . ' ' . $joinAlias, $alias . '.' . $this->foreignKey . '=' . $joinAlias . '.' . $this->localKey, $this->joinType); + } else { + $query->join($joinTable . ' ' . $joinAlias, $alias . '.' . $this->localKey . '=' . $joinAlias . '.' . $this->foreignKey, $this->joinType); + } + + if ($closure) { + // 执行闭包查询 + call_user_func_array($closure, [ & $query]); + // 使用withField指定获取关联的字段,如 + // $query->where(['id'=>1])->withField('id,name'); + if ($query->getOptions('with_field')) { + $field = $query->getOptions('with_field'); + $query->removeOption('with_field'); + } + } elseif (isset($this->option['field'])) { + $field = $this->option['field']; + } + $query->field(isset($field) ? $field : true, false, $joinTable, $joinAlias, $relation . '__'); + } + + /** + * 预载入关联查询(数据集) + * @param array $resultSet + * @param string $relation + * @param string $subRelation + * @param \Closure $closure + * @return mixed + */ + abstract protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure); + + /** + * 预载入关联查询(数据) + * @param Model $result + * @param string $relation + * @param string $subRelation + * @param \Closure $closure + * @return mixed + */ + abstract protected function eagerlyOne(&$result, $relation, $subRelation, $closure); + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + if (1 == $this->eagerlyType) { + // IN查询 + $this->eagerlySet($resultSet, $relation, $subRelation, $closure); + } else { + // 模型关联组装 + foreach ($resultSet as $result) { + $this->match($this->model, $relation, $result); + } + } + } + + /** + * 预载入关联查询(数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + if (1 == $this->eagerlyType) { + // IN查询 + $this->eagerlyOne($result, $relation, $subRelation, $closure); + } else { + // 模型关联组装 + $this->match($this->model, $relation, $result); + } + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @return Model|false + */ + public function save($data) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + $model = new $this->model; + // 保存关联表数据 + $data[$this->foreignKey] = $this->parent->{$this->localKey}; + return $model->save($data) ? $model : false; + } + + /** + * 设置预载入方式 + * @access public + * @param integer $type 预载入方式 0 JOIN查询 1 IN查询 + * @return $this + */ + public function setEagerlyType($type) + { + $this->eagerlyType = $type; + return $this; + } + + /** + * 获取预载入方式 + * @access public + * @return integer + */ + public function getEagerlyType() + { + return $this->eagerlyType; + } + + /** + * 绑定关联表的属性到父模型属性 + * @access public + * @param mixed $attr 要绑定的属性列表 + * @return $this + */ + public function bind($attr) + { + if (is_string($attr)) { + $attr = explode(',', $attr); + } + $this->bindAttr = $attr; + return $this; + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @return integer + */ + public function relationCount($result, $closure) + { + } + + /** + * 一对一 关联模型预查询拼装 + * @access public + * @param string $model 模型名称 + * @param string $relation 关联名 + * @param Model $result 模型对象实例 + * @return void + */ + protected function match($model, $relation, &$result) + { + // 重新组装模型数据 + foreach ($result->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + if ($name == $relation) { + $list[$name][$attr] = $val; + unset($result->$key); + } + } + } + + if (isset($list[$relation])) { + $relationModel = new $model($list[$relation]); + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + + if (!empty($this->bindAttr)) { + $this->bindAttr($relationModel, $result, $this->bindAttr); + } + } else { + $relationModel = null; + } + $result->setRelation(Loader::parseName($relation), $relationModel); + } + + /** + * 绑定关联属性到父模型 + * @access protected + * @param Model $model 关联模型对象 + * @param Model $result 父模型对象 + * @param array $bindAttr 绑定属性 + * @return void + * @throws Exception + */ + protected function bindAttr($model, &$result, $bindAttr) + { + foreach ($bindAttr as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + if (isset($result->$key)) { + throw new Exception('bind attr has exists:' . $key); + } else { + $result->setAttr($key, $model ? $model->$attr : null); + } + } + } + + /** + * 一对一 关联模型预查询(IN方式) + * @access public + * @param object $model 关联模型对象 + * @param array $where 关联预查询条件 + * @param string $key 关联键名 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param bool|\Closure $closure + * @return array + */ + protected function eagerlyWhere($model, $where, $key, $relation, $subRelation = '', $closure = false) + { + // 预载入关联查询 支持嵌套预载入 + if ($closure) { + call_user_func_array($closure, [ & $model]); + if ($field = $model->getOptions('with_field')) { + $model->field($field)->removeOption('with_field'); + } + } + $list = $model->where($where)->with($subRelation)->select(); + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $data[$set->$key] = $set; + } + return $data; + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + {} +} diff --git a/thinkphp/library/think/paginator/driver/Bootstrap.php b/thinkphp/library/think/paginator/driver/Bootstrap.php new file mode 100644 index 0000000..58fa943 --- /dev/null +++ b/thinkphp/library/think/paginator/driver/Bootstrap.php @@ -0,0 +1,205 @@ + +// +---------------------------------------------------------------------- + +namespace think\paginator\driver; + +use think\Paginator; + +class Bootstrap extends Paginator +{ + + /** + * 上一页按钮 + * @param string $text + * @return string + */ + protected function getPreviousButton($text = "«") + { + + if ($this->currentPage() <= 1) { + return $this->getDisabledTextWrapper($text); + } + + $url = $this->url( + $this->currentPage() - 1 + ); + + return $this->getPageLinkWrapper($url, $text); + } + + /** + * 下一页按钮 + * @param string $text + * @return string + */ + protected function getNextButton($text = '»') + { + if (!$this->hasMore) { + return $this->getDisabledTextWrapper($text); + } + + $url = $this->url($this->currentPage() + 1); + + return $this->getPageLinkWrapper($url, $text); + } + + /** + * 页码按钮 + * @return string + */ + protected function getLinks() + { + if ($this->simple) + return ''; + + $block = [ + 'first' => null, + 'slider' => null, + 'last' => null + ]; + + $side = 3; + $window = $side * 2; + + if ($this->lastPage < $window + 6) { + $block['first'] = $this->getUrlRange(1, $this->lastPage); + } elseif ($this->currentPage <= $window) { + $block['first'] = $this->getUrlRange(1, $window + 2); + $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage); + } elseif ($this->currentPage > ($this->lastPage - $window)) { + $block['first'] = $this->getUrlRange(1, 2); + $block['last'] = $this->getUrlRange($this->lastPage - ($window + 2), $this->lastPage); + } else { + $block['first'] = $this->getUrlRange(1, 2); + $block['slider'] = $this->getUrlRange($this->currentPage - $side, $this->currentPage + $side); + $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage); + } + + $html = ''; + + if (is_array($block['first'])) { + $html .= $this->getUrlLinks($block['first']); + } + + if (is_array($block['slider'])) { + $html .= $this->getDots(); + $html .= $this->getUrlLinks($block['slider']); + } + + if (is_array($block['last'])) { + $html .= $this->getDots(); + $html .= $this->getUrlLinks($block['last']); + } + + return $html; + } + + /** + * 渲染分页html + * @return mixed + */ + public function render() + { + if ($this->hasPages()) { + if ($this->simple) { + return sprintf( + '
      %s %s
    ', + $this->getPreviousButton(), + $this->getNextButton() + ); + } else { + return sprintf( + '
      %s %s %s
    ', + $this->getPreviousButton(), + $this->getLinks(), + $this->getNextButton() + ); + } + } + } + + /** + * 生成一个可点击的按钮 + * + * @param string $url + * @param int $page + * @return string + */ + protected function getAvailablePageWrapper($url, $page) + { + return '
  • ' . $page . '
  • '; + } + + /** + * 生成一个禁用的按钮 + * + * @param string $text + * @return string + */ + protected function getDisabledTextWrapper($text) + { + return '
  • ' . $text . '
  • '; + } + + /** + * 生成一个激活的按钮 + * + * @param string $text + * @return string + */ + protected function getActivePageWrapper($text) + { + return '
  • ' . $text . '
  • '; + } + + /** + * 生成省略号按钮 + * + * @return string + */ + protected function getDots() + { + return $this->getDisabledTextWrapper('...'); + } + + /** + * 批量生成页码按钮. + * + * @param array $urls + * @return string + */ + protected function getUrlLinks(array $urls) + { + $html = ''; + + foreach ($urls as $page => $url) { + $html .= $this->getPageLinkWrapper($url, $page); + } + + return $html; + } + + /** + * 生成普通页码按钮 + * + * @param string $url + * @param int $page + * @return string + */ + protected function getPageLinkWrapper($url, $page) + { + if ($page == $this->currentPage()) { + return $this->getActivePageWrapper($page); + } + + return $this->getAvailablePageWrapper($url, $page); + } +} diff --git a/thinkphp/library/think/process/Builder.php b/thinkphp/library/think/process/Builder.php new file mode 100644 index 0000000..da56163 --- /dev/null +++ b/thinkphp/library/think/process/Builder.php @@ -0,0 +1,233 @@ + +// +---------------------------------------------------------------------- + +namespace think\process; + +use think\Process; + +class Builder +{ + private $arguments; + private $cwd; + private $env = null; + private $input; + private $timeout = 60; + private $options = []; + private $inheritEnv = true; + private $prefix = []; + private $outputDisabled = false; + + /** + * 构造方法 + * @param string[] $arguments 参数 + */ + public function __construct(array $arguments = []) + { + $this->arguments = $arguments; + } + + /** + * 创建一个实例 + * @param string[] $arguments 参数 + * @return self + */ + public static function create(array $arguments = []) + { + return new static($arguments); + } + + /** + * 添加一个参数 + * @param string $argument 参数 + * @return self + */ + public function add($argument) + { + $this->arguments[] = $argument; + + return $this; + } + + /** + * 添加一个前缀 + * @param string|array $prefix + * @return self + */ + public function setPrefix($prefix) + { + $this->prefix = is_array($prefix) ? $prefix : [$prefix]; + + return $this; + } + + /** + * 设置参数 + * @param string[] $arguments + * @return self + */ + public function setArguments(array $arguments) + { + $this->arguments = $arguments; + + return $this; + } + + /** + * 设置工作目录 + * @param null|string $cwd + * @return self + */ + public function setWorkingDirectory($cwd) + { + $this->cwd = $cwd; + + return $this; + } + + /** + * 是否初始化环境变量 + * @param bool $inheritEnv + * @return self + */ + public function inheritEnvironmentVariables($inheritEnv = true) + { + $this->inheritEnv = $inheritEnv; + + return $this; + } + + /** + * 设置环境变量 + * @param string $name + * @param null|string $value + * @return self + */ + public function setEnv($name, $value) + { + $this->env[$name] = $value; + + return $this; + } + + /** + * 添加环境变量 + * @param array $variables + * @return self + */ + public function addEnvironmentVariables(array $variables) + { + $this->env = array_replace($this->env, $variables); + + return $this; + } + + /** + * 设置输入 + * @param mixed $input + * @return self + */ + public function setInput($input) + { + $this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input); + + return $this; + } + + /** + * 设置超时时间 + * @param float|null $timeout + * @return self + */ + public function setTimeout($timeout) + { + if (null === $timeout) { + $this->timeout = null; + + return $this; + } + + $timeout = (float) $timeout; + + if ($timeout < 0) { + throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + $this->timeout = $timeout; + + return $this; + } + + /** + * 设置proc_open选项 + * @param string $name + * @param string $value + * @return self + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + + return $this; + } + + /** + * 禁止输出 + * @return self + */ + public function disableOutput() + { + $this->outputDisabled = true; + + return $this; + } + + /** + * 开启输出 + * @return self + */ + public function enableOutput() + { + $this->outputDisabled = false; + + return $this; + } + + /** + * 创建一个Process实例 + * @return Process + */ + public function getProcess() + { + if (0 === count($this->prefix) && 0 === count($this->arguments)) { + throw new \LogicException('You must add() command arguments before calling getProcess().'); + } + + $options = $this->options; + + $arguments = array_merge($this->prefix, $this->arguments); + $script = implode(' ', array_map([__NAMESPACE__ . '\\Utils', 'escapeArgument'], $arguments)); + + if ($this->inheritEnv) { + // include $_ENV for BC purposes + $env = array_replace($_ENV, $_SERVER, $this->env); + } else { + $env = $this->env; + } + + $process = new Process($script, $this->cwd, $env, $this->input, $this->timeout, $options); + + if ($this->outputDisabled) { + $process->disableOutput(); + } + + return $process; + } +} diff --git a/thinkphp/library/think/process/Utils.php b/thinkphp/library/think/process/Utils.php new file mode 100644 index 0000000..f94c648 --- /dev/null +++ b/thinkphp/library/think/process/Utils.php @@ -0,0 +1,75 @@ + +// +---------------------------------------------------------------------- + +namespace think\process; + +class Utils +{ + + /** + * 转义字符串 + * @param string $argument + * @return string + */ + public static function escapeArgument($argument) + { + + if ('' === $argument) { + return escapeshellarg($argument); + } + $escapedArgument = ''; + $quote = false; + foreach (preg_split('/(")/i', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { + if ('"' === $part) { + $escapedArgument .= '\\"'; + } elseif (self::isSurroundedBy($part, '%')) { + // Avoid environment variable expansion + $escapedArgument .= '^%"' . substr($part, 1, -1) . '"^%'; + } else { + // escape trailing backslash + if ('\\' === substr($part, -1)) { + $part .= '\\'; + } + $quote = true; + $escapedArgument .= $part; + } + } + if ($quote) { + $escapedArgument = '"' . $escapedArgument . '"'; + } + return $escapedArgument; + } + + /** + * 验证并进行规范化Process输入。 + * @param string $caller + * @param mixed $input + * @return string + * @throws \InvalidArgumentException + */ + public static function validateInput($caller, $input) + { + if (null !== $input) { + if (is_resource($input)) { + return $input; + } + if (is_scalar($input)) { + return (string) $input; + } + throw new \InvalidArgumentException(sprintf('%s only accepts strings or stream resources.', $caller)); + } + return $input; + } + + private static function isSurroundedBy($arg, $char) + { + return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1]; + } + +} diff --git a/thinkphp/library/think/process/exception/Failed.php b/thinkphp/library/think/process/exception/Failed.php new file mode 100644 index 0000000..5295082 --- /dev/null +++ b/thinkphp/library/think/process/exception/Failed.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\exception; + +use think\Process; + +class Failed extends \RuntimeException +{ + + private $process; + + public function __construct(Process $process) + { + if ($process->isSuccessful()) { + throw new \InvalidArgumentException('Expected a failed process, but the given process was successful.'); + } + + $error = sprintf('The command "%s" failed.' . "\nExit Code: %s(%s)", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText()); + + if (!$process->isOutputDisabled()) { + $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput()); + } + + parent::__construct($error); + + $this->process = $process; + } + + public function getProcess() + { + return $this->process; + } +} diff --git a/thinkphp/library/think/process/exception/Timeout.php b/thinkphp/library/think/process/exception/Timeout.php new file mode 100644 index 0000000..d5f1162 --- /dev/null +++ b/thinkphp/library/think/process/exception/Timeout.php @@ -0,0 +1,61 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\exception; + +use think\Process; + +class Timeout extends \RuntimeException +{ + + const TYPE_GENERAL = 1; + const TYPE_IDLE = 2; + + private $process; + private $timeoutType; + + public function __construct(Process $process, $timeoutType) + { + $this->process = $process; + $this->timeoutType = $timeoutType; + + parent::__construct(sprintf('The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), $this->getExceededTimeout())); + } + + public function getProcess() + { + return $this->process; + } + + public function isGeneralTimeout() + { + return $this->timeoutType === self::TYPE_GENERAL; + } + + public function isIdleTimeout() + { + return $this->timeoutType === self::TYPE_IDLE; + } + + public function getExceededTimeout() + { + switch ($this->timeoutType) { + case self::TYPE_GENERAL: + return $this->process->getTimeout(); + + case self::TYPE_IDLE: + return $this->process->getIdleTimeout(); + + default: + throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)); + } + } +} diff --git a/thinkphp/library/think/process/pipes/Pipes.php b/thinkphp/library/think/process/pipes/Pipes.php new file mode 100644 index 0000000..82396b8 --- /dev/null +++ b/thinkphp/library/think/process/pipes/Pipes.php @@ -0,0 +1,93 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\pipes; + +abstract class Pipes +{ + + /** @var array */ + public $pipes = []; + + /** @var string */ + protected $inputBuffer = ''; + /** @var resource|null */ + protected $input; + + /** @var bool */ + private $blocked = true; + + const CHUNK_SIZE = 16384; + + /** + * 返回用于 proc_open 描述符的数组 + * @return array + */ + abstract public function getDescriptors(); + + /** + * 返回一个数组的索引由其相关的流,以防这些管道使用的临时文件的文件名。 + * @return string[] + */ + abstract public function getFiles(); + + /** + * 文件句柄和管道中读取数据。 + * @param bool $blocking 是否使用阻塞调用 + * @param bool $close 是否要关闭管道,如果他们已经到达 EOF。 + * @return string[] + */ + abstract public function readAndWrite($blocking, $close = false); + + /** + * 返回当前状态如果有打开的文件句柄或管道。 + * @return bool + */ + abstract public function areOpen(); + + /** + * {@inheritdoc} + */ + public function close() + { + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + $this->pipes = []; + } + + /** + * 检查系统调用已被中断 + * @return bool + */ + protected function hasSystemCallBeenInterrupted() + { + $lastError = error_get_last(); + + return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); + } + + protected function unblock() + { + if (!$this->blocked) { + return; + } + + foreach ($this->pipes as $pipe) { + stream_set_blocking($pipe, 0); + } + if (null !== $this->input) { + stream_set_blocking($this->input, 0); + } + + $this->blocked = false; + } +} diff --git a/thinkphp/library/think/process/pipes/Unix.php b/thinkphp/library/think/process/pipes/Unix.php new file mode 100644 index 0000000..fd99a5d --- /dev/null +++ b/thinkphp/library/think/process/pipes/Unix.php @@ -0,0 +1,196 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\pipes; + +use think\Process; + +class Unix extends Pipes +{ + + /** @var bool */ + private $ttyMode; + /** @var bool */ + private $ptyMode; + /** @var bool */ + private $disableOutput; + + public function __construct($ttyMode, $ptyMode, $input, $disableOutput) + { + $this->ttyMode = (bool) $ttyMode; + $this->ptyMode = (bool) $ptyMode; + $this->disableOutput = (bool) $disableOutput; + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = (string) $input; + } + } + + public function __destruct() + { + $this->close(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('/dev/null', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + if ($this->ttyMode) { + return [ + ['file', '/dev/tty', 'r'], + ['file', '/dev/tty', 'w'], + ['file', '/dev/tty', 'w'], + ]; + } + + if ($this->ptyMode && Process::isPtySupported()) { + return [ + ['pty'], + ['pty'], + ['pty'], + ]; + } + + return [ + ['pipe', 'r'], + ['pipe', 'w'], // stdout + ['pipe', 'w'], // stderr + ]; + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return []; + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + + if (1 === count($this->pipes) && [0] === array_keys($this->pipes)) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + if (empty($this->pipes)) { + return []; + } + + $this->unblock(); + + $read = []; + + if (null !== $this->input) { + $r = array_merge($this->pipes, ['input' => $this->input]); + } else { + $r = $this->pipes; + } + + unset($r[0]); + + $w = isset($this->pipes[0]) ? [$this->pipes[0]] : null; + $e = null; + + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = []; + } + + return $read; + } + + if (0 === $n) { + return $read; + } + + foreach ($r as $pipe) { + + $type = (false !== $found = array_search($pipe, $this->pipes)) ? $found : 'input'; + $data = ''; + while ('' !== $dataread = (string) fread($pipe, self::CHUNK_SIZE)) { + $data .= $dataread; + } + + if ('' !== $data) { + if ('input' === $type) { + $this->inputBuffer .= $data; + } else { + $read[$type] = $data; + } + } + + if (false === $data || (true === $close && feof($pipe) && '' === $data)) { + if ('input' === $type) { + $this->input = null; + } else { + fclose($this->pipes[$type]); + unset($this->pipes[$type]); + } + } + } + + if (null !== $w && 0 < count($w)) { + while (strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2 << 18); // write 512k + if ($written > 0) { + $this->inputBuffer = (string) substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool) $this->pipes; + } + + /** + * 创建一个新的 UnixPipes 实例 + * @param Process $process + * @param string|resource $input + * @return self + */ + public static function create(Process $process, $input) + { + return new static($process->isTty(), $process->isPty(), $input, $process->isOutputDisabled()); + } +} diff --git a/thinkphp/library/think/process/pipes/Windows.php b/thinkphp/library/think/process/pipes/Windows.php new file mode 100644 index 0000000..bba7e9b --- /dev/null +++ b/thinkphp/library/think/process/pipes/Windows.php @@ -0,0 +1,228 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\pipes; + +use think\Process; + +class Windows extends Pipes +{ + + /** @var array */ + private $files = []; + /** @var array */ + private $fileHandles = []; + /** @var array */ + private $readBytes = [ + Process::STDOUT => 0, + Process::STDERR => 0, + ]; + /** @var bool */ + private $disableOutput; + + public function __construct($disableOutput, $input) + { + $this->disableOutput = (bool) $disableOutput; + + if (!$this->disableOutput) { + + $this->files = [ + Process::STDOUT => tempnam(sys_get_temp_dir(), 'sf_proc_stdout'), + Process::STDERR => tempnam(sys_get_temp_dir(), 'sf_proc_stderr'), + ]; + foreach ($this->files as $offset => $file) { + $this->fileHandles[$offset] = fopen($this->files[$offset], 'rb'); + if (false === $this->fileHandles[$offset]) { + throw new \RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); + } + } + } + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = $input; + } + } + + public function __destruct() + { + $this->close(); + $this->removeFiles(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('NUL', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + return [ + ['pipe', 'r'], + ['file', 'NUL', 'w'], + ['file', 'NUL', 'w'], + ]; + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return $this->files; + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + $this->write($blocking, $close); + + $read = []; + $fh = $this->fileHandles; + foreach ($fh as $type => $fileHandle) { + if (0 !== fseek($fileHandle, $this->readBytes[$type])) { + continue; + } + $data = ''; + $dataread = null; + while (!feof($fileHandle)) { + if (false !== $dataread = fread($fileHandle, self::CHUNK_SIZE)) { + $data .= $dataread; + } + } + if (0 < $length = strlen($data)) { + $this->readBytes[$type] += $length; + $read[$type] = $data; + } + + if (false === $dataread || (true === $close && feof($fileHandle) && '' === $data)) { + fclose($this->fileHandles[$type]); + unset($this->fileHandles[$type]); + } + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool) $this->pipes && (bool) $this->fileHandles; + } + + /** + * {@inheritdoc} + */ + public function close() + { + parent::close(); + foreach ($this->fileHandles as $handle) { + fclose($handle); + } + $this->fileHandles = []; + } + + /** + * 创建一个新的 WindowsPipes 实例。 + * @param Process $process + * @param $input + * @return self + */ + public static function create(Process $process, $input) + { + return new static($process->isOutputDisabled(), $input); + } + + /** + * 删除临时文件 + */ + private function removeFiles() + { + foreach ($this->files as $filename) { + if (file_exists($filename)) { + @unlink($filename); + } + } + $this->files = []; + } + + /** + * 写入到 stdin 输入 + * @param bool $blocking + * @param bool $close + */ + private function write($blocking, $close) + { + if (empty($this->pipes)) { + return; + } + + $this->unblock(); + + $r = null !== $this->input ? ['input' => $this->input] : null; + $w = isset($this->pipes[0]) ? [$this->pipes[0]] : null; + $e = null; + + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = []; + } + + return; + } + + if (0 === $n) { + return; + } + + if (null !== $w && 0 < count($r)) { + $data = ''; + while ($dataread = fread($r['input'], self::CHUNK_SIZE)) { + $data .= $dataread; + } + + $this->inputBuffer .= $data; + + if (false === $data || (true === $close && feof($r['input']) && '' === $data)) { + $this->input = null; + } + } + + if (null !== $w && 0 < count($w)) { + while (strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2 << 18); + if ($written > 0) { + $this->inputBuffer = (string) substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + } +} diff --git a/thinkphp/library/think/response/Json.php b/thinkphp/library/think/response/Json.php new file mode 100644 index 0000000..538fc5a --- /dev/null +++ b/thinkphp/library/think/response/Json.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class Json extends Response +{ + // 输出参数 + protected $options = [ + 'json_encode_param' => JSON_UNESCAPED_UNICODE, + ]; + + protected $contentType = 'application/json'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + * @throws \Exception + */ + protected function output($data) + { + try { + // 返回JSON数据格式到客户端 包含状态信息 + $data = json_encode($data, $this->options['json_encode_param']); + + if ($data === false) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + return $data; + } catch (\Exception $e) { + if ($e->getPrevious()) { + throw $e->getPrevious(); + } + throw $e; + } + } + +} diff --git a/thinkphp/library/think/response/Jsonp.php b/thinkphp/library/think/response/Jsonp.php new file mode 100644 index 0000000..de8fb30 --- /dev/null +++ b/thinkphp/library/think/response/Jsonp.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Request; +use think\Response; + +class Jsonp extends Response +{ + // 输出参数 + protected $options = [ + 'var_jsonp_handler' => 'callback', + 'default_jsonp_handler' => 'jsonpReturn', + 'json_encode_param' => JSON_UNESCAPED_UNICODE, + ]; + + protected $contentType = 'application/javascript'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + * @throws \Exception + */ + protected function output($data) + { + try { + // 返回JSON数据格式到客户端 包含状态信息 [当url_common_param为false时是无法获取到$_GET的数据的,故使用Request来获取] + $var_jsonp_handler = Request::instance()->param($this->options['var_jsonp_handler'], ""); + $handler = !empty($var_jsonp_handler) ? $var_jsonp_handler : $this->options['default_jsonp_handler']; + + $data = json_encode($data, $this->options['json_encode_param']); + + if ($data === false) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + $data = $handler . '(' . $data . ');'; + return $data; + } catch (\Exception $e) { + if ($e->getPrevious()) { + throw $e->getPrevious(); + } + throw $e; + } + } + +} diff --git a/thinkphp/library/think/response/Redirect.php b/thinkphp/library/think/response/Redirect.php new file mode 100644 index 0000000..0ea90a4 --- /dev/null +++ b/thinkphp/library/think/response/Redirect.php @@ -0,0 +1,105 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Request; +use think\Response; +use think\Session; +use think\Url; + +class Redirect extends Response +{ + + protected $options = []; + + // URL参数 + protected $params = []; + + public function __construct($data = '', $code = 302, array $header = [], array $options = []) + { + parent::__construct($data, $code, $header, $options); + $this->cacheControl('no-cache,must-revalidate'); + } + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + $this->header['Location'] = $this->getTargetUrl(); + return; + } + + /** + * 重定向传值(通过Session) + * @access protected + * @param string|array $name 变量名或者数组 + * @param mixed $value 值 + * @return $this + */ + public function with($name, $value = null) + { + if (is_array($name)) { + foreach ($name as $key => $val) { + Session::flash($key, $val); + } + } else { + Session::flash($name, $value); + } + return $this; + } + + /** + * 获取跳转地址 + * @return string + */ + public function getTargetUrl() + { + if (strpos($this->data, '://') || (0 === strpos($this->data, '/') && empty($this->params))) { + return $this->data; + } else { + return Url::build($this->data, $this->params); + } + } + + public function params($params = []) + { + $this->params = $params; + return $this; + } + + /** + * 记住当前url后跳转 + * @return $this + */ + public function remember() + { + Session::set('redirect_url', Request::instance()->url()); + return $this; + } + + /** + * 跳转到上次记住的url + * @return $this + */ + public function restore() + { + if (Session::has('redirect_url')) { + $this->data = Session::get('redirect_url'); + Session::delete('redirect_url'); + } + return $this; + } +} diff --git a/thinkphp/library/think/response/View.php b/thinkphp/library/think/response/View.php new file mode 100644 index 0000000..de75515 --- /dev/null +++ b/thinkphp/library/think/response/View.php @@ -0,0 +1,89 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Config; +use think\Response; +use think\View as ViewTemplate; + +class View extends Response +{ + // 输出参数 + protected $options = []; + protected $vars = []; + protected $replace = []; + protected $contentType = 'text/html'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + // 渲染模板输出 + return ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str')) + ->fetch($data, $this->vars, $this->replace); + } + + /** + * 获取视图变量 + * @access public + * @param string $name 模板变量 + * @return mixed + */ + public function getVars($name = null) + { + if (is_null($name)) { + return $this->vars; + } else { + return isset($this->vars[$name]) ? $this->vars[$name] : null; + } + } + + /** + * 模板变量赋值 + * @access public + * @param mixed $name 变量名 + * @param mixed $value 变量值 + * @return $this + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->vars = array_merge($this->vars, $name); + return $this; + } else { + $this->vars[$name] = $value; + } + return $this; + } + + /** + * 视图内容替换 + * @access public + * @param string|array $content 被替换内容(支持批量替换) + * @param string $replace 替换内容 + * @return $this + */ + public function replace($content, $replace = '') + { + if (is_array($content)) { + $this->replace = array_merge($this->replace, $content); + } else { + $this->replace[$content] = $replace; + } + return $this; + } + +} diff --git a/thinkphp/library/think/response/Xml.php b/thinkphp/library/think/response/Xml.php new file mode 100644 index 0000000..87479be --- /dev/null +++ b/thinkphp/library/think/response/Xml.php @@ -0,0 +1,102 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Collection; +use think\Model; +use think\Response; + +class Xml extends Response +{ + // 输出参数 + protected $options = [ + // 根节点名 + 'root_node' => 'think', + // 根节点属性 + 'root_attr' => '', + //数字索引的子节点名 + 'item_node' => 'item', + // 数字索引子节点key转换的属性名 + 'item_key' => 'id', + // 数据编码 + 'encoding' => 'utf-8', + ]; + + protected $contentType = 'text/xml'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + // XML数据转换 + return $this->xmlEncode($data, $this->options['root_node'], $this->options['item_node'], $this->options['root_attr'], $this->options['item_key'], $this->options['encoding']); + } + + /** + * XML编码 + * @param mixed $data 数据 + * @param string $root 根节点名 + * @param string $item 数字索引的子节点名 + * @param string $attr 根节点属性 + * @param string $id 数字索引子节点key转换的属性名 + * @param string $encoding 数据编码 + * @return string + */ + protected function xmlEncode($data, $root, $item, $attr, $id, $encoding) + { + if (is_array($attr)) { + $array = []; + foreach ($attr as $key => $value) { + $array[] = "{$key}=\"{$value}\""; + } + $attr = implode(' ', $array); + } + $attr = trim($attr); + $attr = empty($attr) ? '' : " {$attr}"; + $xml = ""; + $xml .= "<{$root}{$attr}>"; + $xml .= $this->dataToXml($data, $item, $id); + $xml .= ""; + return $xml; + } + + /** + * 数据XML编码 + * @param mixed $data 数据 + * @param string $item 数字索引时的节点名称 + * @param string $id 数字索引key转换为的属性名 + * @return string + */ + protected function dataToXml($data, $item, $id) + { + $xml = $attr = ''; + + if ($data instanceof Collection || $data instanceof Model) { + $data = $data->toArray(); + } + + foreach ($data as $key => $val) { + if (is_numeric($key)) { + $id && $attr = " {$id}=\"{$key}\""; + $key = $item; + } + $xml .= "<{$key}{$attr}>"; + $xml .= (is_array($val) || is_object($val)) ? $this->dataToXml($val, $item, $id) : $val; + $xml .= ""; + } + return $xml; + } +} diff --git a/thinkphp/library/think/session/driver/Memcache.php b/thinkphp/library/think/session/driver/Memcache.php new file mode 100644 index 0000000..0c02e23 --- /dev/null +++ b/thinkphp/library/think/session/driver/Memcache.php @@ -0,0 +1,118 @@ + +// +---------------------------------------------------------------------- + +namespace think\session\driver; + +use SessionHandler; +use think\Exception; + +class Memcache extends SessionHandler +{ + protected $handler = null; + protected $config = [ + 'host' => '127.0.0.1', // memcache主机 + 'port' => 11211, // memcache端口 + 'expire' => 3600, // session有效期 + 'timeout' => 0, // 连接超时时间(单位:毫秒) + 'persistent' => true, // 长连接 + 'session_name' => '', // memcache key前缀 + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 打开Session + * @access public + * @param string $savePath + * @param mixed $sessName + */ + public function open($savePath, $sessName) + { + // 检测php环境 + if (!extension_loaded('memcache')) { + throw new Exception('not support:memcache'); + } + $this->handler = new \Memcache; + // 支持集群 + $hosts = explode(',', $this->config['host']); + $ports = explode(',', $this->config['port']); + if (empty($ports[0])) { + $ports[0] = 11211; + } + // 建立连接 + foreach ((array) $hosts as $i => $host) { + $port = isset($ports[$i]) ? $ports[$i] : $ports[0]; + $this->config['timeout'] > 0 ? + $this->handler->addServer($host, $port, $this->config['persistent'], 1, $this->config['timeout']) : + $this->handler->addServer($host, $port, $this->config['persistent'], 1); + } + return true; + } + + /** + * 关闭Session + * @access public + */ + public function close() + { + $this->gc(ini_get('session.gc_maxlifetime')); + $this->handler->close(); + $this->handler = null; + return true; + } + + /** + * 读取Session + * @access public + * @param string $sessID + */ + public function read($sessID) + { + return (string) $this->handler->get($this->config['session_name'] . $sessID); + } + + /** + * 写入Session + * @access public + * @param string $sessID + * @param String $sessData + * @return bool + */ + public function write($sessID, $sessData) + { + return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']); + } + + /** + * 删除Session + * @access public + * @param string $sessID + * @return bool + */ + public function destroy($sessID) + { + return $this->handler->delete($this->config['session_name'] . $sessID); + } + + /** + * Session 垃圾回收 + * @access public + * @param string $sessMaxLifeTime + * @return true + */ + public function gc($sessMaxLifeTime) + { + return true; + } +} diff --git a/thinkphp/library/think/session/driver/Memcached.php b/thinkphp/library/think/session/driver/Memcached.php new file mode 100644 index 0000000..bcaf8a1 --- /dev/null +++ b/thinkphp/library/think/session/driver/Memcached.php @@ -0,0 +1,126 @@ + +// +---------------------------------------------------------------------- + +namespace think\session\driver; + +use SessionHandler; +use think\Exception; + +class Memcached extends SessionHandler +{ + protected $handler = null; + protected $config = [ + 'host' => '127.0.0.1', // memcache主机 + 'port' => 11211, // memcache端口 + 'expire' => 3600, // session有效期 + 'timeout' => 0, // 连接超时时间(单位:毫秒) + 'session_name' => '', // memcache key前缀 + 'username' => '', //账号 + 'password' => '', //密码 + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 打开Session + * @access public + * @param string $savePath + * @param mixed $sessName + */ + public function open($savePath, $sessName) + { + // 检测php环境 + if (!extension_loaded('memcached')) { + throw new Exception('not support:memcached'); + } + $this->handler = new \Memcached; + // 设置连接超时时间(单位:毫秒) + if ($this->config['timeout'] > 0) { + $this->handler->setOption(\Memcached::OPT_CONNECT_TIMEOUT, $this->config['timeout']); + } + // 支持集群 + $hosts = explode(',', $this->config['host']); + $ports = explode(',', $this->config['port']); + if (empty($ports[0])) { + $ports[0] = 11211; + } + // 建立连接 + $servers = []; + foreach ((array) $hosts as $i => $host) { + $servers[] = [$host, (isset($ports[$i]) ? $ports[$i] : $ports[0]), 1]; + } + $this->handler->addServers($servers); + if ('' != $this->config['username']) { + $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $this->handler->setSaslAuthData($this->config['username'], $this->config['password']); + } + return true; + } + + /** + * 关闭Session + * @access public + */ + public function close() + { + $this->gc(ini_get('session.gc_maxlifetime')); + $this->handler->quit(); + $this->handler = null; + return true; + } + + /** + * 读取Session + * @access public + * @param string $sessID + */ + public function read($sessID) + { + return (string) $this->handler->get($this->config['session_name'] . $sessID); + } + + /** + * 写入Session + * @access public + * @param string $sessID + * @param String $sessData + * @return bool + */ + public function write($sessID, $sessData) + { + return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']); + } + + /** + * 删除Session + * @access public + * @param string $sessID + * @return bool + */ + public function destroy($sessID) + { + return $this->handler->delete($this->config['session_name'] . $sessID); + } + + /** + * Session 垃圾回收 + * @access public + * @param string $sessMaxLifeTime + * @return true + */ + public function gc($sessMaxLifeTime) + { + return true; + } +} diff --git a/thinkphp/library/think/session/driver/Redis.php b/thinkphp/library/think/session/driver/Redis.php new file mode 100644 index 0000000..a4c2b54 --- /dev/null +++ b/thinkphp/library/think/session/driver/Redis.php @@ -0,0 +1,128 @@ + +// +---------------------------------------------------------------------- + +namespace think\session\driver; + +use SessionHandler; +use think\Exception; + +class Redis extends SessionHandler +{ + /** @var \Redis */ + protected $handler = null; + protected $config = [ + 'host' => '127.0.0.1', // redis主机 + 'port' => 6379, // redis端口 + 'password' => '', // 密码 + 'select' => 0, // 操作库 + 'expire' => 3600, // 有效期(秒) + 'timeout' => 0, // 超时时间(秒) + 'persistent' => true, // 是否长连接 + 'session_name' => '', // sessionkey前缀 + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 打开Session + * @access public + * @param string $savePath + * @param mixed $sessName + * @return bool + * @throws Exception + */ + public function open($savePath, $sessName) + { + // 检测php环境 + if (!extension_loaded('redis')) { + throw new Exception('not support:redis'); + } + $this->handler = new \Redis; + + // 建立连接 + $func = $this->config['persistent'] ? 'pconnect' : 'connect'; + $this->handler->$func($this->config['host'], $this->config['port'], $this->config['timeout']); + + if ('' != $this->config['password']) { + $this->handler->auth($this->config['password']); + } + + if (0 != $this->config['select']) { + $this->handler->select($this->config['select']); + } + + return true; + } + + /** + * 关闭Session + * @access public + */ + public function close() + { + $this->gc(ini_get('session.gc_maxlifetime')); + $this->handler->close(); + $this->handler = null; + return true; + } + + /** + * 读取Session + * @access public + * @param string $sessID + * @return string + */ + public function read($sessID) + { + return (string) $this->handler->get($this->config['session_name'] . $sessID); + } + + /** + * 写入Session + * @access public + * @param string $sessID + * @param String $sessData + * @return bool + */ + public function write($sessID, $sessData) + { + if ($this->config['expire'] > 0) { + return $this->handler->setex($this->config['session_name'] . $sessID, $this->config['expire'], $sessData); + } else { + return $this->handler->set($this->config['session_name'] . $sessID, $sessData); + } + } + + /** + * 删除Session + * @access public + * @param string $sessID + * @return bool + */ + public function destroy($sessID) + { + return $this->handler->delete($this->config['session_name'] . $sessID) > 0; + } + + /** + * Session 垃圾回收 + * @access public + * @param string $sessMaxLifeTime + * @return bool + */ + public function gc($sessMaxLifeTime) + { + return true; + } +} diff --git a/thinkphp/library/think/template/TagLib.php b/thinkphp/library/think/template/TagLib.php new file mode 100644 index 0000000..d343bed --- /dev/null +++ b/thinkphp/library/think/template/TagLib.php @@ -0,0 +1,334 @@ + +// +---------------------------------------------------------------------- + +namespace think\template; + +use think\Exception; + +/** + * ThinkPHP标签库TagLib解析基类 + * @category Think + * @package Think + * @subpackage Template + * @author liu21st + */ +class TagLib +{ + + /** + * 标签库定义XML文件 + * @var string + * @access protected + */ + protected $xml = ''; + protected $tags = []; // 标签定义 + /** + * 标签库名称 + * @var string + * @access protected + */ + protected $tagLib = ''; + + /** + * 标签库标签列表 + * @var array + * @access protected + */ + protected $tagList = []; + + /** + * 标签库分析数组 + * @var array + * @access protected + */ + protected $parse = []; + + /** + * 标签库是否有效 + * @var bool + * @access protected + */ + protected $valid = false; + + /** + * 当前模板对象 + * @var object + * @access protected + */ + protected $tpl; + + protected $comparison = [' nheq ' => ' !== ', ' heq ' => ' === ', ' neq ' => ' != ', ' eq ' => ' == ', ' egt ' => ' >= ', ' gt ' => ' > ', ' elt ' => ' <= ', ' lt ' => ' < ']; + + /** + * 构造函数 + * @access public + * @param \stdClass $template 模板引擎对象 + */ + public function __construct($template) + { + $this->tpl = $template; + } + + /** + * 按签标库替换页面中的标签 + * @access public + * @param string $content 模板内容 + * @param string $lib 标签库名 + * @return void + */ + public function parseTag(&$content, $lib = '') + { + $tags = []; + $lib = $lib ? strtolower($lib) . ':' : ''; + foreach ($this->tags as $name => $val) { + $close = !isset($val['close']) || $val['close'] ? 1 : 0; + $tags[$close][$lib . $name] = $name; + if (isset($val['alias'])) { + // 别名设置 + $array = (array) $val['alias']; + foreach (explode(',', $array[0]) as $v) { + $tags[$close][$lib . $v] = $name; + } + } + } + + // 闭合标签 + if (!empty($tags[1])) { + $nodes = []; + $regex = $this->getRegex(array_keys($tags[1]), 1); + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + $right = []; + foreach ($matches as $match) { + if ('' == $match[1][0]) { + $name = strtolower($match[2][0]); + // 如果有没闭合的标签头则取出最后一个 + if (!empty($right[$name])) { + // $match[0][1]为标签结束符在模板中的位置 + $nodes[$match[0][1]] = [ + 'name' => $name, + 'begin' => array_pop($right[$name]), // 标签开始符 + 'end' => $match[0], // 标签结束符 + ]; + } + } else { + // 标签头压入栈 + $right[strtolower($match[1][0])][] = $match[0]; + } + } + unset($right, $matches); + // 按标签在模板中的位置从后向前排序 + krsort($nodes); + } + + $break = ''; + if ($nodes) { + $beginArray = []; + // 标签替换 从后向前 + foreach ($nodes as $pos => $node) { + // 对应的标签名 + $name = $tags[1][$node['name']]; + $alias = $lib . $name != $node['name'] ? ($lib ? strstr($node['name'], $lib) : $node['name']) : ''; + // 解析标签属性 + $attrs = $this->parseAttr($node['begin'][0], $name, $alias); + $method = 'tag' . $name; + // 读取标签库中对应的标签内容 replace[0]用来替换标签头,replace[1]用来替换标签尾 + $replace = explode($break, $this->$method($attrs, $break)); + if (count($replace) > 1) { + while ($beginArray) { + $begin = end($beginArray); + // 判断当前标签尾的位置是否在栈中最后一个标签头的后面,是则为子标签 + if ($node['end'][1] > $begin['pos']) { + break; + } else { + // 不为子标签时,取出栈中最后一个标签头 + $begin = array_pop($beginArray); + // 替换标签头部 + $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']); + } + } + // 替换标签尾部 + $content = substr_replace($content, $replace[1], $node['end'][1], strlen($node['end'][0])); + // 把标签头压入栈 + $beginArray[] = ['pos' => $node['begin'][1], 'len' => strlen($node['begin'][0]), 'str' => $replace[0]]; + } + } + while ($beginArray) { + $begin = array_pop($beginArray); + // 替换标签头部 + $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']); + } + } + } + // 自闭合标签 + if (!empty($tags[0])) { + $regex = $this->getRegex(array_keys($tags[0]), 0); + $content = preg_replace_callback($regex, function ($matches) use (&$tags, &$lib) { + // 对应的标签名 + $name = $tags[0][strtolower($matches[1])]; + $alias = $lib . $name != $matches[1] ? ($lib ? strstr($matches[1], $lib) : $matches[1]) : ''; + // 解析标签属性 + $attrs = $this->parseAttr($matches[0], $name, $alias); + $method = 'tag' . $name; + return $this->$method($attrs, ''); + }, $content); + } + return; + } + + /** + * 按标签生成正则 + * @access private + * @param array|string $tags 标签名 + * @param boolean $close 是否为闭合标签 + * @return string + */ + public function getRegex($tags, $close) + { + $begin = $this->tpl->config('taglib_begin'); + $end = $this->tpl->config('taglib_end'); + $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false; + $tagName = is_array($tags) ? implode('|', $tags) : $tags; + if ($single) { + if ($close) { + // 如果是闭合标签 + $regex = $begin . '(?:(' . $tagName . ')\b(?>[^' . $end . ']*)|\/(' . $tagName . '))' . $end; + } else { + $regex = $begin . '(' . $tagName . ')\b(?>[^' . $end . ']*)' . $end; + } + } else { + if ($close) { + // 如果是闭合标签 + $regex = $begin . '(?:(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)|\/(' . $tagName . '))' . $end; + } else { + $regex = $begin . '(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)' . $end; + } + } + return '/' . $regex . '/is'; + } + + /** + * 分析标签属性 正则方式 + * @access public + * @param string $str 标签属性字符串 + * @param string $name 标签名 + * @param string $alias 别名 + * @return array + */ + public function parseAttr($str, $name, $alias = '') + { + $regex = '/\s+(?>(?P[\w-]+)\s*)=(?>\s*)([\"\'])(?P(?:(?!\\2).)*)\\2/is'; + $result = []; + if (preg_match_all($regex, $str, $matches)) { + foreach ($matches['name'] as $key => $val) { + $result[$val] = $matches['value'][$key]; + } + if (!isset($this->tags[$name])) { + // 检测是否存在别名定义 + foreach ($this->tags as $key => $val) { + if (isset($val['alias'])) { + $array = (array) $val['alias']; + if (in_array($name, explode(',', $array[0]))) { + $tag = $val; + $type = !empty($array[1]) ? $array[1] : 'type'; + $result[$type] = $name; + break; + } + } + } + } else { + $tag = $this->tags[$name]; + // 设置了标签别名 + if (!empty($alias) && isset($tag['alias'])) { + $type = !empty($tag['alias'][1]) ? $tag['alias'][1] : 'type'; + $result[$type] = $alias; + } + } + if (!empty($tag['must'])) { + $must = explode(',', $tag['must']); + foreach ($must as $name) { + if (!isset($result[$name])) { + throw new Exception('tag attr must:' . $name); + } + } + } + } else { + // 允许直接使用表达式的标签 + if (!empty($this->tags[$name]['expression'])) { + static $_taglibs; + if (!isset($_taglibs[$name])) { + $_taglibs[$name][0] = strlen(ltrim($this->tpl->config('taglib_begin'), '\\') . $name); + $_taglibs[$name][1] = strlen(ltrim($this->tpl->config('taglib_end'), '\\')); + } + $result['expression'] = substr($str, $_taglibs[$name][0], -$_taglibs[$name][1]); + // 清除自闭合标签尾部/ + $result['expression'] = rtrim($result['expression'], '/'); + $result['expression'] = trim($result['expression']); + } elseif (empty($this->tags[$name]) || !empty($this->tags[$name]['attr'])) { + throw new Exception('tag error:' . $name); + } + } + return $result; + } + + /** + * 解析条件表达式 + * @access public + * @param string $condition 表达式标签内容 + * @return string + */ + public function parseCondition($condition) + { + if (strpos($condition, ':')) { + $condition = ' ' . substr(strstr($condition, ':'), 1); + } + $condition = str_ireplace(array_keys($this->comparison), array_values($this->comparison), $condition); + $this->tpl->parseVar($condition); + // $this->tpl->parseVarFunction($condition); // XXX: 此句能解析表达式中用|分隔的函数,但表达式中如果有|、||这样的逻辑运算就产生了歧异 + return $condition; + } + + /** + * 自动识别构建变量 + * @access public + * @param string $name 变量描述 + * @return string + */ + public function autoBuildVar(&$name) + { + $flag = substr($name, 0, 1); + if (':' == $flag) { + // 以:开头为函数调用,解析前去掉: + $name = substr($name, 1); + } elseif ('$' != $flag && preg_match('/[a-zA-Z_]/', $flag)) { + // XXX: 这句的写法可能还需要改进 + // 常量不需要解析 + if (defined($name)) { + return $name; + } + // 不以$开头并且也不是常量,自动补上$前缀 + $name = '$' . $name; + } + $this->tpl->parseVar($name); + $this->tpl->parseVarFunction($name); + return $name; + } + + /** + * 获取标签列表 + * @access public + * @return array + */ + // 获取标签定义 + public function getTags() + { + return $this->tags; + } +} diff --git a/thinkphp/library/think/template/driver/File.php b/thinkphp/library/think/template/driver/File.php new file mode 100644 index 0000000..b27e726 --- /dev/null +++ b/thinkphp/library/think/template/driver/File.php @@ -0,0 +1,71 @@ + +// +---------------------------------------------------------------------- + +namespace think\template\driver; + +use think\Exception; + +class File +{ + /** + * 写入编译缓存 + * @param string $cacheFile 缓存的文件名 + * @param string $content 缓存的内容 + * @return void|array + */ + public function write($cacheFile, $content) + { + // 检测模板目录 + $dir = dirname($cacheFile); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + // 生成模板缓存文件 + if (false === file_put_contents($cacheFile, $content)) { + throw new Exception('cache write error:' . $cacheFile, 11602); + } + } + + /** + * 读取编译编译 + * @param string $cacheFile 缓存的文件名 + * @param array $vars 变量数组 + * @return void + */ + public function read($cacheFile, $vars = []) + { + if (!empty($vars) && is_array($vars)) { + // 模板阵列变量分解成为独立变量 + extract($vars, EXTR_OVERWRITE); + } + //载入模版缓存文件 + include $cacheFile; + } + + /** + * 检查编译缓存是否有效 + * @param string $cacheFile 缓存的文件名 + * @param int $cacheTime 缓存时间 + * @return boolean + */ + public function check($cacheFile, $cacheTime) + { + // 缓存文件不存在, 直接返回false + if (!file_exists($cacheFile)) { + return false; + } + if (0 != $cacheTime && $_SERVER['REQUEST_TIME'] > filemtime($cacheFile) + $cacheTime) { + // 缓存是否在有效期 + return false; + } + return true; + } +} diff --git a/thinkphp/library/think/template/taglib/Cx.php b/thinkphp/library/think/template/taglib/Cx.php new file mode 100644 index 0000000..af7a54c --- /dev/null +++ b/thinkphp/library/think/template/taglib/Cx.php @@ -0,0 +1,673 @@ + +// +---------------------------------------------------------------------- + +namespace think\template\taglib; + +use think\template\TagLib; + +/** + * CX标签库解析类 + * @category Think + * @package Think + * @subpackage Driver.Taglib + * @author liu21st + */ +class Cx extends Taglib +{ + + // 标签定义 + protected $tags = [ + // 标签定义: attr 属性列表 close 是否闭合(0 或者1 默认1) alias 标签别名 level 嵌套层次 + 'php' => ['attr' => ''], + 'volist' => ['attr' => 'name,id,offset,length,key,mod', 'alias' => 'iterate'], + 'foreach' => ['attr' => 'name,id,item,key,offset,length,mod', 'expression' => true], + 'if' => ['attr' => 'condition', 'expression' => true], + 'elseif' => ['attr' => 'condition', 'close' => 0, 'expression' => true], + 'else' => ['attr' => '', 'close' => 0], + 'switch' => ['attr' => 'name', 'expression' => true], + 'case' => ['attr' => 'value,break', 'expression' => true], + 'default' => ['attr' => '', 'close' => 0], + 'compare' => ['attr' => 'name,value,type', 'alias' => ['eq,equal,notequal,neq,gt,lt,egt,elt,heq,nheq', 'type']], + 'range' => ['attr' => 'name,value,type', 'alias' => ['in,notin,between,notbetween', 'type']], + 'empty' => ['attr' => 'name'], + 'notempty' => ['attr' => 'name'], + 'present' => ['attr' => 'name'], + 'notpresent' => ['attr' => 'name'], + 'defined' => ['attr' => 'name'], + 'notdefined' => ['attr' => 'name'], + 'load' => ['attr' => 'file,href,type,value,basepath', 'close' => 0, 'alias' => ['import,css,js', 'type']], + 'assign' => ['attr' => 'name,value', 'close' => 0], + 'define' => ['attr' => 'name,value', 'close' => 0], + 'for' => ['attr' => 'start,end,name,comparison,step'], + 'url' => ['attr' => 'link,vars,suffix,domain', 'close' => 0, 'expression' => true], + 'function' => ['attr' => 'name,vars,use,call'], + ]; + + /** + * php标签解析 + * 格式: + * {php}echo $name{/php} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagPhp($tag, $content) + { + $parseStr = ''; + return $parseStr; + } + + /** + * volist标签解析 循环输出数据集 + * 格式: + * {volist name="userList" id="user" empty=""} + * {user.username} + * {user.email} + * {/volist} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string|void + */ + public function tagVolist($tag, $content) + { + $name = $tag['name']; + $id = $tag['id']; + $empty = isset($tag['empty']) ? $tag['empty'] : ''; + $key = !empty($tag['key']) ? $tag['key'] : 'i'; + $mod = isset($tag['mod']) ? $tag['mod'] : '2'; + $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0; + $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null'; + // 允许使用函数设定数据集 {$vo.name} + $parseStr = 'autoBuildVar($name); + $parseStr .= '$_result=' . $name . ';'; + $name = '$_result'; + } else { + $name = $this->autoBuildVar($name); + } + + $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): $' . $key . ' = 0;'; + // 设置了输出数组长度 + if (0 != $offset || 'null' != $length) { + $parseStr .= '$__LIST__ = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); '; + } else { + $parseStr .= ' $__LIST__ = ' . $name . ';'; + } + $parseStr .= 'if( count($__LIST__)==0 ) : echo "' . $empty . '" ;'; + $parseStr .= 'else: '; + $parseStr .= 'foreach($__LIST__ as $key=>$' . $id . '): '; + $parseStr .= '$mod = ($' . $key . ' % ' . $mod . ' );'; + $parseStr .= '++$' . $key . ';?>'; + $parseStr .= $content; + $parseStr .= ''; + + if (!empty($parseStr)) { + return $parseStr; + } + return; + } + + /** + * foreach标签解析 循环输出数据集 + * 格式: + * {foreach name="userList" id="user" key="key" index="i" mod="2" offset="3" length="5" empty=""} + * {user.username} + * {/foreach} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string|void + */ + public function tagForeach($tag, $content) + { + // 直接使用表达式 + if (!empty($tag['expression'])) { + $expression = ltrim(rtrim($tag['expression'], ')'), '('); + $expression = $this->autoBuildVar($expression); + $parseStr = ''; + $parseStr .= $content; + $parseStr .= ''; + return $parseStr; + } + $name = $tag['name']; + $key = !empty($tag['key']) ? $tag['key'] : 'key'; + $item = !empty($tag['id']) ? $tag['id'] : $tag['item']; + $empty = isset($tag['empty']) ? $tag['empty'] : ''; + $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0; + $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null'; + + $parseStr = 'autoBuildVar($name); + $parseStr .= $var . '=' . $name . '; '; + $name = $var; + } else { + $name = $this->autoBuildVar($name); + } + $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): '; + // 设置了输出数组长度 + if (0 != $offset || 'null' != $length) { + if (!isset($var)) { + $var = '$_' . uniqid(); + } + $parseStr .= $var . ' = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); '; + } else { + $var = &$name; + } + + $parseStr .= 'if( count(' . $var . ')==0 ) : echo "' . $empty . '" ;'; + $parseStr .= 'else: '; + + // 设置了索引项 + if (isset($tag['index'])) { + $index = $tag['index']; + $parseStr .= '$' . $index . '=0; '; + } + $parseStr .= 'foreach(' . $var . ' as $' . $key . '=>$' . $item . '): '; + // 设置了索引项 + if (isset($tag['index'])) { + $index = $tag['index']; + if (isset($tag['mod'])) { + $mod = (int) $tag['mod']; + $parseStr .= '$mod = ($' . $index . ' % ' . $mod . '); '; + } + $parseStr .= '++$' . $index . '; '; + } + $parseStr .= '?>'; + // 循环体中的内容 + $parseStr .= $content; + $parseStr .= ''; + + if (!empty($parseStr)) { + return $parseStr; + } + return; + } + + /** + * if标签解析 + * 格式: + * {if condition=" $a eq 1"} + * {elseif condition="$a eq 2" /} + * {else /} + * {/if} + * 表达式支持 eq neq gt egt lt elt == > >= < <= or and || && + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagIf($tag, $content) + { + $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition']; + $condition = $this->parseCondition($condition); + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * elseif标签解析 + * 格式:见if标签 + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagElseif($tag, $content) + { + $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition']; + $condition = $this->parseCondition($condition); + $parseStr = ''; + return $parseStr; + } + + /** + * else标签解析 + * 格式:见if标签 + * @access public + * @param array $tag 标签属性 + * @return string + */ + public function tagElse($tag) + { + $parseStr = ''; + return $parseStr; + } + + /** + * switch标签解析 + * 格式: + * {switch name="a.name"} + * {case value="1" break="false"}1{/case} + * {case value="2" }2{/case} + * {default /}other + * {/switch} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagSwitch($tag, $content) + { + $name = !empty($tag['expression']) ? $tag['expression'] : $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * case标签解析 需要配合switch才有效 + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagCase($tag, $content) + { + $value = !empty($tag['expression']) ? $tag['expression'] : $tag['value']; + $flag = substr($value, 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + $value = 'case ' . $value . ':'; + } elseif (strpos($value, '|')) { + $values = explode('|', $value); + $value = ''; + foreach ($values as $val) { + $value .= 'case "' . addslashes($val) . '":'; + } + } else { + $value = 'case "' . $value . '":'; + } + $parseStr = '' . $content; + $isBreak = isset($tag['break']) ? $tag['break'] : ''; + if ('' == $isBreak || $isBreak) { + $parseStr .= ''; + } + return $parseStr; + } + + /** + * default标签解析 需要配合switch才有效 + * 使用: {default /}ddfdf + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagDefault($tag) + { + $parseStr = ''; + return $parseStr; + } + + /** + * compare标签解析 + * 用于值的比较 支持 eq neq gt lt egt elt heq nheq 默认是eq + * 格式: {compare name="" type="eq" value="" }content{/compare} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagCompare($tag, $content) + { + $name = $tag['name']; + $value = $tag['value']; + $type = isset($tag['type']) ? $tag['type'] : 'eq'; // 比较类型 + $name = $this->autoBuildVar($name); + $flag = substr($value, 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + } else { + $value = '\'' . $value . '\''; + } + switch ($type) { + case 'equal': + $type = 'eq'; + break; + case 'notequal': + $type = 'neq'; + break; + } + $type = $this->parseCondition(' ' . $type . ' '); + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * range标签解析 + * 如果某个变量存在于某个范围 则输出内容 type= in 表示在范围内 否则表示在范围外 + * 格式: {range name="var|function" value="val" type='in|notin' }content{/range} + * example: {range name="a" value="1,2,3" type='in' }content{/range} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagRange($tag, $content) + { + $name = $tag['name']; + $value = $tag['value']; + $type = isset($tag['type']) ? $tag['type'] : 'in'; // 比较类型 + + $name = $this->autoBuildVar($name); + $flag = substr($value, 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + $str = 'is_array(' . $value . ')?' . $value . ':explode(\',\',' . $value . ')'; + } else { + $value = '"' . $value . '"'; + $str = 'explode(\',\',' . $value . ')'; + } + if ('between' == $type) { + $parseStr = '= $_RANGE_VAR_[0] && ' . $name . '<= $_RANGE_VAR_[1]):?>' . $content . ''; + } elseif ('notbetween' == $type) { + $parseStr = '$_RANGE_VAR_[1]):?>' . $content . ''; + } else { + $fun = ('in' == $type) ? 'in_array' : '!in_array'; + $parseStr = '' . $content . ''; + } + return $parseStr; + } + + /** + * present标签解析 + * 如果某个变量已经设置 则输出内容 + * 格式: {present name="" }content{/present} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagPresent($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * notpresent标签解析 + * 如果某个变量没有设置,则输出内容 + * 格式: {notpresent name="" }content{/notpresent} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagNotpresent($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * empty标签解析 + * 如果某个变量为empty 则输出内容 + * 格式: {empty name="" }content{/empty} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagEmpty($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = 'isEmpty())): ?>' . $content . ''; + return $parseStr; + } + + /** + * notempty标签解析 + * 如果某个变量不为empty 则输出内容 + * 格式: {notempty name="" }content{/notempty} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagNotempty($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = 'isEmpty()))): ?>' . $content . ''; + return $parseStr; + } + + /** + * 判断是否已经定义了该常量 + * {defined name='TXT'}已定义{/defined} + * @param array $tag + * @param string $content + * @return string + */ + public function tagDefined($tag, $content) + { + $name = $tag['name']; + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * 判断是否没有定义了该常量 + * {notdefined name='TXT'}已定义{/notdefined} + * @param array $tag + * @param string $content + * @return string + */ + public function tagNotdefined($tag, $content) + { + $name = $tag['name']; + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * load 标签解析 {load file="/static/js/base.js" /} + * 格式:{load file="/static/css/base.css" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagLoad($tag, $content) + { + $file = isset($tag['file']) ? $tag['file'] : $tag['href']; + $type = isset($tag['type']) ? strtolower($tag['type']) : ''; + $parseStr = ''; + $endStr = ''; + // 判断是否存在加载条件 允许使用函数判断(默认为isset) + if (isset($tag['value'])) { + $name = $tag['value']; + $name = $this->autoBuildVar($name); + $name = 'isset(' . $name . ')'; + $parseStr .= ''; + $endStr = ''; + } + + // 文件方式导入 + $array = explode(',', $file); + foreach ($array as $val) { + $type = strtolower(substr(strrchr($val, '.'), 1)); + switch ($type) { + case 'js': + $parseStr .= ''; + break; + case 'css': + $parseStr .= ''; + break; + case 'php': + $parseStr .= ''; + break; + } + } + return $parseStr . $endStr; + } + + /** + * assign标签解析 + * 在模板中给某个变量赋值 支持变量赋值 + * 格式: {assign name="" value="" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagAssign($tag, $content) + { + $name = $this->autoBuildVar($tag['name']); + $flag = substr($tag['value'], 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($tag['value']); + } else { + $value = '\'' . $tag['value'] . '\''; + } + $parseStr = ''; + return $parseStr; + } + + /** + * define标签解析 + * 在模板中定义常量 支持变量赋值 + * 格式: {define name="" value="" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagDefine($tag, $content) + { + $name = '\'' . $tag['name'] . '\''; + $flag = substr($tag['value'], 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($tag['value']); + } else { + $value = '\'' . $tag['value'] . '\''; + } + $parseStr = ''; + return $parseStr; + } + + /** + * for标签解析 + * 格式: + * {for start="" end="" comparison="" step="" name=""} + * content + * {/for} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagFor($tag, $content) + { + //设置默认值 + $start = 0; + $end = 0; + $step = 1; + $comparison = 'lt'; + $name = 'i'; + $rand = rand(); //添加随机数,防止嵌套变量冲突 + //获取属性 + foreach ($tag as $key => $value) { + $value = trim($value); + $flag = substr($value, 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + } + + switch ($key) { + case 'start': + $start = $value; + break; + case 'end': + $end = $value; + break; + case 'step': + $step = $value; + break; + case 'comparison': + $comparison = $value; + break; + case 'name': + $name = $value; + break; + } + } + + $parseStr = 'parseCondition('$' . $name . ' ' . $comparison . ' $__FOR_END_' . $rand . '__') . ';$' . $name . '+=' . $step . '){ ?>'; + $parseStr .= $content; + $parseStr .= ''; + return $parseStr; + } + + /** + * url函数的tag标签 + * 格式:{url link="模块/控制器/方法" vars="参数" suffix="true或者false 是否带有后缀" domain="true或者false 是否携带域名" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagUrl($tag, $content) + { + $url = isset($tag['link']) ? $tag['link'] : ''; + $vars = isset($tag['vars']) ? $tag['vars'] : ''; + $suffix = isset($tag['suffix']) ? $tag['suffix'] : 'true'; + $domain = isset($tag['domain']) ? $tag['domain'] : 'false'; + return ''; + } + + /** + * function标签解析 匿名函数,可实现递归 + * 使用: + * {function name="func" vars="$data" call="$list" use="&$a,&$b"} + * {if is_array($data)} + * {foreach $data as $val} + * {~func($val) /} + * {/foreach} + * {else /} + * {$data} + * {/if} + * {/function} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagFunction($tag, $content) + { + $name = !empty($tag['name']) ? $tag['name'] : 'func'; + $vars = !empty($tag['vars']) ? $tag['vars'] : ''; + $call = !empty($tag['call']) ? $tag['call'] : ''; + $use = ['&$' . $name]; + if (!empty($tag['use'])) { + foreach (explode(',', $tag['use']) as $val) { + $use[] = '&' . ltrim(trim($val), '&'); + } + } + $parseStr = '' . $content . '' : '?>'; + return $parseStr; + } +} diff --git a/thinkphp/library/think/view/driver/Php.php b/thinkphp/library/think/view/driver/Php.php new file mode 100644 index 0000000..468d361 --- /dev/null +++ b/thinkphp/library/think/view/driver/Php.php @@ -0,0 +1,164 @@ + +// +---------------------------------------------------------------------- + +namespace think\view\driver; + +use think\App; +use think\exception\TemplateNotFoundException; +use think\Loader; +use think\Log; +use think\Request; + +class Php +{ + // 模板引擎参数 + protected $config = [ + // 视图基础目录(集中式) + 'view_base' => '', + // 模板起始路径 + 'view_path' => '', + // 模板文件后缀 + 'view_suffix' => 'php', + // 模板文件名分隔符 + 'view_depr' => DS, + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 检测是否存在模板文件 + * @access public + * @param string $template 模板文件或者模板规则 + * @return bool + */ + public function exists($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + return is_file($template); + } + + /** + * 渲染模板文件 + * @access public + * @param string $template 模板文件 + * @param array $data 模板变量 + * @return void + */ + public function fetch($template, $data = []) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + // 模板不存在 抛出异常 + if (!is_file($template)) { + throw new TemplateNotFoundException('template not exists:' . $template, $template); + } + // 记录视图信息 + App::$debug && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info'); + if (isset($data['template'])) { + $__template__ = $template; + extract($data, EXTR_OVERWRITE); + include $__template__; + } else { + extract($data, EXTR_OVERWRITE); + include $template; + } + } + + /** + * 渲染模板内容 + * @access public + * @param string $content 模板内容 + * @param array $data 模板变量 + * @return void + */ + public function display($content, $data = []) + { + if (isset($data['content'])) { + $__content__ = $content; + extract($data, EXTR_OVERWRITE); + eval('?>' . $__content__); + } else { + extract($data, EXTR_OVERWRITE); + eval('?>' . $content); + } + } + + /** + * 自动定位模板文件 + * @access private + * @param string $template 模板文件规则 + * @return string + */ + private function parseTemplate($template) + { + if (empty($this->config['view_path'])) { + $this->config['view_path'] = App::$modulePath . 'view' . DS; + } + + $request = Request::instance(); + // 获取视图根目录 + if (strpos($template, '@')) { + // 跨模块调用 + list($module, $template) = explode('@', $template); + } + if ($this->config['view_base']) { + // 基础视图目录 + $module = isset($module) ? $module : $request->module(); + $path = $this->config['view_base'] . ($module ? $module . DS : ''); + } else { + $path = isset($module) ? APP_PATH . $module . DS . 'view' . DS : $this->config['view_path']; + } + + $depr = $this->config['view_depr']; + if (0 !== strpos($template, '/')) { + $template = str_replace(['/', ':'], $depr, $template); + $controller = Loader::parseName($request->controller()); + if ($controller) { + if ('' == $template) { + // 如果模板文件名为空 按照默认规则定位 + $template = str_replace('.', DS, $controller) . $depr . $request->action(); + } elseif (false === strpos($template, $depr)) { + $template = str_replace('.', DS, $controller) . $depr . $template; + } + } + } else { + $template = str_replace(['/', ':'], $depr, substr($template, 1)); + } + return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.'); + } + + /** + * 配置模板引擎 + * @access private + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return void + */ + public function config($name, $value = null) + { + if (is_array($name)) { + $this->config = array_merge($this->config, $name); + } elseif (is_null($value)) { + return isset($this->config[$name]) ? $this->config[$name] : null; + } else { + $this->config[$name] = $value; + } + } + +} diff --git a/thinkphp/library/think/view/driver/Think.php b/thinkphp/library/think/view/driver/Think.php new file mode 100644 index 0000000..39a14ca --- /dev/null +++ b/thinkphp/library/think/view/driver/Think.php @@ -0,0 +1,165 @@ + +// +---------------------------------------------------------------------- + +namespace think\view\driver; + +use think\App; +use think\exception\TemplateNotFoundException; +use think\Loader; +use think\Log; +use think\Request; +use think\Template; + +class Think +{ + // 模板引擎实例 + private $template; + // 模板引擎参数 + protected $config = [ + // 视图基础目录(集中式) + 'view_base' => '', + // 模板起始路径 + 'view_path' => '', + // 模板文件后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DS, + // 是否开启模板编译缓存,设为false则每次都会重新编译 + 'tpl_cache' => true, + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + if (empty($this->config['view_path'])) { + $this->config['view_path'] = App::$modulePath . 'view' . DS; + } + + $this->template = new Template($this->config); + } + + /** + * 检测是否存在模板文件 + * @access public + * @param string $template 模板文件或者模板规则 + * @return bool + */ + public function exists($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + return is_file($template); + } + + /** + * 渲染模板文件 + * @access public + * @param string $template 模板文件 + * @param array $data 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function fetch($template, $data = [], $config = []) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + // 模板不存在 抛出异常 + if (!is_file($template)) { + throw new TemplateNotFoundException('template not exists:' . $template, $template); + } + // 记录视图信息 + App::$debug && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info'); + $this->template->fetch($template, $data, $config); + } + + /** + * 渲染模板内容 + * @access public + * @param string $template 模板内容 + * @param array $data 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function display($template, $data = [], $config = []) + { + $this->template->display($template, $data, $config); + } + + /** + * 自动定位模板文件 + * @access private + * @param string $template 模板文件规则 + * @return string + */ + private function parseTemplate($template) + { + // 分析模板文件规则 + $request = Request::instance(); + // 获取视图根目录 + if (strpos($template, '@')) { + // 跨模块调用 + list($module, $template) = explode('@', $template); + } + if ($this->config['view_base']) { + // 基础视图目录 + $module = isset($module) ? $module : $request->module(); + $path = $this->config['view_base'] . ($module ? $module . DS : ''); + } else { + $path = isset($module) ? APP_PATH . $module . DS . 'view' . DS : $this->config['view_path']; + } + + $depr = $this->config['view_depr']; + if (0 !== strpos($template, '/')) { + $template = str_replace(['/', ':'], $depr, $template); + $controller = Loader::parseName($request->controller()); + if ($controller) { + if ('' == $template) { + // 如果模板文件名为空 按照默认规则定位 + $template = str_replace('.', DS, $controller) . $depr . $request->action(); + } elseif (false === strpos($template, $depr)) { + $template = str_replace('.', DS, $controller) . $depr . $template; + } + } + } else { + $template = str_replace(['/', ':'], $depr, substr($template, 1)); + } + return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.'); + } + + /** + * 配置或者获取模板引擎参数 + * @access private + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return mixed + */ + public function config($name, $value = null) + { + if (is_array($name)) { + $this->template->config($name); + $this->config = array_merge($this->config, $name); + } elseif (is_null($value)) { + return $this->template->config($name); + } else { + $this->template->$name = $value; + $this->config[$name] = $value; + } + } + + public function __call($method, $params) + { + return call_user_func_array([$this->template, $method], $params); + } +} diff --git a/thinkphp/library/traits/controller/Jump.php b/thinkphp/library/traits/controller/Jump.php new file mode 100644 index 0000000..761544c --- /dev/null +++ b/thinkphp/library/traits/controller/Jump.php @@ -0,0 +1,148 @@ +error(); + * $this->redirect(); + * } + * } + */ +namespace traits\controller; + +use think\Config; +use think\exception\HttpResponseException; +use think\Request; +use think\Response; +use think\response\Redirect; +use think\Url; +use think\View as ViewTemplate; + +trait Jump +{ + /** + * 操作成功跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function success($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + if (is_null($url) && isset($_SERVER["HTTP_REFERER"])) { + $url = $_SERVER["HTTP_REFERER"]; + } elseif ('' !== $url) { + $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : Url::build($url); + } + $result = [ + 'code' => 1, + 'msg' => $msg, + 'data' => $data, + 'url' => $url, + 'wait' => $wait, + ]; + + $type = $this->getResponseType(); + if ('html' == strtolower($type)) { + $result = ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str')) + ->fetch(Config::get('dispatch_success_tmpl'), $result); + } + $response = Response::create($result, $type)->header($header); + throw new HttpResponseException($response); + } + + /** + * 操作错误跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function error($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + if (is_null($url)) { + $url = Request::instance()->isAjax() ? '' : 'javascript:history.back(-1);'; + } elseif ('' !== $url) { + $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : Url::build($url); + } + $result = [ + 'code' => 0, + 'msg' => $msg, + 'data' => $data, + 'url' => $url, + 'wait' => $wait, + ]; + + $type = $this->getResponseType(); + if ('html' == strtolower($type)) { + $result = ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str')) + ->fetch(Config::get('dispatch_error_tmpl'), $result); + } + $response = Response::create($result, $type)->header($header); + throw new HttpResponseException($response); + } + + /** + * 返回封装后的API数据到客户端 + * @access protected + * @param mixed $data 要返回的数据 + * @param integer $code 返回的code + * @param mixed $msg 提示信息 + * @param string $type 返回数据格式 + * @param array $header 发送的Header信息 + * @return void + */ + protected function result($data, $code = 0, $msg = '', $type = '', array $header = []) + { + $result = [ + 'code' => $code, + 'msg' => $msg, + 'time' => $_SERVER['REQUEST_TIME'], + 'data' => $data, + ]; + $type = $type ?: $this->getResponseType(); + $response = Response::create($result, $type)->header($header); + throw new HttpResponseException($response); + } + + /** + * URL重定向 + * @access protected + * @param string $url 跳转的URL表达式 + * @param array|integer $params 其它URL参数 + * @param integer $code http code + * @param array $with 隐式传参 + * @return void + */ + protected function redirect($url, $params = [], $code = 302, $with = []) + { + $response = new Redirect($url); + if (is_integer($params)) { + $code = $params; + $params = []; + } + $response->code($code)->params($params)->with($with); + throw new HttpResponseException($response); + } + + /** + * 获取当前的response 输出类型 + * @access protected + * @return string + */ + protected function getResponseType() + { + $isAjax = Request::instance()->isAjax(); + return $isAjax ? Config::get('default_ajax_return') : Config::get('default_return_type'); + } +} diff --git a/thinkphp/library/traits/model/SoftDelete.php b/thinkphp/library/traits/model/SoftDelete.php new file mode 100644 index 0000000..be58634 --- /dev/null +++ b/thinkphp/library/traits/model/SoftDelete.php @@ -0,0 +1,155 @@ +getDeleteTimeField(); + if (!empty($this->data[$field])) { + return true; + } + return false; + } + + /** + * 查询软删除数据 + * @access public + * @return Query + */ + public static function withTrashed() + { + $model = new static(); + $field = $model->getDeleteTimeField(true); + return $model->getQuery(); + } + + /** + * 只查询软删除数据 + * @access public + * @return Query + */ + public static function onlyTrashed() + { + $model = new static(); + $field = $model->getDeleteTimeField(true); + return $model->getQuery() + ->useSoftDelete($field, ['not null', '']); + } + + /** + * 删除当前的记录 + * @access public + * @param bool $force 是否强制删除 + * @return integer + */ + public function delete($force = false) + { + if (false === $this->trigger('before_delete', $this)) { + return false; + } + $name = $this->getDeleteTimeField(); + if (!$force) { + // 软删除 + $this->data[$name] = $this->autoWriteTimestamp($name); + $result = $this->isUpdate()->save(); + } else { + $result = $this->getQuery()->delete($this->data); + } + + $this->trigger('after_delete', $this); + return $result; + } + + /** + * 删除记录 + * @access public + * @param mixed $data 主键列表 支持闭包查询条件 + * @param bool $force 是否强制删除 + * @return integer 成功删除的记录数 + */ + public static function destroy($data, $force = false) + { + // 包含软删除数据 + $query = self::withTrashed(); + if (is_array($data) && key($data) !== 0) { + $query->where($data); + $data = null; + } elseif ($data instanceof \Closure) { + call_user_func_array($data, [ & $query]); + $data = null; + } elseif (is_null($data)) { + return 0; + } + + $resultSet = $query->select($data); + $count = 0; + if ($resultSet) { + foreach ($resultSet as $data) { + $result = $data->delete($force); + $count += $result; + } + } + return $count; + } + + /** + * 恢复被软删除的记录 + * @access public + * @param array $where 更新条件 + * @return integer + */ + public function restore($where = []) + { + $name = $this->getDeleteTimeField(); + if (empty($where)) { + $pk = $this->getPk(); + $where[$pk] = $this->getData($pk); + } + // 恢复删除 + return $this->getQuery() + ->useSoftDelete($name, ['not null', '']) + ->where($where) + ->update([$name => null]); + } + + /** + * 查询默认不包含软删除数据 + * @access protected + * @param Query $query 查询对象 + * @return void + */ + protected function base($query) + { + $field = $this->getDeleteTimeField(true); + $query->useSoftDelete($field); + } + + /** + * 获取软删除字段 + * @access public + * @param bool $read 是否查询操作 写操作的时候会自动去掉表别名 + * @return string + */ + protected function getDeleteTimeField($read = false) + { + $field = property_exists($this, 'deleteTime') && isset($this->deleteTime) ? $this->deleteTime : 'delete_time'; + if (!strpos($field, '.')) { + $field = '__TABLE__.' . $field; + } + if (!$read && strpos($field, '.')) { + $array = explode('.', $field); + $field = array_pop($array); + } + return $field; + } +} diff --git a/thinkphp/library/traits/think/Instance.php b/thinkphp/library/traits/think/Instance.php new file mode 100644 index 0000000..ba45ddd --- /dev/null +++ b/thinkphp/library/traits/think/Instance.php @@ -0,0 +1,45 @@ + +// +---------------------------------------------------------------------- + +namespace traits\think; + +use think\Exception; + +trait Instance +{ + protected static $instance = null; + + /** + * @param array $options + * @return static + */ + public static function instance($options = []) + { + if (is_null(self::$instance)) { + self::$instance = new self($options); + } + return self::$instance; + } + + // 静态调用 + public static function __callStatic($method, $params) + { + if (is_null(self::$instance)) { + self::$instance = new self(); + } + $call = substr($method, 1); + if (0 === strpos($method, '_') && is_callable([self::$instance, $call])) { + return call_user_func_array([self::$instance, $call], $params); + } else { + throw new Exception("method not exists:" . $method); + } + } +} diff --git a/thinkphp/logo.png b/thinkphp/logo.png new file mode 100644 index 0000000..25fd059 Binary files /dev/null and b/thinkphp/logo.png differ diff --git a/thinkphp/phpunit.xml b/thinkphp/phpunit.xml new file mode 100644 index 0000000..7c6ef03 --- /dev/null +++ b/thinkphp/phpunit.xml @@ -0,0 +1,35 @@ + + + + + ./tests/thinkphp/ + + + + + + + + ./ + + tests + vendor + + + + + + + + + + diff --git a/thinkphp/start.php b/thinkphp/start.php new file mode 100644 index 0000000..8af62ef --- /dev/null +++ b/thinkphp/start.php @@ -0,0 +1,18 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +// ThinkPHP 引导文件 +// 加载基础文件 +require __DIR__ . '/base.php'; +// 执行应用 +App::run()->send(); diff --git a/thinkphp/tpl/default_index.tpl b/thinkphp/tpl/default_index.tpl new file mode 100644 index 0000000..8538b4d --- /dev/null +++ b/thinkphp/tpl/default_index.tpl @@ -0,0 +1,10 @@ +*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }

    :)

    ThinkPHP V5
    十年磨一剑 - 为API开发设计的高性能框架

    [ V5.0 版本由 七牛云 独家赞助发布 ]
    '; + } +} diff --git a/thinkphp/tpl/dispatch_jump.tpl b/thinkphp/tpl/dispatch_jump.tpl new file mode 100644 index 0000000..18ee01b --- /dev/null +++ b/thinkphp/tpl/dispatch_jump.tpl @@ -0,0 +1,48 @@ +{__NOLAYOUT__} + + + + 跳转提示 + + + +
    + + +

    :)

    +

    + + +

    :(

    +

    + + +

    +

    + 页面自动 跳转 等待时间: +

    +
    + + + diff --git a/thinkphp/tpl/page_trace.tpl b/thinkphp/tpl/page_trace.tpl new file mode 100644 index 0000000..7c5df6f --- /dev/null +++ b/thinkphp/tpl/page_trace.tpl @@ -0,0 +1,71 @@ +
    + + +
    +
    +
    + +
    + + diff --git a/thinkphp/tpl/think_exception.tpl b/thinkphp/tpl/think_exception.tpl new file mode 100644 index 0000000..0c9d31e --- /dev/null +++ b/thinkphp/tpl/think_exception.tpl @@ -0,0 +1,537 @@ +'.end($names).''; + } + } + + if(!function_exists('parse_file')){ + function parse_file($file, $line) + { + return ''.basename($file)." line {$line}".''; + } + } + + if(!function_exists('parse_args')){ + function parse_args($args) + { + $result = []; + + foreach ($args as $key => $item) { + switch (true) { + case is_object($item): + $value = sprintf('object(%s)', parse_class(get_class($item))); + break; + case is_array($item): + if(count($item) > 3){ + $value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3))); + } else { + $value = sprintf('[%s]', parse_args($item)); + } + break; + case is_string($item): + if(strlen($item) > 20){ + $value = sprintf( + '\'%s...\'', + htmlentities($item), + htmlentities(substr($item, 0, 20)) + ); + } else { + $value = sprintf("'%s'", htmlentities($item)); + } + break; + case is_int($item): + case is_float($item): + $value = $item; + break; + case is_null($item): + $value = 'null'; + break; + case is_bool($item): + $value = '' . ($item ? 'true' : 'false') . ''; + break; + case is_resource($item): + $value = 'resource'; + break; + default: + $value = htmlentities(str_replace("\n", '', var_export(strval($item), true))); + break; + } + + $result[] = is_int($key) ? $value : "'{$key}' => {$value}"; + } + + return implode(', ', $result); + } + } +?> + + + + + <?php echo \think\Lang::get('System Error'); ?> + + + + + +
    + +
    + +
    +
    + +
    +
    +

    []

    +
    +

    +
    + +
    + +
    +
      $value) { ?>
    +
    + +
    +

    Call Stack

    +
      +
    1. + +
    2. + +
    3. + +
    +
    +
    + +
    + +

    + +
    + + + +
    +

    Exception Datas

    + $value) { ?> + + + + + + + $val) { ?> + + + + + + + +
    empty
    + +
    + +
    + + + +
    +

    Environment Variables

    + $value) { ?> +
    + +
    +
    +
    empty
    +
    + +

    +
    + $val) { ?> +
    +
    +
    + +
    +
    + +
    + +
    + +
    + + + + + + + + diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..6eb9acd --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ + array( + __DIR__ . '/autoload_classmap.php', + ), + 'Zend\Loader\StandardAutoloader' => array( + 'namespaces' => array( + __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__, + ), + ), + ); + } +} diff --git a/vendor/bacon/bacon-qr-code/README.md b/vendor/bacon/bacon-qr-code/README.md new file mode 100644 index 0000000..a836bd6 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/README.md @@ -0,0 +1,24 @@ +QR Code generator +================= + +Master: [![Build Status](https://api.travis-ci.org/Bacon/BaconQrCode.png?branch=master)](http://travis-ci.org/Bacon/BaconQrCode) + +Introduction +------------ +BaconQrCode is a port of QR code portion of the ZXing library. It currently +only features the encoder part, but could later receive the decoder part as +well. + +As the Reed Solomon codec implementation of the ZXing library performs quite +slow in PHP, it was exchanged with the implementation by Phil Karn. + + +Example usage +------------- +```php +$renderer = new \BaconQrCode\Renderer\Image\Png(); +$renderer->setHeight(256); +$renderer->setWidth(256); +$writer = new \BaconQrCode\Writer($renderer); +$writer->writeFile('Hello World!', 'qrcode.png'); +``` diff --git a/vendor/bacon/bacon-qr-code/autoload_classmap.php b/vendor/bacon/bacon-qr-code/autoload_classmap.php new file mode 100644 index 0000000..9fbeb35 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/autoload_classmap.php @@ -0,0 +1,43 @@ + __DIR__ . '/src/BaconQrCode/Common/AbstractEnum.php', + 'BaconQrCode\Common\BitArray' => __DIR__ . '/src/BaconQrCode/Common/BitArray.php', + 'BaconQrCode\Common\BitMatrix' => __DIR__ . '/src/BaconQrCode/Common/BitMatrix.php', + 'BaconQrCode\Common\BitUtils' => __DIR__ . '/src/BaconQrCode/Common/BitUtils.php', + 'BaconQrCode\Common\CharacterSetEci' => __DIR__ . '/src/BaconQrCode/Common/CharacterSetEci.php', + 'BaconQrCode\Common\EcBlock' => __DIR__ . '/src/BaconQrCode/Common/EcBlock.php', + 'BaconQrCode\Common\EcBlocks' => __DIR__ . '/src/BaconQrCode/Common/EcBlocks.php', + 'BaconQrCode\Common\ErrorCorrectionLevel' => __DIR__ . '/src/BaconQrCode/Common/ErrorCorrectionLevel.php', + 'BaconQrCode\Common\FormatInformation' => __DIR__ . '/src/BaconQrCode/Common/FormatInformation.php', + 'BaconQrCode\Common\Mode' => __DIR__ . '/src/BaconQrCode/Common/Mode.php', + 'BaconQrCode\Common\ReedSolomonCodec' => __DIR__ . '/src/BaconQrCode/Common/ReedSolomonCodec.php', + 'BaconQrCode\Common\Version' => __DIR__ . '/src/BaconQrCode/Common/Version.php', + 'BaconQrCode\Encoder\BlockPair' => __DIR__ . '/src/BaconQrCode/Encoder/BlockPair.php', + 'BaconQrCode\Encoder\ByteMatrix' => __DIR__ . '/src/BaconQrCode/Encoder/ByteMatrix.php', + 'BaconQrCode\Encoder\Encoder' => __DIR__ . '/src/BaconQrCode/Encoder/Encoder.php', + 'BaconQrCode\Encoder\MaskUtil' => __DIR__ . '/src/BaconQrCode/Encoder/MaskUtil.php', + 'BaconQrCode\Encoder\MatrixUtil' => __DIR__ . '/src/BaconQrCode/Encoder/MatrixUtil.php', + 'BaconQrCode\Encoder\QrCode' => __DIR__ . '/src/BaconQrCode/Encoder/QrCode.php', + 'BaconQrCode\Exception\ExceptionInterface' => __DIR__ . '/src/BaconQrCode/Exception/ExceptionInterface.php', + 'BaconQrCode\Exception\InvalidArgumentException' => __DIR__ . '/src/BaconQrCode/Exception/InvalidArgumentException.php', + 'BaconQrCode\Exception\OutOfBoundsException' => __DIR__ . '/src/BaconQrCode/Exception/OutOfBoundsException.php', + 'BaconQrCode\Exception\RuntimeException' => __DIR__ . '/src/BaconQrCode/Exception/RuntimeException.php', + 'BaconQrCode\Exception\UnexpectedValueException' => __DIR__ . '/src/BaconQrCode/Exception/UnexpectedValueException.php', + 'BaconQrCode\Exception\WriterException' => __DIR__ . '/src/BaconQrCode/Exception/WriterException.php', + 'BaconQrCode\Renderer\Color\Cmyk' => __DIR__ . '/src/BaconQrCode/Renderer/Color/Cmyk.php', + 'BaconQrCode\Renderer\Color\ColorInterface' => __DIR__ . '/src/BaconQrCode/Renderer/Color/ColorInterface.php', + 'BaconQrCode\Renderer\Color\Gray' => __DIR__ . '/src/BaconQrCode/Renderer/Color/Gray.php', + 'BaconQrCode\Renderer\Color\Rgb' => __DIR__ . '/src/BaconQrCode/Renderer/Color/Rgb.php', + 'BaconQrCode\Renderer\Image\AbstractRenderer' => __DIR__ . '/src/BaconQrCode/Renderer/Image/AbstractRenderer.php', + 'BaconQrCode\Renderer\Image\Decorator\DecoratorInterface' => __DIR__ . '/src/BaconQrCode/Renderer/Image/Decorator/DecoratorInterface.php', + 'BaconQrCode\Renderer\Image\Decorator\FinderPattern' => __DIR__ . '/src/BaconQrCode/Renderer/Image/Decorator/FinderPattern.php', + 'BaconQrCode\Renderer\Image\Eps' => __DIR__ . '/src/BaconQrCode/Renderer/Image/Eps.php', + 'BaconQrCode\Renderer\Image\Png' => __DIR__ . '/src/BaconQrCode/Renderer/Image/Png.php', + 'BaconQrCode\Renderer\Image\RendererInterface' => __DIR__ . '/src/BaconQrCode/Renderer/Image/RendererInterface.php', + 'BaconQrCode\Renderer\Image\Svg' => __DIR__ . '/src/BaconQrCode/Renderer/Image/Svg.php', + 'BaconQrCode\Renderer\RendererInterface' => __DIR__ . '/src/BaconQrCode/Renderer/RendererInterface.php', + 'BaconQrCode\Renderer\Text\Plain' => __DIR__ . '/src/BaconQrCode/Renderer/Text/Plain.php', + 'BaconQrCode\Renderer\Text\Html' => __DIR__ . '/src/BaconQrCode/Renderer/Text/Html.php', + 'BaconQrCode\Writer' => __DIR__ . '/src/BaconQrCode/Writer.php', +); \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/autoload_function.php b/vendor/bacon/bacon-qr-code/autoload_function.php new file mode 100644 index 0000000..9148da3 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/autoload_function.php @@ -0,0 +1,12 @@ +=5.3.3" + }, + "suggest": { + "ext-gd": "to generate QR code images" + }, + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "http://www.dasprids.de", + "role": "Developer" + } + ], + "autoload": { + "psr-0": { + "BaconQrCode": "src/" + } + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/AbstractEnum.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/AbstractEnum.php new file mode 100644 index 0000000..9544338 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/AbstractEnum.php @@ -0,0 +1,115 @@ +strict = $strict; + $this->change($initialValue); + } + + /** + * Changes the value of the enum. + * + * @param mixed $value + * @return void + */ + public function change($value) + { + if (!in_array($value, $this->getConstList(), $this->strict)) { + throw new Exception\UnexpectedValueException('Value not a const in enum ' . get_class($this)); + } + + $this->value = $value; + } + + /** + * Gets current value. + * + * @return mixed + */ + public function get() + { + return $this->value; + } + + /** + * Gets all constants (possible values) as an array. + * + * @param boolean $includeDefault + * @return array + */ + public function getConstList($includeDefault = true) + { + if ($this->constants === null) { + $reflection = new ReflectionClass($this); + $this->constants = $reflection->getConstants(); + } + + if ($includeDefault) { + return $this->constants; + } + + $constants = $this->constants; + unset($constants['__default']); + + return $constants; + } + + /** + * Gets the name of the enum. + * + * @return string + */ + public function __toString() + { + return array_search($this->value, $this->getConstList()); + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/BitArray.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/BitArray.php new file mode 100644 index 0000000..0a99d9a --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/BitArray.php @@ -0,0 +1,435 @@ +size = $size; + $this->bits = new SplFixedArray(($this->size + 31) >> 3); + } + + /** + * Gets the size in bits. + * + * @return integer + */ + public function getSize() + { + return $this->size; + } + + /** + * Gets the size in bytes. + * + * @return integer + */ + public function getSizeInBytes() + { + return ($this->size + 7) >> 3; + } + + /** + * Ensures that the array has a minimum capacity. + * + * @param integer $size + * @return void + */ + public function ensureCapacity($size) + { + if ($size > count($this->bits) << 5) { + $this->bits->setSize(($size + 31) >> 5); + } + } + + /** + * Gets a specific bit. + * + * @param integer $i + * @return boolean + */ + public function get($i) + { + return ($this->bits[$i >> 5] & (1 << ($i & 0x1f))) !== 0; + } + + /** + * Sets a specific bit. + * + * @param integer $i + * @return void + */ + public function set($i) + { + $this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f); + } + + /** + * Flips a specific bit. + * + * @param integer $i + * @return void + */ + public function flip($i) + { + $this->bits[$i >> 5] ^= 1 << ($i & 0x1f); + } + + /** + * Gets the next set bit position from a given position. + * + * @param integer $from + * @return integer + */ + public function getNextSet($from) + { + if ($from >= $this->size) { + return $this->size; + } + + $bitsOffset = $from >> 5; + $currentBits = $this->bits[$bitsOffset]; + $bitsLength = count($this->bits); + + $currentBits &= ~((1 << ($from & 0x1f)) - 1); + + while ($currentBits === 0) { + if (++$bitsOffset === $bitsLength) { + return $this->size; + } + + $currentBits = $this->bits[$bitsOffset]; + } + + $result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits); + + return $result > $this->size ? $this->size : $result; + } + + /** + * Gets the next unset bit position from a given position. + * + * @param integer $from + * @return integer + */ + public function getNextUnset($from) + { + if ($from >= $this->size) { + return $this->size; + } + + $bitsOffset = $from >> 5; + $currentBits = ~$this->bits[$bitsOffset]; + $bitsLength = count($this->bits); + + $currentBits &= ~((1 << ($from & 0x1f)) - 1); + + while ($currentBits === 0) { + if (++$bitsOffset === $bitsLength) { + return $this->size; + } + + $currentBits = ~$this->bits[$bitsOffset]; + } + + $result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits); + + return $result > $this->size ? $this->size : $result; + } + + /** + * Sets a bulk of bits. + * + * @param integer $i + * @param integer $newBits + * @return void + */ + public function setBulk($i, $newBits) + { + $this->bits[$i >> 5] = $newBits; + } + + /** + * Sets a range of bits. + * + * @param integer $start + * @param integer $end + * @return void + * @throws Exception\InvalidArgumentException + */ + public function setRange($start, $end) + { + if ($end < $start) { + throw new Exception\InvalidArgumentException('End must be greater or equal to start'); + } + + if ($end === $start) { + return; + } + + $end--; + + $firstInt = $start >> 5; + $lastInt = $end >> 5; + + for ($i = $firstInt; $i <= $lastInt; $i++) { + $firstBit = $i > $firstInt ? 0 : $start & 0x1f; + $lastBit = $i < $lastInt ? 31 : $end & 0x1f; + + if ($firstBit === 0 && $lastBit === 31) { + $mask = 0x7fffffff; + } else { + $mask = 0; + + for ($j = $firstBit; $j < $lastBit; $j++) { + $mask |= 1 << $j; + } + } + + $this->bits[$i] = $this->bits[$i] | $mask; + } + } + + /** + * Clears the bit array, unsetting every bit. + * + * @return void + */ + public function clear() + { + $bitsLength = count($this->bits); + + for ($i = 0; $i < $bitsLength; $i++) { + $this->bits[$i] = 0; + } + } + + /** + * Checks if a range of bits is set or not set. + * + * @param integer $start + * @param integer $end + * @param integer $value + * @return boolean + * @throws Exception\InvalidArgumentException + */ + public function isRange($start, $end, $value) + { + if ($end < $start) { + throw new Exception\InvalidArgumentException('End must be greater or equal to start'); + } + + if ($end === $start) { + return; + } + + $end--; + + $firstInt = $start >> 5; + $lastInt = $end >> 5; + + for ($i = $firstInt; $i <= $lastInt; $i++) { + $firstBit = $i > $firstInt ? 0 : $start & 0x1f; + $lastBit = $i < $lastInt ? 31 : $end & 0x1f; + + if ($firstBit === 0 && $lastBit === 31) { + $mask = 0x7fffffff; + } else { + $mask = 0; + + for ($j = $firstBit; $j <= $lastBit; $j++) { + $mask |= 1 << $j; + } + } + + if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) { + return false; + } + } + + return true; + } + + /** + * Appends a bit to the array. + * + * @param boolean $bit + * @return void + */ + public function appendBit($bit) + { + $this->ensureCapacity($this->size + 1); + + if ($bit) { + $this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f)); + } + + $this->size++; + } + + /** + * Appends a number of bits (up to 32) to the array. + * + * @param integer $value + * @param integer $numBits + * @return void + * @throws Exception\InvalidArgumentException + */ + public function appendBits($value, $numBits) + { + if ($numBits < 0 || $numBits > 32) { + throw new Exception\InvalidArgumentException('Num bits must be between 0 and 32'); + } + + $this->ensureCapacity($this->size + $numBits); + + for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) { + $this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1); + } + } + + /** + * Appends another bit array to this array. + * + * @param BitArray $other + * @return void + */ + public function appendBitArray(self $other) + { + $otherSize = $other->getSize(); + $this->ensureCapacity($this->size + $other->getSize()); + + for ($i = 0; $i < $otherSize; $i++) { + $this->appendBit($other->get($i)); + } + } + + /** + * Makes an exclusive-or comparision on the current bit array. + * + * @param BitArray $other + * @return void + * @throws Exception\InvalidArgumentException + */ + public function xorBits(self $other) + { + $bitsLength = count($this->bits); + $otherBits = $other->getBitArray(); + + if ($bitsLength !== count($otherBits)) { + throw new Exception\InvalidArgumentException('Sizes don\'t match'); + } + + for ($i = 0; $i < $bitsLength; $i++) { + $this->bits[$i] = $this->bits[$i] ^ $otherBits[$i]; + } + } + + /** + * Converts the bit array to a byte array. + * + * @param integer $bitOffset + * @param integer $numBytes + * @return SplFixedArray + */ + public function toBytes($bitOffset, $numBytes) + { + $bytes = new SplFixedArray($numBytes); + + for ($i = 0; $i < $numBytes; $i++) { + $byte = 0; + + for ($j = 0; $j < 8; $j++) { + if ($this->get($bitOffset)) { + $byte |= 1 << (7 - $j); + } + + $bitOffset++; + } + + $bytes[$i] = $byte; + } + + return $bytes; + } + + /** + * Gets the internal bit array. + * + * @return SplFixedArray + */ + public function getBitArray() + { + return $this->bits; + } + + /** + * Reverses the array. + * + * @return void + */ + public function reverse() + { + $newBits = new SplFixedArray(count($this->bits)); + + for ($i = 0; $i < $this->size; $i++) { + if ($this->get($this->size - $i - 1)) { + $newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f)); + } + } + + $this->bits = newBits; + } + + /** + * Returns a string representation of the bit array. + * + * @return string + */ + public function __toString() + { + $result = ''; + + for ($i = 0; $i < $this->size; $i++) { + if (($i & 0x07) === 0) { + $result .= ' '; + } + + $result .= $this->get($i) ? 'X' : '.'; + } + + return $result; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/BitMatrix.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/BitMatrix.php new file mode 100644 index 0000000..b930f88 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/BitMatrix.php @@ -0,0 +1,350 @@ +width = $width; + $this->height = $height; + $this->rowSize = ($width + 31) >> 5; + $this->bits = new SplFixedArray($this->rowSize * $height); + } + + /** + * Gets the requested bit, where true means black. + * + * @param integer $x + * @param integer $y + * @return boolean + */ + public function get($x, $y) + { + $offset = $y * $this->rowSize + ($x >> 5); + return (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1) !== 0; + } + + /** + * Sets the given bit to true. + * + * @param integer $x + * @param integer $y + * @return void + */ + public function set($x, $y) + { + $offset = $y * $this->rowSize + ($x >> 5); + $this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f)); + } + + /** + * Flips the given bit. + * + * @param integer $x + * @param integer $y + * @return void + */ + public function flip($x, $y) + { + $offset = $y * $this->rowSize + ($x >> 5); + $this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f)); + } + + /** + * Clears all bits (set to false). + * + * @return void + */ + public function clear() + { + $max = count($this->bits); + + for ($i = 0; $i < $max; $i++) { + $this->bits[$i] = 0; + } + } + + /** + * Sets a square region of the bit matrix to true. + * + * @param integer $left + * @param integer $top + * @param integer $width + * @param integer $height + * @return void + */ + public function setRegion($left, $top, $width, $height) + { + if ($top < 0 || $left < 0) { + throw new Exception\InvalidArgumentException('Left and top must be non-negative'); + } + + if ($height < 1 || $width < 1) { + throw new Exception\InvalidArgumentException('Width and height must be at least 1'); + } + + $right = $left + $width; + $bottom = $top + $height; + + if ($bottom > $this->height || $right > $this->width) { + throw new Exception\InvalidArgumentException('The region must fit inside the matrix'); + } + + for ($y = $top; $y < $bottom; $y++) { + $offset = $y * $this->rowSize; + + for ($x = $left; $x < $right; $x++) { + $index = $offset + ($x >> 5); + $this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f)); + } + } + } + + /** + * A fast method to retrieve one row of data from the matrix as a BitArray. + * + * @param integer $y + * @param BitArray $row + * @return BitArray + */ + public function getRow($y, BitArray $row = null) + { + if ($row === null || $row->getSize() < $this->width) { + $row = new BitArray($this->width); + } + + $offset = $y * $this->rowSize; + + for ($x = 0; $x < $this->rowSize; $x++) { + $row->setBulk($x << 5, $this->bits[$offset + $x]); + } + + return $row; + } + + /** + * Sets a row of data from a BitArray. + * + * @param integer $y + * @param BitArray $row + * @return void + */ + public function setRow($y, BitArray $row) + { + $bits = $row->getBitArray(); + + for ($i = 0; $i < $this->rowSize; $i++) { + $this->bits[$y * $this->rowSize + $i] = $bits[$i]; + } + } + + /** + * This is useful in detecting the enclosing rectangle of a 'pure' barcode. + * + * @return SplFixedArray + */ + public function getEnclosingRectangle() + { + $left = $this->width; + $top = $this->height; + $right = -1; + $bottom = -1; + + for ($y = 0; $y < $this->height; $y++) { + for ($x32 = 0; $x32 < $this->rowSize; $x32++) { + $bits = $this->bits[$y * $this->rowSize + $x32]; + + if ($bits !== 0) { + if ($y < $top) { + $top = $y; + } + + if ($y > $bottom) { + $bottom = $y; + } + + if ($x32 * 32 < $left) { + $bit = 0; + + while (($bits << (31 - $bit)) === 0) { + $bit++; + } + + if (($x32 * 32 + $bit) < $left) { + $left = $x32 * 32 + $bit; + } + } + } + + if ($x32 * 32 + 31 > $right) { + $bit = 31; + + while (BitUtils::unsignedRightShift($bits, $bit) === 0) { + $bit--; + } + + if (($x32 * 32 + $bit) > $right) { + $right = $x32 * 32 + $bit; + } + } + } + } + + $width = $right - $left; + $height = $bottom - $top; + + if ($width < 0 || $height < 0) { + return null; + } + + return SplFixedArray::fromArray(array($left, $top, $width, $height), false); + } + + /** + * Gets the most top left set bit. + * + * This is useful in detecting a corner of a 'pure' barcode. + * + * @return SplFixedArray + */ + public function getTopLeftOnBit() + { + $bitsOffset = 0; + + while ($bitsOffset < count($this->bits) && $this->bits[$bitsOffset] === 0) { + $bitsOffset++; + } + + if ($bitsOffset === count($this->bits)) { + return null; + } + + $x = intval($bitsOffset / $this->rowSize); + $y = ($bitsOffset % $this->rowSize) << 5; + + $bits = $this->bits[$bitsOffset]; + $bit = 0; + + while (($bits << (31 - $bit)) === 0) { + $bit++; + } + + $x += $bit; + + return SplFixedArray::fromArray(array($x, $y), false); + } + + /** + * Gets the most bottom right set bit. + * + * This is useful in detecting a corner of a 'pure' barcode. + * + * @return SplFixedArray + */ + public function getBottomRightOnBit() + { + $bitsOffset = count($this->bits) - 1; + + while ($bitsOffset >= 0 && $this->bits[$bitsOffset] === 0) { + $bitsOffset--; + } + + if ($bitsOffset < 0) { + return null; + } + + $x = intval($bitsOffset / $this->rowSize); + $y = ($bitsOffset % $this->rowSize) << 5; + + $bits = $this->bits[$bitsOffset]; + $bit = 0; + + while (BitUtils::unsignedRightShift($bits, $bit) === 0) { + $bit--; + } + + $x += $bit; + + return SplFixedArray::fromArray(array($x, $y), false); + } + + /** + * Gets the width of the matrix, + * + * @return integer + */ + public function getWidth() + { + return $this->width; + } + + /** + * Gets the height of the matrix. + * + * @return integer + */ + public function getHeight() + { + return $this->height; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/BitUtils.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/BitUtils.php new file mode 100644 index 0000000..a641244 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/BitUtils.php @@ -0,0 +1,51 @@ +>>" in other + * languages. + * + * @param integer $a + * @param integer $b + * @return integer + */ + public static function unsignedRightShift($a, $b) + { + return ( + $a >= 0 + ? $a >> $b + : (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1)) + ); + } + + /** + * Gets the number of trailing zeros. + * + * @param integer $i + * @return integer + */ + public static function numberOfTrailingZeros($i) + { + $lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1'); + + return $lastPos === false ? 32 : 31 - $lastPos; + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/CharacterSetEci.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/CharacterSetEci.php new file mode 100644 index 0000000..7766236 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/CharacterSetEci.php @@ -0,0 +1,134 @@ + self::ISO8859_1, + 'ISO-8859-2' => self::ISO8859_2, + 'ISO-8859-3' => self::ISO8859_3, + 'ISO-8859-4' => self::ISO8859_4, + 'ISO-8859-5' => self::ISO8859_5, + 'ISO-8859-6' => self::ISO8859_6, + 'ISO-8859-7' => self::ISO8859_7, + 'ISO-8859-8' => self::ISO8859_8, + 'ISO-8859-9' => self::ISO8859_9, + 'ISO-8859-10' => self::ISO8859_10, + 'ISO-8859-11' => self::ISO8859_11, + 'ISO-8859-12' => self::ISO8859_12, + 'ISO-8859-13' => self::ISO8859_13, + 'ISO-8859-14' => self::ISO8859_14, + 'ISO-8859-15' => self::ISO8859_15, + 'ISO-8859-16' => self::ISO8859_16, + 'SHIFT-JIS' => self::SJIS, + 'WINDOWS-1250' => self::CP1250, + 'WINDOWS-1251' => self::CP1251, + 'WINDOWS-1252' => self::CP1252, + 'WINDOWS-1256' => self::CP1256, + 'UTF-16BE' => self::UNICODE_BIG_UNMARKED, + 'UTF-8' => self::UTF8, + 'ASCII' => self::ASCII, + 'GBK' => self::GB18030, + 'EUC-KR' => self::EUC_KR, + ); + + /** + * Additional possible values for character sets. + * + * @var array + */ + protected $additionalValues = array( + self::CP437 => 2, + self::ASCII => 170, + ); + + /** + * Gets character set ECI by value. + * + * @param string $name + * @return CharacterSetEci|null + */ + public static function getCharacterSetECIByValue($value) + { + if ($value < 0 || $value >= 900) { + throw new Exception\InvalidArgumentException('Value must be between 0 and 900'); + } + + if (false !== ($key = array_search($value, self::$additionalValues))) { + $value = $key; + } + + try { + return new self($value); + } catch (Exception\UnexpectedValueException $e) { + return null; + } + } + + /** + * Gets character set ECI by name. + * + * @param string $name + * @return CharacterSetEci|null + */ + public static function getCharacterSetECIByName($name) + { + $name = strtoupper($name); + + if (isset(self::$nameToEci[$name])) { + return new self(self::$nameToEci[$name]); + } + + return null; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/EcBlock.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/EcBlock.php new file mode 100644 index 0000000..cbcc2ba --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/EcBlock.php @@ -0,0 +1,65 @@ +count = $count; + $this->dataCodewords = $dataCodewords; + } + + /** + * Returns how many times the block is used. + * + * @return integer + */ + public function getCount() + { + return $this->count; + } + + /** + * Returns the number of data codewords. + * + * @return integer + */ + public function getDataCodewords() + { + return $this->dataCodewords; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/EcBlocks.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/EcBlocks.php new file mode 100644 index 0000000..87cef5d --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/EcBlocks.php @@ -0,0 +1,101 @@ +ecCodewordsPerBlock = $ecCodewordsPerBlock; + + $this->ecBlocks = new SplFixedArray($ecb2 === null ? 1 : 2); + $this->ecBlocks[0] = $ecb1; + + if ($ecb2 !== null) { + $this->ecBlocks[1] = $ecb2; + } + } + + /** + * Gets the number of EC codewords per block. + * + * @return integer + */ + public function getEcCodewordsPerBlock() + { + return $this->ecCodewordsPerBlock; + } + + /** + * Gets the total number of EC block appearances. + * + * @return integer + */ + public function getNumBlocks() + { + $total = 0; + + foreach ($this->ecBlocks as $ecBlock) { + $total += $ecBlock->getCount(); + } + + return $total; + } + + /** + * Gets the total count of EC codewords. + * + * @return integer + */ + public function getTotalEcCodewords() + { + return $this->ecCodewordsPerBlock * $this->getNumBlocks(); + } + + /** + * Gets the EC blocks included in this collection. + * + * @return SplFixedArray + */ + public function getEcBlocks() + { + return $this->ecBlocks; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/ErrorCorrectionLevel.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/ErrorCorrectionLevel.php new file mode 100644 index 0000000..bd0a60a --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/ErrorCorrectionLevel.php @@ -0,0 +1,62 @@ +value) { + case self::L: + return 0; + break; + + case self::M: + return 1; + break; + + case self::Q: + return 2; + break; + + case self::H: + return 3; + break; + } + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/FormatInformation.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/FormatInformation.php new file mode 100644 index 0000000..5ec9ffd --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/FormatInformation.php @@ -0,0 +1,236 @@ +ecLevel = new ErrorCorrectionLevel(($formatInfo >> 3) & 0x3); + $this->dataMask = $formatInfo & 0x7; + } + + /** + * Checks how many bits are different between two integers. + * + * @param integer $a + * @param integer $b + * @return integer + */ + public static function numBitsDiffering($a, $b) + { + $a ^= $b; + + return ( + self::$bitsSetInHalfByte[$a & 0xf] + + self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 4) & 0xf)] + + self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 8) & 0xf)] + + self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 12) & 0xf)] + + self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 16) & 0xf)] + + self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 20) & 0xf)] + + self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 24) & 0xf)] + + self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 28) & 0xf)] + ); + } + + /** + * Decodes format information. + * + * @param integer $maskedFormatInfo1 + * @param integer $maskedFormatInfo2 + * @return FormatInformation|null + */ + public static function decodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2) + { + $formatInfo = self::doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2); + + if ($formatInfo !== null) { + return $formatInfo; + } + + // Should return null, but, some QR codes apparently do not mask this + // info. Try again by actually masking the pattern first. + return self::doDecodeFormatInformation( + $maskedFormatInfo1 ^ self::FORMAT_INFO_MASK_QR, + $maskedFormatInfo2 ^ self::FORMAT_INFO_MASK_QR + ); + } + + /** + * Internal method for decoding format information. + * + * @param integer $maskedFormatInfo1 + * @param integer $maskedFormatInfo2 + * @return FormatInformation|null + */ + protected static function doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2) + { + $bestDifference = PHP_INT_MAX; + $bestFormatInfo = 0; + + foreach (self::$formatInfoDecodeLookup as $decodeInfo) { + $targetInfo = $decodeInfo[0]; + + if ($targetInfo === $maskedFormatInfo1 || $targetInfo === $maskedFormatInfo2) { + // Found an exact match + return new self($decodeInfo[1]); + } + + $bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $targetInfo); + + if ($bitsDifference < $bestDifference) { + $bestFormatInfo = $decodeInfo[1]; + $bestDifference = $bitsDifference; + } + + if ($maskedFormatInfo1 !== $maskedFormatInfo2) { + // Also try the other option + $bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $targetInfo); + + if ($bitsDifference < $bestDifference) { + $bestFormatInfo = $decodeInfo[1]; + $bestDifference = $bitsDifference; + } + } + } + + // Hamming distance of the 32 masked codes is 7, by construction, so + // <= 3 bits differing means we found a match. + if ($bestDifference <= 3) { + return new self($bestFormatInfo); + } + + return null; + } + + /** + * Gets the error correction level. + * + * @return ErrorCorrectionLevel + */ + public function getErrorCorrectionLevel() + { + return $this->ecLevel; + } + + /** + * Gets the data mask. + * + * @return integer + */ + public function getDataMask() + { + return $this->dataMask; + } + + /** + * Hashes the code of the EC level. + * + * @return integer + */ + public function hashCode() + { + return ($this->ecLevel->get() << 3) | $this->dataMask; + } + + /** + * Verifies if this instance equals another one. + * + * @param mixed $other + * @return boolean + */ + public function equals($other) { + if (!$other instanceof self) { + return false; + } + + return ( + $this->ecLevel->get() === $other->getErrorCorrectionLevel()->get() + && $this->dataMask === $other->getDataMask() + ); + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/Mode.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/Mode.php new file mode 100644 index 0000000..8faf344 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/Mode.php @@ -0,0 +1,70 @@ + array(0, 0, 0), + self::NUMERIC => array(10, 12, 14), + self::ALPHANUMERIC => array(9, 11, 13), + self::STRUCTURED_APPEND => array(0, 0, 0), + self::BYTE => array(8, 16, 16), + self::ECI => array(0, 0, 0), + self::KANJI => array(8, 10, 12), + self::FNC1_FIRST_POSITION => array(0, 0, 0), + self::FNC1_SECOND_POSITION => array(0, 0, 0), + self::HANZI => array(8, 10, 12), + ); + + /** + * Gets the number of bits used in a specific QR code version. + * + * @param Version $version + * @return integer + */ + public function getCharacterCountBits(Version $version) + { + $number = $version->getVersionNumber(); + + if ($number <= 9) { + $offset = 0; + } elseif ($number <= 26) { + $offset = 1; + } else { + $offset = 2; + } + + return self::$characterCountBitsForVersions[$this->value][$offset]; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/ReedSolomonCodec.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/ReedSolomonCodec.php new file mode 100644 index 0000000..e8d45b9 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/ReedSolomonCodec.php @@ -0,0 +1,476 @@ + 8) { + throw new Exception\InvalidArgumentException('Symbol size must be between 0 and 8'); + } + + if ($firstRoot < 0 || $firstRoot >= (1 << $symbolSize)) { + throw new Exception\InvalidArgumentException('First root must be between 0 and ' . (1 << $symbolSize)); + } + + if ($numRoots < 0 || $numRoots >= (1 << $symbolSize)) { + throw new Exception\InvalidArgumentException('Num roots must be between 0 and ' . (1 << $symbolSize)); + } + + if ($padding < 0 || $padding >= ((1 << $symbolSize) - 1 - $numRoots)) { + throw new Exception\InvalidArgumentException('Padding must be between 0 and ' . ((1 << $symbolSize) - 1 - $numRoots)); + } + + $this->symbolSize = $symbolSize; + $this->blockSize = (1 << $symbolSize) - 1; + $this->padding = $padding; + $this->alphaTo = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false); + $this->indexOf = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false); + + // Generate galous field lookup table + $this->indexOf[0] = $this->blockSize; + $this->alphaTo[$this->blockSize] = 0; + + $sr = 1; + + for ($i = 0; $i < $this->blockSize; $i++) { + $this->indexOf[$sr] = $i; + $this->alphaTo[$i] = $sr; + + $sr <<= 1; + + if ($sr & (1 << $symbolSize)) { + $sr ^= $gfPoly; + } + + $sr &= $this->blockSize; + } + + if ($sr !== 1) { + throw new Exception\RuntimeException('Field generator polynomial is not primitive'); + } + + // Form RS code generator polynomial from its roots + $this->generatorPoly = SplFixedArray::fromArray(array_fill(0, $numRoots + 1, 0), false); + $this->firstRoot = $firstRoot; + $this->primitive = $primitive; + $this->numRoots = $numRoots; + + // Find prim-th root of 1, used in decoding + for ($iPrimitive = 1; ($iPrimitive % $primitive) !== 0; $iPrimitive += $this->blockSize); + $this->iPrimitive = intval($iPrimitive / $primitive); + + $this->generatorPoly[0] = 1; + + for ($i = 0, $root = $firstRoot * $primitive; $i < $numRoots; $i++, $root += $primitive) { + $this->generatorPoly[$i + 1] = 1; + + for ($j = $i; $j > 0; $j--) { + if ($this->generatorPoly[$j] !== 0) { + $this->generatorPoly[$j] = $this->generatorPoly[$j - 1] ^ $this->alphaTo[$this->modNn($this->indexOf[$this->generatorPoly[$j]] + $root)]; + } else { + $this->generatorPoly[$j] = $this->generatorPoly[$j - 1]; + } + } + + $this->generatorPoly[$j] = $this->alphaTo[$this->modNn($this->indexOf[$this->generatorPoly[0]] + $root)]; + } + + // Convert generator poly to index form for quicker encoding + for ($i = 0; $i <= $numRoots; $i++) { + $this->generatorPoly[$i] = $this->indexOf[$this->generatorPoly[$i]]; + } + } + + /** + * Encodes data and writes result back into parity array. + * + * @param SplFixedArray $data + * @param SplFixedArray $parity + * @return void + */ + public function encode(SplFixedArray $data, SplFixedArray $parity) + { + for ($i = 0; $i < $this->numRoots; $i++) { + $parity[$i] = 0; + } + + $iterations = $this->blockSize - $this->numRoots - $this->padding; + + for ($i = 0; $i < $iterations; $i++) { + $feedback = $this->indexOf[$data[$i] ^ $parity[0]]; + + if ($feedback !== $this->blockSize) { + // Feedback term is non-zero + $feedback = $this->modNn($this->blockSize - $this->generatorPoly[$this->numRoots] + $feedback); + + for ($j = 1; $j < $this->numRoots; $j++) { + $parity[$j] = $parity[$j] ^ $this->alphaTo[$this->modNn($feedback + $this->generatorPoly[$this->numRoots - $j])]; + } + } + + for ($j = 0; $j < $this->numRoots - 1; $j++) { + $parity[$j] = $parity[$j + 1]; + } + + if ($feedback !== $this->blockSize) { + $parity[$this->numRoots - 1] = $this->alphaTo[$this->modNn($feedback + $this->generatorPoly[0])]; + } else { + $parity[$this->numRoots - 1] = 0; + } + } + } + + /** + * Decodes received data. + * + * @param SplFixedArray $data + * @param SplFixedArray|null $erasures + * @return null|integer + */ + public function decode(SplFixedArray $data, SplFixedArray $erasures = null) + { + // This speeds up the initialization a bit. + $numRootsPlusOne = SplFixedArray::fromArray(array_fill(0, $this->numRoots + 1, 0), false); + $numRoots = SplFixedArray::fromArray(array_fill(0, $this->numRoots, 0), false); + + $lambda = clone $numRootsPlusOne; + $b = clone $numRootsPlusOne; + $t = clone $numRootsPlusOne; + $omega = clone $numRootsPlusOne; + $root = clone $numRoots; + $loc = clone $numRoots; + + $numErasures = ($erasures !== null ? count($erasures) : 0); + + // Form the Syndromes; i.e., evaluate data(x) at roots of g(x) + $syndromes = SplFixedArray::fromArray(array_fill(0, $this->numRoots, $data[0]), false); + + for ($i = 1; $i < $this->blockSize - $this->padding; $i++) { + for ($j = 0; $j < $this->numRoots; $j++) { + if ($syndromes[$j] === 0) { + $syndromes[$j] = $data[$i]; + } else { + $syndromes[$j] = $data[$i] ^ $this->alphaTo[ + $this->modNn($this->indexOf[$syndromes[$j]] + ($this->firstRoot + $j) * $this->primitive) + ]; + } + } + } + + // Convert syndromes to index form, checking for nonzero conditions + $syndromeError = 0; + + for ($i = 0; $i < $this->numRoots; $i++) { + $syndromeError |= $syndromes[$i]; + $syndromes[$i] = $this->indexOf[$syndromes[$i]]; + } + + if (!$syndromeError) { + // If syndrome is zero, data[] is a codeword and there are no errors + // to correct, so return data[] unmodified. + return 0; + } + + $lambda[0] = 1; + + if ($numErasures > 0) { + // Init lambda to be the erasure locator polynomial + $lambda[1] = $this->alphaTo[$this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[0]))]; + + for ($i = 1; $i < $numErasures; $i++) { + $u = $this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[$i])); + + for ($j = $i + 1; $j > 0; $j--) { + $tmp = $this->indexOf[$lambda[$j - 1]]; + + if ($tmp !== $this->blockSize) { + $lambda[$j] = $lambda[$j] ^ $this->alphaTo[$this->modNn($u + $tmp)]; + } + } + } + } + + for ($i = 0; $i <= $this->numRoots; $i++) { + $b[$i] = $this->indexOf[$lambda[$i]]; + } + + // Begin Berlekamp-Massey algorithm to determine error+erasure locator + // polynomial + $r = $numErasures; + $el = $numErasures; + + while (++$r <= $this->numRoots) { + // Compute discrepancy at the r-th step in poly form + $discrepancyR = 0; + + for ($i = 0; $i < $r; $i++) { + if ($lambda[$i] !== 0 && $syndromes[$r - $i - 1] !== $this->blockSize) { + $discrepancyR ^= $this->alphaTo[$this->modNn($this->indexOf[$lambda[$i]] + $syndromes[$r - $i - 1])]; + } + } + + $discrepancyR = $this->indexOf[$discrepancyR]; + + if ($discrepancyR === $this->blockSize) { + $tmp = $b->toArray(); + array_unshift($tmp, $this->blockSize); + array_pop($tmp); + $b = SplFixedArray::fromArray($tmp, false); + } else { + $t[0] = $lambda[0]; + + for ($i = 0; $i < $this->numRoots; $i++) { + if ($b[$i] !== $this->blockSize) { + $t[$i + 1] = $lambda[$i + 1] ^ $this->alphaTo[$this->modNn($discrepancyR + $b[$i])]; + } else { + $t[$i + 1] = $lambda[$i + 1]; + } + } + + if (2 * $el <= $r + $numErasures - 1) { + $el = $r + $numErasures - $el; + + for ($i = 0; $i <= $this->numRoots; $i++) { + $b[$i] = ( + $lambda[$i] === 0 + ? $this->blockSize + : $this->modNn($this->indexOf[$lambda[$i]] - $discrepancyR + $this->blockSize) + ); + } + } else { + $tmp = $b->toArray(); + array_unshift($tmp, $this->blockSize); + array_pop($tmp); + $b = SplFixedArray::fromArray($tmp, false); + } + + $lambda = clone $t; + } + } + + // Convert lambda to index form and compute deg(lambda(x)) + $degLambda = 0; + + for ($i = 0; $i <= $this->numRoots; $i++) { + $lambda[$i] = $this->indexOf[$lambda[$i]]; + + if ($lambda[$i] !== $this->blockSize) { + $degLambda = $i; + } + } + + // Find roots of the error+erasure locator polynomial by Chien search. + $reg = clone $lambda; + $reg[0] = 0; + $count = 0; + + for ($i = 1, $k = $this->iPrimitive - 1; $i <= $this->blockSize; $i++, $k = $this->modNn($k + $this->iPrimitive)) { + $q = 1; + + for ($j = $degLambda; $j > 0; $j--) { + if ($reg[$j] !== $this->blockSize) { + $reg[$j] = $this->modNn($reg[$j] + $j); + $q ^= $this->alphaTo[$reg[$j]]; + } + } + + if ($q !== 0) { + // Not a root + continue; + } + + // Store root (index-form) and error location number + $root[$count] = $i; + $loc[$count] = $k; + + if (++$count === $degLambda) { + break; + } + } + + if ($degLambda !== $count) { + // deg(lambda) unequal to number of roots: uncorreactable error + // detected + return null; + } + + // Compute err+eras evaluate poly omega(x) = s(x)*lambda(x) (modulo + // x**numRoots). In index form. Also find deg(omega). + $degOmega = $degLambda - 1; + + for ($i = 0; $i <= $degOmega; $i++) { + $tmp = 0; + + for ($j = $i; $j >= 0; $j--) { + if ($syndromes[$i - $j] !== $this->blockSize && $lambda[$j] !== $this->blockSize) { + $tmp ^= $this->alphaTo[$this->modNn($syndromes[$i - $j] + $lambda[$j])]; + } + } + + $omega[$i] = $this->indexOf[$tmp]; + } + + // Compute error values in poly-form. num1 = omega(inv(X(l))), num2 = + // inv(X(l))**(firstRoot-1) and den = lambda_pr(inv(X(l))) all in poly + // form. + for ($j = $count - 1; $j >= 0; $j--) { + $num1 = 0; + + for ($i = $degOmega; $i >= 0; $i--) { + if ($omega[$i] !== $this->blockSize) { + $num1 ^= $this->alphaTo[$this->modNn($omega[$i] + $i * $root[$j])]; + } + } + + $num2 = $this->alphaTo[$this->modNn($root[$j] * ($this->firstRoot - 1) + $this->blockSize)]; + $den = 0; + + // lambda[i+1] for i even is the formal derivativelambda_pr of + // lambda[i] + for ($i = min($degLambda, $this->numRoots - 1) & ~1; $i >= 0; $i -= 2) { + if ($lambda[$i + 1] !== $this->blockSize) { + $den ^= $this->alphaTo[$this->modNn($lambda[$i + 1] + $i * $root[$j])]; + } + } + + // Apply error to data + if ($num1 !== 0 && $loc[$j] >= $this->padding) { + $data[$loc[$j] - $this->padding] = $data[$loc[$j] - $this->padding] ^ ( + $this->alphaTo[ + $this->modNn( + $this->indexOf[$num1] + $this->indexOf[$num2] + $this->blockSize - $this->indexOf[$den] + ) + ] + ); + } + } + + if ($erasures !== null) { + if (count($erasures) < $count) { + $erasures->setSize($count); + } + + for ($i = 0; $i < $count; $i++) { + $erasures[$i] = $loc[$i]; + } + } + + return $count; + } + + /** + * Computes $x % GF_SIZE, where GF_SIZE is 2**GF_BITS - 1, without a slow + * divide. + * + * @param itneger $x + * @return integer + */ + protected function modNn($x) + { + while ($x >= $this->blockSize) { + $x -= $this->blockSize; + $x = ($x >> $this->symbolSize) + ($x & $this->blockSize); + } + + return $x; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/Version.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/Version.php new file mode 100644 index 0000000..a4df355 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/Version.php @@ -0,0 +1,687 @@ +versionNumber = $versionNumber; + $this->alignmentPatternCenters = $alignmentPatternCenters; + $this->errorCorrectionBlocks = $ecBlocks; + + $totalCodewords = 0; + $ecCodewords = $ecBlocks[0]->getEcCodewordsPerBlock(); + + foreach ($ecBlocks[0]->getEcBlocks() as $ecBlock) { + $totalCodewords += $ecBlock->getCount() * ($ecBlock->getDataCodewords() + $ecCodewords); + } + + $this->totalCodewords = $totalCodewords; + } + + /** + * Gets the version number. + * + * @return integer + */ + public function getVersionNumber() + { + return $this->versionNumber; + } + + /** + * Gets the alignment pattern centers. + * + * @return SplFixedArray + */ + public function getAlignmentPatternCenters() + { + return $this->alignmentPatternCenters; + } + + /** + * Gets the total number of codewords. + * + * @return integer + */ + public function getTotalCodewords() + { + return $this->totalCodewords; + } + + /** + * Gets the dimension for the current version. + * + * @return integer + */ + public function getDimensionForVersion() + { + return 17 + 4 * $this->versionNumber; + } + + /** + * Gets the number of EC blocks for a specific EC level. + * + * @param ErrorCorrectionLevel $ecLevel + * @return integer + */ + public function getEcBlocksForLevel(ErrorCorrectionLevel $ecLevel) + { + return $this->errorCorrectionBlocks[$ecLevel->getOrdinal()]; + } + + /** + * Gets a provisional version number for a specific dimension. + * + * @param integer $dimension + * @return Version + * @throws Exception\InvalidArgumentException + */ + public static function getProvisionalVersionForDimension($dimension) + { + if ($dimension % 4 !== 1) { + throw new Exception\InvalidArgumentException('Dimension is not 1 mod 4'); + } + + return self::getVersionForNumber(($dimension - 17) >> 2); + } + + /** + * Gets a version instance for a specific version number. + * + * @param integer $versionNumber + * @return Version + * @throws Exception\InvalidArgumentException + */ + public static function getVersionForNumber($versionNumber) + { + if ($versionNumber < 1 || $versionNumber > 40) { + throw new Exception\InvalidArgumentException('Version number must be between 1 and 40'); + } + + if (!isset(self::$versions[$versionNumber])) { + self::buildVersion($versionNumber); + } + + return self::$versions[$versionNumber - 1]; + } + + /** + * Decodes version information from an integer and returns the version. + * + * @param integer $versionBits + * @return Version|null + */ + public static function decodeVersionInformation($versionBits) + { + $bestDifference = PHP_INT_MAX; + $bestVersion = 0; + + foreach (self::$versionDecodeInfo as $i => $targetVersion) { + if ($targetVersion === $versionBits) { + return self::getVersionForNumber($i + 7); + } + + $bitsDifference = FormatInformation::numBitsDiffering($versionBits, $targetVersion); + + if ($bitsDifference < $bestDifference) { + $bestVersion = $i + 7; + $bestDifference = $bitsDifference; + } + } + + if ($bestDifference <= 3) { + return self::getVersionForNumber($bestVersion); + } + + return null; + } + + /** + * Builds the function pattern for the current version. + * + * @return BitMatrix + */ + public function buildFunctionPattern() + { + $dimension = $this->getDimensionForVersion(); + $bitMatrix = new BitMatrix($dimension); + + // Top left finder pattern + separator + format + $bitMatrix->setRegion(0, 0, 9, 9); + // Top right finder pattern + separator + format + $bitMatrix->setRegion($dimension - 8, 0, 8, 9); + // Bottom left finder pattern + separator + format + $bitMatrix->setRegion(0, $dimension - 8, 9, 8); + + // Alignment patterns + $max = count($this->alignmentPatternCenters); + + for ($x = 0; $x < $max; $x++) { + $i = $this->alignmentPatternCenters[$x] - 2; + + for ($y = 0; $y < $max; $y++) { + if (($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)) { + // No alignment patterns near the three finder paterns + continue; + } + + $bitMatrix->setRegion($this->alignmentPatternCenters[$y] - 2, $i, 5, 5); + } + } + + // Vertical timing pattern + $bitMatrix->setRegion(6, 9, 1, $dimension - 17); + // Horizontal timing pattern + $bitMatrix->setRegion(9, 6, $dimension - 17, 1); + + if ($this->versionNumber > 6) { + // Version info, top right + $bitMatrix->setRegion($dimension - 11, 0, 3, 6); + // Version info, bottom left + $bitMatrix->setRegion(0, $dimension - 11, 6, 3); + } + + return $bitMatrix; + } + + /** + * Returns a string representation for the version. + * + * @return string + */ + public function __toString() + { + return (string) $this->versionNumber; + } + + /** + * Build and cache a specific version. + * + * See ISO 18004:2006 6.5.1 Table 9. + * + * @param integer $versionNumber + * @return void + */ + protected static function buildVersion($versionNumber) + { + switch ($versionNumber) { + case 1: + $patterns = array(); + $ecBlocks = array( + new EcBlocks(7, new EcBlock(1, 19)), + new EcBlocks(10, new EcBlock(1, 16)), + new EcBlocks(13, new EcBlock(1, 13)), + new EcBlocks(17, new EcBlock(1, 9)), + ); + break; + + case 2: + $patterns = array(6, 18); + $ecBlocks = array( + new EcBlocks(10, new EcBlock(1, 34)), + new EcBlocks(16, new EcBlock(1, 28)), + new EcBlocks(22, new EcBlock(1, 22)), + new EcBlocks(28, new EcBlock(1, 16)), + ); + break; + + case 3: + $patterns = array(6, 22); + $ecBlocks = array( + new EcBlocks(15, new EcBlock(1, 55)), + new EcBlocks(26, new EcBlock(1, 44)), + new EcBlocks(18, new EcBlock(2, 17)), + new EcBlocks(22, new EcBlock(2, 13)), + ); + break; + + case 4: + $patterns = array(6, 26); + $ecBlocks = array( + new EcBlocks(20, new EcBlock(1, 80)), + new EcBlocks(18, new EcBlock(2, 32)), + new EcBlocks(26, new EcBlock(3, 24)), + new EcBlocks(16, new EcBlock(4, 9)), + ); + break; + + case 5: + $patterns = array(6, 30); + $ecBlocks = array( + new EcBlocks(26, new EcBlock(1, 108)), + new EcBlocks(24, new EcBlock(2, 43)), + new EcBlocks(18, new EcBlock(2, 15), new EcBlock(2, 16)), + new EcBlocks(22, new EcBlock(2, 11), new EcBlock(2, 12)), + ); + break; + + case 6: + $patterns = array(6, 34); + $ecBlocks = array( + new EcBlocks(18, new EcBlock(2, 68)), + new EcBlocks(16, new EcBlock(4, 27)), + new EcBlocks(24, new EcBlock(4, 19)), + new EcBlocks(28, new EcBlock(4, 15)), + ); + break; + + case 7: + $patterns = array(6, 22, 38); + $ecBlocks = array( + new EcBlocks(20, new EcBlock(2, 78)), + new EcBlocks(18, new EcBlock(4, 31)), + new EcBlocks(18, new EcBlock(2, 14), new EcBlock(4, 15)), + new EcBlocks(26, new EcBlock(4, 13)), + ); + break; + + case 8: + $patterns = array(6, 24, 42); + $ecBlocks = array( + new EcBlocks(24, new EcBlock(2, 97)), + new EcBlocks(22, new EcBlock(2, 38), new EcBlock(2, 39)), + new EcBlocks(22, new EcBlock(4, 18), new EcBlock(2, 19)), + new EcBlocks(26, new EcBlock(4, 14), new EcBlock(2, 15)), + ); + break; + + case 9: + $patterns = array(6, 26, 46); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(2, 116)), + new EcBlocks(22, new EcBlock(3, 36), new EcBlock(2, 37)), + new EcBlocks(20, new EcBlock(4, 16), new EcBlock(4, 17)), + new EcBlocks(24, new EcBlock(4, 12), new EcBlock(4, 13)), + ); + break; + + case 10: + $patterns = array(6, 28, 50); + $ecBlocks = array( + new EcBlocks(18, new EcBlock(2, 68), new EcBlock(2, 69)), + new EcBlocks(26, new EcBlock(4, 43), new EcBlock(1, 44)), + new EcBlocks(24, new EcBlock(6, 19), new EcBlock(2, 20)), + new EcBlocks(28, new EcBlock(6, 15), new EcBlock(2, 16)), + ); + break; + + case 11: + $patterns = array(6, 30, 54); + $ecBlocks = array( + new EcBlocks(20, new EcBlock(4, 81)), + new EcBlocks(30, new EcBlock(1, 50), new EcBlock(4, 51)), + new EcBlocks(28, new EcBlock(4, 22), new EcBlock(4, 23)), + new EcBlocks(24, new EcBlock(3, 12), new EcBlock(8, 13)), + ); + break; + + case 12: + $patterns = array(6, 32, 58); + $ecBlocks = array( + new EcBlocks(24, new EcBlock(2, 92), new EcBlock(2, 93)), + new EcBlocks(22, new EcBlock(6, 36), new EcBlock(2, 37)), + new EcBlocks(26, new EcBlock(4, 20), new EcBlock(6, 21)), + new EcBlocks(28, new EcBlock(7, 14), new EcBlock(4, 15)), + ); + break; + + case 13: + $patterns = array(6, 34, 62); + $ecBlocks = array( + new EcBlocks(26, new EcBlock(4, 107)), + new EcBlocks(22, new EcBlock(8, 37), new EcBlock(1, 38)), + new EcBlocks(24, new EcBlock(8, 20), new EcBlock(4, 21)), + new EcBlocks(22, new EcBlock(12, 11), new EcBlock(4, 12)), + ); + break; + + case 14: + $patterns = array(6, 26, 46, 66); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(3, 115), new EcBlock(1, 116)), + new EcBlocks(24, new EcBlock(4, 40), new EcBlock(5, 41)), + new EcBlocks(20, new EcBlock(11, 16), new EcBlock(5, 17)), + new EcBlocks(24, new EcBlock(11, 12), new EcBlock(5, 13)), + ); + break; + + case 15: + $patterns = array(6, 26, 48, 70); + $ecBlocks = array( + new EcBlocks(22, new EcBlock(5, 87), new EcBlock(1, 88)), + new EcBlocks(24, new EcBlock(5, 41), new EcBlock(5, 42)), + new EcBlocks(30, new EcBlock(5, 24), new EcBlock(7, 25)), + new EcBlocks(24, new EcBlock(11, 12), new EcBlock(7, 13)), + ); + break; + + case 16: + $patterns = array(6, 26, 50, 74); + $ecBlocks = array( + new EcBlocks(24, new EcBlock(5, 98), new EcBlock(1, 99)), + new EcBlocks(28, new EcBlock(7, 45), new EcBlock(3, 46)), + new EcBlocks(24, new EcBlock(15, 19), new EcBlock(2, 20)), + new EcBlocks(30, new EcBlock(3, 15), new EcBlock(13, 16)), + ); + break; + + case 17: + $patterns = array(6, 30, 54, 78); + $ecBlocks = array( + new EcBlocks(28, new EcBlock(1, 107), new EcBlock(5, 108)), + new EcBlocks(28, new EcBlock(10, 46), new EcBlock(1, 47)), + new EcBlocks(28, new EcBlock(1, 22), new EcBlock(15, 23)), + new EcBlocks(28, new EcBlock(2, 14), new EcBlock(17, 15)), + ); + break; + + case 18: + $patterns = array(6, 30, 56, 82); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(5, 120), new EcBlock(1, 121)), + new EcBlocks(26, new EcBlock(9, 43), new EcBlock(4, 44)), + new EcBlocks(28, new EcBlock(17, 22), new EcBlock(1, 23)), + new EcBlocks(28, new EcBlock(2, 14), new EcBlock(19, 15)), + ); + break; + + case 19: + $patterns = array(6, 30, 58, 86); + $ecBlocks = array( + new EcBlocks(28, new EcBlock(3, 113), new EcBlock(4, 114)), + new EcBlocks(26, new EcBlock(3, 44), new EcBlock(11, 45)), + new EcBlocks(26, new EcBlock(17, 21), new EcBlock(4, 22)), + new EcBlocks(26, new EcBlock(9, 13), new EcBlock(16, 14)), + ); + break; + + case 20: + $patterns = array(6, 34, 62, 90); + $ecBlocks = array( + new EcBlocks(28, new EcBlock(3, 107), new EcBlock(5, 108)), + new EcBlocks(26, new EcBlock(3, 41), new EcBlock(13, 42)), + new EcBlocks(30, new EcBlock(15, 24), new EcBlock(5, 25)), + new EcBlocks(28, new EcBlock(15, 15), new EcBlock(10, 16)), + ); + break; + + case 21: + $patterns = array(6, 28, 50, 72, 94); + $ecBlocks = array( + new EcBlocks(28, new EcBlock(4, 116), new EcBlock(4, 117)), + new EcBlocks(26, new EcBlock(17, 42)), + new EcBlocks(28, new EcBlock(17, 22), new EcBlock(6, 23)), + new EcBlocks(30, new EcBlock(19, 16), new EcBlock(6, 17)), + ); + break; + + case 22: + $patterns = array(6, 26, 50, 74, 98); + $ecBlocks = array( + new EcBlocks(28, new EcBlock(2, 111), new EcBlock(7, 112)), + new EcBlocks(28, new EcBlock(17, 46)), + new EcBlocks(30, new EcBlock(7, 24), new EcBlock(16, 25)), + new EcBlocks(24, new EcBlock(34, 13)), + ); + break; + + case 23: + $patterns = array(6, 30, 54, 78, 102); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(4, 121), new EcBlock(5, 122)), + new EcBlocks(28, new EcBlock(4, 47), new EcBlock(14, 48)), + new EcBlocks(30, new EcBlock(11, 24), new EcBlock(14, 25)), + new EcBlocks(30, new EcBlock(16, 15), new EcBlock(14, 16)), + ); + break; + + case 24: + $patterns = array(6, 28, 54, 80, 106); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(6, 117), new EcBlock(4, 118)), + new EcBlocks(28, new EcBlock(6, 45), new EcBlock(14, 46)), + new EcBlocks(30, new EcBlock(11, 24), new EcBlock(16, 25)), + new EcBlocks(30, new EcBlock(30, 16), new EcBlock(2, 17)), + ); + break; + + case 25: + $patterns = array(6, 32, 58, 84, 110); + $ecBlocks = array( + new EcBlocks(26, new EcBlock(8, 106), new EcBlock(4, 107)), + new EcBlocks(28, new EcBlock(8, 47), new EcBlock(13, 48)), + new EcBlocks(30, new EcBlock(7, 24), new EcBlock(22, 25)), + new EcBlocks(30, new EcBlock(22, 15), new EcBlock(13, 16)), + ); + break; + + case 26: + $patterns = array(6, 30, 58, 86, 114); + $ecBlocks = array( + new EcBlocks(28, new EcBlock(10, 114), new EcBlock(2, 115)), + new EcBlocks(28, new EcBlock(19, 46), new EcBlock(4, 47)), + new EcBlocks(28, new EcBlock(28, 22), new EcBlock(6, 23)), + new EcBlocks(30, new EcBlock(33, 16), new EcBlock(4, 17)), + ); + break; + + case 27: + $patterns = array(6, 34, 62, 90, 118); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(8, 122), new EcBlock(4, 123)), + new EcBlocks(28, new EcBlock(22, 45), new EcBlock(3, 46)), + new EcBlocks(30, new EcBlock(8, 23), new EcBlock(26, 24)), + new EcBlocks(30, new EcBlock(12, 15), new EcBlock(28, 16)), + ); + break; + + case 28: + $patterns = array(6, 26, 50, 74, 98, 122); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(3, 117), new EcBlock(10, 118)), + new EcBlocks(28, new EcBlock(3, 45), new EcBlock(23, 46)), + new EcBlocks(30, new EcBlock(4, 24), new EcBlock(31, 25)), + new EcBlocks(30, new EcBlock(11, 15), new EcBlock(31, 16)), + ); + break; + + case 29: + $patterns = array(6, 30, 54, 78, 102, 126); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(7, 116), new EcBlock(7, 117)), + new EcBlocks(28, new EcBlock(21, 45), new EcBlock(7, 46)), + new EcBlocks(30, new EcBlock(1, 23), new EcBlock(37, 24)), + new EcBlocks(30, new EcBlock(19, 15), new EcBlock(26, 16)), + ); + break; + + case 30: + $patterns = array(6, 26, 52, 78, 104, 130); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(5, 115), new EcBlock(10, 116)), + new EcBlocks(28, new EcBlock(19, 47), new EcBlock(10, 48)), + new EcBlocks(30, new EcBlock(15, 24), new EcBlock(25, 25)), + new EcBlocks(30, new EcBlock(23, 15), new EcBlock(25, 16)), + ); + break; + + case 31: + $patterns = array(6, 30, 56, 82, 108, 134); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(13, 115), new EcBlock(3, 116)), + new EcBlocks(28, new EcBlock(2, 46), new EcBlock(29, 47)), + new EcBlocks(30, new EcBlock(42, 24), new EcBlock(1, 25)), + new EcBlocks(30, new EcBlock(23, 15), new EcBlock(28, 16)), + ); + break; + + case 32: + $patterns = array(6, 34, 60, 86, 112, 138); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(17, 115)), + new EcBlocks(28, new EcBlock(10, 46), new EcBlock(23, 47)), + new EcBlocks(30, new EcBlock(10, 24), new EcBlock(35, 25)), + new EcBlocks(30, new EcBlock(19, 15), new EcBlock(35, 16)), + ); + break; + + case 33: + $patterns = array(6, 30, 58, 86, 114, 142); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(17, 115), new EcBlock(1, 116)), + new EcBlocks(28, new EcBlock(14, 46), new EcBlock(21, 47)), + new EcBlocks(30, new EcBlock(29, 24), new EcBlock(19, 25)), + new EcBlocks(30, new EcBlock(11, 15), new EcBlock(46, 16)), + ); + break; + + case 34: + $patterns = array(6, 34, 62, 90, 118, 146); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(13, 115), new EcBlock(6, 116)), + new EcBlocks(28, new EcBlock(14, 46), new EcBlock(23, 47)), + new EcBlocks(30, new EcBlock(44, 24), new EcBlock(7, 25)), + new EcBlocks(30, new EcBlock(59, 16), new EcBlock(1, 17)), + ); + break; + + case 35: + $patterns = array(6, 30, 54, 78, 102, 126, 150); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(12, 121), new EcBlock(7, 122)), + new EcBlocks(28, new EcBlock(12, 47), new EcBlock(26, 48)), + new EcBlocks(30, new EcBlock(39, 24), new EcBlock(14, 25)), + new EcBlocks(30, new EcBlock(22, 15), new EcBlock(41, 16)), + ); + break; + + case 36: + $patterns = array(6, 24, 50, 76, 102, 128, 154); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(6, 121), new EcBlock(14, 122)), + new EcBlocks(28, new EcBlock(6, 47), new EcBlock(34, 48)), + new EcBlocks(30, new EcBlock(46, 24), new EcBlock(10, 25)), + new EcBlocks(30, new EcBlock(2, 15), new EcBlock(64, 16)), + ); + break; + + case 37: + $patterns = array(6, 28, 54, 80, 106, 132, 158); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(17, 122), new EcBlock(4, 123)), + new EcBlocks(28, new EcBlock(29, 46), new EcBlock(14, 47)), + new EcBlocks(30, new EcBlock(49, 24), new EcBlock(10, 25)), + new EcBlocks(30, new EcBlock(24, 15), new EcBlock(46, 16)), + ); + break; + + case 38: + $patterns = array(6, 32, 58, 84, 110, 136, 162); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(4, 122), new EcBlock(18, 123)), + new EcBlocks(28, new EcBlock(13, 46), new EcBlock(32, 47)), + new EcBlocks(30, new EcBlock(48, 24), new EcBlock(14, 25)), + new EcBlocks(30, new EcBlock(42, 15), new EcBlock(32, 16)), + ); + break; + + case 39: + $patterns = array(6, 26, 54, 82, 110, 138, 166); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(20, 117), new EcBlock(4, 118)), + new EcBlocks(28, new EcBlock(40, 47), new EcBlock(7, 48)), + new EcBlocks(30, new EcBlock(43, 24), new EcBlock(22, 25)), + new EcBlocks(30, new EcBlock(10, 15), new EcBlock(67, 16)), + ); + break; + + case 40: + $patterns = array(6, 30, 58, 86, 114, 142, 170); + $ecBlocks = array( + new EcBlocks(30, new EcBlock(19, 118), new EcBlock(6, 119)), + new EcBlocks(28, new EcBlock(18, 47), new EcBlock(31, 48)), + new EcBlocks(30, new EcBlock(34, 24), new EcBlock(34, 25)), + new EcBlocks(30, new EcBlock(20, 15), new EcBlock(61, 16)), + ); + break; + } + + self::$versions[$versionNumber - 1] = new self( + $versionNumber, + SplFixedArray::fromArray($patterns, false), + SplFixedArray::fromArray($ecBlocks, false) + ); + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/BlockPair.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/BlockPair.php new file mode 100644 index 0000000..090db29 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/BlockPair.php @@ -0,0 +1,64 @@ +dataBytes = $data; + $this->errorCorrectionBytes = $errorCorrection; + } + + /** + * Gets the data bytes. + * + * @return SplFixedArray + */ + public function getDataBytes() + { + return $this->dataBytes; + } + + /** + * Gets the error correction bytes. + * + * @return SplFixedArray + */ + public function getErrorCorrectionBytes() + { + return $this->errorCorrectionBytes; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/ByteMatrix.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/ByteMatrix.php new file mode 100644 index 0000000..a378f08 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/ByteMatrix.php @@ -0,0 +1,158 @@ +height = $height; + $this->width = $width; + $this->bytes = new SplFixedArray($height); + + for ($y = 0; $y < $height; $y++) { + $this->bytes[$y] = new SplFixedArray($width); + } + } + + /** + * Gets the width of the matrix. + * + * @return integer + */ + public function getWidth() + { + return $this->width; + } + + /** + * Gets the height of the matrix. + * + * @return integer + */ + public function getHeight() + { + return $this->height; + } + + /** + * Gets the internal representation of the matrix. + * + * @return SplFixedArray + */ + public function getArray() + { + return $this->bytes; + } + + /** + * Gets the byte for a specific position. + * + * @param integer $x + * @param integer $y + * @return integer + */ + public function get($x, $y) + { + return $this->bytes[$y][$x]; + } + + /** + * Sets the byte for a specific position. + * + * @param integer $x + * @param integer $y + * @param integer $value + * @return void + */ + public function set($x, $y, $value) + { + $this->bytes[$y][$x] = (int) $value; + } + + /** + * Clears the matrix with a specific value. + * + * @param integer $value + * @return void + */ + public function clear($value) + { + for ($y = 0; $y < $this->height; $y++) { + for ($x = 0; $x < $this->width; $x++) { + $this->bytes[$y][$x] = $value; + } + } + } + + /** + * Returns a string representation of the matrix. + * + * @return string + */ + public function __toString() + { + $result = ''; + + for ($y = 0; $y < $this->height; $y++) { + for ($x = 0; $x < $this->width; $x++) { + switch ($this->bytes[$y][$x]) { + case 0: + $result .= ' 0'; + break; + + case 1: + $result .= ' 1'; + break; + + default: + $result .= ' '; + break; + } + } + + $result .= "\n"; + } + + return $result; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/Encoder.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/Encoder.php new file mode 100644 index 0000000..c8efc35 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/Encoder.php @@ -0,0 +1,687 @@ +get() === Mode::BYTE && $encoding !== self::DEFAULT_BYTE_MODE_ECODING) { + $eci = CharacterSetEci::getCharacterSetEciByName($encoding); + + if ($eci !== null) { + self::appendEci($eci, $headerBits); + } + } + + // (With ECI in place,) Write the mode marker + self::appendModeInfo($mode, $headerBits); + + // Collect data within the main segment, separately, to count its size + // if needed. Don't add it to main payload yet. + $dataBits = new BitArray(); + self::appendBytes($content, $mode, $dataBits, $encoding); + + // Hard part: need to know version to know how many bits length takes. + // But need to know how many bits it takes to know version. First we + // take a guess at version by assuming version will be the minimum, 1: + $provisionalBitsNeeded = $headerBits->getSize() + + $mode->getCharacterCountBits(Version::getVersionForNumber(1)) + + $dataBits->getSize(); + $provisionalVersion = self::chooseVersion($provisionalBitsNeeded, $ecLevel); + + // Use that guess to calculate the right version. I am still not sure + // this works in 100% of cases. + $bitsNeeded = $headerBits->getSize() + + $mode->getCharacterCountBits($provisionalVersion) + + $dataBits->getSize(); + $version = self::chooseVersion($bitsNeeded, $ecLevel); + + $headerAndDataBits = new BitArray(); + $headerAndDataBits->appendBitArray($headerBits); + + // Find "length" of main segment and write it. + $numLetters = ($mode->get() === Mode::BYTE ? $dataBits->getSizeInBytes() : strlen($content)); + self::appendLengthInfo($numLetters, $version, $mode, $headerAndDataBits); + + // Put data together into the overall payload. + $headerAndDataBits->appendBitArray($dataBits); + $ecBlocks = $version->getEcBlocksForLevel($ecLevel); + $numDataBytes = $version->getTotalCodewords() - $ecBlocks->getTotalEcCodewords(); + + // Terminate the bits properly. + self::terminateBits($numDataBytes, $headerAndDataBits); + + // Interleave data bits with error correction code. + $finalBits = self::interleaveWithEcBytes( + $headerAndDataBits, + $version->getTotalCodewords(), + $numDataBytes, + $ecBlocks->getNumBlocks() + ); + + $qrCode = new QrCode(); + $qrCode->setErrorCorrectionLevel($ecLevel); + $qrCode->setMode($mode); + $qrCode->setVersion($version); + + // Choose the mask pattern and set to "qrCode". + $dimension = $version->getDimensionForVersion(); + $matrix = new ByteMatrix($dimension, $dimension); + $maskPattern = self::chooseMaskPattern($finalBits, $ecLevel, $version, $matrix); + $qrCode->setMaskPattern($maskPattern); + + // Build the matrix and set it to "qrCode". + MatrixUtil::buildMatrix($finalBits, $ecLevel, $version, $maskPattern, $matrix); + $qrCode->setMatrix($matrix); + + return $qrCode; + } + + /** + * Gets the alphanumeric code for a byte. + * + * @param string|integer $code + * @return integer + */ + protected static function getAlphanumericCode($code) + { + $code = (is_string($code) ? ord($code) : $code); + + if (isset(self::$alphanumericTable[$code])) { + return self::$alphanumericTable[$code]; + } + + return -1; + } + + /** + * Chooses the best mode for a given content. + * + * @param string $content + * @param string $encoding + * @return Mode + */ + protected static function chooseMode($content, $encoding = null) + { + if (strcasecmp($encoding, 'SHIFT-JIS') === 0) { + return self::isOnlyDoubleByteKanji($content) ? new Mode(Mode::KANJI) : new Mode(Mode::BYTE); + } + + $hasNumeric = false; + $hasAlphanumeric = false; + $contentLength = strlen($content); + + for ($i = 0; $i < $contentLength; $i++) { + $char = $content[$i]; + + if (ctype_digit($char)) { + $hasNumeric = true; + } elseif (self::getAlphanumericCode($char) !== -1) { + $hasAlphanumeric = true; + } else { + return new Mode(Mode::BYTE); + } + } + + if ($hasAlphanumeric) { + return new Mode(Mode::ALPHANUMERIC); + } elseif ($hasNumeric) { + return new Mode(Mode::NUMERIC); + } + + return new Mode(Mode::BYTE); + } + + /** + * Calculates the mask penalty for a matrix. + * + * @param ByteMatrix $matrix + * @return integer + */ + protected static function calculateMaskPenalty(ByteMatrix $matrix) + { + return ( + MaskUtil::applyMaskPenaltyRule1($matrix) + + MaskUtil::applyMaskPenaltyRule2($matrix) + + MaskUtil::applyMaskPenaltyRule3($matrix) + + MaskUtil::applyMaskPenaltyRule4($matrix) + ); + } + + /** + * Chooses the best mask pattern for a matrix. + * + * @param BitArray $bits + * @param ErrorCorrectionLevel $ecLevel + * @param Version $version + * @param ByteMatrix $matrix + * @return integer + */ + protected static function chooseMaskPattern( + BitArray $bits, + ErrorCorrectionLevel $ecLevel, + Version $version, + ByteMatrix $matrix + ) { + $minPenality = PHP_INT_MAX; + $bestMaskPattern = -1; + + for ($maskPattern = 0; $maskPattern < QrCode::NUM_MASK_PATTERNS; $maskPattern++) { + MatrixUtil::buildMatrix($bits, $ecLevel, $version, $maskPattern, $matrix); + $penalty = self::calculateMaskPenalty($matrix); + + if ($penalty < $minPenality) { + $minPenality = $penalty; + $bestMaskPattern = $maskPattern; + } + } + + return $bestMaskPattern; + } + + /** + * Chooses the best version for the input. + * + * @param integer $numInputBits + * @param ErrorCorrectionLevel $ecLevel + * @return Version + * @throws Exception\WriterException + */ + protected static function chooseVersion($numInputBits, ErrorCorrectionLevel $ecLevel) + { + for ($versionNum = 1; $versionNum <= 40; $versionNum++) { + $version = Version::getVersionForNumber($versionNum); + $numBytes = $version->getTotalCodewords(); + + $ecBlocks = $version->getEcBlocksForLevel($ecLevel); + $numEcBytes = $ecBlocks->getTotalEcCodewords(); + + $numDataBytes = $numBytes - $numEcBytes; + $totalInputBytes = intval(($numInputBits + 8) / 8); + + if ($numDataBytes >= $totalInputBytes) { + return $version; + } + } + + throw new Exception\WriterException('Data too big'); + } + + /** + * Terminates the bits in a bit array. + * + * @param integer $numDataBytes + * @param BitArray $bits + * @throws Exception\WriterException + */ + protected static function terminateBits($numDataBytes, BitArray $bits) + { + $capacity = $numDataBytes << 3; + + if ($bits->getSize() > $capacity) { + throw new Exception\WriterException('Data bits cannot fit in the QR code'); + } + + for ($i = 0; $i < 4 && $bits->getSize() < $capacity; $i++) { + $bits->appendBit(false); + } + + $numBitsInLastByte = $bits->getSize() & 0x7; + + if ($numBitsInLastByte > 0) { + for ($i = $numBitsInLastByte; $i < 8; $i++) { + $bits->appendBit(false); + } + } + + $numPaddingBytes = $numDataBytes - $bits->getSizeInBytes(); + + for ($i = 0; $i < $numPaddingBytes; $i++) { + $bits->appendBits(($i & 0x1) === 0 ? 0xec : 0x11, 8); + } + + if ($bits->getSize() !== $capacity) { + throw new Exception\WriterException('Bits size does not equal capacity'); + } + } + + /** + * Gets number of data- and EC bytes for a block ID. + * + * @param integer $numTotalBytes + * @param integer $numDataBytes + * @param integer $numRsBlocks + * @param integer $blockId + * @return array + * @throws Exception\WriterException + */ + protected static function getNumDataBytesAndNumEcBytesForBlockId( + $numTotalBytes, + $numDataBytes, + $numRsBlocks, + $blockId + ) { + if ($blockId >= $numRsBlocks) { + throw new Exception\WriterException('Block ID too large'); + } + + $numRsBlocksInGroup2 = $numTotalBytes % $numRsBlocks; + $numRsBlocksInGroup1 = $numRsBlocks - $numRsBlocksInGroup2; + $numTotalBytesInGroup1 = intval($numTotalBytes / $numRsBlocks); + $numTotalBytesInGroup2 = $numTotalBytesInGroup1 + 1; + $numDataBytesInGroup1 = intval($numDataBytes / $numRsBlocks); + $numDataBytesInGroup2 = $numDataBytesInGroup1 + 1; + $numEcBytesInGroup1 = $numTotalBytesInGroup1 - $numDataBytesInGroup1; + $numEcBytesInGroup2 = $numTotalBytesInGroup2 - $numDataBytesInGroup2; + + if ($numEcBytesInGroup1 !== $numEcBytesInGroup2) { + throw new Exception\WriterException('EC bytes mismatch'); + } + + if ($numRsBlocks !== $numRsBlocksInGroup1 + $numRsBlocksInGroup2) { + throw new Exception\WriterException('RS blocks mismatch'); + } + + if ($numTotalBytes !== + (($numDataBytesInGroup1 + $numEcBytesInGroup1) * $numRsBlocksInGroup1) + + (($numDataBytesInGroup2 + $numEcBytesInGroup2) * $numRsBlocksInGroup2) + ) { + throw new Exception\WriterException('Total bytes mismatch'); + } + + if ($blockId < $numRsBlocksInGroup1) { + return array($numDataBytesInGroup1, $numEcBytesInGroup1); + } else { + return array($numDataBytesInGroup2, $numEcBytesInGroup2); + } + } + + /** + * Interleaves data with EC bytes. + * + * @param BitArray $bits + * @param integer $numTotalBytes + * @param integer $numDataBytes + * @param integer $numRsBlocks + * @return BitArray + * @throws Exception\WriterException + */ + protected static function interleaveWithEcBytes(BitArray $bits, $numTotalBytes, $numDataBytes, $numRsBlocks) + { + if ($bits->getSizeInBytes() !== $numDataBytes) { + throw new Exception\WriterException('Number of bits and data bytes does not match'); + } + + $dataBytesOffset = 0; + $maxNumDataBytes = 0; + $maxNumEcBytes = 0; + + $blocks = new SplFixedArray($numRsBlocks); + + for ($i = 0; $i < $numRsBlocks; $i++) { + list($numDataBytesInBlock, $numEcBytesInBlock) = self::getNumDataBytesAndNumEcBytesForBlockId( + $numTotalBytes, + $numDataBytes, + $numRsBlocks, + $i + ); + + $size = $numDataBytesInBlock; + $dataBytes = $bits->toBytes(8 * $dataBytesOffset, $size); + $ecBytes = self::generateEcBytes($dataBytes, $numEcBytesInBlock); + $blocks[$i] = new BlockPair($dataBytes, $ecBytes); + + $maxNumDataBytes = max($maxNumDataBytes, $size); + $maxNumEcBytes = max($maxNumEcBytes, count($ecBytes)); + $dataBytesOffset += $numDataBytesInBlock; + } + + if ($numDataBytes !== $dataBytesOffset) { + throw new Exception\WriterException('Data bytes does not match offset'); + } + + $result = new BitArray(); + + for ($i = 0; $i < $maxNumDataBytes; $i++) { + foreach ($blocks as $block) { + $dataBytes = $block->getDataBytes(); + + if ($i < count($dataBytes)) { + $result->appendBits($dataBytes[$i], 8); + } + } + } + + for ($i = 0; $i < $maxNumEcBytes; $i++) { + foreach ($blocks as $block) { + $ecBytes = $block->getErrorCorrectionBytes(); + + if ($i < count($ecBytes)) { + $result->appendBits($ecBytes[$i], 8); + } + } + } + + if ($numTotalBytes !== $result->getSizeInBytes()) { + throw new Exception\WriterException('Interleaving error: ' . $numTotalBytes . ' and ' . $result->getSizeInBytes() . ' differ'); + } + + return $result; + } + + /** + * Generates EC bytes for given data. + * + * @param SplFixedArray $dataBytes + * @param integer $numEcBytesInBlock + * @return SplFixedArray + */ + protected static function generateEcBytes(SplFixedArray $dataBytes, $numEcBytesInBlock) + { + $numDataBytes = count($dataBytes); + $toEncode = new SplFixedArray($numDataBytes + $numEcBytesInBlock); + + for ($i = 0; $i < $numDataBytes; $i++) { + $toEncode[$i] = $dataBytes[$i] & 0xff; + } + + $ecBytes = new SplFixedArray($numEcBytesInBlock); + $codec = self::getCodec($numDataBytes, $numEcBytesInBlock); + $codec->encode($toEncode, $ecBytes); + + return $ecBytes; + } + + /** + * Gets an RS codec and caches it. + * + * @param integer $numDataBytes + * @param integer $numEcBytesInBlock + * @return ReedSolomonCodec + */ + protected static function getCodec($numDataBytes, $numEcBytesInBlock) + { + $cacheId = $numDataBytes . '-' . $numEcBytesInBlock; + + if (!isset(self::$codecs[$cacheId])) { + self::$codecs[$cacheId] = new ReedSolomonCodec( + 8, + 0x11d, + 0, + 1, + $numEcBytesInBlock, + 255 - $numDataBytes - $numEcBytesInBlock + ); + } + + return self::$codecs[$cacheId]; + } + + /** + * Appends mode information to a bit array. + * + * @param Mode $mode + * @param BitArray $bits + * @return void + */ + protected static function appendModeInfo(Mode $mode, BitArray $bits) + { + $bits->appendBits($mode->get(), 4); + } + + /** + * Appends length information to a bit array. + * + * @param integer $numLetters + * @param Version $version + * @param Mode $mode + * @param BitArray $bits + * @return void + * @throws Exception\WriterException + */ + protected static function appendLengthInfo($numLetters, Version $version, Mode $mode, BitArray $bits) + { + $numBits = $mode->getCharacterCountBits($version); + + if ($numLetters >= (1 << $numBits)) { + throw new Exception\WriterException($numLetters . ' is bigger than ' . ((1 << $numBits) - 1)); + } + + $bits->appendBits($numLetters, $numBits); + } + + /** + * Appends bytes to a bit array in a specific mode. + * + * @param stirng $content + * @param Mode $mode + * @param BitArray $bits + * @param string $encoding + * @return void + * @throws Exception\WriterException + */ + protected static function appendBytes($content, Mode $mode, BitArray $bits, $encoding) + { + switch ($mode->get()) { + case Mode::NUMERIC: + self::appendNumericBytes($content, $bits); + break; + + case Mode::ALPHANUMERIC: + self::appendAlphanumericBytes($content, $bits); + break; + + case Mode::BYTE: + self::append8BitBytes($content, $bits, $encoding); + break; + + case Mode::KANJI: + self::appendKanjiBytes($content, $bits); + break; + + default: + throw new Exception\WriterException('Invalid mode: ' . $mode->get()); + } + } + + /** + * Appends numeric bytes to a bit array. + * + * @param string $content + * @param BitArray $bits + * @return void + */ + protected static function appendNumericBytes($content, BitArray $bits) + { + $length = strlen($content); + $i = 0; + + while ($i < $length) { + $num1 = (int) $content[$i]; + + if ($i + 2 < $length) { + // Encode three numeric letters in ten bits. + $num2 = (int) $content[$i + 1]; + $num3 = (int) $content[$i + 2]; + $bits->appendBits($num1 * 100 + $num2 * 10 + $num3, 10); + $i += 3; + } elseif ($i + 1 < $length) { + // Encode two numeric letters in seven bits. + $num2 = (int) $content[$i + 1]; + $bits->appendBits($num1 * 10 + $num2, 7); + $i += 2; + } else { + // Encode one numeric letter in four bits. + $bits->appendBits($num1, 4); + $i++; + } + } + } + + /** + * Appends alpha-numeric bytes to a bit array. + * + * @param string $content + * @param BitArray $bits + * @return void + */ + protected static function appendAlphanumericBytes($content, BitArray $bits) + { + $length = strlen($content); + $i = 0; + + while ($i < $length) { + if (-1 === ($code1 = self::getAlphanumericCode($content[$i]))) { + throw new Exception\WriterException('Invalid alphanumeric code'); + } + + if ($i + 1 < $length) { + if (-1 === ($code2 = self::getAlphanumericCode($content[$i + 1]))) { + throw new Exception\WriterException('Invalid alphanumeric code'); + } + + // Encode two alphanumeric letters in 11 bits. + $bits->appendBits($code1 * 45 + $code2, 11); + $i += 2; + } else { + // Encode one alphanumeric letter in six bits. + $bits->appendBits($code1, 6); + $i++; + } + } + } + + /** + * Appends regular 8-bit bytes to a bit array. + * + * @param string $content + * @param BitArray $bits + * @return void + */ + protected static function append8BitBytes($content, BitArray $bits, $encoding) + { + if (false === ($bytes = @iconv('utf-8', $encoding, $content))) { + throw new Exception\WriterException('Could not encode content to ' . $encoding); + } + + $length = strlen($bytes); + + for ($i = 0; $i < $length; $i++) { + $bits->appendBits(ord($bytes[$i]), 8); + } + } + + /** + * Appends KANJI bytes to a bit array. + * + * @param string $content + * @param BitArray $bits + * @return void + */ + protected static function appendKanjiBytes($content, BitArray $bits) + { + if (strlen($content) % 2 > 0) { + // We just do a simple length check here. The for loop will check + // individual characters. + throw new Exception\WriterException('Content does not seem to be encoded in SHIFT-JIS'); + } + + $length = strlen($content); + + for ($i = 0; $i < $length; $i += 2) { + $byte1 = ord($content[$i]) & 0xff; + $byte2 = ord($content[$i + 1]) & 0xff; + $code = ($byte1 << 8) | $byte2; + + if ($code >= 0x8140 && $code <= 0x9ffc) { + $subtracted = $code - 0x8140; + } elseif ($code >= 0xe040 && $code <= 0xebbf) { + $subtracted = $code - 0xc140; + } else { + throw new Exception\WriterException('Invalid byte sequence'); + } + + $encoded = (($subtracted >> 8) * 0xc0) + ($subtracted & 0xff); + + $bits->appendBits($encoded, 13); + } + } + + /** + * Appends ECI information to a bit array. + * + * @param CharacterSetEci $eci + * @param BitArray $bits + * @return void + */ + protected static function appendEci(CharacterSetEci $eci, BitArray $bits) + { + $mode = new Mode(Mode::ECI); + $bits->appendBits($mode->get(), 4); + $bits->appendBits($eci->get(), 8); + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/MaskUtil.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/MaskUtil.php new file mode 100644 index 0000000..c294d55 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/MaskUtil.php @@ -0,0 +1,291 @@ +getArray(); + $width = $matrix->getWidth(); + $height = $matrix->getHeight(); + + for ($y = 0; $y < $height - 1; $y++) { + for ($x = 0; $x < $width - 1; $x++) { + $value = $array[$y][$x]; + + if ($value === $array[$y][$x + 1] && $value === $array[$y + 1][$x] && $value === $array[$y + 1][$x + 1]) { + $penalty++; + } + } + } + + return self::N2 * $penalty; + } + + /** + * Applies mask penalty rule 3 and returns the penalty. + * + * Finds consecutive cells of 00001011101 or 10111010000, and gives penalty + * to them. If we find patterns like 000010111010000, we give penalties + * twice (i.e. 40 * 2). + * + * @param ByteMatrix $matrix + * @return integer + */ + public static function applyMaskPenaltyRule3(ByteMatrix $matrix) + { + $penalty = 0; + $array = $matrix->getArray(); + $width = $matrix->getWidth(); + $height = $matrix->getHeight(); + + for ($y = 0; $y < $height; $y++) { + for ($x = 0; $x < $width; $x++) { + if ( + $x + 6 < $width + && $array[$y][$x] === 1 + && $array[$y][$x + 1] === 0 + && $array[$y][$x + 2] === 1 + && $array[$y][$x + 3] === 1 + && $array[$y][$x + 4] === 1 + && $array[$y][$x + 5] === 0 + && $array[$y][$x + 6] === 1 + && ( + ( + $x + 10 < $width + && $array[$y][$x + 7] === 0 + && $array[$y][$x + 8] === 0 + && $array[$y][$x + 9] === 0 + && $array[$y][$x + 10] === 0 + ) + || ( + $x - 4 >= 0 + && $array[$y][$x - 1] === 0 + && $array[$y][$x - 2] === 0 + && $array[$y][$x - 3] === 0 + && $array[$y][$x - 4] === 0 + ) + ) + ) { + $penalty += self::N3; + } + + if ( + $y + 6 < $height + && $array[$y][$x] === 1 + && $array[$y + 1][$x] === 0 + && $array[$y + 2][$x] === 1 + && $array[$y + 3][$x] === 1 + && $array[$y + 4][$x] === 1 + && $array[$y + 5][$x] === 0 + && $array[$y + 6][$x] === 1 + && ( + ( + $y + 10 < $height + && $array[$y + 7][$x] === 0 + && $array[$y + 8][$x] === 0 + && $array[$y + 9][$x] === 0 + && $array[$y + 10][$x] === 0 + ) + || ( + $y - 4 >= 0 + && $array[$y - 1][$x] === 0 + && $array[$y - 2][$x] === 0 + && $array[$y - 3][$x] === 0 + && $array[$y - 4][$x] === 0 + ) + ) + ) { + $penalty += self::N3; + } + } + } + + return $penalty; + } + + /** + * Applies mask penalty rule 4 and returns the penalty. + * + * Calculates the ratio of dark cells and gives penalty if the ratio is far + * from 50%. It gives 10 penalty for 5% distance. + * + * @param ByteMatrix $matrix + * @return integer + */ + public static function applyMaskPenaltyRule4(ByteMatrix $matrix) + { + $numDarkCells = 0; + + $array = $matrix->getArray(); + $width = $matrix->getWidth(); + $height = $matrix->getHeight(); + + for ($y = 0; $y < $height; $y++) { + $arrayY = $array[$y]; + + for ($x = 0; $x < $width; $x++) { + if ($arrayY[$x] === 1) { + $numDarkCells++; + } + } + } + + $numTotalCells = $height * $width; + $darkRatio = $numDarkCells / $numTotalCells; + $fixedPercentVariances = (int) (abs($darkRatio - 0.5) * 20); + + return $fixedPercentVariances * self::N4; + } + + /** + * Returns the mask bit for "getMaskPattern" at "x" and "y". + * + * See 8.8 of JISX0510:2004 for mask pattern conditions. + * + * @param integer $maskPattern + * @param integer $x + * @param integer $y + * @return integer + * @throws Exception\InvalidArgumentException + */ + public static function getDataMaskBit($maskPattern, $x, $y) + { + switch ($maskPattern) { + case 0: + $intermediate = ($y + $x) & 0x1; + break; + + case 1: + $intermediate = $y & 0x1; + break; + + case 2: + $intermediate = $x % 3; + break; + + case 3: + $intermediate = ($y + $x) % 3; + break; + + case 4: + $intermediate = (BitUtils::unsignedRightShift($y, 1) + ($x / 3)) & 0x1; + break; + + case 5: + $temp = $y * $x; + $intermediate = ($temp & 0x1) + ($temp % 3); + break; + + case 6: + $temp = $y * $x; + $intermediate = (($temp & 0x1) + ($temp % 3)) & 0x1; + break; + + case 7: + $temp = $y * $x; + $intermediate = (($temp % 3) + (($y + $x) & 0x1)) & 0x1; + break; + + default: + throw new Exception\InvalidArgumentException('Invalid mask pattern: ' . $maskPattern); + } + + return $intermediate === 0; + } + + /** + * Helper function for applyMaskPenaltyRule1. + * + * We need this for doing this calculation in both vertical and horizontal + * orders respectively. + * + * @param ByteMatrix $matrix + * @param boolean $isHorizontal + * @return integer + */ + protected static function applyMaskPenaltyRule1Internal(ByteMatrix $matrix, $isHorizontal) + { + $penalty = 0; + $iLimit = $isHorizontal ? $matrix->getHeight() : $matrix->getWidth(); + $jLimit = $isHorizontal ? $matrix->getWidth() : $matrix->getHeight(); + $array = $matrix->getArray(); + + for ($i = 0; $i < $iLimit; $i++) { + $numSameBitCells = 0; + $prevBit = -1; + + for ($j = 0; $j < $jLimit; $j++) { + $bit = $isHorizontal ? $array[$i][$j] : $array[$j][$i]; + + if ($bit === $prevBit) { + $numSameBitCells++; + } else { + if ($numSameBitCells >= 5) { + $penalty += self::N1 + ($numSameBitCells - 5); + } + + $numSameBitCells = 1; + $prevBit = $bit; + } + } + + if ($numSameBitCells >= 5) { + $penalty += self::N1 + ($numSameBitCells - 5); + } + } + + return $penalty; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/MatrixUtil.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/MatrixUtil.php new file mode 100644 index 0000000..8327381 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/MatrixUtil.php @@ -0,0 +1,580 @@ +clear(-1); + } + + /** + * Builds a complete matrix. + * + * @param BitArray $dataBits + * @param ErrorCorrectionLevel $level + * @param Version $version + * @param integer $maskPattern + * @param ByteMatrix $matrix + * @return void + */ + public static function buildMatrix( + BitArray $dataBits, + ErrorCorrectionLevel $level, + Version $version, + $maskPattern, + ByteMatrix $matrix + ) { + self::clearMatrix($matrix); + self::embedBasicPatterns($version, $matrix); + self::embedTypeInfo($level, $maskPattern, $matrix); + self::maybeEmbedVersionInfo($version, $matrix); + self::embedDataBits($dataBits, $maskPattern, $matrix); + } + + /** + * Embeds type information into a matrix. + * + * @param ErrorCorrectionLevel $level + * @param integer $maskPattern + * @param ByteMatrix $matrix + * @return void + */ + protected static function embedTypeInfo(ErrorCorrectionLevel $level, $maskPattern, ByteMatrix $matrix) + { + $typeInfoBits = new BitArray(); + self::makeTypeInfoBits($level, $maskPattern, $typeInfoBits); + + $typeInfoBitsSize = $typeInfoBits->getSize(); + + for ($i = 0; $i < $typeInfoBitsSize; $i++) { + $bit = $typeInfoBits->get($typeInfoBitsSize - 1 - $i); + + $x1 = self::$typeInfoCoordinates[$i][0]; + $y1 = self::$typeInfoCoordinates[$i][1]; + + $matrix->set($x1, $y1, $bit); + + if ($i < 8) { + $x2 = $matrix->getWidth() - $i - 1; + $y2 = 8; + } else { + $x2 = 8; + $y2 = $matrix->getHeight() - 7 + ($i - 8); + } + + $matrix->set($x2, $y2, $bit); + } + } + + /** + * Generates type information bits and appends them to a bit array. + * + * @param ErrorCorrectionLevel $level + * @param integer $maskPattern + * @param BitArray $bits + * @return void + * @throws Exception\RuntimeException + */ + protected static function makeTypeInfoBits(ErrorCorrectionLevel $level, $maskPattern, BitArray $bits) + { + $typeInfo = ($level->get() << 3) | $maskPattern; + $bits->appendBits($typeInfo, 5); + + $bchCode = self::calculateBchCode($typeInfo, self::$typeInfoPoly); + $bits->appendBits($bchCode, 10); + + $maskBits = new BitArray(); + $maskBits->appendBits(self::$typeInfoMaskPattern, 15); + $bits->xorBits($maskBits); + + if ($bits->getSize() !== 15) { + throw new Exception\RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize()); + } + } + + /** + * Embeds version information if required. + * + * @param Version $version + * @param ByteMatrix $matrix + * @return void + */ + protected static function maybeEmbedVersionInfo(Version $version, ByteMatrix $matrix) + { + if ($version->getVersionNumber() < 7) { + return; + } + + $versionInfoBits = new BitArray(); + self::makeVersionInfoBits($version, $versionInfoBits); + + $bitIndex = 6 * 3 - 1; + + for ($i = 0; $i < 6; $i++) { + for ($j = 0; $j < 3; $j++) { + $bit = $versionInfoBits->get($bitIndex); + $bitIndex--; + + $matrix->set($i, $matrix->getHeight() - 11 + $j, $bit); + $matrix->set($matrix->getHeight() - 11 + $j, $i, $bit); + } + } + } + + /** + * Generates version information bits and appends them to a bit array. + * + * @param Version $version + * @param BitArray $bits + * @return void + * @throws Exception\RuntimeException + */ + protected static function makeVersionInfoBits(Version $version, BitArray $bits) + { + $bits->appendBits($version->getVersionNumber(), 6); + + $bchCode = self::calculateBchCode($version->getVersionNumber(), self::$versionInfoPoly); + $bits->appendBits($bchCode, 12); + + if ($bits->getSize() !== 18) { + throw new Exception\RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize()); + } + } + + /** + * Calculates the BHC code for a value and a polynomial. + * + * @param integer $value + * @param integer $poly + * @return integer + */ + protected static function calculateBchCode($value, $poly) + { + $msbSetInPoly = self::findMsbSet($poly); + $value <<= $msbSetInPoly - 1; + + while (self::findMsbSet($value) >= $msbSetInPoly) { + $value ^= $poly << (self::findMsbSet($value) - $msbSetInPoly); + } + + return $value; + } + + /** + * Finds and MSB set. + * + * @param integer $value + * @return integer + */ + protected static function findMsbSet($value) + { + $numDigits = 0; + + while ($value !== 0) { + $value >>= 1; + $numDigits++; + } + + return $numDigits; + } + + /** + * Embeds basic patterns into a matrix. + * + * @param Version $version + * @param ByteMatrix $matrix + * @return void + */ + protected static function embedBasicPatterns(Version $version, ByteMatrix $matrix) + { + self::embedPositionDetectionPatternsAndSeparators($matrix); + self::embedDarkDotAtLeftBottomCorner($matrix); + self::maybeEmbedPositionAdjustmentPatterns($version, $matrix); + self::embedTimingPatterns($matrix); + } + + /** + * Embeds position detection patterns and separators into a byte matrix. + * + * @param ByteMatrix $matrix + * @return void + */ + protected static function embedPositionDetectionPatternsAndSeparators(ByteMatrix $matrix) + { + $pdpWidth = count(self::$positionDetectionPattern[0]); + + self::embedPositionDetectionPattern(0, 0, $matrix); + self::embedPositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix); + self::embedPositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix); + + $hspWidth = 8; + + self::embedHorizontalSeparationPattern(0, $hspWidth - 1, $matrix); + self::embedHorizontalSeparationPattern($matrix->getWidth() - $hspWidth, $hspWidth - 1, $matrix); + self::embedHorizontalSeparationPattern(0, $matrix->getWidth() - $hspWidth, $matrix); + + $vspSize = 7; + + self::embedVerticalSeparationPattern($vspSize, 0, $matrix); + self::embedVerticalSeparationPattern($matrix->getHeight() - $vspSize - 1, 0, $matrix); + self::embedVerticalSeparationPattern($vspSize, $matrix->getHeight() - $vspSize, $matrix); + } + + /** + * Embeds a single position detection pattern into a byte matrix. + * + * @param integer $xStart + * @param integer $yStart + * @param ByteMatrix $matrix + * @return void + */ + protected static function embedPositionDetectionPattern($xStart, $yStart, ByteMatrix $matrix) + { + for ($y = 0; $y < 7; $y++) { + for ($x = 0; $x < 7; $x++) { + $matrix->set($xStart + $x, $yStart + $y, self::$positionDetectionPattern[$y][$x]); + } + } + } + + /** + * Embeds a single horizontal separation pattern. + * + * @param integer $xStart + * @param integer $yStart + * @param ByteMatrix $matrix + * @return void + * @throws Exception\RuntimeException + */ + protected static function embedHorizontalSeparationPattern($xStart, $yStart, ByteMatrix $matrix) + { + for ($x = 0; $x < 8; $x++) { + if ($matrix->get($xStart + $x, $yStart) !== -1) { + throw new Exception\RuntimeException('Byte already set'); + } + + $matrix->set($xStart + $x, $yStart, 0); + } + } + + /** + * Embeds a single vertical separation pattern. + * + * @param integer $xStart + * @param integer $yStart + * @param ByteMatrix $matrix + * @return void + * @throws Exception\RuntimeException + */ + protected static function embedVerticalSeparationPattern($xStart, $yStart, ByteMatrix $matrix) + { + for ($y = 0; $y < 7; $y++) { + if ($matrix->get($xStart, $yStart + $y) !== -1) { + throw new Exception\RuntimeException('Byte already set'); + } + + $matrix->set($xStart, $yStart + $y, 0); + } + } + + /** + * Embeds a dot at the left bottom corner. + * + * @param ByteMatrix $matrix + * @return void + * @throws Exception\RuntimeException + */ + protected static function embedDarkDotAtLeftBottomCorner(ByteMatrix $matrix) + { + if ($matrix->get(8, $matrix->getHeight() - 8) === 0) { + throw new Exception\RuntimeException('Byte already set to 0'); + } + + $matrix->set(8, $matrix->getHeight() - 8, 1); + } + + /** + * Embeds position adjustment patterns if required. + * + * @param Version $version + * @param ByteMatrix $matrix + * @return void + */ + protected static function maybeEmbedPositionAdjustmentPatterns(Version $version, ByteMatrix $matrix) + { + if ($version->getVersionNumber() < 2) { + return; + } + + $index = $version->getVersionNumber() - 1; + + $coordinates = self::$positionAdjustmentPatternCoordinateTable[$index]; + $numCoordinates = count($coordinates); + + for ($i = 0; $i < $numCoordinates; $i++) { + for ($j = 0; $j < $numCoordinates; $j++) { + $y = $coordinates[$i]; + $x = $coordinates[$j]; + + if ($x === null || $y === null) { + continue; + } + + if ($matrix->get($x, $y) === -1) { + self::embedPositionAdjustmentPattern($x - 2, $y - 2, $matrix); + } + } + } + } + + /** + * Embeds a single position adjustment pattern. + * + * @param integer $xStart + * @param intger $yStart + * @param ByteMatrix $matrix + * @return void + */ + protected static function embedPositionAdjustmentPattern($xStart, $yStart, ByteMatrix $matrix) + { + for ($y = 0; $y < 5; $y++) { + for ($x = 0; $x < 5; $x++) { + $matrix->set($xStart + $x, $yStart + $y, self::$positionAdjustmentPattern[$y][$x]); + } + } + } + + /** + * Embeds timing patterns into a matrix. + * + * @param ByteMatrix $matrix + * @return void + */ + protected static function embedTimingPatterns(ByteMatrix $matrix) + { + $matrixWidth = $matrix->getWidth(); + + for ($i = 8; $i < $matrixWidth - 8; $i++) { + $bit = ($i + 1) % 2; + + if ($matrix->get($i, 6) === -1) { + $matrix->set($i, 6, $bit); + } + + if ($matrix->get(6, $i) === -1) { + $matrix->set(6, $i, $bit); + } + } + } + + /** + * Embeds "dataBits" using "getMaskPattern". + * + * For debugging purposes, it skips masking process if "getMaskPattern" is + * -1. See 8.7 of JISX0510:2004 (p.38) for how to embed data bits. + * + * @param BitArray $dataBits + * @param integer $maskPattern + * @param ByteMatrix $matrix + * @return void + * @throws Exception\WriterException + */ + protected static function embedDataBits(BitArray $dataBits, $maskPattern, ByteMatrix $matrix) + { + $bitIndex = 0; + $direction = -1; + + // Start from the right bottom cell. + $x = $matrix->getWidth() - 1; + $y = $matrix->getHeight() - 1; + + while ($x > 0) { + // Skip vertical timing pattern. + if ($x === 6) { + $x--; + } + + while ($y >= 0 && $y < $matrix->getHeight()) { + for ($i = 0; $i < 2; $i++) { + $xx = $x - $i; + + // Skip the cell if it's not empty. + if ($matrix->get($xx, $y) !== -1) { + continue; + } + + if ($bitIndex < $dataBits->getSize()) { + $bit = $dataBits->get($bitIndex); + $bitIndex++; + } else { + // Padding bit. If there is no bit left, we'll fill the + // left cells with 0, as described in 8.4.9 of + // JISX0510:2004 (p. 24). + $bit = false; + } + + // Skip masking if maskPattern is -1. + if ($maskPattern !== -1 && MaskUtil::getDataMaskBit($maskPattern, $xx, $y)) { + $bit = !$bit; + } + + $matrix->set($xx, $y, $bit); + } + + $y += $direction; + } + + $direction = -$direction; + $y += $direction; + $x -= 2; + } + + // All bits should be consumed + if ($bitIndex !== $dataBits->getSize()) { + throw new Exception\WriterException('Not all bits consumed (' . $bitIndex . ' out of ' . $dataBits->getSize() .')'); + } + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/QrCode.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/QrCode.php new file mode 100644 index 0000000..07e1c38 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/QrCode.php @@ -0,0 +1,201 @@ +mode; + } + + /** + * Sets the mode. + * + * @param Mode $mode + * @return void + */ + public function setMode(Mode $mode) + { + $this->mode = $mode; + } + + /** + * Gets the EC level. + * + * @return ErrorCorrectionLevel + */ + public function getErrorCorrectionLevel() + { + return $this->errorCorrectionLevel; + } + + /** + * Sets the EC level. + * + * @param ErrorCorrectionLevel $errorCorrectionLevel + * @return void + */ + public function setErrorCorrectionLevel(ErrorCorrectionLevel $errorCorrectionLevel) + { + $this->errorCorrectionLevel = $errorCorrectionLevel; + } + + /** + * Gets the version. + * + * @return Version + */ + public function getVersion() + { + return $this->version; + } + + /** + * Sets the version. + * + * @param Version $version + * @return void + */ + public function setVersion(Version $version) + { + $this->version = $version; + } + + /** + * Gets the mask pattern. + * + * @return integer + */ + public function getMaskPattern() + { + return $this->maskPattern; + } + + /** + * Sets the mask pattern. + * + * @param integer $maskPattern + * @return void + */ + public function setMaskPattern($maskPattern) + { + $this->maskPattern = $maskPattern; + } + + /** + * Gets the matrix. + * + * @return ByteMatrix + */ + public function getMatrix() + { + return $this->matrix; + } + + /** + * Sets the matrix. + * + * @param ByteMatrix $matrix + * @return void + */ + public function setMatrix(ByteMatrix $matrix) + { + $this->matrix = $matrix; + } + + /** + * Validates whether a mask pattern is valid. + * + * @param integer $maskPattern + * @return boolean + */ + public static function isValidMaskPattern($maskPattern) + { + return $maskPattern > 0 && $maskPattern < self::NUM_MASK_PATTERNS; + } + + /** + * Returns a string representation of the QR code. + * + * @return string + */ + public function __toString() + { + $result = "<<\n" + . " mode: " . $this->mode . "\n" + . " ecLevel: " . $this->errorCorrectionLevel . "\n" + . " version: " . $this->version . "\n" + . " maskPattern: " . $this->maskPattern . "\n"; + + if ($this->matrix === null) { + $result .= " matrix: null\n"; + } else { + $result .= " matrix:\n"; + $result .= $this->matrix; + } + + $result .= ">>\n"; + + return $result; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Exception/ExceptionInterface.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Exception/ExceptionInterface.php new file mode 100644 index 0000000..5c58fc5 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Exception/ExceptionInterface.php @@ -0,0 +1,14 @@ + 100) { + throw new Exception\InvalidArgumentException('Cyan must be between 0 and 100'); + } + + if ($magenta < 0 || $magenta > 100) { + throw new Exception\InvalidArgumentException('Magenta must be between 0 and 100'); + } + + if ($yellow < 0 || $yellow > 100) { + throw new Exception\InvalidArgumentException('Yellow must be between 0 and 100'); + } + + if ($black < 0 || $black > 100) { + throw new Exception\InvalidArgumentException('Black must be between 0 and 100'); + } + + $this->cyan = (int) $cyan; + $this->magenta = (int) $magenta; + $this->yellow = (int) $yellow; + $this->black = (int) $black; + } + + /** + * Returns the cyan value. + * + * @return integer + */ + public function getCyan() + { + return $this->cyan; + } + + /** + * Returns the magenta value. + * + * @return integer + */ + public function getMagenta() + { + return $this->magenta; + } + + /** + * Returns the yellow value. + * + * @return integer + */ + public function getYellow() + { + return $this->yellow; + } + + /** + * Returns the black value. + * + * @return integer + */ + public function getBlack() + { + return $this->black; + } + + /** + * toRgb(): defined by ColorInterface. + * + * @see ColorInterface::toRgb() + * @return Rgb + */ + public function toRgb() + { + $k = $this->black / 100; + $c = (-$k * $this->cyan + $k * 100 + $this->cyan) / 100; + $m = (-$k * $this->magenta + $k * 100 + $this->magenta) / 100; + $y = (-$k * $this->yellow + $k * 100 + $this->yellow) / 100; + + return new Rgb( + -$c * 255 + 255, + -$m * 255 + 255, + -$y * 255 + 255 + ); + } + + /** + * toCmyk(): defined by ColorInterface. + * + * @see ColorInterface::toCmyk() + * @return Cmyk + */ + public function toCmyk() + { + return $this; + } + + /** + * toGray(): defined by ColorInterface. + * + * @see ColorInterface::toGray() + * @return Gray + */ + public function toGray() + { + return $this->toRgb()->toGray(); + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Color/ColorInterface.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Color/ColorInterface.php new file mode 100644 index 0000000..747accc --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Color/ColorInterface.php @@ -0,0 +1,37 @@ + 100) { + throw new Exception\InvalidArgumentException('Gray must be between 0 and 100'); + } + + $this->gray = (int) $gray; + } + + /** + * Returns the gray value. + * + * @return integer + */ + public function getGray() + { + return $this->gray; + } + + /** + * toRgb(): defined by ColorInterface. + * + * @see ColorInterface::toRgb() + * @return Rgb + */ + public function toRgb() + { + return new Rgb($this->gray * 2.55, $this->gray * 2.55, $this->gray * 2.55); + } + + /** + * toCmyk(): defined by ColorInterface. + * + * @see ColorInterface::toCmyk() + * @return Cmyk + */ + public function toCmyk() + { + return new Cmyk(0, 0, 0, 100 - $this->gray); + } + + /** + * toGray(): defined by ColorInterface. + * + * @see ColorInterface::toGray() + * @return Gray + */ + public function toGray() + { + return $this; + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Color/Rgb.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Color/Rgb.php new file mode 100644 index 0000000..44e4060 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Color/Rgb.php @@ -0,0 +1,148 @@ + 255) { + throw new Exception\InvalidArgumentException('Red must be between 0 and 255'); + } + + if ($green < 0 || $green > 255) { + throw new Exception\InvalidArgumentException('Green must be between 0 and 255'); + } + + if ($blue < 0 || $blue > 255) { + throw new Exception\InvalidArgumentException('Blue must be between 0 and 255'); + } + + $this->red = (int) $red; + $this->green = (int) $green; + $this->blue = (int) $blue; + } + + /** + * Returns the red value. + * + * @return integer + */ + public function getRed() + { + return $this->red; + } + + /** + * Returns the green value. + * + * @return integer + */ + public function getGreen() + { + return $this->green; + } + + /** + * Returns the blue value. + * + * @return integer + */ + public function getBlue() + { + return $this->blue; + } + + /** + * Returns a hexadecimal string representation of the RGB value. + * + * @return string + */ + public function __toString() + { + return sprintf('%02x%02x%02x', $this->red, $this->green, $this->blue); + } + + /** + * toRgb(): defined by ColorInterface. + * + * @see ColorInterface::toRgb() + * @return Rgb + */ + public function toRgb() + { + return $this; + } + + /** + * toCmyk(): defined by ColorInterface. + * + * @see ColorInterface::toCmyk() + * @return Cmyk + */ + public function toCmyk() + { + $c = 1 - ($this->red / 255); + $m = 1 - ($this->green / 255); + $y = 1 - ($this->blue / 255); + $k = min($c, $m, $y); + + return new Cmyk( + 100 * ($c - $k) / (1 - $k), + 100 * ($m - $k) / (1 - $k), + 100 * ($y - $k) / (1 - $k), + 100 * $k + ); + } + + /** + * toGray(): defined by ColorInterface. + * + * @see ColorInterface::toGray() + * @return Gray + */ + public function toGray() + { + return new Gray(($this->red * 0.21 + $this->green * 0.71 + $this->blue * 0.07) / 2.55); + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/AbstractRenderer.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/AbstractRenderer.php new file mode 100644 index 0000000..b0bb02a --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/AbstractRenderer.php @@ -0,0 +1,338 @@ +margin = (int) $margin; + return $this; + } + + /** + * Gets the margin around the QR code. + * + * @return integer + */ + public function getMargin() + { + return $this->margin; + } + + /** + * Sets the height around the renderd image. + * + * If the width is smaller than the matrix width plus padding, the renderer + * will automatically use that as the width instead of the specified one. + * + * @param integer $width + * @return AbstractRenderer + */ + public function setWidth($width) + { + $this->width = (int) $width; + return $this; + } + + /** + * Gets the width of the rendered image. + * + * @return integer + */ + public function getWidth() + { + return $this->width; + } + + /** + * Sets the height around the renderd image. + * + * If the height is smaller than the matrix height plus padding, the + * renderer will automatically use that as the height instead of the + * specified one. + * + * @param integer $height + * @return AbstractRenderer + */ + public function setHeight($height) + { + $this->height = (int) $height; + return $this; + } + + /** + * Gets the height around the rendered image. + * + * @return integer + */ + public function getHeight() + { + return $this->height; + } + + /** + * Sets whether dimensions should be rounded down. + * + * @param boolean $flag + * @return AbstractRenderer + */ + public function setRoundDimensions($flag) + { + $this->floorToClosestDimension = $flag; + return $this; + } + + /** + * Gets whether dimensions should be rounded down. + * + * @return boolean + */ + public function shouldRoundDimensions() + { + return $this->floorToClosestDimension; + } + + /** + * Sets background color. + * + * @param Color\ColorInterface $color + * @return AbstractRenderer + */ + public function setBackgroundColor(Color\ColorInterface $color) + { + $this->backgroundColor = $color; + return $this; + } + + /** + * Gets background color. + * + * @return Color\ColorInterface + */ + public function getBackgroundColor() + { + if ($this->backgroundColor === null) { + $this->backgroundColor = new Color\Gray(100); + } + + return $this->backgroundColor; + } + + /** + * Sets foreground color. + * + * @param Color\ColorInterface $color + * @return AbstractRenderer + */ + public function setForegroundColor(Color\ColorInterface $color) + { + $this->foregroundColor = $color; + return $this; + } + + /** + * Gets foreground color. + * + * @return Color\ColorInterface + */ + public function getForegroundColor() + { + if ($this->foregroundColor === null) { + $this->foregroundColor = new Color\Gray(0); + } + + return $this->foregroundColor; + } + + /** + * Adds a decorator to the renderer. + * + * @param DecoratorInterface $decorator + * @return AbstractRenderer + */ + public function addDecorator(DecoratorInterface $decorator) + { + $this->decorators[] = $decorator; + return $this; + } + + /** + * render(): defined by RendererInterface. + * + * @see RendererInterface::render() + * @param QrCode $qrCode + * @return string + */ + public function render(QrCode $qrCode) + { + $input = $qrCode->getMatrix(); + $inputWidth = $input->getWidth(); + $inputHeight = $input->getHeight(); + $qrWidth = $inputWidth + ($this->getMargin() << 1); + $qrHeight = $inputHeight + ($this->getMargin() << 1); + $outputWidth = max($this->getWidth(), $qrWidth); + $outputHeight = max($this->getHeight(), $qrHeight); + $multiple = (int) min($outputWidth / $qrWidth, $outputHeight / $qrHeight); + + if ($this->shouldRoundDimensions()) { + $outputWidth -= $outputWidth % $multiple; + $outputHeight -= $outputHeight % $multiple; + } + + // Padding includes both the quiet zone and the extra white pixels to + // accommodate the requested dimensions. For example, if input is 25x25 + // the QR will be 33x33 including the quiet zone. If the requested size + // is 200x160, the multiple will be 4, for a QR of 132x132. These will + // handle all the padding from 100x100 (the actual QR) up to 200x160. + $leftPadding = (int) (($outputWidth - ($inputWidth * $multiple)) / 2); + $topPadding = (int) (($outputHeight - ($inputHeight * $multiple)) / 2); + + // Store calculated parameters + $this->finalWidth = $outputWidth; + $this->finalHeight = $outputHeight; + $this->blockSize = $multiple; + + $this->init(); + $this->addColor('background', $this->getBackgroundColor()); + $this->addColor('foreground', $this->getForegroundColor()); + $this->drawBackground('background'); + + foreach ($this->decorators as $decorator) { + $decorator->preProcess( + $qrCode, + $this, + $outputWidth, + $outputHeight, + $leftPadding, + $topPadding, + $multiple + ); + } + + for ($inputY = 0, $outputY = $topPadding; $inputY < $inputHeight; $inputY++, $outputY += $multiple) { + for ($inputX = 0, $outputX = $leftPadding; $inputX < $inputWidth; $inputX++, $outputX += $multiple) { + if ($input->get($inputX, $inputY) === 1) { + $this->drawBlock($outputX, $outputY, 'foreground'); + } + } + } + + foreach ($this->decorators as $decorator) { + $decorator->postProcess( + $qrCode, + $this, + $outputWidth, + $outputHeight, + $leftPadding, + $topPadding, + $multiple + ); + } + + return $this->getByteStream(); + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Decorator/DecoratorInterface.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Decorator/DecoratorInterface.php new file mode 100644 index 0000000..e67268b --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Decorator/DecoratorInterface.php @@ -0,0 +1,63 @@ +outerColor = $color; + return $this; + } + + /** + * Gets outer color. + * + * @return Color\ColorInterface + */ + public function getOuterColor() + { + if ($this->outerColor === null) { + $this->outerColor = new Color\Gray(100); + } + + return $this->outerColor; + } + + /** + * Sets inner color. + * + * @param Color\ColorInterface $color + * @return FinderPattern + */ + public function setInnerColor(Color\ColorInterface $color) + { + $this->innerColor = $color; + return $this; + } + + /** + * Gets inner color. + * + * @return Color\ColorInterface + */ + public function getInnerColor() + { + if ($this->innerColor === null) { + $this->innerColor = new Color\Gray(0); + } + + return $this->innerColor; + } + + /** + * preProcess(): defined by DecoratorInterface. + * + * @see DecoratorInterface::preProcess() + * @param QrCode $qrCode + * @param RendererInterface $renderer + * @param integer $outputWidth + * @param integer $outputHeight + * @param integer $leftPadding + * @param integer $topPadding + * @param integer $multiple + * @return void + */ + public function preProcess( + QrCode $qrCode, + RendererInterface $renderer, + $outputWidth, + $outputHeight, + $leftPadding, + $topPadding, + $multiple + ) { + $matrix = $qrCode->getMatrix(); + $positions = array( + array(0, 0), + array($matrix->getWidth() - 7, 0), + array(0, $matrix->getHeight() - 7), + ); + + foreach (self::$outerPositionDetectionPattern as $y => $row) { + foreach ($row as $x => $isSet) { + foreach ($positions as $position) { + $matrix->set($x + $position[0], $y + $position[1], 0); + } + } + } + } + + /** + * postProcess(): defined by DecoratorInterface. + * + * @see DecoratorInterface::postProcess() + * + * @param QrCode $qrCode + * @param RendererInterface $renderer + * @param integer $outputWidth + * @param integer $outputHeight + * @param integer $leftPadding + * @param integer $topPadding + * @param integer $multiple + * @return void + */ + public function postProcess( + QrCode $qrCode, + RendererInterface $renderer, + $outputWidth, + $outputHeight, + $leftPadding, + $topPadding, + $multiple + ) { + $matrix = $qrCode->getMatrix(); + $positions = array( + array(0, 0), + array($matrix->getWidth() - 7, 0), + array(0, $matrix->getHeight() - 7), + ); + + $renderer->addColor('finder-outer', $this->getOuterColor()); + $renderer->addColor('finder-inner', $this->getInnerColor()); + + foreach (self::$outerPositionDetectionPattern as $y => $row) { + foreach ($row as $x => $isOuterSet) { + $isInnerSet = self::$innerPositionDetectionPattern[$y][$x]; + + if ($isOuterSet) { + foreach ($positions as $position) { + $renderer->drawBlock( + $leftPadding + $x * $multiple + $position[0] * $multiple, + $topPadding + $y * $multiple + $position[1] * $multiple, + 'finder-outer' + ); + } + } + + if ($isInnerSet) { + foreach ($positions as $position) { + $renderer->drawBlock( + $leftPadding + $x * $multiple + $position[0] * $multiple, + $topPadding + $y * $multiple + $position[1] * $multiple, + 'finder-inner' + ); + } + } + } + } + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Eps.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Eps.php new file mode 100644 index 0000000..1b930d8 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Eps.php @@ -0,0 +1,152 @@ +eps = "%!PS-Adobe-3.0 EPSF-3.0\n" + . "%%BoundingBox: 0 0 " . $this->finalWidth . " " . $this->finalHeight . "\n" + . "/F { rectfill } def\n"; + } + + /** + * addColor(): defined by RendererInterface. + * + * @see ImageRendererInterface::addColor() + * @param string $id + * @param ColorInterface $color + * @return void + */ + public function addColor($id, ColorInterface $color) + { + if ( + !$color instanceof Rgb + && !$color instanceof Cmyk + && !$color instanceof Gray + ) { + $color = $color->toCmyk(); + } + + $this->colors[$id] = $color; + } + + /** + * drawBackground(): defined by RendererInterface. + * + * @see ImageRendererInterface::drawBackground() + * @param string $colorId + * @return void + */ + public function drawBackground($colorId) + { + $this->setColor($colorId); + $this->eps .= "0 0 " . $this->finalWidth . " " . $this->finalHeight . " F\n"; + } + + /** + * drawBlock(): defined by RendererInterface. + * + * @see ImageRendererInterface::drawBlock() + * @param integer $x + * @param integer $y + * @param string $colorId + * @return void + */ + public function drawBlock($x, $y, $colorId) + { + $this->setColor($colorId); + $this->eps .= $x . " " . ($this->finalHeight - $y) . " " . $this->blockSize . " " . $this->blockSize . " F\n"; + } + + /** + * getByteStream(): defined by RendererInterface. + * + * @see ImageRendererInterface::getByteStream() + * @return string + */ + public function getByteStream() + { + return $this->eps; + } + + /** + * Sets color to use. + * + * @param string $colorId + * @return void + */ + protected function setColor($colorId) + { + if ($colorId !== $this->currentColor) { + $color = $this->colors[$colorId]; + + if ($color instanceof Rgb) { + $this->eps .= sprintf( + "%F %F %F setrgbcolor\n", + $color->getRed() / 100, + $color->getGreen() / 100, + $color->getBlue() / 100 + ); + } elseif ($color instanceof Cmyk) { + $this->eps .= sprintf( + "%F %F %F %F setcmykcolor\n", + $color->getCyan() / 100, + $color->getMagenta() / 100, + $color->getYellow() / 100, + $color->getBlack() / 100 + ); + } elseif ($color instanceof Gray) { + $this->eps .= sprintf( + "%F setgray\n", + $color->getGray() / 100 + ); + } + + $this->currentColor = $colorId; + } + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Png.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Png.php new file mode 100644 index 0000000..dd593a8 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Png.php @@ -0,0 +1,115 @@ +image = imagecreatetruecolor($this->finalWidth, $this->finalHeight); + } + + /** + * addColor(): defined by RendererInterface. + * + * @see ImageRendererInterface::addColor() + * @param string $id + * @param ColorInterface $color + * @return void + * @throws Exception\RuntimeException + */ + public function addColor($id, ColorInterface $color) + { + if ($this->image === null) { + throw new Exception\RuntimeException('Colors can only be added after init'); + } + + $color = $color->toRgb(); + + $this->colors[$id] = imagecolorallocate( + $this->image, + $color->getRed(), + $color->getGreen(), + $color->getBlue() + ); + } + + /** + * drawBackground(): defined by RendererInterface. + * + * @see ImageRendererInterface::drawBackground() + * @param string $colorId + * @return void + */ + public function drawBackground($colorId) + { + imagefill($this->image, 0, 0, $this->colors[$colorId]); + } + + /** + * drawBlock(): defined by RendererInterface. + * + * @see ImageRendererInterface::drawBlock() + * @param integer $x + * @param integer $y + * @param string $colorId + * @return void + */ + public function drawBlock($x, $y, $colorId) + { + imagefilledrectangle( + $this->image, + $x, + $y, + $x + $this->blockSize - 1, + $y + $this->blockSize - 1, + $this->colors[$colorId] + ); + } + + /** + * getByteStream(): defined by RendererInterface. + * + * @see ImageRendererInterface::getByteStream() + * @return string + */ + public function getByteStream() + { + ob_start(); + imagepng($this->image); + return ob_get_clean(); + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/RendererInterface.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/RendererInterface.php new file mode 100644 index 0000000..52101a6 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/RendererInterface.php @@ -0,0 +1,61 @@ +svg = new SimpleXMLElement( + '' + . '' + ); + $this->svg->addAttribute('version', '1.1'); + $this->svg->addAttribute('width', $this->finalWidth . 'px'); + $this->svg->addAttribute('height', $this->finalHeight . 'px'); + $this->svg->addAttribute('viewBox', '0 0 ' . $this->finalWidth . ' ' . $this->finalHeight); + $this->svg->addChild('defs'); + } + + /** + * addColor(): defined by RendererInterface. + * + * @see ImageRendererInterface::addColor() + * @param string $id + * @param ColorInterface $color + * @return void + * @throws Exception\InvalidArgumentException + */ + public function addColor($id, ColorInterface $color) + { + $this->colors[$id] = (string) $color->toRgb(); + } + + /** + * drawBackground(): defined by RendererInterface. + * + * @see ImageRendererInterface::drawBackground() + * @param string $colorId + * @return void + */ + public function drawBackground($colorId) + { + $rect = $this->svg->addChild('rect'); + $rect->addAttribute('x', 0); + $rect->addAttribute('y', 0); + $rect->addAttribute('width', $this->finalWidth); + $rect->addAttribute('height', $this->finalHeight); + $rect->addAttribute('fill', '#' . $this->colors[$colorId]); + } + + /** + * drawBlock(): defined by RendererInterface. + * + * @see ImageRendererInterface::drawBlock() + * @param integer $x + * @param integer $y + * @param string $colorId + * @return void + */ + public function drawBlock($x, $y, $colorId) + { + $use = $this->svg->addChild('use'); + $use->addAttribute('x', $x); + $use->addAttribute('y', $y); + $use->addAttribute( + 'xlink:href', + $this->getRectPrototypeId($colorId), + 'http://www.w3.org/1999/xlink' + ); + } + + /** + * getByteStream(): defined by RendererInterface. + * + * @see ImageRendererInterface::getByteStream() + * @return string + */ + public function getByteStream() + { + return $this->svg->asXML(); + } + + /** + * Get the prototype ID for a color. + * + * @param integer $colorId + * @return string + */ + protected function getRectPrototypeId($colorId) + { + if (!isset($this->prototypeIds[$colorId])) { + $id = 'r' . dechex(count($this->prototypeIds)); + + $rect = $this->svg->defs->addChild('rect'); + $rect->addAttribute('id', $id); + $rect->addAttribute('width', $this->blockSize); + $rect->addAttribute('height', $this->blockSize); + $rect->addAttribute('fill', '#' . $this->colors[$colorId]); + + $this->prototypeIds[$colorId] = '#' . $id; + } + + return $this->prototypeIds[$colorId]; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/RendererInterface.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/RendererInterface.php new file mode 100644 index 0000000..554e1d8 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/RendererInterface.php @@ -0,0 +1,26 @@ +class = $class; + } + + /** + * Get CSS class name. + * + * @return string + */ + public function getClass() + { + return $this->class; + } + + /** + * Set CSS style value. + * + * @param string $style + */ + public function setStyle($style) + { + $this->style = $style; + } + + /** + * Get CSS style value. + * + * @return string + */ + public function getStyle() + { + return $this->style; + } + + /** + * render(): defined by RendererInterface. + * + * @see RendererInterface::render() + * @param QrCode $qrCode + * @return string + */ + public function render(QrCode $qrCode) + { + $textCode = parent::render($qrCode); + + $result = '' . $textCode . ''; + + return $result; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Text/Plain.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Text/Plain.php new file mode 100644 index 0000000..a7e4cfb --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Text/Plain.php @@ -0,0 +1,150 @@ +fullBlock = $fullBlock; + } + + /** + * Get char used as full block (occupied space, "black"). + * + * @return string + */ + public function getFullBlock() + { + return $this->fullBlock; + } + + /** + * Set char used as empty block (empty space, "white"). + * + * @param string $emptyBlock + */ + public function setEmptyBlock($emptyBlock) + { + $this->emptyBlock = $emptyBlock; + } + + /** + * Get char used as empty block (empty space, "white"). + * + * @return string + */ + public function getEmptyBlock() + { + return $this->emptyBlock; + } + + /** + * Sets the margin around the QR code. + * + * @param integer $margin + * @return AbstractRenderer + * @throws Exception\InvalidArgumentException + */ + public function setMargin($margin) + { + if ($margin < 0) { + throw new Exception\InvalidArgumentException('Margin must be equal to greater than 0'); + } + + $this->margin = (int) $margin; + + return $this; + } + + /** + * Gets the margin around the QR code. + * + * @return integer + */ + public function getMargin() + { + return $this->margin; + } + + /** + * render(): defined by RendererInterface. + * + * @see RendererInterface::render() + * @param QrCode $qrCode + * @return string + */ + public function render(QrCode $qrCode) + { + $result = ''; + $matrix = $qrCode->getMatrix(); + $width = $matrix->getWidth(); + + // Top margin + for ($x = 0; $x < $this->margin; $x++) { + $result .= str_repeat($this->emptyBlock, $width + 2 * $this->margin)."\n"; + } + + // Body + $array = $matrix->getArray(); + + foreach ($array as $row) { + $result .= str_repeat($this->emptyBlock, $this->margin); // left margin + foreach ($row as $byte) { + $result .= $byte ? $this->fullBlock : $this->emptyBlock; + } + $result .= str_repeat($this->emptyBlock, $this->margin); // right margin + $result .= "\n"; + } + + // Bottom margin + for ($x = 0; $x < $this->margin; $x++) { + $result .= str_repeat($this->emptyBlock, $width + 2 * $this->margin)."\n"; + } + + return $result; + } +} diff --git a/vendor/bacon/bacon-qr-code/src/BaconQrCode/Writer.php b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Writer.php new file mode 100644 index 0000000..0f80313 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/src/BaconQrCode/Writer.php @@ -0,0 +1,105 @@ +renderer = $renderer; + } + + /** + * Sets the renderer used to create a byte stream. + * + * @param RendererInterface $renderer + * @return Writer + */ + public function setRenderer(RendererInterface $renderer) + { + $this->renderer = $renderer; + return $this; + } + + /** + * Gets the renderer used to create a byte stream. + * + * @return RendererInterface + */ + public function getRenderer() + { + return $this->renderer; + } + + /** + * Writes QR code and returns it as string. + * + * Content is a string which *should* be encoded in UTF-8, in case there are + * non ASCII-characters present. + * + * @param string $content + * @param string $encoding + * @param integer $ecLevel + * @return string + * @throws Exception\InvalidArgumentException + */ + public function writeString( + $content, + $encoding = Encoder::DEFAULT_BYTE_MODE_ECODING, + $ecLevel = ErrorCorrectionLevel::L + ) { + if (strlen($content) === 0) { + throw new Exception\InvalidArgumentException('Found empty contents'); + } + + $qrCode = Encoder::encode($content, new ErrorCorrectionLevel($ecLevel), $encoding); + + return $this->getRenderer()->render($qrCode); + } + + /** + * Writes QR code to a file. + * + * @see Writer::writeString() + * @param string $content + * @param string $filename + * @param string $encoding + * @param integer $ecLevel + * @return void + */ + public function writeFile( + $content, + $filename, + $encoding = Encoder::DEFAULT_BYTE_MODE_ECODING, + $ecLevel = ErrorCorrectionLevel::L + ) { + file_put_contents($filename, $this->writeString($content, $encoding, $ecLevel)); + } +} diff --git a/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/BitArrayTest.php b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/BitArrayTest.php new file mode 100644 index 0000000..06aa4e1 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/BitArrayTest.php @@ -0,0 +1,197 @@ +assertFalse($array->get($i)); + $array->set($i); + $this->assertTrue($array->get($i)); + } + } + + public function testGetNextSet1() + { + $array = new BitArray(32); + + for ($i = 0; $i < $array->getSize(); $i++) { + $this->assertEquals($i, 32, '', $array->getNextSet($i)); + } + + $array = new BitArray(33); + + for ($i = 0; $i < $array->getSize(); $i++) { + $this->assertEquals($i, 33, '', $array->getNextSet($i)); + } + } + + public function testGetNextSet2() + { + $array = new BitArray(33); + + for ($i = 0; $i < $array->getSize(); $i++) { + $this->assertEquals($i, $i <= 31 ? 31 : 33, '', $array->getNextSet($i)); + } + + $array = new BitArray(33); + + for ($i = 0; $i < $array->getSize(); $i++) { + $this->assertEquals($i, 32, '', $array->getNextSet($i)); + } + } + + public function testGetNextSet3() + { + $array = new BitArray(63); + $array->set(31); + $array->set(32); + + for ($i = 0; $i < $array->getSize(); $i++) { + if ($i <= 31) { + $expected = 31; + } elseif ($i <= 32) { + $expected = 32; + } else { + $expected = 63; + } + + $this->assertEquals($i, $expected, '', $array->getNextSet($i)); + } + } + + public function testGetNextSet4() + { + $array = new BitArray(63); + $array->set(33); + $array->set(40); + + for ($i = 0; $i < $array->getSize(); $i++) { + if ($i <= 33) { + $expected = 33; + } elseif ($i <= 40) { + $expected = 40; + } else { + $expected = 63; + } + + $this->assertEquals($i, $expected, '', $array->getNextSet($i)); + } + } + + public function testGetNextSet5() + { + mt_srand(hexdec('deadbeef')); + + for ($i = 0; $i < 10; $i++) { + $array = new BitArray(mt_rand(1, 100)); + $numSet = mt_rand(0, 19); + + for ($j = 0; $j < $numSet; $j++) { + $array->set(mt_rand(0, $array->getSize() - 1)); + } + + $numQueries = mt_rand(0, 19); + + for ($j = 0; $j < $numQueries; $j++) { + $query = mt_rand(0, $array->getSize() - 1); + $expected = $query; + + while ($expected < $array->getSize() && !$array->get($expected)) { + $expected++; + } + + $actual = $array->getNextSet($query); + + if ($actual !== $expected) { + $array->getNextSet($query); + } + + $this->assertEquals($expected, $actual); + } + } + } + + public function testSetBulk() + { + $array = new BitArray(64); + $array->setBulk(32, 0xFFFF0000); + + for ($i = 0; $i < 48; $i++) { + $this->assertFalse($array->get($i)); + } + + for ($i = 48; $i < 64; $i++) { + $this->assertTrue($array->get($i)); + } + } + + public function testClear() + { + $array = new BitArray(32); + + for ($i = 0; $i < 32; $i++) { + $array->set($i); + } + + $array->clear(); + + for ($i = 0; $i < 32; $i++) { + $this->assertFalse($array->get($i)); + } + } + + public function testGetArray() + { + $array = new BitArray(64); + $array->set(0); + $array->set(63); + + $ints = $array->getBitArray(); + + $this->assertEquals(1, $ints[0]); + $this->assertEquals(0x80000000, $ints[1]); + } + + public function testIsRange() + { + $array = new BitArray(64); + $this->assertTrue($array->isRange(0, 64, false)); + $this->assertFalse($array->isRange(0, 64, true)); + + $array->set(32); + $this->assertTrue($array->isRange(32, 33, true)); + + $array->set(31); + $this->assertTrue($array->isRange(31, 33, true)); + + $array->set(34); + $this->assertFalse($array->isRange(31, 35, true)); + + for ($i = 0; $i < 31; $i++) { + $array->set($i); + } + + $this->assertTrue($array->isRange(0, 33, true)); + + for ($i = 33; $i < 64; $i++) { + $array->set($i); + } + + $this->assertTrue($array->isRange(0, 64, true)); + $this->assertFalse($array->isRange(0, 64, false)); + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/BitMatrixTest.php b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/BitMatrixTest.php new file mode 100644 index 0000000..89a5881 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/BitMatrixTest.php @@ -0,0 +1,119 @@ +assertEquals(33, $matrix->getHeight()); + + for ($y = 0; $y < 33; $y++) { + for ($x = 0; $x < 33; $x++) { + if ($y * $x % 3 === 0) { + $matrix->set($x, $y); + } + } + } + + for ($y = 0; $y < 33; $y++) { + for ($x = 0; $x < 33; $x++) { + $this->assertEquals($x * $y % 3 === 0, $matrix->get($x, $y)); + } + } + } + + public function testSetRegion() + { + $matrix = new BitMatrix(5); + $matrix->setRegion(1, 1, 3, 3); + + for ($y = 0; $y < 5; $y++) { + for ($x = 0; $x < 5; $x++) { + $this->assertEquals($y >= 1 && $y <= 3 && $x >= 1 && $x <= 3, $matrix->get($x, $y)); + } + } + } + + public function testRectangularMatrix() + { + $matrix = new BitMatrix(75, 20); + $this->assertEquals(75, $matrix->getWidth()); + $this->assertEquals(20, $matrix->getHeight()); + + $matrix->set(10, 0); + $matrix->set(11, 1); + $matrix->set(50, 2); + $matrix->set(51, 3); + $matrix->flip(74, 4); + $matrix->flip(0, 5); + + $this->assertTrue($matrix->get(10, 0)); + $this->assertTrue($matrix->get(11, 1)); + $this->assertTrue($matrix->get(50, 2)); + $this->assertTrue($matrix->get(51, 3)); + $this->assertTrue($matrix->get(74, 4)); + $this->assertTrue($matrix->get(0, 5)); + + $matrix->flip(50, 2); + $matrix->flip(51, 3); + + $this->assertFalse($matrix->get(50, 2)); + $this->assertFalse($matrix->get(51, 3)); + } + + public function testRectangularSetRegion() + { + $matrix = new BitMatrix(320, 240); + $this->assertEquals(320, $matrix->getWidth()); + $this->assertEquals(240, $matrix->getHeight()); + + $matrix->setRegion(105, 22, 80, 12); + + for ($y = 0; $y < 240; $y++) { + for ($x = 0; $x < 320; $x++) { + $this->assertEquals($y >= 22 && $y < 34 && $x >= 105 && $x < 185, $matrix->get($x, $y)); + } + } + } + + public function testGetRow() + { + $matrix = new BitMatrix(102, 5); + + for ($x = 0; $x < 102; $x++) { + if ($x & 3 === 0) { + $matrix->set($x, 2); + } + } + + $array1 = $matrix->getRow(2, null); + $this->assertEquals(102, $array1->getSize()); + + $array2 = new BitArray(60); + $array2 = $matrix->getRow(2, $array2); + $this->assertEquals(102, $array2->getSize()); + + $array3 = new BitArray(200); + $array3 = $matrix->getRow(2, $array3); + $this->assertEquals(200, $array3->getSize()); + + for ($x = 0; $x < 102; $x++) { + $on = ($x & 3 === 0); + + $this->assertEquals($on, $array1->get($x)); + $this->assertEquals($on, $array2->get($x)); + $this->assertEquals($on, $array3->get($x)); + } + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/BitUtilsTest.php b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/BitUtilsTest.php new file mode 100644 index 0000000..b80ff7d --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/BitUtilsTest.php @@ -0,0 +1,30 @@ +assertEquals(1, BitUtils::unsignedRightShift(1, 0)); + $this->assertEquals(1, BitUtils::unsignedRightShift(10, 3)); + $this->assertEquals(536870910, BitUtils::unsignedRightShift(-10, 3)); + } + + public function testNumberOfTrailingZeros() + { + $this->assertEquals(32, BitUtils::numberOfTrailingZeros(0)); + $this->assertEquals(1, BitUtils::numberOfTrailingZeros(10)); + $this->assertEquals(0, BitUtils::numberOfTrailingZeros(15)); + $this->assertEquals(2, BitUtils::numberOfTrailingZeros(20)); + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/ErrorCorrectionLevelTest.php b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/ErrorCorrectionLevelTest.php new file mode 100644 index 0000000..736e995 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/ErrorCorrectionLevelTest.php @@ -0,0 +1,40 @@ +assertEquals(0x0, ErrorCorrectionLevel::M); + $this->assertEquals(0x1, ErrorCorrectionLevel::L); + $this->assertEquals(0x2, ErrorCorrectionLevel::H); + $this->assertEquals(0x3, ErrorCorrectionLevel::Q); + } + + public function testInvalidErrorCorrectionLevelThrowsException() + { + $this->setExpectedException( + 'BaconQrCode\Exception\UnexpectedValueException', + 'Value not a const in enum BaconQrCode\Common\ErrorCorrectionLevel' + ); + new ErrorCorrectionLevel(4); + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/FormatInformationTest.php b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/FormatInformationTest.php new file mode 100644 index 0000000..5f6ee58 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/FormatInformationTest.php @@ -0,0 +1,104 @@ +unmaskedTestFormatInfo = $this->maskedTestFormatInfo ^ 0x5412; + } + + + public function testBitsDiffering() + { + $this->assertEquals(0, FormatInformation::numBitsDiffering(1, 1)); + $this->assertEquals(1, FormatInformation::numBitsDiffering(0, 2)); + $this->assertEquals(2, FormatInformation::numBitsDiffering(1, 2)); + $this->assertEquals(32, FormatInformation::numBitsDiffering(-1, 0)); + } + + public function testDecode() + { + $expected = FormatInformation::decodeFormatInformation( + $this->maskedTestFormatInfo, + $this->maskedTestFormatInfo + ); + + $this->assertNotNull($expected); + $this->assertEquals(7, $expected->getDataMask()); + $this->assertEquals(ErrorCorrectionLevel::Q, $expected->getErrorCorrectionLevel()->get()); + + $this->assertEquals( + $expected, + FormatInformation::decodeFormatInformation( + $this->unmaskedTestFormatInfo, + $this->maskedTestFormatInfo + ) + ); + } + + public function testDecodeWithBitDifference() + { + $expected = FormatInformation::decodeFormatInformation( + $this->maskedTestFormatInfo, + $this->maskedTestFormatInfo + ); + + $this->assertEquals( + $expected, + FormatInformation::decodeFormatInformation( + $this->maskedTestFormatInfo ^ 0x1, + $this->maskedTestFormatInfo ^ 0x1 + ) + ); + $this->assertEquals( + $expected, + FormatInformation::decodeFormatInformation( + $this->maskedTestFormatInfo ^ 0x3, + $this->maskedTestFormatInfo ^ 0x3 + ) + ); + $this->assertEquals( + $expected, + FormatInformation::decodeFormatInformation( + $this->maskedTestFormatInfo ^ 0x7, + $this->maskedTestFormatInfo ^ 0x7 + ) + ); + $this->assertNull( + FormatInformation::decodeFormatInformation( + $this->maskedTestFormatInfo ^ 0xf, + $this->maskedTestFormatInfo ^ 0xf + ) + ); + } + + public function testDecodeWithMisRead() + { + $expected = FormatInformation::decodeFormatInformation( + $this->maskedTestFormatInfo, + $this->maskedTestFormatInfo + ); + + $this->assertEquals( + $expected, + FormatInformation::decodeFormatInformation( + $this->maskedTestFormatInfo ^ 0x3, + $this->maskedTestFormatInfo ^ 0xf + ) + ); + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/ModeTest.php b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/ModeTest.php new file mode 100644 index 0000000..4daab7c --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/ModeTest.php @@ -0,0 +1,42 @@ +assertEquals(0x0, Mode::TERMINATOR); + $this->assertEquals(0x1, Mode::NUMERIC); + $this->assertEquals(0x2, Mode::ALPHANUMERIC); + $this->assertEquals(0x4, Mode::BYTE); + $this->assertEquals(0x8, Mode::KANJI); + } + + public function testInvalidModeThrowsException() + { + $this->setExpectedException( + 'BaconQrCode\Exception\UnexpectedValueException', + 'Value not a const in enum BaconQrCode\Common\Mode' + ); + new Mode(10); + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/ReedSolomonCodecTest.php b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/ReedSolomonCodecTest.php new file mode 100644 index 0000000..99a6c72 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/ReedSolomonCodecTest.php @@ -0,0 +1,107 @@ +encode($block, $parity); + + // Copy parity into test blocks + for ($i = 0; $i < $numRoots; $i++) { + $block[$i + $dataSize] = $parity[$i]; + $tBlock[$i + $dataSize] = $parity[$i]; + } + + // Seed with errors + for ($i = 0; $i < $errors; $i++) { + $errorValue = mt_rand(1, $blockSize); + + do { + $errorLocation = mt_rand(0, $blockSize); + } while ($errorLocations[$errorLocation] !== 0); + + $errorLocations[$errorLocation] = 1; + + if (mt_rand(0, 1)) { + $erasures[] = $errorLocation; + } + + $tBlock[$errorLocation] ^= $errorValue; + } + + $erasures = SplFixedArray::fromArray($erasures, false); + + // Decode the errored block + $foundErrors = $codec->decode($tBlock, $erasures); + + if ($errors > 0 && $foundErrors === null) { + $this->assertEquals($block, $tBlock, 'Decoder failed to correct errors'); + } + + $this->assertEquals($errors, $foundErrors, 'Found errors do not equal expected errors'); + + for ($i = 0; $i < $foundErrors; $i++) { + if ($errorLocations[$erasures[$i]] === 0) { + $this->fail(sprintf('Decoder indicates error in location %d without error', $erasures[$i])); + } + } + + $this->assertEquals($block, $tBlock, 'Decoder did not correct errors'); + } + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/VersionTest.php b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/VersionTest.php new file mode 100644 index 0000000..8b3fc01 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/VersionTest.php @@ -0,0 +1,88 @@ +assertNotNull($version); + $this->assertEquals($versionNumber, $version->getVersionNumber()); + $this->assertNotNull($version->getAlignmentPatternCenters()); + + if ($versionNumber > 1) { + $this->assertTrue(count($version->getAlignmentPatternCenters()) > 0); + } + + $this->assertEquals($dimension, $version->getDimensionForVersion()); + $this->assertNotNull($version->getEcBlocksForLevel(new ErrorCorrectionLevel(ErrorCorrectionLevel::H))); + $this->assertNotNull($version->getEcBlocksForLevel(new ErrorCorrectionLevel(ErrorCorrectionLevel::L))); + $this->assertNotNull($version->getEcBlocksForLevel(new ErrorCorrectionLevel(ErrorCorrectionLevel::M))); + $this->assertNotNull($version->getEcBlocksForLevel(new ErrorCorrectionLevel(ErrorCorrectionLevel::Q))); + $this->assertNotNull($version->buildFunctionPattern()); + } + + /** + * @dataProvider versionProvider + * @param integer $versionNumber + * @param integer $dimension + */ + public function testGetProvisionalVersionForDimension($versionNumber, $dimension) + { + $this->assertEquals( + $versionNumber, + Version::getProvisionalVersionForDimension($dimension)->getVersionNumber() + ); + } + + /** + * @dataProvider decodeInformationProvider + * @param integer $expectedVersion + * @param integer $mask + */ + public function testDecodeVersionInformation($expectedVersion, $mask) + { + $version = Version::decodeVersionInformation($mask); + $this->assertNotNull($version); + $this->assertEquals($expectedVersion, $version->getVersionNumber()); + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Encoder/EncoderTest.php b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Encoder/EncoderTest.php new file mode 100644 index 0000000..31cdaa4 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Encoder/EncoderTest.php @@ -0,0 +1,468 @@ +getMethods(ReflectionMethod::IS_STATIC) as $method) { + $method->setAccessible(true); + $this->methods[$method->getName()] = $method; + } + } + + public function testGetAlphanumericCode() + { + // The first ten code points are numbers. + for ($i = 0; $i < 10; $i++) { + $this->assertEquals($i, $this->methods['getAlphanumericCode']->invoke(null, ord('0') + $i)); + } + + // The next 26 code points are capital alphabet letters. + for ($i = 10; $i < 36; $i++) { + // The first ten code points are numbers + $this->assertEquals($i, $this->methods['getAlphanumericCode']->invoke(null, ord('A') + $i - 10)); + } + + // Others are symbol letters. + $this->assertEquals(36, $this->methods['getAlphanumericCode']->invoke(null, ' ')); + $this->assertEquals(37, $this->methods['getAlphanumericCode']->invoke(null, '$')); + $this->assertEquals(38, $this->methods['getAlphanumericCode']->invoke(null, '%')); + $this->assertEquals(39, $this->methods['getAlphanumericCode']->invoke(null, '*')); + $this->assertEquals(40, $this->methods['getAlphanumericCode']->invoke(null, '+')); + $this->assertEquals(41, $this->methods['getAlphanumericCode']->invoke(null, '-')); + $this->assertEquals(42, $this->methods['getAlphanumericCode']->invoke(null, '.')); + $this->assertEquals(43, $this->methods['getAlphanumericCode']->invoke(null, '/')); + $this->assertEquals(44, $this->methods['getAlphanumericCode']->invoke(null, ':')); + + // Should return -1 for other letters. + $this->assertEquals(-1, $this->methods['getAlphanumericCode']->invoke(null, 'a')); + $this->assertEquals(-1, $this->methods['getAlphanumericCode']->invoke(null, '#')); + $this->assertEquals(-1, $this->methods['getAlphanumericCode']->invoke(null, "\0")); + } + + public function testChooseMode() + { + // Numeric mode + $this->assertSame(Mode::NUMERIC, $this->methods['chooseMode']->invoke(null, '0')->get()); + $this->assertSame(Mode::NUMERIC, $this->methods['chooseMode']->invoke(null, '0123456789')->get()); + + // Alphanumeric mode + $this->assertSame(Mode::ALPHANUMERIC, $this->methods['chooseMode']->invoke(null, 'A')->get()); + $this->assertSame(Mode::ALPHANUMERIC, $this->methods['chooseMode']->invoke(null, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:')->get()); + + // 8-bit byte mode + $this->assertSame(Mode::BYTE, $this->methods['chooseMode']->invoke(null, 'a')->get()); + $this->assertSame(Mode::BYTE, $this->methods['chooseMode']->invoke(null, '#')->get()); + $this->assertSame(Mode::BYTE, $this->methods['chooseMode']->invoke(null, '')->get()); + + // AIUE in Hiragana in SHIFT-JIS + $this->assertSame(Mode::BYTE, $this->methods['chooseMode']->invoke(null, "\x8\xa\x8\xa\x8\xa\x8\xa6")->get()); + + // Nihon in Kanji in SHIFT-JIS + $this->assertSame(Mode::BYTE, $this->methods['chooseMode']->invoke(null, "\x9\xf\x9\x7b")->get()); + + // Sou-Utso-Byou in Kanji in SHIFT-JIS + $this->assertSame(Mode::BYTE, $this->methods['chooseMode']->invoke(null, "\xe\x4\x9\x5\x9\x61")->get()); + } + + public function testEncode() + { + $qrCode = Encoder::encode('ABCDEF', new ErrorCorrectionLevel(ErrorCorrectionLevel::H)); + $expected = "<<\n" + . " mode: ALPHANUMERIC\n" + . " ecLevel: H\n" + . " version: 1\n" + . " maskPattern: 0\n" + . " matrix:\n" + . " 1 1 1 1 1 1 1 0 1 1 1 1 0 0 1 1 1 1 1 1 1\n" + . " 1 0 0 0 0 0 1 0 0 1 1 1 0 0 1 0 0 0 0 0 1\n" + . " 1 0 1 1 1 0 1 0 0 1 0 1 1 0 1 0 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 1 1 1 0 1 0 1 0 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 0 1 1 1 0 0 1 0 1 1 1 0 1\n" + . " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 1\n" + . " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n" + . " 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0\n" + . " 0 0 1 0 1 1 1 0 1 1 0 0 1 1 0 0 0 1 0 0 1\n" + . " 1 0 1 1 1 0 0 1 0 0 0 1 0 1 0 0 0 0 0 0 0\n" + . " 0 0 1 1 0 0 1 0 1 0 0 0 1 0 1 0 1 0 1 1 0\n" + . " 1 1 0 1 0 1 0 1 1 1 0 1 0 1 0 0 0 0 0 1 0\n" + . " 0 0 1 1 0 1 1 1 1 0 0 0 1 0 1 0 1 1 1 1 0\n" + . " 0 0 0 0 0 0 0 0 1 0 0 1 1 1 0 1 0 1 0 0 0\n" + . " 1 1 1 1 1 1 1 0 0 0 1 0 1 0 1 1 0 0 0 0 1\n" + . " 1 0 0 0 0 0 1 0 1 1 1 1 0 1 0 1 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 1 0 1 1 0 1 0 1 0 0 0 0 1\n" + . " 1 0 1 1 1 0 1 0 0 1 1 0 1 1 1 1 0 1 0 1 0\n" + . " 1 0 1 1 1 0 1 0 1 0 0 0 1 0 1 0 1 1 1 0 1\n" + . " 1 0 0 0 0 0 1 0 0 1 1 0 1 1 0 1 0 0 0 1 1\n" + . " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 0 1 0 1\n" + . ">>\n"; + + $this->assertEquals($expected, $qrCode->__toString()); + } + + public function testSimpleUtf8Eci() + { + $qrCode = Encoder::encode('hello', new ErrorCorrectionLevel(ErrorCorrectionLevel::H), 'utf-8'); + $expected = "<<\n" + . " mode: BYTE\n" + . " ecLevel: H\n" + . " version: 1\n" + . " maskPattern: 3\n" + . " matrix:\n" + . " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1\n" + . " 1 0 0 0 0 0 1 0 0 0 1 0 1 0 1 0 0 0 0 0 1\n" + . " 1 0 1 1 1 0 1 0 0 1 0 1 0 0 1 0 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 0 1 1 0 1 0 1 0 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 1 0 1 0 1 0 1 0 1 1 1 0 1\n" + . " 1 0 0 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 1\n" + . " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n" + . " 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0\n" + . " 0 0 1 1 0 0 1 1 1 1 0 0 0 1 1 0 1 0 0 0 0\n" + . " 0 0 1 1 1 0 0 0 0 0 1 1 0 0 0 1 0 1 1 1 0\n" + . " 0 1 0 1 0 1 1 1 0 1 0 1 0 0 0 0 0 1 1 1 1\n" + . " 1 1 0 0 1 0 0 1 1 0 0 1 1 1 1 0 1 0 1 1 0\n" + . " 0 0 0 0 1 0 1 1 1 1 0 0 0 0 0 1 0 0 1 0 0\n" + . " 0 0 0 0 0 0 0 0 1 1 1 1 0 0 1 1 1 0 0 0 1\n" + . " 1 1 1 1 1 1 1 0 1 1 1 0 1 0 1 1 0 0 1 0 0\n" + . " 1 0 0 0 0 0 1 0 0 0 1 0 0 1 1 1 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 0 1 0 0 0 0 1 1 0 0 0 0 0\n" + . " 1 0 1 1 1 0 1 0 1 1 1 0 1 0 0 0 1 1 0 0 0\n" + . " 1 0 1 1 1 0 1 0 1 1 0 0 0 1 0 0 1 0 0 0 0\n" + . " 1 0 0 0 0 0 1 0 0 0 0 1 1 0 1 0 1 0 1 1 0\n" + . " 1 1 1 1 1 1 1 0 0 1 0 1 1 1 0 1 1 0 0 0 0\n" + . ">>\n"; + + $this->assertEquals($expected, $qrCode->__toString()); + } + + public function testAppendModeInfo() + { + $bits = new BitArray(); + $this->methods['appendModeInfo']->invoke(null, new Mode(Mode::NUMERIC), $bits); + $this->assertEquals(' ...X', $bits->__toString()); + } + + public function testAppendLengthInfo() + { + // 1 letter (1/1), 10 bits. + $bits = new BitArray(); + $this->methods['appendLengthInfo']->invoke( + null, + 1, + Version::getVersionForNumber(1), + new Mode(Mode::NUMERIC), + $bits + ); + $this->assertEquals(' ........ .X', $bits->__toString()); + + // 2 letters (2/1), 11 bits. + $bits = new BitArray(); + $this->methods['appendLengthInfo']->invoke( + null, + 2, + Version::getVersionForNumber(10), + new Mode(Mode::ALPHANUMERIC), + $bits + ); + $this->assertEquals(' ........ .X.', $bits->__toString()); + + // 255 letters (255/1), 16 bits. + $bits = new BitArray(); + $this->methods['appendLengthInfo']->invoke( + null, + 255, + Version::getVersionForNumber(27), + new Mode(Mode::BYTE), + $bits + ); + $this->assertEquals(' ........ XXXXXXXX', $bits->__toString()); + + // 512 letters (1024/2), 12 bits. + $bits = new BitArray(); + $this->methods['appendLengthInfo']->invoke( + null, + 512, + Version::getVersionForNumber(40), + new Mode(Mode::KANJI), + $bits + ); + $this->assertEquals(' ..X..... ....', $bits->__toString()); + } + + public function testAppendBytes() + { + // Should use appendNumericBytes. + // 1 = 01 = 0001 in 4 bits. + $bits = new BitArray(); + $this->methods['appendBytes']->invoke( + null, + '1', + new Mode(Mode::NUMERIC), + $bits, + Encoder::DEFAULT_BYTE_MODE_ECODING + ); + $this->assertEquals(' ...X', $bits->__toString()); + + // Should use appendAlphaNumericBytes. + // A = 10 = 0xa = 001010 in 6 bits. + $bits = new BitArray(); + $this->methods['appendBytes']->invoke( + null, + 'A', + new Mode(Mode::ALPHANUMERIC), + $bits, + Encoder::DEFAULT_BYTE_MODE_ECODING + ); + $this->assertEquals(' ..X.X.', $bits->__toString()); + + // Should use append8BitBytes. + // 0x61, 0x62, 0x63 + $bits = new BitArray(); + $this->methods['appendBytes']->invoke( + null, + 'abc', + new Mode(Mode::BYTE), + $bits, + Encoder::DEFAULT_BYTE_MODE_ECODING + ); + $this->assertEquals(' .XX....X .XX...X. .XX...XX', $bits->__toString()); + + // Should use appendKanjiBytes. + // 0x93, 0x5f + $bits = new BitArray(); + $this->methods['appendBytes']->invoke( + null, + "\x93\x5f", + new Mode(Mode::KANJI), + $bits, + Encoder::DEFAULT_BYTE_MODE_ECODING + ); + $this->assertEquals(' .XX.XX.. XXXXX', $bits->__toString()); + + // Lower letters such as 'a' cannot be encoded in alphanumeric mode. + $this->setExpectedException( + 'BaconQrCode\Exception\WriterException', + 'Invalid alphanumeric code' + ); + $this->methods['appendBytes']->invoke( + null, + "a", + new Mode(Mode::ALPHANUMERIC), + $bits, + Encoder::DEFAULT_BYTE_MODE_ECODING + ); + } + + public function testTerminateBits() + { + $bits = new BitArray(); + $this->methods['terminateBits']->invoke(null, 0, $bits); + $this->assertEquals('', $bits->__toString()); + + $bits = new BitArray(); + $this->methods['terminateBits']->invoke(null, 1, $bits); + $this->assertEquals(' ........', $bits->__toString()); + + $bits = new BitArray(); + $bits->appendBits(0, 3); + $this->methods['terminateBits']->invoke(null, 1, $bits); + $this->assertEquals(' ........', $bits->__toString()); + + $bits = new BitArray(); + $bits->appendBits(0, 5); + $this->methods['terminateBits']->invoke(null, 1, $bits); + $this->assertEquals(' ........', $bits->__toString()); + + $bits = new BitArray(); + $bits->appendBits(0, 8); + $this->methods['terminateBits']->invoke(null, 1, $bits); + $this->assertEquals(' ........', $bits->__toString()); + + $bits = new BitArray(); + $this->methods['terminateBits']->invoke(null, 2, $bits); + $this->assertEquals(' ........ XXX.XX..', $bits->__toString()); + + $bits = new BitArray(); + $bits->appendBits(0, 1); + $this->methods['terminateBits']->invoke(null, 3, $bits); + $this->assertEquals(' ........ XXX.XX.. ...X...X', $bits->__toString()); + } + + public function testGetNumDataBytesAndNumEcBytesForBlockId() + { + // Version 1-H. + list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']->invoke(null, 26, 9, 1, 0); + $this->assertEquals(9, $numDataBytes); + $this->assertEquals(17, $numEcBytes); + + // Version 3-H. 2 blocks. + list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']->invoke(null, 70, 26, 2, 0); + $this->assertEquals(13, $numDataBytes); + $this->assertEquals(22, $numEcBytes); + list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']->invoke(null, 70, 26, 2, 1); + $this->assertEquals(13, $numDataBytes); + $this->assertEquals(22, $numEcBytes); + + // Version 7-H. (4 + 1) blocks. + list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']->invoke(null, 196, 66, 5, 0); + $this->assertEquals(13, $numDataBytes); + $this->assertEquals(26, $numEcBytes); + list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']->invoke(null, 196, 66, 5, 4); + $this->assertEquals(14, $numDataBytes); + $this->assertEquals(26, $numEcBytes); + + // Version 40-H. (20 + 61) blocks. + list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']->invoke(null, 3706, 1276, 81, 0); + $this->assertEquals(15, $numDataBytes); + $this->assertEquals(30, $numEcBytes); + list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']->invoke(null, 3706, 1276, 81, 20); + $this->assertEquals(16, $numDataBytes); + $this->assertEquals(30, $numEcBytes); + list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']->invoke(null, 3706, 1276, 81, 80); + $this->assertEquals(16, $numDataBytes); + $this->assertEquals(30, $numEcBytes); + } + + public function testInterleaveWithEcBytes() + { + $dataBytes = SplFixedArray::fromArray(array(32, 65, 205, 69, 41, 220, 46, 128, 236), false); + $in = new BitArray(); + + foreach ($dataBytes as $dataByte) { + $in->appendBits($dataByte, 8); + } + + $outBits = $this->methods['interleaveWithEcBytes']->invoke(null, $in, 26, 9, 1); + $expected = SplFixedArray::fromArray(array( + // Data bytes. + 32, 65, 205, 69, 41, 220, 46, 128, 236, + // Error correction bytes. + 42, 159, 74, 221, 244, 169, 239, 150, 138, 70, 237, 85, 224, 96, 74, 219, 61, + ), false); + + $out = $outBits->toBytes(0, count($expected)); + + $this->assertEquals($expected, $out); + } + + public function testAppendNumericBytes() + { + // 1 = 01 = 0001 in 4 bits. + $bits = new BitArray(); + $this->methods['appendNumericBytes']->invoke(null, '1', $bits); + $this->assertEquals(' ...X', $bits->__toString()); + + // 12 = 0xc = 0001100 in 7 bits. + $bits = new BitArray(); + $this->methods['appendNumericBytes']->invoke(null, '12', $bits); + $this->assertEquals(' ...XX..', $bits->__toString()); + + // 123 = 0x7b = 0001111011 in 10 bits. + $bits = new BitArray(); + $this->methods['appendNumericBytes']->invoke(null, '123', $bits); + $this->assertEquals(' ...XXXX. XX', $bits->__toString()); + + // 1234 = "123" + "4" = 0001111011 + 0100 in 14 bits. + $bits = new BitArray(); + $this->methods['appendNumericBytes']->invoke(null, '1234', $bits); + $this->assertEquals(' ...XXXX. XX.X..', $bits->__toString()); + + // Empty + $bits = new BitArray(); + $this->methods['appendNumericBytes']->invoke(null, '', $bits); + $this->assertEquals('', $bits->__toString()); + } + + public function testAppendAlphanumericBytes() + { + $bits = new BitArray(); + $this->methods['appendAlphanumericBytes']->invoke(null, 'A', $bits); + $this->assertEquals(' ..X.X.', $bits->__toString()); + + $bits = new BitArray(); + $this->methods['appendAlphanumericBytes']->invoke(null, 'AB', $bits); + $this->assertEquals(' ..XXX..X X.X', $bits->__toString()); + + $bits = new BitArray(); + $this->methods['appendAlphanumericBytes']->invoke(null, 'ABC', $bits); + $this->assertEquals(' ..XXX..X X.X..XX. .', $bits->__toString()); + + // Empty + $bits = new BitArray(); + $this->methods['appendAlphanumericBytes']->invoke(null, '', $bits); + $this->assertEquals('', $bits->__toString()); + + // Invalid data + $this->setExpectedException('BaconQrCode\Exception\WriterException', 'Invalid alphanumeric code'); + $bits = new BitArray(); + $this->methods['appendAlphanumericBytes']->invoke(null, 'abc', $bits); + } + + public function testAppend8BitBytes() + { + // 0x61, 0x62, 0x63 + $bits = new BitArray(); + $this->methods['append8BitBytes']->invoke(null, 'abc', $bits, Encoder::DEFAULT_BYTE_MODE_ECODING); + $this->assertEquals(' .XX....X .XX...X. .XX...XX', $bits->__toString()); + + // Empty + $bits = new BitArray(); + $this->methods['append8BitBytes']->invoke(null, '', $bits, Encoder::DEFAULT_BYTE_MODE_ECODING); + $this->assertEquals('', $bits->__toString()); + } + + public function testAppendKanjiBytes() + { + // Numbers are from page 21 of JISX0510:2004 + $bits = new BitArray(); + $this->methods['appendKanjiBytes']->invoke(null, "\x93\x5f", $bits); + $this->assertEquals(' .XX.XX.. XXXXX', $bits->__toString()); + + $this->methods['appendKanjiBytes']->invoke(null, "\xe4\xaa", $bits); + $this->assertEquals(' .XX.XX.. XXXXXXX. X.X.X.X. X.', $bits->__toString()); + } + + public function testGenerateEcBytes() + { + // Numbers are from http://www.swetake.com/qr/qr3.html and + // http://www.swetake.com/qr/qr9.html + $dataBytes = SplFixedArray::fromArray(array(32, 65, 205, 69, 41, 220, 46, 128, 236), false); + $ecBytes = $this->methods['generateEcBytes']->invoke(null, $dataBytes, 17); + $expected = SplFixedArray::fromArray(array(42, 159, 74, 221, 244, 169, 239, 150, 138, 70, 237, 85, 224, 96, 74, 219, 61), false); + $this->assertEquals($expected, $ecBytes); + + $dataBytes = SplFixedArray::fromArray(array(67, 70, 22, 38, 54, 70, 86, 102, 118, 134, 150, 166, 182, 198, 214), false); + $ecBytes = $this->methods['generateEcBytes']->invoke(null, $dataBytes, 18); + $expected = SplFixedArray::fromArray(array(175, 80, 155, 64, 178, 45, 214, 233, 65, 209, 12, 155, 117, 31, 140, 214, 27, 187), false); + $this->assertEquals($expected, $ecBytes); + + // High-order zero coefficient case. + $dataBytes = SplFixedArray::fromArray(array(32, 49, 205, 69, 42, 20, 0, 236, 17), false); + $ecBytes = $this->methods['generateEcBytes']->invoke(null, $dataBytes, 17); + $expected = SplFixedArray::fromArray(array(0, 3, 130, 179, 194, 0, 55, 211, 110, 79, 98, 72, 170, 96, 211, 137, 213), false); + $this->assertEquals($expected, $ecBytes); + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Encoder/MaskUtilTest.php b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Encoder/MaskUtilTest.php new file mode 100644 index 0000000..a5c3865 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Encoder/MaskUtilTest.php @@ -0,0 +1,281 @@ +fail('Data mask bit did not match'); + } + } + } + } + + public function testApplyMaskPenaltyRule1() + { + $matrix = new ByteMatrix(4, 1); + $matrix->set(0, 0, 0); + $matrix->set(1, 0, 0); + $matrix->set(2, 0, 0); + $matrix->set(3, 0, 0); + + $this->assertEquals(0, MaskUtil::applyMaskPenaltyRule1($matrix)); + + // Horizontal + $matrix = new ByteMatrix(6, 1); + $matrix->set(0, 0, 0); + $matrix->set(1, 0, 0); + $matrix->set(2, 0, 0); + $matrix->set(3, 0, 0); + $matrix->set(4, 0, 0); + $matrix->set(5, 0, 1); + $this->assertEquals(3, MaskUtil::applyMaskPenaltyRule1($matrix)); + $matrix->set(5, 0, 0); + $this->assertEquals(4, MaskUtil::applyMaskPenaltyRule1($matrix)); + + // Vertical + $matrix = new ByteMatrix(1, 6); + $matrix->set(0, 0, 0); + $matrix->set(0, 1, 0); + $matrix->set(0, 2, 0); + $matrix->set(0, 3, 0); + $matrix->set(0, 4, 0); + $matrix->set(0, 5, 1); + $this->assertEquals(3, MaskUtil::applyMaskPenaltyRule1($matrix)); + $matrix->set(0, 5, 0); + $this->assertEquals(4, MaskUtil::applyMaskPenaltyRule1($matrix)); + } + + public function testApplyMaskPenaltyRule2() + { + $matrix = new ByteMatrix(1, 1); + $matrix->set(0, 0, 0); + $this->assertEquals(0, MaskUtil::applyMaskPenaltyRule2($matrix)); + + $matrix = new ByteMatrix(2, 2); + $matrix->set(0, 0, 0); + $matrix->set(1, 0, 0); + $matrix->set(0, 1, 0); + $matrix->set(1, 1, 1); + $this->assertEquals(0, MaskUtil::applyMaskPenaltyRule2($matrix)); + + $matrix = new ByteMatrix(2, 2); + $matrix->set(0, 0, 0); + $matrix->set(1, 0, 0); + $matrix->set(0, 1, 0); + $matrix->set(1, 1, 0); + $this->assertEquals(3, MaskUtil::applyMaskPenaltyRule2($matrix)); + + $matrix = new ByteMatrix(3, 3); + $matrix->set(0, 0, 0); + $matrix->set(1, 0, 0); + $matrix->set(2, 0, 0); + $matrix->set(0, 1, 0); + $matrix->set(1, 1, 0); + $matrix->set(2, 1, 0); + $matrix->set(0, 2, 0); + $matrix->set(1, 2, 0); + $matrix->set(2, 2, 0); + $this->assertEquals(3 * 4, MaskUtil::applyMaskPenaltyRule2($matrix)); + } + + public function testApplyMaskPenalty3() + { + // Horizontal 00001011101 + $matrix = new ByteMatrix(11, 1); + $matrix->set(0, 0, 0); + $matrix->set(1, 0, 0); + $matrix->set(2, 0, 0); + $matrix->set(3, 0, 0); + $matrix->set(4, 0, 1); + $matrix->set(5, 0, 0); + $matrix->set(6, 0, 1); + $matrix->set(7, 0, 1); + $matrix->set(8, 0, 1); + $matrix->set(9, 0, 0); + $matrix->set(10, 0, 1); + $this->assertEquals(40, MaskUtil::applyMaskPenaltyRule3($matrix)); + + // Horizontal 10111010000 + $matrix = new ByteMatrix(11, 1); + $matrix->set(0, 0, 1); + $matrix->set(1, 0, 0); + $matrix->set(2, 0, 1); + $matrix->set(3, 0, 1); + $matrix->set(4, 0, 1); + $matrix->set(5, 0, 0); + $matrix->set(6, 0, 1); + $matrix->set(7, 0, 0); + $matrix->set(8, 0, 0); + $matrix->set(9, 0, 0); + $matrix->set(10, 0, 0); + $this->assertEquals(40, MaskUtil::applyMaskPenaltyRule3($matrix)); + + // Vertical 00001011101 + $matrix = new ByteMatrix(1, 11); + $matrix->set(0, 0, 0); + $matrix->set(0, 1, 0); + $matrix->set(0, 2, 0); + $matrix->set(0, 3, 0); + $matrix->set(0, 4, 1); + $matrix->set(0, 5, 0); + $matrix->set(0, 6, 1); + $matrix->set(0, 7, 1); + $matrix->set(0, 8, 1); + $matrix->set(0, 9, 0); + $matrix->set(0, 10, 1); + $this->assertEquals(40, MaskUtil::applyMaskPenaltyRule3($matrix)); + + // Vertical 10111010000 + $matrix = new ByteMatrix(1, 11); + $matrix->set(0, 0, 1); + $matrix->set(0, 1, 0); + $matrix->set(0, 2, 1); + $matrix->set(0, 3, 1); + $matrix->set(0, 4, 1); + $matrix->set(0, 5, 0); + $matrix->set(0, 6, 1); + $matrix->set(0, 7, 0); + $matrix->set(0, 8, 0); + $matrix->set(0, 9, 0); + $matrix->set(0, 10, 0); + $this->assertEquals(40, MaskUtil::applyMaskPenaltyRule3($matrix)); + } + + public function testApplyMaskPenaltyRule4() + { + // Dark cell ratio = 0% + $matrix = new ByteMatrix(1, 1); + $matrix->set(0, 0, 0); + $this->assertEquals(100, MaskUtil::applyMaskPenaltyRule4($matrix)); + + // Dark cell ratio = 5% + $matrix = new ByteMatrix(2, 1); + $matrix->set(0, 0, 0); + $matrix->set(0, 0, 1); + $this->assertEquals(0, MaskUtil::applyMaskPenaltyRule4($matrix)); + + // Dark cell ratio = 66.67% + $matrix = new ByteMatrix(6, 1); + $matrix->set(0, 0, 0); + $matrix->set(1, 0, 1); + $matrix->set(2, 0, 1); + $matrix->set(3, 0, 1); + $matrix->set(4, 0, 1); + $matrix->set(5, 0, 0); + $this->assertEquals(30, MaskUtil::applyMaskPenaltyRule4($matrix)); + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Encoder/MatrixUtilTest.php b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Encoder/MatrixUtilTest.php new file mode 100644 index 0000000..bf3544f --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Encoder/MatrixUtilTest.php @@ -0,0 +1,336 @@ +getMethods(ReflectionMethod::IS_STATIC) as $method) { + $method->setAccessible(true); + $this->methods[$method->getName()] = $method; + } + } + + public function testToString() + { + $matrix= new ByteMatrix(3, 3); + $matrix->set(0, 0, 0); + $matrix->set(1, 0, 1); + $matrix->set(2, 0, 0); + $matrix->set(0, 1, 1); + $matrix->set(1, 1, 0); + $matrix->set(2, 1, 1); + $matrix->set(0, 2, -1); + $matrix->set(1, 2, -1); + $matrix->set(2, 2, -1); + + $expected = " 0 1 0\n 1 0 1\n \n"; + $this->assertEquals($expected, $matrix->__toString()); + } + + public function testClearMatrix() + { + $matrix = new ByteMatrix(2, 2); + MatrixUtil::clearMatrix($matrix); + + $this->assertEquals(-1, $matrix->get(0, 0)); + $this->assertEquals(-1, $matrix->get(1, 0)); + $this->assertEquals(-1, $matrix->get(0, 1)); + $this->assertEquals(-1, $matrix->get(1, 1)); + } + + public function testEmbedBasicPatterns1() + { + $matrix = new ByteMatrix(21, 21); + MatrixUtil::clearMatrix($matrix); + $this->methods['embedBasicPatterns']->invoke( + null, + Version::getVersionForNumber(1), + $matrix + ); + $expected = " 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1\n" + . " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1\n" + . " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n" + . " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1\n" + . " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n" + . " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 1 \n" + . " 0 \n" + . " 1 \n" + . " 0 \n" + . " 1 \n" + . " 0 0 0 0 0 0 0 0 1 \n" + . " 1 1 1 1 1 1 1 0 \n" + . " 1 0 0 0 0 0 1 0 \n" + . " 1 0 1 1 1 0 1 0 \n" + . " 1 0 1 1 1 0 1 0 \n" + . " 1 0 1 1 1 0 1 0 \n" + . " 1 0 0 0 0 0 1 0 \n" + . " 1 1 1 1 1 1 1 0 \n"; + + $this->assertEquals($expected, $matrix->__toString()); + } + + public function testEmbedBasicPatterns2() + { + $matrix = new ByteMatrix(25, 25); + MatrixUtil::clearMatrix($matrix); + $this->methods['embedBasicPatterns']->invoke( + null, + Version::getVersionForNumber(2), + $matrix + ); + $expected = " 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1\n" + . " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1\n" + . " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n" + . " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1\n" + . " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n" + . " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 1 \n" + . " 0 \n" + . " 1 \n" + . " 0 \n" + . " 1 \n" + . " 0 \n" + . " 1 \n" + . " 0 \n" + . " 1 1 1 1 1 1 \n" + . " 0 0 0 0 0 0 0 0 1 1 0 0 0 1 \n" + . " 1 1 1 1 1 1 1 0 1 0 1 0 1 \n" + . " 1 0 0 0 0 0 1 0 1 0 0 0 1 \n" + . " 1 0 1 1 1 0 1 0 1 1 1 1 1 \n" + . " 1 0 1 1 1 0 1 0 \n" + . " 1 0 1 1 1 0 1 0 \n" + . " 1 0 0 0 0 0 1 0 \n" + . " 1 1 1 1 1 1 1 0 \n"; + + $this->assertEquals($expected, $matrix->__toString()); + } + + public function testEmbedTypeInfo() + { + $matrix = new ByteMatrix(21, 21); + MatrixUtil::clearMatrix($matrix); + $this->methods['embedTypeInfo']->invoke( + null, + new ErrorCorrectionLevel(ErrorCorrectionLevel::M), + 5, + $matrix + ); + $expected = " 0 \n" + . " 1 \n" + . " 1 \n" + . " 1 \n" + . " 0 \n" + . " 0 \n" + . " \n" + . " 1 \n" + . " 1 0 0 0 0 0 0 1 1 1 0 0 1 1 1 0\n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " 0 \n" + . " 0 \n" + . " 0 \n" + . " 0 \n" + . " 0 \n" + . " 0 \n" + . " 1 \n"; + + $this->assertEquals($expected, $matrix->__toString()); + } + + public function testEmbedVersionInfo() + { + $matrix = new ByteMatrix(21, 21); + MatrixUtil::clearMatrix($matrix); + $this->methods['maybeEmbedVersionInfo']->invoke( + null, + Version::getVersionForNumber(7), + $matrix + ); + $expected = " 0 0 1 \n" + . " 0 1 0 \n" + . " 0 1 0 \n" + . " 0 1 1 \n" + . " 1 1 1 \n" + . " 0 0 0 \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " 0 0 0 0 1 0 \n" + . " 0 1 1 1 1 0 \n" + . " 1 0 0 1 1 0 \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n"; + + $this->assertEquals($expected, $matrix->__toString()); + } + + public function testEmbedDataBits() + { + $matrix = new ByteMatrix(21, 21); + MatrixUtil::clearMatrix($matrix); + $this->methods['embedBasicPatterns']->invoke( + null, + Version::getVersionForNumber(1), + $matrix + ); + + $bits = new BitArray(); + $this->methods['embedDataBits']->invoke( + null, + $bits, + -1, + $matrix + ); + + $expected = " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1\n" + . " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1\n" + . " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 1 0 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 1 0 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 1 0 1 1 1 0 1\n" + . " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1\n" + . " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n" + . " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + . " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"; + + $this->assertEquals($expected, $matrix->__toString()); + } + + public function testBuildMatrix() + { + $bytes = array( + 32, 65, 205, 69, 41, 220, 46, 128, 236, 42, 159, 74, 221, 244, 169, + 239, 150, 138, 70, 237, 85, 224, 96, 74, 219 , 61 + ); + $bits = new BitArray(); + + foreach ($bytes as $byte) { + $bits->appendBits($byte, 8); + } + + $matrix = new ByteMatrix(21, 21); + MatrixUtil::buildMatrix( + $bits, + new ErrorCorrectionLevel(ErrorCorrectionLevel::H), + Version::getVersionForNumber(1), + 3, + $matrix + ); + + $expected = " 1 1 1 1 1 1 1 0 0 1 1 0 0 0 1 1 1 1 1 1 1\n" + . " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1\n" + . " 1 0 1 1 1 0 1 0 0 0 0 1 0 0 1 0 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 0 1 1 0 0 0 1 0 1 1 1 0 1\n" + . " 1 0 1 1 1 0 1 0 1 1 0 0 1 0 1 0 1 1 1 0 1\n" + . " 1 0 0 0 0 0 1 0 0 0 1 1 1 0 1 0 0 0 0 0 1\n" + . " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n" + . " 0 0 0 0 0 0 0 0 1 1 0 1 1 0 0 0 0 0 0 0 0\n" + . " 0 0 1 1 0 0 1 1 1 0 0 1 1 1 1 0 1 0 0 0 0\n" + . " 1 0 1 0 1 0 0 0 0 0 1 1 1 0 0 1 0 1 1 1 0\n" + . " 1 1 1 1 0 1 1 0 1 0 1 1 1 0 0 1 1 1 0 1 0\n" + . " 1 0 1 0 1 1 0 1 1 1 0 0 1 1 1 0 0 1 0 1 0\n" + . " 0 0 1 0 0 1 1 1 0 0 0 0 0 0 1 0 1 1 1 1 1\n" + . " 0 0 0 0 0 0 0 0 1 1 0 1 0 0 0 0 0 1 0 1 1\n" + . " 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 0 1 0 1 1 0\n" + . " 1 0 0 0 0 0 1 0 0 0 0 1 0 1 1 1 0 0 0 0 0\n" + . " 1 0 1 1 1 0 1 0 0 1 0 0 1 1 0 0 1 0 0 1 1\n" + . " 1 0 1 1 1 0 1 0 1 1 0 1 0 0 0 0 0 1 1 1 0\n" + . " 1 0 1 1 1 0 1 0 1 1 1 1 0 0 0 0 1 1 1 0 0\n" + . " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 1 0 0\n" + . " 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 0 1 0 0 1 0\n"; + + $this->assertEquals($expected, $matrix->__toString()); + } + + public function testFindMsbSet() + { + $this->assertEquals(0, $this->methods['findMsbSet']->invoke(null, 0)); + $this->assertEquals(1, $this->methods['findMsbSet']->invoke(null, 1)); + $this->assertEquals(8, $this->methods['findMsbSet']->invoke(null, 0x80)); + $this->assertEquals(32, $this->methods['findMsbSet']->invoke(null, 0x80000000)); + } + + public function testCalculateBchCode() + { + // Encoding of type information. + // From Appendix C in JISX0510:2004 (p 65) + $this->assertEquals(0xdc, $this->methods['calculateBchCode']->invoke(null, 5, 0x537)); + // From http://www.swetake.com/qr/qr6.html + $this->assertEquals(0x1c2, $this->methods['calculateBchCode']->invoke(null, 0x13, 0x537)); + // From http://www.swetake.com/qr/qr11.html + $this->assertEquals(0x214, $this->methods['calculateBchCode']->invoke(null, 0x1b, 0x537)); + + // Encoding of version information. + // From Appendix D in JISX0510:2004 (p 68) + $this->assertEquals(0xc94, $this->methods['calculateBchCode']->invoke(null, 7, 0x1f25)); + $this->assertEquals(0x5bc, $this->methods['calculateBchCode']->invoke(null, 8, 0x1f25)); + $this->assertEquals(0xa99, $this->methods['calculateBchCode']->invoke(null, 9, 0x1f25)); + $this->assertEquals(0x4d3, $this->methods['calculateBchCode']->invoke(null, 10, 0x1f25)); + $this->assertEquals(0x9a6, $this->methods['calculateBchCode']->invoke(null, 20, 0x1f25)); + $this->assertEquals(0xd75, $this->methods['calculateBchCode']->invoke(null, 30, 0x1f25)); + $this->assertEquals(0xc69, $this->methods['calculateBchCode']->invoke(null, 40, 0x1f25)); + } + + public function testMakeVersionInfoBits() + { + // From Appendix D in JISX0510:2004 (p 68) + $bits = new BitArray(); + $this->methods['makeVersionInfoBits']->invoke(null, Version::getVersionForNumber(7), $bits); + $this->assertEquals(' ...XXXXX ..X..X.X ..', $bits->__toString()); + } + + public function testMakeTypeInfoBits() + { + // From Appendix D in JISX0510:2004 (p 68) + $bits = new BitArray(); + $this->methods['makeTypeInfoBits']->invoke(null, new ErrorCorrectionLevel(ErrorCorrectionLevel::M), 5, $bits); + $this->assertEquals(' X......X X..XXX.', $bits->__toString()); + } +} \ No newline at end of file diff --git a/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Renderer/Text/HtmlTest.php b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Renderer/Text/HtmlTest.php new file mode 100644 index 0000000..0c69dd2 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Renderer/Text/HtmlTest.php @@ -0,0 +1,99 @@ +renderer = new Html(); + $this->writer = new Writer($this->renderer); + } + + public function testBasicRender() + { + $content = 'foobar'; + $expected = + '
    ' .
    +            "                       \n" .
    +            " ███████ █████ ███████ \n" .
    +            " █     █  █ █  █     █ \n" .
    +            " █ ███ █  ██   █ ███ █ \n" .
    +            " █ ███ █  ███  █ ███ █ \n" .
    +            " █ ███ █   █ █ █ ███ █ \n" .
    +            " █     █    ██ █     █ \n" .
    +            " ███████ █ █ █ ███████ \n" .
    +            "         █████         \n" .
    +            " ██ ██ █  ██ █ █     █ \n" .
    +            "    ██    ██ █ █ ██    \n" .
    +            "  ████████ █  ██ █  ██ \n" .
    +            "           ██      █ █ \n" .
    +            "  ██  ███  █   █  █  █ \n" .
    +            "         █ ███    █ █  \n" .
    +            " ███████  ██ ██████    \n" .
    +            " █     █   ████   ██   \n" .
    +            " █ ███ █ ██ ██ ██ █ ██ \n" .
    +            " █ ███ █ ██ ██  █ ██   \n" .
    +            " █ ███ █   █   █ ██ ██ \n" .
    +            " █     █ ███  ███ ████ \n" .
    +            " ███████ ████   ██     \n" .
    +            "                       \n" .
    +            '
    ' + ; + + $qrCode = Encoder::encode( + $content, + new ErrorCorrectionLevel(ErrorCorrectionLevel::L), + Encoder::DEFAULT_BYTE_MODE_ECODING + ); + $this->assertEquals($expected, $this->renderer->render($qrCode)); + } + + public function testSetStyle() + { + $content = 'foobar'; + $qrCode = Encoder::encode( + $content, + new ErrorCorrectionLevel(ErrorCorrectionLevel::L), + Encoder::DEFAULT_BYTE_MODE_ECODING + ); + $this->renderer->setStyle('bar'); + $this->assertEquals('bar', $this->renderer->getStyle()); + $this->assertStringMatchesFormat('%astyle="bar"%a', $this->renderer->render($qrCode)); + } + + public function testSetClass() + { + $content = 'foobar'; + $qrCode = Encoder::encode( + $content, + new ErrorCorrectionLevel(ErrorCorrectionLevel::L), + Encoder::DEFAULT_BYTE_MODE_ECODING + ); + $this->renderer->setClass('bar'); + $this->assertEquals('bar', $this->renderer->getClass()); + $this->assertStringMatchesFormat('%aclass="bar"%a', $this->renderer->render($qrCode)); + } +} diff --git a/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Renderer/Text/TextTest.php b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Renderer/Text/TextTest.php new file mode 100644 index 0000000..d94e8e5 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Renderer/Text/TextTest.php @@ -0,0 +1,149 @@ +renderer = new Plain(); + $this->writer = new Writer($this->renderer); + } + + public function testBasicRender() + { + $content = 'foobar'; + $expected = + " \n" . + " ███████ █████ ███████ \n" . + " █ █ █ █ █ █ \n" . + " █ ███ █ ██ █ ███ █ \n" . + " █ ███ █ ███ █ ███ █ \n" . + " █ ███ █ █ █ █ ███ █ \n" . + " █ █ ██ █ █ \n" . + " ███████ █ █ █ ███████ \n" . + " █████ \n" . + " ██ ██ █ ██ █ █ █ \n" . + " ██ ██ █ █ ██ \n" . + " ████████ █ ██ █ ██ \n" . + " ██ █ █ \n" . + " ██ ███ █ █ █ █ \n" . + " █ ███ █ █ \n" . + " ███████ ██ ██████ \n" . + " █ █ ████ ██ \n" . + " █ ███ █ ██ ██ ██ █ ██ \n" . + " █ ███ █ ██ ██ █ ██ \n" . + " █ ███ █ █ █ ██ ██ \n" . + " █ █ ███ ███ ████ \n" . + " ███████ ████ ██ \n" . + " \n" + ; + + $qrCode = Encoder::encode( + $content, + new ErrorCorrectionLevel(ErrorCorrectionLevel::L), + Encoder::DEFAULT_BYTE_MODE_ECODING + ); + $this->assertEquals($expected, $this->renderer->render($qrCode)); + } + + public function testBasicRenderNoMargins() + { + $content = 'foobar'; + $expected = + "███████ █████ ███████\n" . + "█ █ █ █ █ █\n" . + "█ ███ █ ██ █ ███ █\n" . + "█ ███ █ ███ █ ███ █\n" . + "█ ███ █ █ █ █ ███ █\n" . + "█ █ ██ █ █\n" . + "███████ █ █ █ ███████\n" . + " █████ \n" . + "██ ██ █ ██ █ █ █\n" . + " ██ ██ █ █ ██ \n" . + " ████████ █ ██ █ ██\n" . + " ██ █ █\n" . + " ██ ███ █ █ █ █\n" . + " █ ███ █ █ \n" . + "███████ ██ ██████ \n" . + "█ █ ████ ██ \n" . + "█ ███ █ ██ ██ ██ █ ██\n" . + "█ ███ █ ██ ██ █ ██ \n" . + "█ ███ █ █ █ ██ ██\n" . + "█ █ ███ ███ ████\n" . + "███████ ████ ██ \n" + ; + + $qrCode = Encoder::encode( + $content, + new ErrorCorrectionLevel(ErrorCorrectionLevel::L), + Encoder::DEFAULT_BYTE_MODE_ECODING + ); + $this->renderer->setMargin(0); + $this->assertEquals(0, $this->renderer->getMargin()); + $this->assertEquals($expected, $this->renderer->render($qrCode)); + } + + public function testBasicRenderCustomChar() + { + $content = 'foobar'; + $expected = + "-----------------------\n" . + "-#######-#####-#######-\n" . + "-#-----#--#-#--#-----#-\n" . + "-#-###-#--##---#-###-#-\n" . + "-#-###-#--###--#-###-#-\n" . + "-#-###-#---#-#-#-###-#-\n" . + "-#-----#----##-#-----#-\n" . + "-#######-#-#-#-#######-\n" . + "---------#####---------\n" . + "-##-##-#--##-#-#-----#-\n" . + "----##----##-#-#-##----\n" . + "--########-#--##-#--##-\n" . + "-----------##------#-#-\n" . + "--##--###--#---#--#--#-\n" . + "---------#-###----#-#--\n" . + "-#######--##-######----\n" . + "-#-----#---####---##---\n" . + "-#-###-#-##-##-##-#-##-\n" . + "-#-###-#-##-##--#-##---\n" . + "-#-###-#---#---#-##-##-\n" . + "-#-----#-###--###-####-\n" . + "-#######-####---##-----\n" . + "-----------------------\n" + ; + + $qrCode = Encoder::encode( + $content, + new ErrorCorrectionLevel(ErrorCorrectionLevel::L), + Encoder::DEFAULT_BYTE_MODE_ECODING + ); + $this->renderer->setFullBlock('#'); + $this->renderer->setEmptyBlock('-'); + $this->assertEquals('#', $this->renderer->getFullBlock()); + $this->assertEquals('-', $this->renderer->getEmptyBlock()); + $this->assertEquals($expected, $this->renderer->render($qrCode)); + } +} diff --git a/vendor/bacon/bacon-qr-code/tests/bootstrap.php b/vendor/bacon/bacon-qr-code/tests/bootstrap.php new file mode 100644 index 0000000..05a4941 --- /dev/null +++ b/vendor/bacon/bacon-qr-code/tests/bootstrap.php @@ -0,0 +1,10 @@ + + + + . + + + + ../src/ + + + diff --git a/vendor/cjango/wechat/composer.json b/vendor/cjango/wechat/composer.json new file mode 100644 index 0000000..fc7ebe9 --- /dev/null +++ b/vendor/cjango/wechat/composer.json @@ -0,0 +1,24 @@ +{ + "name": "cjango/wechat", + "description": "wechat sdk", + "keywords": ["cjango", "wechat", "sdk", "code"], + "homepage": "https://github.com/cjango/wechat", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Jason.Chen", + "email": "chenjxlg@163.com", + "homepage": "http://www.cjango.com/" + } + ], + "minimum-stability": "dev", + "require": { + "php": ">=5.5.0" + }, + "autoload": { + "psr-4": { + "cjango\\": "src/" + } + } +} diff --git a/vendor/cjango/wechat/src/Wechat.php b/vendor/cjango/wechat/src/Wechat.php new file mode 100644 index 0000000..8f9f01f --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat.php @@ -0,0 +1,111 @@ + | +// +------------------------------------------------+ +namespace cjango; + +/** + * 微信公众平台开发SDK, + * 架构问题又凌乱了. + * @author 小陈叔叔 <20511924@qq.com> + */ +class Wechat +{ + /** + * 保存错误信息 + * @var string + */ + protected static $error = ''; + + /** + * 默认的配置参数 + * @var array + */ + protected static $config = [ + 'token' => '', + 'appid' => '', + 'secret' => '', + 'access_token' => '', + 'encode' => false, + 'AESKey' => '', + 'mch_id' => '', + 'paykey' => '', + 'pem' => '', + ]; + + /** + * 传入初始化参数 + * 接受消息,如果是加密的需要传入一些参数,否则用不到 + */ + public function __construct($config = []) + { + if (!empty($config) && is_array($config)) { + self::$config = array_merge(self::$config, $config); + } + } + + /** + * 初始化 + * @param array $config + * @param boolean $force 强制初始化 + * @return [type] + */ + public static function instance($config = [], $force = false) + { + static $wechat; + if (is_null($wechat) || $force == true) { + $wechat = new Wechat($config); + } + return $wechat; + } + + /** + * 验证URL有效性,校验请求签名 + * @return string|boolean + */ + public static function valid() + { + $echoStr = isset($_GET["echostr"]) ? $_GET["echostr"] : ''; + if ($echoStr) { + self::checkSignature() && exit($echoStr); + } else { + !self::checkSignature() && exit('Access Denied!'); + } + } + + /** + * 检查请求URL签名 + * @return boolean + */ + private static function checkSignature() + { + $signature = isset($_GET['signature']) ? $_GET['signature'] : ''; + $timestamp = isset($_GET['timestamp']) ? $_GET['timestamp'] : ''; + $nonce = isset($_GET['nonce']) ? $_GET['nonce'] : ''; + if (empty($signature) || empty($timestamp) || empty($nonce)) { + return false; + } + $token = self::$config['token']; + $tmpArr = [$token, $timestamp, $nonce]; + sort($tmpArr, SORT_STRING); + $tmpStr = implode($tmpArr); + return sha1($tmpStr) == $signature; + } + + /** + * 返回错误信息 + * @return string + */ + public static function error($msg = null) + { + if (!is_null($msg)) { + self::$error = $msg; + } else { + return self::$error; + } + } +} diff --git a/vendor/cjango/wechat/src/Wechat/Card.php b/vendor/cjango/wechat/src/Wechat/Card.php new file mode 100644 index 0000000..10f073d --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/Card.php @@ -0,0 +1,50 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * 微信卡券相关部分,太难了 等我喝醉了再说吧! + */ +class Card extends Wechat +{ + /** + * 接口名称与URL映射 + * @var array + */ + protected static $url = [ + 'card_create' => 'https://api.weixin.qq.com/card/create', + 'card_active' => 'https://api.weixin.qq.com/card/membercard/activate', + ]; + + public static function create() + { + $params = [ + 'card' => [ + 'card_type' => 'MEMBER_CARD', + 'member_card' => [ + 'base_info' => [ + 'logo_url' => '', + 'brand_name' => '', + 'code_type' => 'CODE_TYPE_TEXT', + 'title' => 'Color010', + ], + ], + ], + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + return Utils::api(self::$url['card_create'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + public static function active() + { + #Todo.. + } +} diff --git a/vendor/cjango/wechat/src/Wechat/Datacube.php b/vendor/cjango/wechat/src/Wechat/Datacube.php new file mode 100644 index 0000000..1967167 --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/Datacube.php @@ -0,0 +1,41 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * 数据统计接口 + */ +class Datacube extends Wechat +{ + /** + * 接口名称与URL映射 + * @var array + */ + protected static $url = [ + 'user_summary' => 'https://api.weixin.qq.com/datacube/getusersummary', + 'user_cumulate' => 'https://api.weixin.qq.com/datacube/getusercumulate', + '' => 'https://api.weixin.qq.com/datacube/getarticlesummary', + '' => 'https://api.weixin.qq.com/datacube/getarticletotal', + '' => 'https://api.weixin.qq.com/datacube/getuserread', + '' => 'https://api.weixin.qq.com/datacube/getuserreadhour', + '' => 'https://api.weixin.qq.com/datacube/getusershare', + '' => 'https://api.weixin.qq.com/datacube/getusersharehour', + '' => 'https://api.weixin.qq.com/datacube/getupstreammsg', + '' => 'https://api.weixin.qq.com/datacube/getupstreammsghour', + '' => 'https://api.weixin.qq.com/datacube/getupstreammsgweek', + '' => 'https://api.weixin.qq.com/datacube/getupstreammsgmonth', + '' => 'https://api.weixin.qq.com/datacube/getupstreammsgdist', + '' => 'https://api.weixin.qq.com/datacube/getupstreammsgdistweek', + '' => 'https://api.weixin.qq.com/datacube/getupstreammsgdistmonth', + '' => 'https://api.weixin.qq.com/datacube/getinterfacesummary', + '' => 'https://api.weixin.qq.com/datacube/getinterfacesummaryhour', + ]; +} diff --git a/vendor/cjango/wechat/src/Wechat/Group.php b/vendor/cjango/wechat/src/Wechat/Group.php new file mode 100644 index 0000000..5186fa7 --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/Group.php @@ -0,0 +1,100 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * 分组管理 + */ +class Group extends Wechat +{ + + /** + * 接口名称与URL映射 + * @var array + */ + protected static $url = [ + 'group_get' => 'https://api.weixin.qq.com/cgi-bin/groups/get', + 'group_create' => 'https://api.weixin.qq.com/cgi-bin/groups/create', + 'group_update' => 'https://api.weixin.qq.com/cgi-bin/groups/update', + 'group_delete' => 'https://api.weixin.qq.com/cgi-bin/groups/delete', + ]; + + /** + * 获取全部分组 + */ + public static function get() + { + $params = [ + 'access_token' => parent::$config['access_token'], + ]; + $result = Utils::api(self::$url['group_get'], $params); + if ($result) { + return $result['groups']; + } else { + return false; + } + } + + /** + * 新增分组 + * @param string $name + * @return array ['id' => ID, 'name' => NAME] + */ + public static function create($name) + { + $params = [ + 'group' => [ + 'name' => $name, + ], + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + $result = Utils::api(self::$url['group_create'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + if ($result) { + return $result['group']; + } else { + return false; + } + } + + /** + * 修改分组名 + * @param [type] $id [description] + * @param [type] $name [description] + * @return [type] [description] + */ + public static function update($id, $name) + { + $params = [ + 'group' => [ + 'id' => $id, + 'name' => $name, + ], + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + return Utils::api(self::$url['group_update'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + /** + * 删除分组 + * @param [type] $id [description] + * @return [type] [description] + */ + public static function delete($id) + { + $params = [ + 'group' => [ + 'id' => $id, + ], + ]; + $params = json_encode($params); + return Utils::api(self::$url['group_delete'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } +} diff --git a/vendor/cjango/wechat/src/Wechat/Material.php b/vendor/cjango/wechat/src/Wechat/Material.php new file mode 100644 index 0000000..4a7a737 --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/Material.php @@ -0,0 +1,51 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * 素材管理 + */ +class Material extends Wechat +{ + /** + * 接口名称与URL映射 + * @var array + */ + protected static $url = [ + 'media_upload' => 'https://api.weixin.qq.com/cgi-bin/media/upload', // 新增临时素材 + 'media_get' => 'https://api.weixin.qq.com/cgi-bin/media/get', // 获取临时素材 + 'material_news' => 'https://api.weixin.qq.com/cgi-bin/material/add_news', // 新增永久图文素材 + 'material_material' => 'https://api.weixin.qq.com/cgi-bin/material/add_material', // 新增永久素材 + 'material_get' => 'https://api.weixin.qq.com/cgi-bin/material/get_material', // 获取永久素材 + 'material_del' => 'https://api.weixin.qq.com/cgi-bin/material/del_material', // 删除永久素材 + 'material_update' => 'https://api.weixin.qq.com/cgi-bin/material/update_news', // 修改永久图文素材 + 'material_count' => 'https://api.weixin.qq.com/cgi-bin/material/get_materialcount', // 获取永久素材数量 + 'material_lists' => 'https://api.weixin.qq.com/cgi-bin/material/batchget_material', // 获取永久素材列表 + ]; + + /** + * 新增临时素材 + * @return [type] [description] + */ + public static function upload() + { + #Todo.. + } + + /** + * 获取临时素材 + * @return [type] [description] + */ + public static function get() + { + #Todo.. + } +} diff --git a/vendor/cjango/wechat/src/Wechat/Menu.php b/vendor/cjango/wechat/src/Wechat/Menu.php new file mode 100644 index 0000000..0e936bc --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/Menu.php @@ -0,0 +1,70 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * 自定义菜单 + */ +class Menu extends Wechat +{ + /** + * 接口名称与URL映射 + * @var array + */ + protected static $url = [ + 'menu_get' => 'https://api.weixin.qq.com/cgi-bin/menu/get', // 获取菜单 + 'menu_create' => 'https://api.weixin.qq.com/cgi-bin/menu/create', // 创建菜单 + 'menu_delete' => 'https://api.weixin.qq.com/cgi-bin/menu/delete', // 删除菜单 + ]; + + /** + * 获取自定义菜单 + * @return [type] [description] + */ + public static function get() + { + $params = [ + 'access_token' => parent::$config['access_token'], + ]; + $result = Utils::api(self::$url['menu_get'], $params); + if ($result) { + return $result['menu']['button']; + } else { + return false; + } + } + + /** + * 创建菜单 + * @param [type] $menu [description] + * @return [type] [description] + */ + public static function create($menu) + { + $params = [ + 'button' => $menu, + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + return Utils::api(self::$url['menu_create'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + /** + * 删除自定义菜单 + * @return [type] [description] + */ + public static function delete() + { + $params = [ + 'access_token' => parent::$config['access_token'], + ]; + return Utils::api(self::$url['menu_delete'], $params); + } +} diff --git a/vendor/cjango/wechat/src/Wechat/Oauth.php b/vendor/cjango/wechat/src/Wechat/Oauth.php new file mode 100644 index 0000000..daf2d9b --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/Oauth.php @@ -0,0 +1,83 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * ACCESS TOKEN获取 + */ +class Oauth extends Wechat +{ + + /** + * 接口名称与URL映射 + * @var array + */ + protected static $url = [ + 'oauth_authorize' => 'https://open.weixin.qq.com/connect/oauth2/authorize', + 'oauth_user_token' => 'https://api.weixin.qq.com/sns/oauth2/access_token', + 'oauth_get_userinfo' => 'https://api.weixin.qq.com/sns/userinfo', + ]; + + /** + * OAuth 用户同意授权,获取code + * @param [type] $callback 回调URI,填写完整地址,带http:// + * @param string $state 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值 + * @param string $scope snsapi_userinfo 获取用户授权信息,snsapi_base直接返回openid + * @return string + */ + public static function url($callback, $state = 'STATE', $scope = 'snsapi_base') + { + $params = [ + 'appid' => self::$config['appid'], + 'redirect_uri' => $callback, + 'response_type' => 'code', + 'scope' => $scope, + 'state' => $state, + ]; + return self::$url['oauth_authorize'] . '?' . http_build_query($params) . '#wechat_redirect'; + } + + /** + * 通过code获取Access Token + * @return array|boolean + */ + public static function token() + { + $code = isset($_GET['code']) ? $_GET['code'] : ''; + if (!$code) { + parent::$error = '未获取到CODE信息'; + return false; + } + $params = [ + 'appid' => self::$config['appid'], + 'secret' => self::$config['secret'], + 'code' => $code, + 'grant_type' => 'authorization_code', + ]; + return Utils::api(self::$url['oauth_user_token'], $params); + } + + /** + * 网页获取用户信息 + * @param string $access_token 通过getOauthAccessToken方法获取到的token + * @param string $openid 用户的OPENID + * @return array + */ + public static function info($token, $openid) + { + $params = [ + 'access_token' => $token, + 'openid' => $openid, + 'lang' => 'zh_CN', + ]; + return Utils::api(self::$url['oauth_get_userinfo'], $params); + } +} diff --git a/vendor/cjango/wechat/src/Wechat/Pay.php b/vendor/cjango/wechat/src/Wechat/Pay.php new file mode 100644 index 0000000..47e700d --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/Pay.php @@ -0,0 +1,280 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * 微信支付相关部分 + */ +class Pay extends Wechat +{ + /** + * 接口名称与URL映射 + * @var array + */ + protected static $url = [ + 'unified_order' => 'https://api.mch.weixin.qq.com/pay/unifiedorder', // 统一下单地址 + 'order_query' => 'https://api.mch.weixin.qq.com/pay/orderquery', // 订单状态查询 + 'close_order' => 'https://api.mch.weixin.qq.com/pay/closeorder', // 关闭订单 + 'refund_order' => 'https://api.mch.weixin.qq.com/secapi/pay/refund', // 退款地址 + 'refund_query' => 'https://api.mch.weixin.qq.com/pay/refundquery', // 退款查询 + 'pay_transfers' => 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers', // 企业付款 + 'get_pay_transfers' => 'https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo', // 企业付款查询 + 'download_bill' => 'https://api.mch.weixin.qq.com/pay/downloadbill', // 下载对账单 + 'send_red_pack' => 'https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack', // 发放红包高级接口 + 'send_group_red_pack' => 'https://api.mch.weixin.qq.com/mmpaymkttransfers/sendgroupredpack', // 发送裂变红包接口(拼手气) + 'get_red_pack_info' => 'https://api.mch.weixin.qq.com/mmpaymkttransfers/gethbinfo', // 红包查询接口 + ]; + + /** + * 统一下单支付 + * @param string $orderId 系统订单号 + * @param string $body 产品描述 + * @param string $money 支付金额 + * @param string $trade_type (JSAPI NATIVE APP) + * @param string $notify_url 回调URL + * @param string $openid 支付用户 + * @param string $attach 附加信息 + * @param array $extends 订单扩展数据 + * @return boolean|JSON 直接用于H5调用支付的API参数 + */ + public static function unified($orderId, $body, $money, $tradeType = 'JSAPI', $notifyUrl = '', $openid = '', $attach = '', $extends = []) + { + $params = [ + 'appid' => parent::$config['appid'], + 'mch_id' => parent::$config['mch_id'], + 'nonce_str' => uniqid(), + 'body' => $body, + 'out_trade_no' => $orderId, + 'total_fee' => $money * 100, // 转换成分 + 'spbill_create_ip' => self::_getClientIp(), + 'notify_url' => $notifyUrl, + 'trade_type' => $tradeType, + ]; + if (!empty($openid) && $tradeType == 'JSAPI') { + $params['openid'] = $openid; + } + if (!empty($attach)) { + $params['attach'] = $attach; + } + $params['sign'] = self::_getOrderSign($params); + + $data = Utils::array2xml($params); + $data = Utils::http(self::$url['unified_order'], $data, 'POST'); + $result = self::parsePayResult(Utils::xml2array($data)); + + if ($result) { + return self::createPayParams($result['prepay_id']); + } else { + return false; + } + } + + /** + * 创建支付参数 + * @param [type] $prepay_id [description] + * @return JSON + */ + private static function createPayParams($prepay_id) + { + $params['appId'] = parent::$config['appid']; + $params['timeStamp'] = (string) time(); + $params['nonceStr'] = uniqid(); + $params['package'] = 'prepay_id=' . $prepay_id; + $params['signType'] = 'MD5'; + $params['paySign'] = self::_getOrderSign($params); + return json_encode($params); + } + + /** + * 查询订单 + * @param [type] $out_trade_no [description] + * @return [type] [description] + */ + public static function query($orderId = '', $type = 'out_trade_no') + { + $params = [ + 'appid' => parent::$config['appid'], + 'mch_id' => parent::$config['mch_id'], + 'nonce_str' => uniqid(), + ]; + if ($type == 'out_trade_no') { + $params['out_trade_no'] = $orderId; + } else { + $params['transaction_id'] = $orderId; + } + $params['sign'] = self::_getOrderSign($params); + $data = Utils::array2xml($params); + $data = Utils::http(self::$url['order_query'], $data, 'POST'); + return Utils::xml2array($data); + } + + /** + * 关闭订单 + * @param string $orderId 商户订单号 + * @return boolean + */ + public static function close($orderId) + { + $params = [ + 'appid' => parent::$config['appid'], + 'mch_id' => parent::$config['mch_id'], + 'out_trade_no' => $orderId, + 'nonce_str' => uniqid(), + ]; + + $params['sign'] = self::_getOrderSign($params); + + $data = Utils::array2xml($params); + $data = Utils::http(self::$url['close_order'], $data, 'POST'); + $result = self::parsePayResult(Utils::xml2array($data)); + + if ($result) { + return true; + } else { + return false; + } + } + + /** + * 退款申请 + * @param [type] $transaction_id [description] + * @param [type] $orderId [description] + * @param [type] $out_refund_no [description] + * @param [type] $total_fee [description] + * @param [type] $refund_fee [description] + * @param [type] $op_user_id [description] + * @return [type] [description] + */ + public static function refund($orderId, $refundId, $total_fee, $refund_fee) + { + $params = [ + 'appid' => parent::$config['appid'], + 'mch_id' => parent::$config['mch_id'], + 'out_trade_no' => $orderId, + 'out_refund_no' => $refundId, + 'total_fee' => $total_fee, + 'refund_fee' => $refund_fee, + 'op_user_id' => parent::$config['mch_id'], + 'nonce_str' => uniqid(), + ]; + + $params['sign'] = self::_getOrderSign($params); + + $data = Utils::array2xml($params); + $data = Utils::http(self::$url['close_order'], $data, 'POST'); + $result = self::parsePayResult(Utils::xml2array($data)); + + if ($result) { + return $result; + } else { + return false; + } + } + + /** + * 解析支付接口的返回结果 + * @param xmlstring $data 接口返回的数据 + * @param boolean $checkSign 是否需要签名校验 + * @return boolean|array + */ + public static function parsePayRequest($checkSign = true) + { + $post = file_get_contents("php://input"); + $data = Utils::xml2array($post); + if (empty($data)) { + Wechat::error('回调结果解析失败'); + return false; + } + if ($checkSign) { + $sign = $data['sign']; + unset($data['sign']); + if (self::_getOrderSign($data) != $sign) { + Wechat::error('签名校验失败'); + return false; + } + } + return self::parsePayResult($data); + } + + /** + * 解析支付借口的返回数据 + * @param [type] $data [description] + */ + private static function parsePayResult($data) + { + if ($data['return_code'] == 'SUCCESS') { + if ($data['result_code'] == 'SUCCESS') { + return $data; + } else { + Wechat::error($data['err_code']); + return false; + } + } else { + Wechat::error($data['return_msg']); + return false; + } + } + + /** + * 支付结果通知 + * @param [type] $code 支付结果 + * @param [type] $msg 返回信息 + * @return xml + */ + public static function returnNotify($msg = true) + { + if ($msg === true) { + $params = [ + 'return_code' => 'SUCCESS', + 'return_msg' => '', + ]; + } else { + $params = [ + 'return_code' => 'FAIL', + 'return_msg' => $msg, + ]; + } + exit(Utils::array2xml($params)); + } + + /** + * 获取客户端IP地址 + * @return string + */ + private static function _getClientIp() + { + if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); + $pos = array_search('unknown', $arr); + if (false !== $pos) { + unset($arr[$pos]); + } + $ip = trim(current($arr)); + } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + } elseif (isset($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + return $ip; + } + + /** + * 本地MD5签名 + * @param array $params 需要签名的数据 + * @return string 大写字母与数字签名(串32位) + */ + private static function _getOrderSign($params) + { + ksort($params); + $params['key'] = parent::$config['paykey']; + return strtoupper(md5(urldecode(http_build_query($params)))); + } +} diff --git a/vendor/cjango/wechat/src/Wechat/QRcode.php b/vendor/cjango/wechat/src/Wechat/QRcode.php new file mode 100644 index 0000000..123d8b3 --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/QRcode.php @@ -0,0 +1,98 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * 自定义菜单 + */ +class QRcode extends Wechat +{ + + /** + * 接口名称与URL映射 + * @var array + */ + protected static $url = [ + 'qrcode_create' => 'https://api.weixin.qq.com/cgi-bin/qrcode/create', + 'qrcode_show' => 'https://mp.weixin.qq.com/cgi-bin/showqrcode', + 'short_url' => 'https://api.mch.weixin.qq.com/tools/shorturl', // 转换短链接 + ]; + + /** + * 临时二维码 + * @param [type] $scene_id [description] + * @param integer $expire [description] + * @return [type] [description] + */ + public static function temp($scene_id, $expire = 604800) + { + $params = [ + 'expire_seconds' => $expire, + 'action_name' => 'QR_SCENE', + 'action_info' => [ + 'scene' => [ + 'scene_id' => $scene_id, + ], + ], + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + $result = Utils::api(self::$url['qrcode_create'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + if ($result) { + return self::$url['qrcode_show'] . '?ticket=' . $result['ticket']; + } else { + return false; + } + } + + /** + * 永久二维码 + * @return [type] [description] + */ + public static function limit($scene_str) + { + $params = [ + 'action_name' => 'QR_LIMIT_SCENE', + 'action_info' => [ + 'scene' => [ + 'scene_str' => $scene_str, + ], + ], + 'access_token' => parent::$config['access_token'], + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + $result = Utils::api(self::$url['qrcode_create'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + if ($result) { + return self::$url['qrcode_show'] . '?ticket=' . $result['ticket']; + } else { + return false; + } + } + + /** + * 转换短链接 + * @param [type] $longUrl + * @return [type] + */ + public static function short($longUrl) + { + $params = [ + 'action' => 'long2short', + 'long_url' => $longUrl, + ]; + $params = json_encode($params); + $result = Utils::api(self::$url['short_url'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + if ($result) { + return $result; + } else { + return false; + } + } +} diff --git a/vendor/cjango/wechat/src/Wechat/Reply.php b/vendor/cjango/wechat/src/Wechat/Reply.php new file mode 100644 index 0000000..e4e46c8 --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/Reply.php @@ -0,0 +1,170 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * 被动消息回复 + */ +class Reply extends Wechat +{ + + /** + * 接收到的消息内容 + * @var array + */ + private static $request = []; + + private static $response = []; + + /** + * 接受消息,通用,接受到的消息 + * 用户自己处理消息类型就可以 + * 暂时不处理加密问题 + * @return array|boolean + */ + public static function request() + { + $postStr = file_get_contents("php://input"); + + if (!empty($postStr)) { + $data = Utils::xml2array($postStr); + return self::$request = $data; + } else { + return false; + } + } + + /** + * 回复消息 + * @param array|string $content 回复的消息内容 + * @param string $type 回复的消息类型 + * @return xml + */ + public static function response($content, $type = 'text') + { + self::$response = [ + 'ToUserName' => self::$request['fromusername'], + 'FromUserName' => self::$request['tousername'], + 'CreateTime' => time(), + 'MsgType' => $type, + ]; + + self::$type($content); + + $response = Utils::array2xml(self::$response); + exit($response); + } + + /** + * 回复文本类型消息 + * @param string $content + */ + private static function text($content) + { + self::$response['Content'] = $content; + } + + /** + * 回复图片类型消息 + * @param string $mediaId + */ + private static function image($mediaId) + { + self::$response['Image']['MediaId'] = $mediaId; + } + + /** + * 回复语音类型消息 + * @param string $mediaId + */ + private static function voice($mediaId) + { + self::$response['Voice']['MediaId'] = $mediaId; + } + + /** + * 回复视频类型消息 + * @param array $media + */ + private static function video($video) + { + list( + $video['MediaId'], + $video['Title'], + $video['Description'] + ) = $video; + + self::$response['Video'] = $video; + } + + /** + * 回复音乐信息 + * @param string $content 要回复的音乐 + */ + private static function music($music) + { + list( + $music['Title'], + $music['Description'], + $music['MusicUrl'], + $music['HQMusicUrl'], + $music['ThumbMediaId'] + ) = $music; + + self::$response['Music'] = $music; + } + + /** + * 回复图文列表消息 + * @param [type] $content [description] + */ + private static function news($news) + { + $articles = []; + + foreach ($news as $key => $value) { + list( + $articles[$key]['Title'], + $articles[$key]['Description'], + $articles[$key]['PicUrl'], + $articles[$key]['Url'] + ) = $value; + if ($key >= 9) { + break; + } + } + + self::$response['ArticleCount'] = count($articles); + self::$response['Articles'] = $articles; + } + + /** + * 将消息转发至客服 + * @param string $account 指定的客服账号 + * @return xml + */ + public static function transfer($account = '') + { + self::$response = [ + 'ToUserName' => self::$request['fromusername'], + 'FromUserName' => self::$request['tousername'], + 'CreateTime' => time(), + 'MsgType' => 'transfer_customer_service', + ]; + + if (!empty($account)) { + self::$response['TransInfo']['KfAccount'] = $account; + } + + $response = Utils::array2xml(self::$response); + exit($response); + } +} diff --git a/vendor/cjango/wechat/src/Wechat/Semantic.php b/vendor/cjango/wechat/src/Wechat/Semantic.php new file mode 100644 index 0000000..ffc1a9e --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/Semantic.php @@ -0,0 +1,30 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * 语义理解 + */ +class Semantic extends Wechat +{ + /** + * 接口名称与URL映射 + * @var array + */ + protected static $url = [ + 'semproxy' => 'https://api.weixin.qq.com/semantic/semproxy/search', + ]; + + public static function search() + { + #Todo.. + } +} diff --git a/vendor/cjango/wechat/src/Wechat/Service.php b/vendor/cjango/wechat/src/Wechat/Service.php new file mode 100644 index 0000000..231d88e --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/Service.php @@ -0,0 +1,255 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * 客服相关接口 + */ +class Service extends Wechat +{ + /** + * 接口名称与URL映射 + * @var array + */ + protected static $url = [ + 'service_list' => 'https://api.weixin.qq.com/cgi-bin/customservice/getkflist', + 'online_list' => 'https://api.weixin.qq.com/cgi-bin/customservice/getonlinekflist', + 'service_add' => 'https://api.weixin.qq.com/customservice/kfaccount/add', + 'service_update' => 'https://api.weixin.qq.com/customservice/kfaccount/update', + 'service_delete' => 'https://api.weixin.qq.com/customservice/kfaccount/del', + 'invite_worker' => 'https://api.weixin.qq.com/customservice/kfaccount/inviteworker', + 'upload_avatr' => 'http://api.weixin.qq.com/customservice/kfaccount/uploadheadimg', + 'msg_record' => 'https://api.weixin.qq.com/customservice/msgrecord/getrecord', + 'custom_send' => 'https://api.weixin.qq.com/cgi-bin/message/custom/send', + 'session_create' => 'https://api.weixin.qq.com/customservice/kfsession/create', + 'session_close' => 'https://api.weixin.qq.com/customservice/kfsession/close', + 'session_get' => 'https://api.weixin.qq.com/customservice/kfsession/getsession', + 'session_list' => 'https://api.weixin.qq.com/customservice/kfsession/getsessionlist', + 'session_wait' => 'https://api.weixin.qq.com/customservice/kfsession/getwaitcase', + ]; + + /** + * 获取客服列表 + * @return array|boolean + */ + public static function get() + { + $result = Utils::api(self::$url['service_list'] . '?access_token=' . parent::$config['access_token']); + if ($result) { + return $result['kf_list']; + } else { + return false; + } + } + + /** + * 获取在线客服 + * @return array|boolean + */ + public static function online() + { + $result = Utils::api(self::$url['online_list'] . '?access_token=' . parent::$config['access_token']); + if ($result) { + return $result['kf_online_list']; + } else { + return false; + } + } + + /** + * 添加客服账号 + * @param string $account + * @param string $nickname + */ + public static function add($account, $nickname) + { + $params = [ + 'kf_account' => $account . '@' . parent::$config['appid'], + 'nickname' => $nickname, + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + return Utils::api(self::$url['service_add'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + /** + * 修改客服账号 + * @param string $account + * @param string $nickname + * @param string $password + * @return boolean + */ + public static function update($account, $nickname) + { + $params = [ + 'kf_account' => $account, + 'nickname' => $nickname, + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + return Utils::api(self::$url['service_update'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + /** + * 删除客服账号 + * @param string $account + * @return boolean + */ + public static function delete($account) + { + $params = [ + 'kf_account' => $account, + 'access_token' => parent::$config['access_token'], + ]; + return Utils::api(self::$url['service_delete'], $params); + } + + /** + * 邀请绑定客服 + * @param string $account + * @param string $weixin + * @return boolean + */ + public static function invite($account, $weixin) + { + $params = [ + 'kf_account' => $account, + 'invite_wx' => $weixin, + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + return Utils::api(self::$url['invite_worker'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + /** + * 获取客服聊天记录 + * @param integer $starttime + * @param integer $endtime + * @param integer $pageindex + * @param integer $pagesize + * @return array|boolean + */ + public static function record($starttime, $endtime, $pageindex = 1, $pagesize = 50) + { + $params = [ + 'starttime' => $starttime, + 'endtime' => $endtime, + 'pageindex' => $pageindex, + 'pagesize' => $pagesize, + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + $result = Utils::api(self::$url['msg_record'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + if ($result) { + return $result['recordlist']; + } else { + return false; + } + } + + /** + * 发送客服消息 + * @param string $openid + * @param string $type + * @param string $content + * @return boolean + */ + public static function send($openid, $type, $content) + { + $params = [ + 'touser' => $openid, + 'msgtype' => $type, + 'text' => [ + 'content' => $content, + ], + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + return Utils::api(self::$url['custom_send'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + /** + * 创建会话 + * @param string $account 完整客服账号 + * @param string $openid 客户openid + * @return boolean + */ + public static function session_create($account, $openid) + { + $params = [ + 'kf_account' => $account, + 'openid' => $openid, + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + return Utils::api(self::$url['session_create'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + /** + * 关闭会话 + * @param string $account 完整客服账号 + * @param string $openid 客户openid + * @return boolean + */ + public static function session_close($account, $openid) + { + $params = [ + 'kf_account' => $account, + 'openid' => $openid, + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + return Utils::api(self::$url['session_close'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + /** + * 获取客户的会话状态 + * @param string $openid 客户openid + * @return array + */ + public static function session_get($openid) + { + $params = [ + 'openid' => $openid, + 'access_token' => parent::$config['access_token'], + ]; + return Utils::api(self::$url['session_get'], $params); + } + + /** + * 获取客服的会话列表 + * @param string $account + * @return array + */ + public static function session_list($account) + { + $params = [ + 'kf_account' => $account, + 'access_token' => parent::$config['access_token'], + ]; + $result = Utils::api(self::$url['session_list'], $params); + if ($result) { + return $result['sessionlist']; + } else { + return false; + } + } + + /** + * 获取未接入会话列表 + * @return array + */ + public static function session_wait() + { + $params = [ + 'access_token' => parent::$config['access_token'], + ]; + $result = Utils::api(self::$url['session_wait'], $params); + if ($result) { + return $result['waitcaselist']; + } else { + return false; + } + } +} diff --git a/vendor/cjango/wechat/src/Wechat/Template.php b/vendor/cjango/wechat/src/Wechat/Template.php new file mode 100644 index 0000000..4bef82e --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/Template.php @@ -0,0 +1,111 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * 模板消息 + */ +class Template extends Wechat +{ + + /** + * 接口名称与URL映射 + * @var array + */ + protected static $url = [ + 'set_industry' => 'https://api.weixin.qq.com/cgi-bin/template/api_set_industry', // 设置所属行业 + 'get_industry' => 'https://api.weixin.qq.com/cgi-bin/template/get_industry', // 获取设置的行业信息 + 'add_template' => 'https://api.weixin.qq.com/cgi-bin/template/api_add_template', // 获得模板ID + 'get_template' => 'https://api.weixin.qq.com/cgi-bin/template/get_all_private_template', // 获取模板列表 + 'del_template' => 'https://api.weixin.qq.com/cgi-bin/template/del_private_template', // 删除模板 + 'send_template' => 'https://api.weixin.qq.com/cgi-bin/message/template/send', // 发送模板消息 + ]; + + /** + * 设置所属行业 + * @param [type] $industry1 [description] + * @param [type] $industry2 [description] + */ + public static function setIndustry($primary, $secondary) + { + $params = [ + 'industry_id1' => $primary, + 'industry_id2' => $secondary, + ]; + $params = json_encode($params); + return Utils::api(self::$url['set_industry'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + /** + * 获取设置的行业信息 + * @return [type] [description] + */ + public static function getIndustry() + { + return Utils::api(self::$url['get_industry'] . '?access_token=' . parent::$config['access_token'], '', 'GET'); + } + + /** + * 获得模板ID + * @return [type] [description] + */ + public static function add($shortTemplateId) + { + $params = [ + 'template_id_short' => $shortTemplateId, + ]; + $params = json_encode($params); + return Utils::api(self::$url['add_template'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + /** + * 获取模板列表 + * @return [type] [description] + */ + public static function get() + { + return Utils::api(self::$url['get_template'] . '?access_token=' . parent::$config['access_token'], '', 'GET'); + } + + /** + * 删除模板 + * @param [type] $templateId 长模板ID 例:Dyvp3-Ff0cnail_CDSzk1fIc6-9lOkxsQE7exTJbwUE + * @return boolean + */ + public static function delete($templateId) + { + $params = [ + 'template_id' => $templateId, + ]; + $params = json_encode($params); + return Utils::api(self::$url['del_template'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + /** + * 发送模板消息 + * @param [type] $openid 接收用户 + * @param [type] $templateId 模板ID + * @param array $data 消息体 + * @param string $url 连接URL + * @return boolean + */ + public static function send($openid, $templateId, $data = [], $url = '') + { + $params = [ + 'touser' => $openid, + 'template_id' => $templateId, + 'url' => $url, + 'data' => $data, + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + return Utils::api(self::$url['send_template'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } +} diff --git a/vendor/cjango/wechat/src/Wechat/Token.php b/vendor/cjango/wechat/src/Wechat/Token.php new file mode 100644 index 0000000..f7f484a --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/Token.php @@ -0,0 +1,65 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * ACCESS TOKEN获取 + */ +class Token extends Wechat +{ + + /** + * 接口名称与URL映射 + * @var array + */ + protected static $url = [ + 'access_token' => 'https://api.weixin.qq.com/cgi-bin/token', // 获取ACCESS_TOKEN + 'jsapi_ticket' => 'https://api.weixin.qq.com/cgi-bin/ticket/getticket', // JSAPI_TICKET获取地址 + ]; + + /** + * 获取ACCESS_TOKEN + * @return [type] [description] + */ + public static function get() + { + $params = [ + 'appid' => parent::$config['appid'], + 'secret' => parent::$config['secret'], + 'grant_type' => 'client_credential', + ]; + $result = Utils::api(self::$url['access_token'], $params); + if ($result) { + return $result['access_token']; + } else { + return false; + } + } + + /** + * 获取JSAPI_TICKET + * @return [type] [description] + */ + public static function ticket() + { + $params = [ + 'access_token' => parent::$config['access_token'], + 'type' => 'jsapi', + ]; + $result = Utils::http(self::$url['jsapi_ticket'], $params); + if ($result) { + $result = json_decode($result, true); + return $result['ticket']; + } else { + return false; + } + } +} diff --git a/vendor/cjango/wechat/src/Wechat/User.php b/vendor/cjango/wechat/src/Wechat/User.php new file mode 100644 index 0000000..6fce3a8 --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/User.php @@ -0,0 +1,135 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * 用户管理 + */ +class User extends Wechat +{ + + /** + * 接口名称与URL映射 + * @var array + */ + protected static $url = [ + 'user_get' => 'https://api.weixin.qq.com/cgi-bin/user/get', + 'user_info' => 'https://api.weixin.qq.com/cgi-bin/user/info', + 'user_info_batch' => 'https://api.weixin.qq.com/cgi-bin/user/info/batchget', + 'user_remark' => 'https://api.weixin.qq.com/cgi-bin/user/info/updateremark', + 'user_in_group' => 'https://api.weixin.qq.com/cgi-bin/groups/getid', + 'user_to_group' => 'https://api.weixin.qq.com/cgi-bin/groups/members/update', + 'batch_to_group' => 'https://api.weixin.qq.com/cgi-bin/groups/members/batchupdate', + ]; + + /** + * 获取全部关注用户 + * @param [type] $nextOpenid [description] + */ + public static function get($nextOpenid = '') + { + $params = [ + 'next_openid' => $nextOpenid, + 'access_token' => parent::$config['access_token'], + ]; + $result = Utils::api(self::$url['user_get'], $params); + if ($result) { + return $result['data']['openid']; + } else { + return false; + } + } + + /** + * 获取用户信息 + * @param [type] $openid [description] + * @param [type] $lang [description] + * @return [type] [description] + */ + public static function info($openid, $lang = 'zh_CN') + { + $params = [ + 'openid' => $openid, + 'access_token' => parent::$config['access_token'], + 'lang' => $lang, + ]; + $result = Utils::api(self::$url['user_info'], $params); + if ($result) { + return $result; + } else { + return false; + } + } + + /** + * 设置用户备注名 + * @param [type] $openid + * @param [type] $remark + * @return [type] + */ + public static function remark($openid, $remark) + { + $params = [ + 'openid' => $openid, + 'remark' => $remark, + ]; + $params = json_encode($params, JSON_UNESCAPED_UNICODE); + return Utils::api(self::$url['user_remark'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + /** + * 查询用户所在分组 + * @param [type] $openid [description] + * @return [type] [description] + */ + public static function getgroup($openid) + { + $params = [ + 'openid' => $openid, + ]; + $params = json_encode($params); + $result = Utils::api(self::$url['user_in_group'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + if ($result) { + return $result['groupid']; + } else { + return false; + } + } + + /** + * 移动用户分组 + */ + public static function togroup($openid, $groupid) + { + $params = [ + 'openid' => $openid, + 'to_groupid' => $groupid, + ]; + $params = json_encode($params); + return Utils::api(self::$url['user_to_group'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } + + /** + * 批量移动用户分组 + * @param array $openid_list [description] + * @param [type] $groupid [description] + * @return [type] [description] + */ + public static function batchgroup($openid_list = [], $groupid) + { + $params = [ + 'openid_list' => $openid_list, + 'to_groupid' => $groupid, + ]; + $params = json_encode($params); + return Utils::api(self::$url['batch_to_group'] . '?access_token=' . parent::$config['access_token'], $params, 'POST'); + } +} diff --git a/vendor/cjango/wechat/src/Wechat/Utils.php b/vendor/cjango/wechat/src/Wechat/Utils.php new file mode 100644 index 0000000..321d890 --- /dev/null +++ b/vendor/cjango/wechat/src/Wechat/Utils.php @@ -0,0 +1,325 @@ + | +// +------------------------------------------------+ +namespace cjango\Wechat; + +use cjango\Wechat; + +/** + * + */ +class Utils +{ + /** + * 错误信息 + * @var array + */ + private static $errMsg = [ + -1 => '系统繁忙,此时请开发者稍候再试', + 0 => '请求成功', + 40001 => '获取access_token时AppSecret错误,或者access_token无效。请开发者认真比对AppSecret的正确性,或查看是否正在为恰当的公众号调用接口', + 40002 => '不合法的凭证类型', + 40003 => '不合法的OpenID,请开发者确认OpenID(该用户)是否已关注公众号,或是否是其他公众号的OpenID', + 40004 => '不合法的媒体文件类型', + 40005 => '不合法的文件类型', + 40006 => '不合法的文件大小', + 40007 => '不合法的媒体文件id', + 40008 => '不合法的消息类型', + 40009 => '不合法的图片文件大小', + 40010 => '不合法的语音文件大小', + 40011 => '不合法的视频文件大小', + 40012 => '不合法的缩略图文件大小', + 40013 => '不合法的AppID,请开发者检查AppID的正确性,避免异常字符,注意大小写', + 40014 => '不合法的access_token,请开发者认真比对access_token的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口', + 40015 => '不合法的菜单类型', + 40016 => '不合法的按钮个数', + 40017 => '不合法的按钮个数', + 40018 => '不合法的按钮名字长度', + 40019 => '不合法的按钮KEY长度', + 40020 => '不合法的按钮URL长度', + 40021 => '不合法的菜单版本号', + 40022 => '不合法的子菜单级数', + 40023 => '不合法的子菜单按钮个数', + 40024 => '不合法的子菜单按钮类型', + 40025 => '不合法的子菜单按钮名字长度', + 40026 => '不合法的子菜单按钮KEY长度', + 40027 => '不合法的子菜单按钮URL长度', + 40028 => '不合法的自定义菜单使用用户', + 40029 => '不合法的oauth_code', + 40030 => '不合法的refresh_token', + 40031 => '不合法的openid列表', + 40032 => '不合法的openid列表长度', + 40033 => '不合法的请求字符,不能包含\uxxxx格式的字符', + 40035 => '不合法的参数', + 40038 => '不合法的请求格式', + 40039 => '不合法的URL长度', + 40050 => '不合法的分组id', + 40051 => '分组名字不合法', + 40117 => '分组名字不合法', + 40118 => 'media_id大小不合法', + 40119 => 'button类型错误', + 40120 => 'button类型错误', + 40121 => '不合法的media_id类型', + 40132 => '微信号不合法', + 40137 => '不支持的图片格式', + 41001 => '缺少access_token参数', + 41002 => '缺少appid参数', + 41003 => '缺少refresh_token参数', + 41004 => '缺少secret参数', + 41005 => '缺少多媒体文件数据', + 41006 => '缺少media_id参数', + 41007 => '缺少子菜单数据', + 41008 => '缺少oauth code', + 41009 => '缺少openid', + 42001 => 'access_token超时,请检查access_token的有效期', + 42002 => 'refresh_token超时', + 42003 => 'oauth_code超时', + 42007 => '用户修改微信密码,accesstoken和refreshtoken失效,需要重新授权', + 43001 => '需要GET请求', + 43002 => '需要POST请求', + 43003 => '需要HTTPS请求', + 43004 => '需要接收者关注', + 43005 => '需要好友关系', + 44001 => '多媒体文件为空', + 44002 => 'POST的数据包为空', + 44003 => '图文消息内容为空', + 44004 => '文本消息内容为空', + 45001 => '多媒体文件大小超过限制', + 45002 => '消息内容超过限制', + 45003 => '标题字段超过限制', + 45004 => '描述字段超过限制', + 45005 => '链接字段超过限制', + 45006 => '图片链接字段超过限制', + 45007 => '语音播放时间超过限制', + 45008 => '图文消息超过限制', + 45009 => '接口调用超过限制', + 45010 => '创建菜单个数超过限制', + 45015 => '回复时间超过限制', + 45016 => '系统分组,不允许修改', + 45017 => '分组名字过长', + 45018 => '分组数量超过上限', + 45047 => '客服接口下行条数超过上限', + 46001 => '不存在媒体数据', + 46002 => '不存在的菜单版本', + 46003 => '不存在的菜单数据', + 46004 => '不存在的用户', + 47001 => '解析JSON/XML内容错误', + 48001 => 'api功能未授权,请确认公众号已获得该接口,可以在公众平台官网-开发者中心页中查看接口权限', + 48004 => 'api接口被封禁,请登录mp.weixin.qq.com查看详情', + 48005 => 'api禁止删除被自动回复和自定义菜单引用的素材', + 48006 => 'api禁止清零调用次数,因为清零次数达到上限', + 50001 => '用户未授权该api', + 50002 => '用户受限,可能是违规后接口被封禁', + 61450 => '客服系统错误', + 61451 => '参数错误', + 61452 => '无效客服账号', + 61453 => '客服帐号已存在', + 61454 => '客服帐号名长度超过限制(仅允许10个英文字符,不包括@及@后的公众号的微信号)', + 61455 => '客服帐号名包含非法字符(仅允许英文+数字)', + 61456 => '客服帐号个数超过限制(10个客服账号)', + 61457 => '无效头像文件类型', + 61458 => '客户正在被其他客服接待', + 61459 => '客服不在线', + 61500 => '日期格式错误', + 65301 => '不存在此menuid对应的个性化菜单', + 65302 => '没有相应的用户', + 65303 => '没有默认菜单,不能创建个性化菜单', + 65304 => 'MatchRule信息为空', + 65305 => '个性化菜单数量受限', + 65306 => '不支持个性化菜单的帐号', + 65307 => '个性化菜单信息为空', + 65308 => '包含没有响应类型的button', + 65309 => '个性化菜单开关处于关闭状态', + 65310 => '填写了省份或城市信息,国家信息不能为空', + 65311 => '填写了城市信息,省份信息不能为空', + 65312 => '不合法的国家信息', + 65313 => '不合法的省份信息', + 65314 => '不合法的城市信息', + 65316 => '该公众号的菜单设置了过多的域名外跳(最多跳转到3个域名的链接)', + 65317 => '不合法的URL', + 65400 => 'API不可用,即没有开通/升级到新版客服', + 65401 => '无效客服帐号', + 65402 => '客服帐号尚未绑定微信号,不能投入使用', + 65403 => '客服昵称不合法', + 65404 => '客服帐号不合法', + 65405 => '帐号数目已达到上限,不能继续添加', + 65406 => '已经存在的客服帐号', + 65407 => '邀请对象已经是本公众号客服', + 65408 => '本公众号已发送邀请给该微信号', + 65409 => '无效的微信号', + 65410 => '邀请对象绑定公众号客服数量达到上限', + 65411 => '该帐号已经有一个等待确认的邀请,不能重复邀请', + 65412 => '该帐号已经绑定微信号,不能进行邀请', + 65413 => '不存在对应用户的会话信息', + 65414 => '粉丝正在被其他客服接待', + 65415 => '指定的客服不在线', + 9001001 => 'POST数据参数不合法', + 9001002 => '远端服务不可用', + 9001003 => 'Ticket不合法', + 9001004 => '获取摇周边用户信息失败', + 9001005 => '获取商户信息失败', + 9001006 => '获取OpenID失败', + 9001007 => '上传文件缺失', + 9001008 => '上传素材的文件类型不合法', + 9001009 => '上传素材的文件尺寸不合法', + 9001010 => '上传失败', + 9001020 => '帐号不合法', + 9001021 => '已有设备激活率低于50%,不能新增设备', + 9001022 => '设备申请数不合法,必须为大于0的数字', + 9001023 => '已存在审核中的设备ID申请', + 9001024 => '一次查询设备ID数量不能超过50', + 9001025 => '设备ID不合法', + 9001026 => '页面ID不合法', + 9001027 => '页面参数不合法', + 9001028 => '一次删除页面ID数量不能超过10', + 9001029 => '页面已应用在设备中,请先解除应用关系再删除', + 9001030 => '一次查询页面ID数量不能超过50', + 9001031 => '时间区间不合法', + 9001032 => '保存设备与页面的绑定关系参数错误', + 9001033 => '门店ID不合法', + 9001034 => '设备备注信息过长', + 9001035 => '设备申请参数不合法', + 9001036 => '查询起始值begin不合法', + ]; + + /** + * 返回数据结果集 + * @var mixed + */ + public static $result; + + public static function api($url, $params = [], $method = 'GET') + { + $result = self::http($url, $params, $method); + return self::parseRequestJson($result); + } + + /** + * 发送HTTP请求方法,目前只支持CURL发送请求 + * @param string $url 请求URL + * @param array $params 请求参数 + * @param string $method 请求方法GET/POST + * @param boolean $ssl 是否进行SSL双向认证 + * @return array $data 响应数据 + */ + public static function http($url, $params = [], $method = 'GET', $ssl = false) + { + $opts = [ + CURLOPT_TIMEOUT => 30, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + ]; + /* 根据请求类型设置特定参数 */ + switch (strtoupper($method)) { + case 'GET': + $getQuerys = !empty($params) ? '?' . urldecode(http_build_query($params)) : ''; + $opts[CURLOPT_URL] = $url . $getQuerys; + break; + case 'POST': + $opts[CURLOPT_URL] = $url; + $opts[CURLOPT_POST] = 1; + $opts[CURLOPT_POSTFIELDS] = $params; + break; + } + /* 初始化并执行curl请求 */ + $ch = curl_init(); + curl_setopt_array($ch, $opts); + $data = curl_exec($ch); + $err = curl_errno($ch); + $errmsg = curl_error($ch); + curl_close($ch); + if ($err > 0) { + \cjango\Wechat::error('CURL:' . $errmsg); + return false; + } else { + return $data; + } + } + + /** + * XML文档解析成数组,并将键值转成小写 + * @param xml $xml + * @return array + */ + public static function xml2array($xml) + { + $data = (array) simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA); + return array_change_key_case($data, CASE_LOWER); + } + + /** + * 将数组转换成XML + * @param array $array + * @return xml + */ + public static function array2xml($array = []) + { + $xml = new \SimpleXMLElement(''); + self::_data2xml($xml, $array); + return $xml->asXML(); + } + + /** + * 数据XML编码 + * @param xml $xml XML对象 + * @param mixed $data 数据 + * @param string $item 数字索引时的节点名称 + * @return string xml + */ + private static function _data2xml($xml, $data, $item = 'item') + { + foreach ($data as $key => $value) { + is_numeric($key) && $key = $item; + if (is_array($value) || is_object($value)) { + $child = $xml->addChild($key); + self::_data2xml($child, $value, $item); + } else { + if (is_numeric($value)) { + $child = $xml->addChild($key, $value); + } else { + $child = $xml->addChild($key); + $node = dom_import_simplexml($child); + $node->appendChild($node->ownerDocument->createCDATASection($value)); + } + } + } + } + + /** + * 解析返回的json数据 + * @param [type] $json + * @return + */ + private static function parseRequestJson($json) + { + $result = json_decode($json, true); + if (isset($result['errcode'])) { + if ($result['errcode'] == 0) { + self::$result = $result; + return true; + } else { + Wechat::error(self::parseError($result)); + return false; + } + } else { + return $result; + } + } + + /** + * 解析错误信息 + * @param array $result + * @return string + */ + public static function parseError($result) + { + $code = $result['errcode']; + return '[' . $code . '] ' . (isset(self::$errMsg[$code]) ? self::$errMsg[$code] : $result['errmsg']); + } +} diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php new file mode 100644 index 0000000..4626994 --- /dev/null +++ b/vendor/composer/ClassLoader.php @@ -0,0 +1,441 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..1a28124 --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) 2016 Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..e899a33 --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,74 @@ + $vendorDir . '/symfony/polyfill-php70/Resources/stubs/ArithmeticError.php', + 'AssertionError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/AssertionError.php', + 'DivisionByZeroError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/DivisionByZeroError.php', + 'Error' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/Error.php', + 'ParseError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/ParseError.php', + 'QrReader' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/QrReader.php', + 'TypeError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/TypeError.php', + 'Zxing\\Binarizer' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/Binarizer.php', + 'Zxing\\BinaryBitmap' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/BinaryBitmap.php', + 'Zxing\\ChecksumException' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/ChecksumException.php', + 'Zxing\\Common\\BitArray' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/BitArray.php', + 'Zxing\\Common\\BitMatrix' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/BitMatrix.php', + 'Zxing\\Common\\BitSource' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/BitSource.php', + 'Zxing\\Common\\CharacterSetECI' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/CharacterSetEci.php', + 'Zxing\\Common\\CharacterSetEci\\AbstractEnum\\AbstractEnum' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/AbstractEnum.php', + 'Zxing\\Common\\DecoderResult' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/DecoderResult.php', + 'Zxing\\Common\\DefaultGridSampler' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/DefaultGridSampler.php', + 'Zxing\\Common\\DetectorResult' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/DetectorResult.php', + 'Zxing\\Common\\Detector\\MathUtils' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/detector/MathUtils.php', + 'Zxing\\Common\\Detector\\MonochromeRectangleDetector' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/detector/MonochromeRectangleDetector.php', + 'Zxing\\Common\\GlobalHistogramBinarizer' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/GlobalHistogramBinarizer.php', + 'Zxing\\Common\\GridSampler' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/GridSampler.php', + 'Zxing\\Common\\HybridBinarizer' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/HybridBinarizer.php', + 'Zxing\\Common\\PerspectiveTransform' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/PerspectiveTransform.php', + 'Zxing\\Common\\Reedsolomon\\GenericGF' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/GenericGF.php', + 'Zxing\\Common\\Reedsolomon\\GenericGFPoly' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/GenericGFPoly.php', + 'Zxing\\Common\\Reedsolomon\\ReedSolomonDecoder' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/ReedSolomonDecoder.php', + 'Zxing\\Common\\Reedsolomon\\ReedSolomonException' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/ReedSolomonException.php', + 'Zxing\\FormatException' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/FormatException.php', + 'Zxing\\GDLuminanceSource' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/GDLuminanceSource.php', + 'Zxing\\IMagickLuminanceSource' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/IMagickLuminanceSource.php', + 'Zxing\\LuminanceSource' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/LuminanceSource.php', + 'Zxing\\NotFoundException' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/NotFoundException.php', + 'Zxing\\PlanarYUVLuminanceSource' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/PlanarYUVLuminanceSource.php', + 'Zxing\\Qrcode\\Decoder\\BitMatrixParser' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/BitMatrixParser.php', + 'Zxing\\Qrcode\\Decoder\\DataBlock' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataBlock.php', + 'Zxing\\Qrcode\\Decoder\\DataMask' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask000' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask001' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask010' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask011' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask100' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask101' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask110' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask111' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DecodedBitStreamParser' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DecodedBitStreamParser.php', + 'Zxing\\Qrcode\\Decoder\\Decoder' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Decoder.php', + 'Zxing\\Qrcode\\Decoder\\ECB' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Version.php', + 'Zxing\\Qrcode\\Decoder\\ECBlocks' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Version.php', + 'Zxing\\Qrcode\\Decoder\\ErrorCorrectionLevel' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/ErrorCorrectionLevel.php', + 'Zxing\\Qrcode\\Decoder\\FormatInformation' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/FormatInformation.php', + 'Zxing\\Qrcode\\Decoder\\Mode' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Mode.php', + 'Zxing\\Qrcode\\Decoder\\Version' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Version.php', + 'Zxing\\Qrcode\\Detector\\AlignmentPattern' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/AlignmentPattern.php', + 'Zxing\\Qrcode\\Detector\\AlignmentPatternFinder' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/AlignmentPatternFinder.php', + 'Zxing\\Qrcode\\Detector\\Detector' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/Detector.php', + 'Zxing\\Qrcode\\Detector\\FinderPattern' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPattern.php', + 'Zxing\\Qrcode\\Detector\\FinderPatternFinder' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPatternFinder.php', + 'Zxing\\Qrcode\\Detector\\FinderPatternInfo' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPatternInfo.php', + 'Zxing\\Qrcode\\QRCodeReader' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/QRCodeReader.php', + 'Zxing\\RGBLuminanceSource' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/RGBLuminanceSource.php', + 'Zxing\\Reader' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/Reader.php', + 'Zxing\\ReaderException' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/ReaderException.php', + 'Zxing\\Result' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/Result.php', + 'Zxing\\ResultPoint' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/ResultPoint.php', +); diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php new file mode 100644 index 0000000..b437605 --- /dev/null +++ b/vendor/composer/autoload_files.php @@ -0,0 +1,13 @@ + $vendorDir . '/paragonie/random_compat/lib/random.php', + '023d27dca8066ef29e6739335ea73bad' => $vendorDir . '/symfony/polyfill-php70/bootstrap.php', + '626dcc41390ebdaa619faa02d99943b0' => $vendorDir . '/khanamiryan/qrcode-detector-decoder/lib/common/customFunctions.php', + '1cfd2761b63b0a29ed23657ea394cb2d' => $vendorDir . '/topthink/think-captcha/src/helper.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..c3a2bfe --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,10 @@ + array($vendorDir . '/bacon/bacon-qr-code/src'), +); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php new file mode 100644 index 0000000..87fcacb --- /dev/null +++ b/vendor/composer/autoload_psr4.php @@ -0,0 +1,19 @@ + array($vendorDir . '/topthink/think-installer/src'), + 'think\\captcha\\' => array($vendorDir . '/topthink/think-captcha/src'), + 'think\\' => array($baseDir . '/thinkphp/library/think'), + 'cjango\\' => array($vendorDir . '/cjango/wechat/src'), + 'Symfony\\Polyfill\\Php70\\' => array($vendorDir . '/symfony/polyfill-php70'), + 'Symfony\\Component\\PropertyAccess\\' => array($vendorDir . '/symfony/property-access'), + 'Symfony\\Component\\OptionsResolver\\' => array($vendorDir . '/symfony/options-resolver'), + 'Symfony\\Component\\Inflector\\' => array($vendorDir . '/symfony/inflector'), + 'MyCLabs\\Enum\\' => array($vendorDir . '/myclabs/php-enum/src'), + 'Endroid\\QrCode\\' => array($vendorDir . '/endroid/qrcode/src'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..cc946f0 --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,70 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInitfea605573c13ab63a5388432dc557bc5::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + if ($useStaticLoader) { + $includeFiles = Composer\Autoload\ComposerStaticInitfea605573c13ab63a5388432dc557bc5::$files; + } else { + $includeFiles = require __DIR__ . '/autoload_files.php'; + } + foreach ($includeFiles as $fileIdentifier => $file) { + composerRequirefea605573c13ab63a5388432dc557bc5($fileIdentifier, $file); + } + + return $loader; + } +} + +function composerRequirefea605573c13ab63a5388432dc557bc5($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..9eabcca --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,175 @@ + __DIR__ . '/..' . '/paragonie/random_compat/lib/random.php', + '023d27dca8066ef29e6739335ea73bad' => __DIR__ . '/..' . '/symfony/polyfill-php70/bootstrap.php', + '626dcc41390ebdaa619faa02d99943b0' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/customFunctions.php', + '1cfd2761b63b0a29ed23657ea394cb2d' => __DIR__ . '/..' . '/topthink/think-captcha/src/helper.php', + ); + + public static $prefixLengthsPsr4 = array ( + 't' => + array ( + 'think\\composer\\' => 15, + 'think\\captcha\\' => 14, + 'think\\' => 6, + ), + 'c' => + array ( + 'cjango\\' => 7, + ), + 'S' => + array ( + 'Symfony\\Polyfill\\Php70\\' => 23, + 'Symfony\\Component\\PropertyAccess\\' => 33, + 'Symfony\\Component\\OptionsResolver\\' => 34, + 'Symfony\\Component\\Inflector\\' => 28, + ), + 'M' => + array ( + 'MyCLabs\\Enum\\' => 13, + ), + 'E' => + array ( + 'Endroid\\QrCode\\' => 15, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'think\\composer\\' => + array ( + 0 => __DIR__ . '/..' . '/topthink/think-installer/src', + ), + 'think\\captcha\\' => + array ( + 0 => __DIR__ . '/..' . '/topthink/think-captcha/src', + ), + 'think\\' => + array ( + 0 => __DIR__ . '/../..' . '/thinkphp/library/think', + ), + 'cjango\\' => + array ( + 0 => __DIR__ . '/..' . '/cjango/wechat/src', + ), + 'Symfony\\Polyfill\\Php70\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php70', + ), + 'Symfony\\Component\\PropertyAccess\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/property-access', + ), + 'Symfony\\Component\\OptionsResolver\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/options-resolver', + ), + 'Symfony\\Component\\Inflector\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/inflector', + ), + 'MyCLabs\\Enum\\' => + array ( + 0 => __DIR__ . '/..' . '/myclabs/php-enum/src', + ), + 'Endroid\\QrCode\\' => + array ( + 0 => __DIR__ . '/..' . '/endroid/qrcode/src', + ), + ); + + public static $prefixesPsr0 = array ( + 'B' => + array ( + 'BaconQrCode' => + array ( + 0 => __DIR__ . '/..' . '/bacon/bacon-qr-code/src', + ), + ), + ); + + public static $classMap = array ( + 'ArithmeticError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/ArithmeticError.php', + 'AssertionError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/AssertionError.php', + 'DivisionByZeroError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/DivisionByZeroError.php', + 'Error' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/Error.php', + 'ParseError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/ParseError.php', + 'QrReader' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/QrReader.php', + 'TypeError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/TypeError.php', + 'Zxing\\Binarizer' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/Binarizer.php', + 'Zxing\\BinaryBitmap' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/BinaryBitmap.php', + 'Zxing\\ChecksumException' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/ChecksumException.php', + 'Zxing\\Common\\BitArray' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/BitArray.php', + 'Zxing\\Common\\BitMatrix' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/BitMatrix.php', + 'Zxing\\Common\\BitSource' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/BitSource.php', + 'Zxing\\Common\\CharacterSetECI' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/CharacterSetEci.php', + 'Zxing\\Common\\CharacterSetEci\\AbstractEnum\\AbstractEnum' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/AbstractEnum.php', + 'Zxing\\Common\\DecoderResult' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/DecoderResult.php', + 'Zxing\\Common\\DefaultGridSampler' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/DefaultGridSampler.php', + 'Zxing\\Common\\DetectorResult' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/DetectorResult.php', + 'Zxing\\Common\\Detector\\MathUtils' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/detector/MathUtils.php', + 'Zxing\\Common\\Detector\\MonochromeRectangleDetector' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/detector/MonochromeRectangleDetector.php', + 'Zxing\\Common\\GlobalHistogramBinarizer' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/GlobalHistogramBinarizer.php', + 'Zxing\\Common\\GridSampler' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/GridSampler.php', + 'Zxing\\Common\\HybridBinarizer' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/HybridBinarizer.php', + 'Zxing\\Common\\PerspectiveTransform' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/PerspectiveTransform.php', + 'Zxing\\Common\\Reedsolomon\\GenericGF' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/GenericGF.php', + 'Zxing\\Common\\Reedsolomon\\GenericGFPoly' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/GenericGFPoly.php', + 'Zxing\\Common\\Reedsolomon\\ReedSolomonDecoder' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/ReedSolomonDecoder.php', + 'Zxing\\Common\\Reedsolomon\\ReedSolomonException' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/ReedSolomonException.php', + 'Zxing\\FormatException' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/FormatException.php', + 'Zxing\\GDLuminanceSource' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/GDLuminanceSource.php', + 'Zxing\\IMagickLuminanceSource' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/IMagickLuminanceSource.php', + 'Zxing\\LuminanceSource' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/LuminanceSource.php', + 'Zxing\\NotFoundException' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/NotFoundException.php', + 'Zxing\\PlanarYUVLuminanceSource' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/PlanarYUVLuminanceSource.php', + 'Zxing\\Qrcode\\Decoder\\BitMatrixParser' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/BitMatrixParser.php', + 'Zxing\\Qrcode\\Decoder\\DataBlock' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataBlock.php', + 'Zxing\\Qrcode\\Decoder\\DataMask' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask000' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask001' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask010' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask011' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask100' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask101' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask110' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DataMask111' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php', + 'Zxing\\Qrcode\\Decoder\\DecodedBitStreamParser' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DecodedBitStreamParser.php', + 'Zxing\\Qrcode\\Decoder\\Decoder' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Decoder.php', + 'Zxing\\Qrcode\\Decoder\\ECB' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Version.php', + 'Zxing\\Qrcode\\Decoder\\ECBlocks' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Version.php', + 'Zxing\\Qrcode\\Decoder\\ErrorCorrectionLevel' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/ErrorCorrectionLevel.php', + 'Zxing\\Qrcode\\Decoder\\FormatInformation' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/FormatInformation.php', + 'Zxing\\Qrcode\\Decoder\\Mode' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Mode.php', + 'Zxing\\Qrcode\\Decoder\\Version' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Version.php', + 'Zxing\\Qrcode\\Detector\\AlignmentPattern' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/AlignmentPattern.php', + 'Zxing\\Qrcode\\Detector\\AlignmentPatternFinder' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/AlignmentPatternFinder.php', + 'Zxing\\Qrcode\\Detector\\Detector' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/Detector.php', + 'Zxing\\Qrcode\\Detector\\FinderPattern' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPattern.php', + 'Zxing\\Qrcode\\Detector\\FinderPatternFinder' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPatternFinder.php', + 'Zxing\\Qrcode\\Detector\\FinderPatternInfo' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPatternInfo.php', + 'Zxing\\Qrcode\\QRCodeReader' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/qrcode/QRCodeReader.php', + 'Zxing\\RGBLuminanceSource' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/RGBLuminanceSource.php', + 'Zxing\\Reader' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/Reader.php', + 'Zxing\\ReaderException' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/ReaderException.php', + 'Zxing\\Result' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/Result.php', + 'Zxing\\ResultPoint' => __DIR__ . '/..' . '/khanamiryan/qrcode-detector-decoder/lib/ResultPoint.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitfea605573c13ab63a5388432dc557bc5::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitfea605573c13ab63a5388432dc557bc5::$prefixDirsPsr4; + $loader->prefixesPsr0 = ComposerStaticInitfea605573c13ab63a5388432dc557bc5::$prefixesPsr0; + $loader->classMap = ComposerStaticInitfea605573c13ab63a5388432dc557bc5::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..e63996e --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,691 @@ +[ + { + "name": "topthink/think-installer", + "version": "v1.0.12", + "version_normalized": "1.0.12.0", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-installer.git", + "reference": "1be326e68f63de4e95977ed50f46ae75f017556d" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/top-think/think-installer/1be326e68f63de4e95977ed50f46ae75f017556d.zip", + "reference": "1be326e68f63de4e95977ed50f46ae75f017556d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0" + }, + "require-dev": { + "composer/composer": "1.0.*@dev" + }, + "time": "2017-05-27T06:58:09+00:00", + "type": "composer-plugin", + "extra": { + "class": "think\\composer\\Plugin" + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "think\\composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ] + }, + { + "name": "topthink/think-captcha", + "version": "v1.0.7", + "version_normalized": "1.0.7.0", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-captcha.git", + "reference": "0c55455df26a1626a60d0dc35d2d89002b741d44" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/top-think/think-captcha/0c55455df26a1626a60d0dc35d2d89002b741d44.zip", + "reference": "0c55455df26a1626a60d0dc35d2d89002b741d44", + "shasum": "" + }, + "time": "2016-07-06T01:47:11+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "think\\captcha\\": "src/" + }, + "files": [ + "src/helper.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "captcha package for thinkphp5" + }, + { + "name": "cjango/wechat", + "version": "dev-master", + "version_normalized": "9999999-dev", + "source": { + "type": "git", + "url": "https://github.com/cjango/wechat.git", + "reference": "085ece24569802ebc08488fdfad252c0ce12e5bc" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/cjango/wechat/085ece24569802ebc08488fdfad252c0ce12e5bc.zip", + "reference": "085ece24569802ebc08488fdfad252c0ce12e5bc", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "time": "2017-06-08T03:09:34+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "cjango\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jason.Chen", + "email": "chenjxlg@163.com", + "homepage": "http://www.cjango.com/" + } + ], + "description": "wechat sdk", + "homepage": "https://github.com/cjango/wechat", + "keywords": [ + "cjango", + "code", + "sdk", + "wechat" + ] + }, + { + "name": "topthink/framework", + "version": "dev-master", + "version_normalized": "9999999-dev", + "source": { + "type": "git", + "url": "https://github.com/top-think/framework.git", + "reference": "d1c2044745a7465f827c733affbcfcb6e0f1bb49" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/top-think/framework/d1c2044745a7465f827c733affbcfcb6e0f1bb49.zip", + "reference": "d1c2044745a7465f827c733affbcfcb6e0f1bb49", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "topthink/think-installer": "~1.0" + }, + "require-dev": { + "johnkary/phpunit-speedtrap": "^1.0", + "mikey179/vfsstream": "~1.6", + "phpdocumentor/reflection-docblock": "^2.0", + "phploc/phploc": "2.*", + "phpunit/phpunit": "4.8.*", + "sebastian/phpcpd": "2.*" + }, + "time": "2017-07-27T10:03:26+00:00", + "type": "think-framework", + "installation-source": "dist", + "autoload": { + "psr-4": { + "think\\": "library/think" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "the new thinkphp framework", + "homepage": "http://thinkphp.cn/", + "keywords": [ + "framework", + "orm", + "thinkphp" + ] + }, + { + "name": "khanamiryan/qrcode-detector-decoder", + "version": "1", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/khanamiryan/php-qrcode-detector-decoder.git", + "reference": "96d5f80680b04803c4f1b69d6e01735e876b80c7" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/khanamiryan/php-qrcode-detector-decoder/96d5f80680b04803c4f1b69d6e01735e876b80c7.zip", + "reference": "96d5f80680b04803c4f1b69d6e01735e876b80c7", + "shasum": "" + }, + "require": { + "php": "^5.6|^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7" + }, + "time": "2017-01-13T09:11:46+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "lib/" + ], + "files": [ + "lib/common/customFunctions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ashot Khanamiryan", + "email": "a.khanamiryan@gmail.com", + "homepage": "https://github.com/khanamiryan", + "role": "Developer" + } + ], + "description": "QR code decoder / reader", + "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder", + "keywords": [ + "barcode", + "qr", + "zxing" + ] + }, + { + "name": "symfony/inflector", + "version": "v3.3.5", + "version_normalized": "3.3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/inflector.git", + "reference": "aed5a0874a3bcfd8d0393a2d91b4cf828f29c7fb" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/symfony/inflector/aed5a0874a3bcfd8d0393a2d91b4cf828f29c7fb.zip", + "reference": "aed5a0874a3bcfd8d0393a2d91b4cf828f29c7fb", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "time": "2017-04-12T14:14:56+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Inflector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Inflector Component", + "homepage": "https://symfony.com", + "keywords": [ + "inflection", + "pluralize", + "singularize", + "string", + "symfony", + "words" + ] + }, + { + "name": "paragonie/random_compat", + "version": "v2.0.10", + "version_normalized": "2.0.10.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/paragonie/random_compat/634bae8e911eefa89c1abfbf1b66da679ac8f54d.zip", + "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "time": "2017-03-13T16:27:32+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "lib/random.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "pseudorandom", + "random" + ] + }, + { + "name": "symfony/polyfill-php70", + "version": "v1.4.0", + "version_normalized": "1.4.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "032fd647d5c11a9ceab8ee8747e13b5448e93874" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/symfony/polyfill-php70/032fd647d5c11a9ceab8ee8747e13b5448e93874.zip", + "reference": "032fd647d5c11a9ceab8ee8747e13b5448e93874", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "~1.0|~2.0", + "php": ">=5.3.3" + }, + "time": "2017-06-09T14:24:12+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php70\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ] + }, + { + "name": "symfony/property-access", + "version": "v3.3.5", + "version_normalized": "3.3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "4cd2bc4afdfd914ad18cec97bb4159fc403384ea" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/symfony/property-access/4cd2bc4afdfd914ad18cec97bb4159fc403384ea.zip", + "reference": "4cd2bc4afdfd914ad18cec97bb4159fc403384ea", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/inflector": "~3.1", + "symfony/polyfill-php70": "~1.0" + }, + "require-dev": { + "symfony/cache": "~3.1" + }, + "suggest": { + "psr/cache-implementation": "To cache access methods." + }, + "time": "2017-07-03T08:12:02+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony PropertyAccess Component", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property path", + "reflection" + ] + }, + { + "name": "symfony/options-resolver", + "version": "v3.3.5", + "version_normalized": "3.3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/symfony/options-resolver/ff48982d295bcac1fd861f934f041ebc73ae40f0.zip", + "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "time": "2017-04-12T14:14:56+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony OptionsResolver Component", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ] + }, + { + "name": "myclabs/php-enum", + "version": "1.5.2", + "version_normalized": "1.5.2.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "3ed7088cfd0a0e06534b7f8b0eee82acea574fac" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/myclabs/php-enum/3ed7088cfd0a0e06534b7f8b0eee82acea574fac.zip", + "reference": "3ed7088cfd0a0e06534b7f8b0eee82acea574fac", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|^5.7|^6.0", + "squizlabs/php_codesniffer": "1.*" + }, + "time": "2017-06-28T16:24:08+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "http://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ] + }, + { + "name": "bacon/bacon-qr-code", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "031a2ce68c5794064b49d11775b2daf45c96e21c" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/Bacon/BaconQrCode/031a2ce68c5794064b49d11775b2daf45c96e21c.zip", + "reference": "031a2ce68c5794064b49d11775b2daf45c96e21c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-gd": "to generate QR code images" + }, + "time": "2016-01-09T22:55:35+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "BaconQrCode": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "http://www.dasprids.de", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode" + }, + { + "name": "endroid/qrcode", + "version": "dev-master", + "version_normalized": "9999999-dev", + "source": { + "type": "git", + "url": "https://github.com/endroid/QrCode.git", + "reference": "2974f1c1fd020eb323c3c97179a40dd1d7795627" + }, + "dist": { + "type": "zip", + "url": "https://files.phpcomposer.com/files/endroid/QrCode/2974f1c1fd020eb323c3c97179a40dd1d7795627.zip", + "reference": "2974f1c1fd020eb323c3c97179a40dd1d7795627", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^1.0", + "ext-gd": "*", + "khanamiryan/qrcode-detector-decoder": "^1.0", + "myclabs/php-enum": "^1.5", + "php": ">=5.6", + "symfony/options-resolver": "^2.7|^3.0", + "symfony/property-access": "^2.7|^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7|^6.0", + "sensio/framework-extra-bundle": "^3.0", + "symfony/asset": "^2.7|^3.0", + "symfony/browser-kit": "^2.7|^3.0", + "symfony/finder": "^2.7|^3.0", + "symfony/framework-bundle": "^2.7|^3.0", + "symfony/http-kernel": "^2.7|^3.0", + "symfony/templating": "^2.7|^3.0", + "symfony/twig-bundle": "^2.7|^3.0", + "symfony/yaml": "^2.7|^3.0" + }, + "time": "2017-07-23T14:07:09+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Endroid\\QrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeroen van den Enden", + "email": "info@endroid.nl", + "homepage": "http://endroid.nl/" + } + ], + "description": "Endroid QR Code", + "homepage": "https://github.com/endroid/QrCode", + "keywords": [ + "bundle", + "code", + "endroid", + "qr", + "qrcode", + "symfony" + ] + } +] diff --git a/vendor/endroid/qrcode/.gitignore b/vendor/endroid/qrcode/.gitignore new file mode 100644 index 0000000..d0eeeda --- /dev/null +++ b/vendor/endroid/qrcode/.gitignore @@ -0,0 +1,5 @@ +/bin +/composer.lock +/composer.phar +/phpunit.xml +/vendor diff --git a/vendor/endroid/qrcode/.travis.yml b/vendor/endroid/qrcode/.travis.yml new file mode 100644 index 0000000..1a695e5 --- /dev/null +++ b/vendor/endroid/qrcode/.travis.yml @@ -0,0 +1,18 @@ +language: php + +php: + - 5.6 + - 7.0 + - 7.1 + +matrix: + fast_finish: true + +before_install: + - phpenv config-rm xdebug.ini + - composer self-update && composer install --no-interaction + +script: bin/phpunit + +notifications: + email: info@endroid.nl diff --git a/vendor/endroid/qrcode/LICENSE b/vendor/endroid/qrcode/LICENSE new file mode 100644 index 0000000..0966ce0 --- /dev/null +++ b/vendor/endroid/qrcode/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) Jeroen van den Enden + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/endroid/qrcode/README.md b/vendor/endroid/qrcode/README.md new file mode 100644 index 0000000..48eb0a0 --- /dev/null +++ b/vendor/endroid/qrcode/README.md @@ -0,0 +1,147 @@ +QR Code +======= + +*By [endroid](http://endroid.nl/)* + +[![Latest Stable Version](http://img.shields.io/packagist/v/endroid/qrcode.svg)](https://packagist.org/packages/endroid/qrcode) +[![Build Status](http://img.shields.io/travis/endroid/QrCode.svg)](http://travis-ci.org/endroid/QrCode) +[![Total Downloads](http://img.shields.io/packagist/dt/endroid/qrcode.svg)](https://packagist.org/packages/endroid/qrcode) +[![Monthly Downloads](http://img.shields.io/packagist/dm/endroid/qrcode.svg)](https://packagist.org/packages/endroid/qrcode) +[![License](http://img.shields.io/packagist/l/endroid/qrcode.svg)](https://packagist.org/packages/endroid/qrcode) + +This library helps you generate QR codes in an easy way and provides a Symfony +bundle for rapid integration in your project. + +## Installation + +Use [Composer](https://getcomposer.org/) to install the library. + +``` bash +$ composer require endroid/qrcode +``` + +## Usage + +```php +use Endroid\QrCode\ErrorCorrectionLevel; +use Endroid\QrCode\LabelAlignment; +use Endroid\QrCode\QrCode; +use Symfony\Component\HttpFoundation\Response; + +// Create a basic QR code +$qrCode = new QrCode('Life is too short to be generating QR codes'); +$qrCode->setSize(300); + +// Set advanced options +$qrCode + ->setWriterByName('png') + ->setMargin(10) + ->setEncoding('UTF-8') + ->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH) + ->setForegroundColor(['r' => 0, 'g' => 0, 'b' => 0]) + ->setBackgroundColor(['r' => 255, 'g' => 255, 'b' => 255]) + ->setLabel('Scan the code', 16, __DIR__.'/../assets/noto_sans.otf', LabelAlignment::CENTER) + ->setLogoPath(__DIR__.'/../assets/symfony.png') + ->setLogoWidth(150) + ->setValidateResult(false) +; + +// Directly output the QR code +header('Content-Type: '.$qrCode->getContentType()); +echo $qrCode->writeString(); + +// Save it to a file +$qrCode->writeFile(__DIR__.'/qrcode.png'); + +// Create a response object +$response = new Response($qrCode->writeString(), Response::HTTP_OK, ['Content-Type' => $qrCode->getContentType()]); +``` + +![QR Code](http://endroid.nl/qrcode/Dit%20is%20een%20test.png) + +## Symfony integration + +Register the Symfony bundle in the kernel. + +```php +// app/AppKernel.php + +public function registerBundles() +{ + $bundles = [ + // ... + new Endroid\QrCode\Bundle\EndroidQrCodeBundle(), + ]; +} +``` + +The bundle makes use of a factory to create QR codes. The default parameters +applied by the factory can optionally be overridden via the configuration. + +```yaml +endroid_qr_code: + writer: 'png' + size: 300 + margin: 10 + foreground_color: { r: 0, g: 0, b: 0 } + background_color: { r: 255, g: 255, b: 255 } + error_correction_level: low # low, medium, quartile or high + encoding: UTF-8 + label: Scan the code + label_font_size: 20 + label_alignment: left # left, center or right + label_margin: { b: 20 } + logo_path: '%kernel.root_dir%/../vendor/endroid/qrcode/assets/symfony.png' + logo_width: 150 + validate_result: false # checks if the result is readable +``` + +The readability of a QR code is primarily determined by the size, the input +length, the error correction level and any possible logo over the image. The +`validate_result` option uses a built-in reader to validate the resulting +image. This does not guarantee that the code will be readable by all readers +but this helps you provide a minimum level of quality. Take note that the +validator can consume quite an amount of resources and is disabled by default. + +Now you can retrieve the factory from the service container and create a QR +code. For instance in your controller this would look like this. + +```php +$qrCode = $this->get('endroid.qrcode.factory')->create('QR Code', ['size' => 200]); +``` + +Add the following section to your routing to be able to handle QR code URLs. +This step can be skipped if you only use data URIs to display your images. + +``` yml +EndroidQrCodeBundle: + resource: "@EndroidQrCodeBundle/Controller/" + type: annotation + prefix: /qrcode +``` + +After installation and configuration, QR codes can be generated by appending +the QR code text to the url followed by any of the supported extensions. + +## Twig extension + +The bundle provides a Twig extension for generating a QR code URL, path or data +URI. You can use the second argument of any of these functions to override any +defaults defined by the bundle or set via your configuration. + +``` twig + + + +``` + +## Versioning + +Version numbers follow the MAJOR.MINOR.PATCH scheme. Backwards compatibility +breaking changes will be kept to a minimum but be aware that these can occur. +Lock your dependencies for production and test your code when upgrading. + +## License + +This bundle is under the MIT license. For the full copyright and license +information please view the LICENSE file that was distributed with this source code. diff --git a/vendor/endroid/qrcode/assets/noto_sans.otf b/vendor/endroid/qrcode/assets/noto_sans.otf new file mode 100644 index 0000000..296fbeb Binary files /dev/null and b/vendor/endroid/qrcode/assets/noto_sans.otf differ diff --git a/vendor/endroid/qrcode/assets/open_sans.ttf b/vendor/endroid/qrcode/assets/open_sans.ttf new file mode 100644 index 0000000..db43334 Binary files /dev/null and b/vendor/endroid/qrcode/assets/open_sans.ttf differ diff --git a/vendor/endroid/qrcode/assets/symfony.png b/vendor/endroid/qrcode/assets/symfony.png new file mode 100644 index 0000000..55fe198 Binary files /dev/null and b/vendor/endroid/qrcode/assets/symfony.png differ diff --git a/vendor/endroid/qrcode/composer.json b/vendor/endroid/qrcode/composer.json new file mode 100644 index 0000000..359393b --- /dev/null +++ b/vendor/endroid/qrcode/composer.json @@ -0,0 +1,54 @@ +{ + "name": "endroid/qrcode", + "description": "Endroid QR Code", + "keywords": ["endroid", "qrcode", "qr", "code", "bundle", "symfony"], + "homepage": "https://github.com/endroid/QrCode", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Jeroen van den Enden", + "email": "info@endroid.nl", + "homepage": "http://endroid.nl/" + } + ], + "require": { + "php": ">=5.6", + "ext-gd": "*", + "symfony/options-resolver": "^2.7|^3.0", + "bacon/bacon-qr-code": "^1.0", + "khanamiryan/qrcode-detector-decoder": "^1.0", + "symfony/property-access": "^2.7|^3.0", + "myclabs/php-enum": "^1.5" + }, + "require-dev": { + "symfony/asset": "^2.7|^3.0", + "symfony/browser-kit": "^2.7|^3.0", + "symfony/finder": "^2.7|^3.0", + "symfony/framework-bundle": "^2.7|^3.0", + "symfony/http-kernel": "^2.7|^3.0", + "symfony/templating": "^2.7|^3.0", + "symfony/twig-bundle": "^2.7|^3.0", + "symfony/yaml": "^2.7|^3.0", + "sensio/framework-extra-bundle": "^3.0", + "phpunit/phpunit": "^5.7|^6.0" + }, + "autoload": { + "psr-4": { + "Endroid\\QrCode\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Endroid\\QrCode\\Tests\\": "tests/" + } + }, + "config": { + "bin-dir": "bin" + }, + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + } +} diff --git a/vendor/endroid/qrcode/phpunit.xml.dist b/vendor/endroid/qrcode/phpunit.xml.dist new file mode 100644 index 0000000..0b5f706 --- /dev/null +++ b/vendor/endroid/qrcode/phpunit.xml.dist @@ -0,0 +1,11 @@ + + + + + tests + + + + + + diff --git a/vendor/endroid/qrcode/src/Bundle/Controller/QrCodeController.php b/vendor/endroid/qrcode/src/Bundle/Controller/QrCodeController.php new file mode 100644 index 0000000..b7774ee --- /dev/null +++ b/vendor/endroid/qrcode/src/Bundle/Controller/QrCodeController.php @@ -0,0 +1,62 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Bundle\Controller; + +use Endroid\QrCode\Factory\QrCodeFactory; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; +use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Request; + +/** + * QR code controller. + */ +class QrCodeController extends Controller +{ + /** + * @Route("/{text}.{extension}", name="endroid_qrcode_generate", requirements={"text"="[\w\W]+"}) + * + * @param Request $request + * @param string $text + * @param string $extension + * @return Response + */ + public function generateAction(Request $request, $text, $extension) + { + $options = $request->query->all(); + + $qrCode = $this->getQrCodeFactory()->create($text, $options); + $qrCode->setWriterByExtension($extension); + + return new Response($qrCode->writeString(), Response::HTTP_OK, ['Content-Type' => $qrCode->getContentType()]); + } + + /** + * @Route("/twig", name="endroid_qrcode_twig_functions") + * @Template() + * + * @return array + */ + public function twigFunctionsAction() + { + return [ + 'message' => 'QR Code' + ]; + } + + /** + * @return QrCodeFactory + */ + protected function getQrCodeFactory() + { + return $this->get('endroid.qrcode.factory'); + } +} diff --git a/vendor/endroid/qrcode/src/Bundle/DependencyInjection/Compiler/WriterRegistryCompilerPass.php b/vendor/endroid/qrcode/src/Bundle/DependencyInjection/Compiler/WriterRegistryCompilerPass.php new file mode 100644 index 0000000..3564b7d --- /dev/null +++ b/vendor/endroid/qrcode/src/Bundle/DependencyInjection/Compiler/WriterRegistryCompilerPass.php @@ -0,0 +1,42 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Bundle\DependencyInjection\Compiler; + +use Endroid\QrCode\ErrorCorrectionLevel; +use Endroid\QrCode\LabelAlignment; +use Endroid\QrCode\QrCode; +use Predis\Response\Error; +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +class WriterRegistryCompilerPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->has('endroid.qrcode.writer_registry')) { + return; + } + + $writerRegistryDefinition = $container->findDefinition('endroid.qrcode.writer_registry'); + + $taggedServices = $container->findTaggedServiceIds('endroid.qrcode.writer'); + foreach ($taggedServices as $id => $tags) { + foreach ($tags as $attributes) { + $writerRegistryDefinition->addMethodCall('addWriter', [new Reference($id), isset($attributes['set_as_default']) && $attributes['set_as_default']]); + } + } + } +} diff --git a/vendor/endroid/qrcode/src/Bundle/DependencyInjection/Configuration.php b/vendor/endroid/qrcode/src/Bundle/DependencyInjection/Configuration.php new file mode 100644 index 0000000..2bf60ff --- /dev/null +++ b/vendor/endroid/qrcode/src/Bundle/DependencyInjection/Configuration.php @@ -0,0 +1,78 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Bundle\DependencyInjection; + +use Endroid\QrCode\ErrorCorrectionLevel; +use Endroid\QrCode\LabelAlignment; +use Endroid\QrCode\QrCode; +use Predis\Response\Error; +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; + +class Configuration implements ConfigurationInterface +{ + public function getConfigTreeBuilder() + { + $treeBuilder = new TreeBuilder(); + + $treeBuilder + ->root('endroid_qr_code') + ->children() + ->scalarNode('writer')->end() + ->integerNode('size')->min(0)->end() + ->integerNode('margin')->min(0)->end() + ->scalarNode('encoding')->defaultValue('UTF-8')->end() + ->scalarNode('error_correction_level') + ->validate() + ->ifNotInArray(ErrorCorrectionLevel::toArray()) + ->thenInvalid('Invalid error correction level %s') + ->end() + ->end() + ->arrayNode('foreground_color') + ->children() + ->scalarNode('r')->isRequired()->end() + ->scalarNode('g')->isRequired()->end() + ->scalarNode('b')->isRequired()->end() + ->end() + ->end() + ->arrayNode('background_color') + ->children() + ->scalarNode('r')->isRequired()->end() + ->scalarNode('g')->isRequired()->end() + ->scalarNode('b')->isRequired()->end() + ->end() + ->end() + ->scalarNode('logo_path')->end() + ->integerNode('logo_width')->end() + ->scalarNode('label')->end() + ->integerNode('label_font_size')->end() + ->scalarNode('label_font_path')->end() + ->scalarNode('label_alignment') + ->validate() + ->ifNotInArray(LabelAlignment::toArray()) + ->thenInvalid('Invalid label alignment %s') + ->end() + ->end() + ->arrayNode('label_margin') + ->children() + ->scalarNode('t')->end() + ->scalarNode('r')->end() + ->scalarNode('b')->end() + ->scalarNode('l')->end() + ->end() + ->end() + ->booleanNode('validate_result')->end() + ->end() + ->end() + ; + + return $treeBuilder; + } +} diff --git a/vendor/endroid/qrcode/src/Bundle/DependencyInjection/EndroidQrCodeExtension.php b/vendor/endroid/qrcode/src/Bundle/DependencyInjection/EndroidQrCodeExtension.php new file mode 100644 index 0000000..5f06877 --- /dev/null +++ b/vendor/endroid/qrcode/src/Bundle/DependencyInjection/EndroidQrCodeExtension.php @@ -0,0 +1,34 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Bundle\DependencyInjection; + +use Symfony\Component\Config\Definition\Processor; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; +use Symfony\Component\Config\FileLocator; + +class EndroidQrCodeExtension extends Extension +{ + /** + * {@inheritdoc} + */ + public function load(array $configs, ContainerBuilder $container) + { + $processor = new Processor(); + $config = $processor->processConfiguration(new Configuration(), $configs); + + $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.yml'); + + $factoryDefinition = $container->getDefinition('endroid.qrcode.factory'); + $factoryDefinition->replaceArgument(0, $config); + } +} diff --git a/vendor/endroid/qrcode/src/Bundle/EndroidQrCodeBundle.php b/vendor/endroid/qrcode/src/Bundle/EndroidQrCodeBundle.php new file mode 100644 index 0000000..9d51978 --- /dev/null +++ b/vendor/endroid/qrcode/src/Bundle/EndroidQrCodeBundle.php @@ -0,0 +1,27 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Bundle; + +use Endroid\QrCode\Bundle\DependencyInjection\Compiler\WriterRegistryCompilerPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class EndroidQrCodeBundle extends Bundle +{ + /** + * {@inheritdoc} + */ + public function build(ContainerBuilder $container) + { + parent::build($container); + + $container->addCompilerPass(new WriterRegistryCompilerPass()); + } +} diff --git a/vendor/endroid/qrcode/src/Bundle/Resources/config/services.yml b/vendor/endroid/qrcode/src/Bundle/Resources/config/services.yml new file mode 100644 index 0000000..cbb4d6a --- /dev/null +++ b/vendor/endroid/qrcode/src/Bundle/Resources/config/services.yml @@ -0,0 +1,31 @@ +services: + endroid.qrcode.factory: + class: Endroid\QrCode\Factory\QrCodeFactory + arguments: [ null, '@endroid.qrcode.writer_registry' ] + endroid.qrcode.twig.extension: + class: Endroid\QrCode\Twig\Extension\QrCodeExtension + arguments: [ '@endroid.qrcode.factory', '@router'] + tags: + - { name: twig.extension } + endroid.qrcode.writer_registry: + class: Endroid\QrCode\WriterRegistry + endroid.qrcode.writer.binary_writer: + class: Endroid\QrCode\Writer\BinaryWriter + tags: + - { name: endroid.qrcode.writer } + endroid.qrcode.writer.debug_writer: + class: Endroid\QrCode\Writer\DebugWriter + tags: + - { name: endroid.qrcode.writer } + endroid.qrcode.writer.eps_writer: + class: Endroid\QrCode\Writer\EpsWriter + tags: + - { name: endroid.qrcode.writer } + endroid.qrcode.writer.png_writer: + class: Endroid\QrCode\Writer\PngWriter + tags: + - { name: endroid.qrcode.writer, set_as_default: true } + endroid.qrcode.writer.svg_writer: + class: Endroid\QrCode\Writer\SvgWriter + tags: + - { name: endroid.qrcode.writer } \ No newline at end of file diff --git a/vendor/endroid/qrcode/src/Bundle/Resources/views/QrCode/twigFunctions.html.twig b/vendor/endroid/qrcode/src/Bundle/Resources/views/QrCode/twigFunctions.html.twig new file mode 100644 index 0000000..ff448c1 --- /dev/null +++ b/vendor/endroid/qrcode/src/Bundle/Resources/views/QrCode/twigFunctions.html.twig @@ -0,0 +1,3 @@ + + + diff --git a/vendor/endroid/qrcode/src/ErrorCorrectionLevel.php b/vendor/endroid/qrcode/src/ErrorCorrectionLevel.php new file mode 100644 index 0000000..4a963f3 --- /dev/null +++ b/vendor/endroid/qrcode/src/ErrorCorrectionLevel.php @@ -0,0 +1,20 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode; + +use MyCLabs\Enum\Enum; + +class ErrorCorrectionLevel extends Enum +{ + const LOW = 'low'; + const MEDIUM = 'medium'; + const QUARTILE = 'quartile'; + const HIGH = 'high'; +} diff --git a/vendor/endroid/qrcode/src/Exception/InvalidPathException.php b/vendor/endroid/qrcode/src/Exception/InvalidPathException.php new file mode 100644 index 0000000..6492b54 --- /dev/null +++ b/vendor/endroid/qrcode/src/Exception/InvalidPathException.php @@ -0,0 +1,14 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Exception; + +class InvalidPathException extends QrCodeException +{ +} diff --git a/vendor/endroid/qrcode/src/Exception/InvalidWriterException.php b/vendor/endroid/qrcode/src/Exception/InvalidWriterException.php new file mode 100644 index 0000000..4663403 --- /dev/null +++ b/vendor/endroid/qrcode/src/Exception/InvalidWriterException.php @@ -0,0 +1,14 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Exception; + +class InvalidWriterException extends QrCodeException +{ +} diff --git a/vendor/endroid/qrcode/src/Exception/MissingFunctionException.php b/vendor/endroid/qrcode/src/Exception/MissingFunctionException.php new file mode 100644 index 0000000..9afe997 --- /dev/null +++ b/vendor/endroid/qrcode/src/Exception/MissingFunctionException.php @@ -0,0 +1,14 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Exception; + +class MissingFunctionException extends QrCodeException +{ +} diff --git a/vendor/endroid/qrcode/src/Exception/QrCodeException.php b/vendor/endroid/qrcode/src/Exception/QrCodeException.php new file mode 100644 index 0000000..f076a95 --- /dev/null +++ b/vendor/endroid/qrcode/src/Exception/QrCodeException.php @@ -0,0 +1,16 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Exception; + +use Exception; + +abstract class QrCodeException extends Exception +{ +} diff --git a/vendor/endroid/qrcode/src/Exception/UnsupportedExtensionException.php b/vendor/endroid/qrcode/src/Exception/UnsupportedExtensionException.php new file mode 100644 index 0000000..4b1c6f2 --- /dev/null +++ b/vendor/endroid/qrcode/src/Exception/UnsupportedExtensionException.php @@ -0,0 +1,14 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Exception; + +class UnsupportedExtensionException extends QrCodeException +{ +} diff --git a/vendor/endroid/qrcode/src/Exception/ValidationException.php b/vendor/endroid/qrcode/src/Exception/ValidationException.php new file mode 100644 index 0000000..85d8380 --- /dev/null +++ b/vendor/endroid/qrcode/src/Exception/ValidationException.php @@ -0,0 +1,14 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Exception; + +class ValidationException extends QrCodeException +{ +} diff --git a/vendor/endroid/qrcode/src/Factory/QrCodeFactory.php b/vendor/endroid/qrcode/src/Factory/QrCodeFactory.php new file mode 100644 index 0000000..2eca79e --- /dev/null +++ b/vendor/endroid/qrcode/src/Factory/QrCodeFactory.php @@ -0,0 +1,119 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Factory; + +use Endroid\QrCode\QrCode; +use Endroid\QrCode\WriterRegistryInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\PropertyAccess\PropertyAccess; + +class QrCodeFactory +{ + /** + * @var array + */ + protected $definedOptions = [ + 'writer', + 'size', + 'margin', + 'foreground_color', + 'background_color', + 'encoding', + 'error_correction_level', + 'logo_path', + 'logo_width', + 'label', + 'label_font_size', + 'label_font_path', + 'label_alignment', + 'label_margin', + 'validate_result' + ]; + + /** + * @var array + */ + protected $defaultOptions; + + /** + * @var WriterRegistryInterface + */ + protected $writerRegistry; + + /** + * @var OptionsResolver + */ + protected $optionsResolver; + + /** + * @param array $defaultOptions + * @param WriterRegistryInterface $writerRegistry + */ + public function __construct(array $defaultOptions = [], WriterRegistryInterface $writerRegistry = null) + { + $this->defaultOptions = $defaultOptions; + $this->writerRegistry = $writerRegistry; + } + + /** + * @param string $text + * @param array $options + * @return QrCode + */ + public function create($text = '', array $options = []) + { + $options = $this->getOptionsResolver()->resolve($options); + $accessor = PropertyAccess::createPropertyAccessor(); + + $qrCode = new QrCode($text); + + if ($this->writerRegistry instanceof WriterRegistryInterface) { + $qrCode->setWriterRegistry($this->writerRegistry); + } + + foreach ($this->definedOptions as $option) { + if (isset($options[$option])) { + if ($option === 'writer') { + $options['writer_by_name'] = $options[$option]; + $option = 'writer_by_name'; + } + $accessor->setValue($qrCode, $option, $options[$option]); + } + } + + return $qrCode; + } + + /** + * @return OptionsResolver + */ + protected function getOptionsResolver() + { + if (!$this->optionsResolver instanceof OptionsResolver) { + $this->optionsResolver = $this->createOptionsResolver(); + } + + return $this->optionsResolver; + } + + /** + * @return OptionsResolver + */ + protected function createOptionsResolver() + { + $optionsResolver = new OptionsResolver(); + $optionsResolver + ->setDefaults($this->defaultOptions) + ->setDefined($this->definedOptions) + ; + + return $optionsResolver; + } +} diff --git a/vendor/endroid/qrcode/src/LabelAlignment.php b/vendor/endroid/qrcode/src/LabelAlignment.php new file mode 100644 index 0000000..ac30cc9 --- /dev/null +++ b/vendor/endroid/qrcode/src/LabelAlignment.php @@ -0,0 +1,19 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode; + +use MyCLabs\Enum\Enum; + +class LabelAlignment extends Enum +{ + const LEFT = 'left'; + const CENTER = 'center'; + const RIGHT = 'right'; +} diff --git a/vendor/endroid/qrcode/src/QrCode.php b/vendor/endroid/qrcode/src/QrCode.php new file mode 100644 index 0000000..9dbef3a --- /dev/null +++ b/vendor/endroid/qrcode/src/QrCode.php @@ -0,0 +1,566 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode; + +use Endroid\QrCode\Exception\InvalidPathException; +use Endroid\QrCode\Exception\InvalidWriterException; +use Endroid\QrCode\Exception\UnsupportedExtensionException; +use Endroid\QrCode\Writer\WriterInterface; + +class QrCode implements QrCodeInterface +{ + const LABEL_FONT_PATH_DEFAULT = __DIR__ . '/../assets/noto_sans.otf'; + + /** + * @var string + */ + protected $text; + + /** + * @var int + */ + protected $size = 300; + + /** + * @var int + */ + protected $margin = 10; + + /** + * @var array + */ + protected $foregroundColor = [ + 'r' => 0, + 'g' => 0, + 'b' => 0 + ]; + + /** + * @var array + */ + protected $backgroundColor = [ + 'r' => 255, + 'g' => 255, + 'b' => 255 + ]; + + /** + * @var string + */ + protected $encoding = 'UTF-8'; + + /** + * @var ErrorCorrectionLevel + */ + protected $errorCorrectionLevel; + + /** + * @var string + */ + protected $logoPath; + + /** + * @var int + */ + protected $logoWidth; + + /** + * @var string + */ + protected $label; + + /** + * @var int + */ + protected $labelFontSize = 16; + + /** + * @var string + */ + protected $labelFontPath = self::LABEL_FONT_PATH_DEFAULT; + + /** + * @var LabelAlignment + */ + protected $labelAlignment; + + /** + * @var array + */ + protected $labelMargin = [ + 't' => 0, + 'r' => 10, + 'b' => 10, + 'l' => 10, + ]; + + /** + * @var WriterRegistryInterface + */ + protected $writerRegistry; + + /** + * @var WriterInterface + */ + protected $writer; + + /** + * @var bool + */ + protected $validateResult = false; + + /** + * @param string $text + */ + public function __construct($text = '') + { + $this->text = $text; + + $this->errorCorrectionLevel = new ErrorCorrectionLevel(ErrorCorrectionLevel::LOW); + $this->labelAlignment = new LabelAlignment(LabelAlignment::CENTER); + + $this->writerRegistry = new StaticWriterRegistry(); + } + + /** + * @param string $text + * @return $this + */ + public function setText($text) + { + $this->text = $text; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getText() + { + return $this->text; + } + + /** + * @param int $size + * @return $this + */ + public function setSize($size) + { + $this->size = $size; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + return $this->size; + } + + /** + * @param int $margin + * @return $this + */ + public function setMargin($margin) + { + $this->margin = $margin; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getMargin() + { + return $this->margin; + } + + /** + * @param array $foregroundColor + * @return $this + */ + public function setForegroundColor($foregroundColor) + { + $this->foregroundColor = $foregroundColor; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getForegroundColor() + { + return $this->foregroundColor; + } + + /** + * @param array $backgroundColor + * @return $this + */ + public function setBackgroundColor($backgroundColor) + { + $this->backgroundColor = $backgroundColor; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getBackgroundColor() + { + return $this->backgroundColor; + } + + /** + * @param string $encoding + * @return $this + */ + public function setEncoding($encoding) + { + $this->encoding = $encoding; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * @param string $errorCorrectionLevel + * @return $this + */ + public function setErrorCorrectionLevel($errorCorrectionLevel) + { + $this->errorCorrectionLevel = new ErrorCorrectionLevel($errorCorrectionLevel); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getErrorCorrectionLevel() + { + return $this->errorCorrectionLevel->getValue(); + } + + /** + * @param string $logoPath + * @return $this + * @throws InvalidPathException + */ + public function setLogoPath($logoPath) + { + $logoPath = realpath($logoPath); + + if (!is_file($logoPath)) { + throw new InvalidPathException('Invalid logo path: ' . $logoPath); + } + + $this->logoPath = $logoPath; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLogoPath() + { + return $this->logoPath; + } + + /** + * @param int $logoWidth + * @return $this + */ + public function setLogoWidth($logoWidth) + { + $this->logoWidth = $logoWidth; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLogoWidth() + { + return $this->logoWidth; + } + + /** + * @param string $label + * @param int $labelFontSize + * @param string $labelFontPath + * @param string $labelAlignment + * @param array $labelMargin + * @return $this + */ + public function setLabel($label, $labelFontSize = null, $labelFontPath = null, $labelAlignment = null, $labelMargin = null) + { + $this->label = $label; + + if ($labelFontSize !== null) { + $this->setLabelFontSize($labelFontSize); + } + + if ($labelFontPath !== null) { + $this->setLabelFontPath($labelFontPath); + } + + if ($labelAlignment !== null) { + $this->setLabelAlignment($labelAlignment); + } + + if ($labelMargin !== null) { + $this->setLabelMargin($labelMargin); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLabel() + { + return $this->label; + } + + /** + * @param int $labelFontSize + * @return $this + */ + public function setLabelFontSize($labelFontSize) + { + $this->labelFontSize = $labelFontSize; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLabelFontSize() + { + return $this->labelFontSize; + } + + /** + * @param string $labelFontPath + * @return $this + * @throws InvalidPathException + */ + public function setLabelFontPath($labelFontPath) + { + $labelFontPath = realpath($labelFontPath); + + if (!is_file($labelFontPath)) { + throw new InvalidPathException('Invalid label font path: ' . $labelFontPath); + } + + $this->labelFontPath = $labelFontPath; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLabelFontPath() + { + return $this->labelFontPath; + } + + /** + * @param string $labelAlignment + * @return $this + */ + public function setLabelAlignment($labelAlignment) + { + $this->labelAlignment = new LabelAlignment($labelAlignment); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLabelAlignment() + { + return $this->labelAlignment->getValue(); + } + + /** + * @param int[] $labelMargin + * @return $this + */ + public function setLabelMargin(array $labelMargin) + { + $this->labelMargin = array_merge($this->labelMargin, $labelMargin); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLabelMargin() + { + return $this->labelMargin; + } + + /** + * @param WriterRegistryInterface $writerRegistry + * @return $this + */ + public function setWriterRegistry(WriterRegistryInterface $writerRegistry) + { + $this->writerRegistry = $writerRegistry; + + return $this; + } + + /** + * @param WriterInterface $writer + * @return $this + */ + public function setWriter(WriterInterface $writer) + { + $this->writer = $writer; + + return $this; + } + + /** + * @param WriterInterface $name + * @return WriterInterface + */ + public function getWriter($name = null) + { + if (!is_null($name)) { + return $this->writerRegistry->getWriter($name); + } + + if ($this->writer instanceof WriterInterface) { + return $this->writer; + } + + return $this->writerRegistry->getDefaultWriter(); + } + + /** + * @param string $name + * @return $this + * @throws InvalidWriterException + */ + public function setWriterByName($name) + { + $this->writer = $this->writerRegistry->getWriter($name); + + return $this; + } + + /** + * @param string $path + * @return $this + */ + public function setWriterByPath($path) + { + $extension = pathinfo($path, PATHINFO_EXTENSION); + + $this->setWriterByExtension($extension); + + return $this; + } + + /** + * @param string $extension + * @return $this + * @throws UnsupportedExtensionException + */ + public function setWriterByExtension($extension) + { + foreach ($this->writerRegistry->getWriters() as $writer) { + if ($writer->supportsExtension($extension)) { + $this->writer = $writer; + return $this; + } + } + + die; + + throw new UnsupportedExtensionException('Missing writer for extension "'.$extension.'"'); + } + + /** + * @param bool $validateResult + * @return $this + */ + public function setValidateResult($validateResult) + { + $this->validateResult = $validateResult; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getValidateResult() + { + return $this->validateResult; + } + + /** + * @return string + */ + public function writeString() + { + return $this->getWriter()->writeString($this); + } + + /** + * @return string + */ + public function writeDataUri() + { + return $this->getWriter()->writeDataUri($this); + } + + /** + * @param string $path + */ + public function writeFile($path) + { + return $this->getWriter()->writeFile($this, $path); + } + + /** + * @return string + * @throws InvalidWriterException + */ + public function getContentType() + { + return $this->getWriter()->getContentType(); + } +} diff --git a/vendor/endroid/qrcode/src/QrCodeInterface.php b/vendor/endroid/qrcode/src/QrCodeInterface.php new file mode 100644 index 0000000..8f9c6cf --- /dev/null +++ b/vendor/endroid/qrcode/src/QrCodeInterface.php @@ -0,0 +1,94 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode; + +interface QrCodeInterface +{ + /** + * @return string + */ + public function getText(); + + /** + * @return int + */ + public function getSize(); + + /** + * @return int + */ + public function getMargin(); + + /** + * @return int[] + */ + public function getForegroundColor(); + + /** + * @return int[] + */ + public function getBackgroundColor(); + + /** + * @return string + */ + public function getEncoding(); + + /** + * @return string + */ + public function getErrorCorrectionLevel(); + + /** + * @return string + */ + public function getLogoPath(); + + /** + * @return int + */ + public function getLogoWidth(); + + /** + * @return string + */ + public function getLabel(); + + /** + * @return string + */ + public function getLabelFontPath(); + + /** + * @return int + */ + public function getLabelFontSize(); + + /** + * @return string + */ + public function getLabelAlignment(); + + /** + * @return int[] + */ + public function getLabelMargin(); + + /** + * @return bool + */ + public function getValidateResult(); + + /** + * @param WriterRegistryInterface $writerRegistry + * @return mixed + */ + public function setWriterRegistry(WriterRegistryInterface $writerRegistry); +} diff --git a/vendor/endroid/qrcode/src/StaticWriterRegistry.php b/vendor/endroid/qrcode/src/StaticWriterRegistry.php new file mode 100644 index 0000000..3b1a54c --- /dev/null +++ b/vendor/endroid/qrcode/src/StaticWriterRegistry.php @@ -0,0 +1,43 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode; + +use Endroid\QrCode\Writer\BinaryWriter; +use Endroid\QrCode\Writer\DebugWriter; +use Endroid\QrCode\Writer\EpsWriter; +use Endroid\QrCode\Writer\PngWriter; +use Endroid\QrCode\Writer\SvgWriter; +use Endroid\QrCode\Writer\WriterInterface; + +class StaticWriterRegistry extends WriterRegistry +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + parent::__construct(); + + $this->loadWriters(); + } + + protected function loadWriters() + { + if (count($this->writers) > 0) { + return; + } + + $this->addWriter(new BinaryWriter()); + $this->addWriter(new DebugWriter()); + $this->addWriter(new EpsWriter()); + $this->addWriter(new PngWriter(), true); + $this->addWriter(new SvgWriter()); + } +} diff --git a/vendor/endroid/qrcode/src/Twig/Extension/QrCodeExtension.php b/vendor/endroid/qrcode/src/Twig/Extension/QrCodeExtension.php new file mode 100644 index 0000000..e5a57db --- /dev/null +++ b/vendor/endroid/qrcode/src/Twig/Extension/QrCodeExtension.php @@ -0,0 +1,117 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Twig\Extension; + +use Endroid\QrCode\Exception\UnsupportedExtensionException; +use Endroid\QrCode\Factory\QrCodeFactory; +use Endroid\QrCode\Writer\AbstractDataUriWriter; +use Endroid\QrCode\Writer\PngDataUriWriter; +use Endroid\QrCode\Writer\PngWriter; +use Endroid\QrCode\Writer\SvgDataUriWriter; +use Endroid\QrCode\WriterRegistryInterface; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\RouterInterface; +use Twig_Extension; +use Twig_SimpleFunction; + +class QrCodeExtension extends Twig_Extension +{ + /** + * @var QrCodeFactory + */ + protected $qrCodeFactory; + + /** + * @var RouterInterface + */ + protected $router; + + /** + * @param QrCodeFactory $qrCodeFactory + * @param RouterInterface $router + * @param WriterRegistryInterface $writerRegistry + */ + public function __construct(QrCodeFactory $qrCodeFactory, RouterInterface $router) + { + $this->qrCodeFactory = $qrCodeFactory; + $this->router = $router; + } + + /** + * {@inheritdoc} + */ + public function getFunctions() + { + return [ + new Twig_SimpleFunction('qrcode_path', [$this, 'qrCodePathFunction']), + new Twig_SimpleFunction('qrcode_url', [$this, 'qrCodeUrlFunction']), + new Twig_SimpleFunction('qrcode_data_uri', [$this, 'qrCodeDataUriFunction']), + ]; + } + + /** + * @param string $text + * @param array $options + * + * @return string + */ + public function qrcodeUrlFunction($text, array $options = []) + { + return $this->getQrCodeReference($text, $options, UrlGeneratorInterface::ABSOLUTE_URL); + } + + /** + * @param string $text + * @param array $options + * @return string + */ + public function qrCodePathFunction($text, array $options = []) + { + return $this->getQrCodeReference($text, $options, UrlGeneratorInterface::ABSOLUTE_PATH); + } + + /** + * @param string $text + * @param array $options + * @param int $referenceType + * @return string + */ + public function getQrCodeReference($text, array $options = [], $referenceType) + { + $qrCode = $this->qrCodeFactory->create($text, $options); + $supportedExtensions = $qrCode->getWriter()->getSupportedExtensions(); + + $options['text'] = $text; + $options['extension'] = current($supportedExtensions); + + return $this->router->generate('endroid_qrcode_generate', $options, $referenceType); + } + + /** + * @param string $text + * @param array $options + * @return string + * @throws UnsupportedExtensionException + */ + public function qrcodeDataUriFunction($text, array $options = []) + { + $qrCode = $this->qrCodeFactory->create($text, $options); + + return $qrCode->writeDataUri(); + } + + /** + * @return string + */ + public function getName() + { + return 'qrcode'; + } +} diff --git a/vendor/endroid/qrcode/src/Writer/AbstractBaconWriter.php b/vendor/endroid/qrcode/src/Writer/AbstractBaconWriter.php new file mode 100644 index 0000000..a7bd659 --- /dev/null +++ b/vendor/endroid/qrcode/src/Writer/AbstractBaconWriter.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Writer; + +use BaconQrCode\Renderer\Color\Rgb; + +abstract class AbstractBaconWriter extends AbstractWriter +{ + /** + * @param array $color + * @return Rgb + */ + protected function convertColor(array $color) + { + $color = new Rgb($color['r'], $color['g'], $color['b']); + + return $color; + } + + /** + * @param string $errorCorrectionLevel + * @return string + */ + protected function convertErrorCorrectionLevel($errorCorrectionLevel) + { + $name = strtoupper(substr($errorCorrectionLevel, 0, 1)); + $errorCorrectionLevel = constant('BaconQrCode\Common\ErrorCorrectionLevel::'.$name); + + return $errorCorrectionLevel; + } +} diff --git a/vendor/endroid/qrcode/src/Writer/AbstractWriter.php b/vendor/endroid/qrcode/src/Writer/AbstractWriter.php new file mode 100644 index 0000000..27c564c --- /dev/null +++ b/vendor/endroid/qrcode/src/Writer/AbstractWriter.php @@ -0,0 +1,63 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Writer; + +use Endroid\QrCode\QrCodeInterface; +use ReflectionClass; + +abstract class AbstractWriter implements WriterInterface +{ + /** + * {@inheritdoc} + */ + public function writeDataUri(QrCodeInterface $qrCode) + { + $dataUri = 'data:'.$this->getContentType().';base64,'.base64_encode($this->writeString($qrCode)); + + return $dataUri; + } + + /** + * {@inheritdoc} + */ + public function writeFile(QrCodeInterface $qrCode, $path) + { + $string = $this->writeString($qrCode); + file_put_contents($path, $string); + } + + /** + * {@inheritdoc} + */ + public static function supportsExtension($extension) + { + return in_array($extension, static::getSupportedExtensions()); + } + + /** + * {@inheritdoc} + */ + public static function getSupportedExtensions() + { + return []; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + $reflectionClass = new ReflectionClass($this); + $className = $reflectionClass->getShortName(); + $name = strtolower(preg_replace('/(? + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Writer; + +use Endroid\QrCode\QrCodeInterface; + +class BinaryWriter extends AbstractWriter +{ + /** + * {@inheritdoc} + */ + public function writeString(QrCodeInterface $qrCode) + { + $string = ' + 0001010101 + 0001010101 + 1000101010 + 0001010101 + 0101010101 + 0001010101 + 0001010101 + 0001010101 + 0001010101 + 1000101010 + '; + + return $string; + } + + /** + * {@inheritdoc} + */ + public static function getContentType() + { + return 'text/plain'; + } + + /** + * {@inheritdoc} + */ + public static function getSupportedExtensions() + { + return ['bin', 'txt']; + } +} diff --git a/vendor/endroid/qrcode/src/Writer/DebugWriter.php b/vendor/endroid/qrcode/src/Writer/DebugWriter.php new file mode 100644 index 0000000..d521b2b --- /dev/null +++ b/vendor/endroid/qrcode/src/Writer/DebugWriter.php @@ -0,0 +1,58 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Writer; + +use Endroid\QrCode\QrCodeInterface; +use ReflectionClass; +use Exception; + +class DebugWriter extends AbstractWriter +{ + /** + * {@inheritdoc} + */ + public function writeString(QrCodeInterface $qrCode) + { + $data = []; + + $reflectionClass = new ReflectionClass($qrCode); + foreach ($reflectionClass->getMethods() as $method) { + $methodName = $method->getShortName(); + if (strpos($methodName, 'get') === 0 && $method->getNumberOfParameters() == 0) { + $value = $qrCode->{$methodName}(); + if (is_array($value) && !is_object(current($value))) { + $value = '['.implode(', ', $value).']'; + } elseif (is_bool($value)) { + $value = $value ? 'true' : 'false'; + } elseif (is_string($value)) { + $value = '"'.$value.'"'; + } elseif (is_null($value)) { + $value = 'null'; + } + try { + $data[] = $methodName . ': ' . $value; + } catch (Exception $exception) { + } + } + } + + $string = implode(" \n", $data); + + return $string; + } + + /** + * {@inheritdoc} + */ + public static function getContentType() + { + return 'text/plain'; + } +} diff --git a/vendor/endroid/qrcode/src/Writer/EpsWriter.php b/vendor/endroid/qrcode/src/Writer/EpsWriter.php new file mode 100644 index 0000000..41dce56 --- /dev/null +++ b/vendor/endroid/qrcode/src/Writer/EpsWriter.php @@ -0,0 +1,97 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Writer; + +use BaconQrCode\Renderer\Image\Eps; +use BaconQrCode\Writer; +use Endroid\QrCode\QrCodeInterface; + +class EpsWriter extends AbstractBaconWriter +{ + /** + * {@inheritdoc} + */ + public function writeString(QrCodeInterface $qrCode) + { + $renderer = new Eps(); + $renderer->setWidth($qrCode->getSize()); + $renderer->setHeight($qrCode->getSize()); + $renderer->setMargin(0); + $renderer->setForegroundColor($this->convertColor($qrCode->getForegroundColor())); + $renderer->setBackgroundColor($this->convertColor($qrCode->getBackgroundColor())); + + $writer = new Writer($renderer); + $string = $writer->writeString($qrCode->getText(), $qrCode->getEncoding(), $this->convertErrorCorrectionLevel($qrCode->getErrorCorrectionLevel())); + + $string = $this->addMargin($string, $qrCode); + + return $string; + } + + /** + * @param string $string + * @param QrCodeInterface $qrCode + * @return string + */ + protected function addMargin($string, QrCodeInterface $qrCode) + { + $targetSize = $qrCode->getSize() + $qrCode->getMargin() * 2; + + $lines = explode("\n", $string); + + $sourceBlockSize = 0; + $additionalWhitespace = $qrCode->getSize(); + foreach ($lines as $line) { + if (preg_match('#[0-9]+ [0-9]+ [0-9]+ [0-9]+ F#i', $line) && strpos($line, $qrCode->getSize().' '.$qrCode->getSize().' F') === false) { + $parts = explode(' ', $line); + $sourceBlockSize = $parts[2]; + $additionalWhitespace = min($additionalWhitespace, $parts[0]); + } + } + + $blockCount = ($qrCode->getSize() - 2 * $additionalWhitespace) / $sourceBlockSize; + $targetBlockSize = $qrCode->getSize() / $blockCount; + + foreach ($lines as &$line) { + if (strpos($line, 'BoundingBox') !== false) { + $line = '%%BoundingBox: 0 0 '.$targetSize.' '.$targetSize; + } elseif (strpos($line, $qrCode->getSize().' '.$qrCode->getSize().' F') !== false) { + $line = '0 0 '.$targetSize.' '.$targetSize.' F'; + } elseif (preg_match('#[0-9]+ [0-9]+ [0-9]+ [0-9]+ F#i', $line)) { + $parts = explode(' ', $line); + $parts[0] = $qrCode->getMargin() + $targetBlockSize * ($parts[0] - $additionalWhitespace) / $sourceBlockSize; + $parts[1] = $qrCode->getMargin() + $targetBlockSize * ($parts[1] - $sourceBlockSize - $additionalWhitespace) / $sourceBlockSize; + $parts[2] = $targetBlockSize; + $parts[3] = $targetBlockSize; + $line = implode(' ', $parts); + } + } + + $string = implode("\n", $lines); + + return $string; + } + + /** + * {@inheritdoc} + */ + public static function getContentType() + { + return 'image/eps'; + } + + /** + * {@inheritdoc} + */ + public static function getSupportedExtensions() + { + return ['eps']; + } +} diff --git a/vendor/endroid/qrcode/src/Writer/PngWriter.php b/vendor/endroid/qrcode/src/Writer/PngWriter.php new file mode 100644 index 0000000..fa0cb41 --- /dev/null +++ b/vendor/endroid/qrcode/src/Writer/PngWriter.php @@ -0,0 +1,224 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Writer; + +use BaconQrCode\Renderer\Image\Png; +use BaconQrCode\Writer; +use Endroid\QrCode\ColorableQrCodeInterface; +use Endroid\QrCode\Exception\MissingFunctionException; +use Endroid\QrCode\Exception\ValidationException; +use Endroid\QrCode\LabelAlignment; +use Endroid\QrCode\QrCodeInterface; +use Endroid\QrCode\ValidateableQrCodeInterface; +use QrReader; + +class PngWriter extends AbstractBaconWriter +{ + /** + * {@inheritdoc} + */ + public function writeString(QrCodeInterface $qrCode) + { + $renderer = new Png(); + $renderer->setWidth($qrCode->getSize()); + $renderer->setHeight($qrCode->getSize()); + $renderer->setMargin(0); + $renderer->setForegroundColor($this->convertColor($qrCode->getForegroundColor())); + $renderer->setBackgroundColor($this->convertColor($qrCode->getBackgroundColor())); + + $writer = new Writer($renderer); + $string = $writer->writeString($qrCode->getText(), $qrCode->getEncoding(), $this->convertErrorCorrectionLevel($qrCode->getErrorCorrectionLevel())); + + $image = imagecreatefromstring($string); + $image = $this->addMargin($image, $qrCode->getMargin(), $qrCode->getSize(), $qrCode->getForegroundColor(), $qrCode->getBackgroundColor()); + + if ($qrCode->getLogoPath()) { + $image = $this->addLogo($image, $qrCode->getLogoPath(), $qrCode->getLogoWidth()); + } + + if ($qrCode->getLabel()) { + $image = $this->addLabel($image, $qrCode->getLabel(), $qrCode->getLabelFontPath(), $qrCode->getLabelFontSize(), $qrCode->getLabelAlignment(), $qrCode->getLabelMargin(), $qrCode->getForegroundColor(), $qrCode->getBackgroundColor()); + } + + $string = $this->imageToString($image); + + if ($qrCode->getValidateResult()) { + $reader = new QrReader($string, QrReader::SOURCE_TYPE_BLOB); + if ($reader->text() !== $qrCode->getText()) { + throw new ValidationException( + 'Built-in validation reader read "'.$reader->text().'" instead of "'.$qrCode->getText().'". + Adjust your parameters to increase readability or disable built-in validation.'); + } + } + + return $string; + } + + /** + * @param resource $sourceImage + * @param int $margin + * @param int $size + * @param int[] $foregroundColor + * @param int[] $backgroundColor + * @return resource + */ + protected function addMargin($sourceImage, $margin, $size, array $foregroundColor, array $backgroundColor) + { + $additionalWhitespace = $this->calculateAdditionalWhiteSpace($sourceImage, $foregroundColor); + + if ($additionalWhitespace == 0 && $margin == 0) { + return $sourceImage; + } + + $targetImage = imagecreatetruecolor($size + $margin * 2, $size + $margin * 2); + $backgroundColor = imagecolorallocate($targetImage, $backgroundColor['r'], $backgroundColor['g'], $backgroundColor['b']); + imagefill($targetImage, 0, 0, $backgroundColor); + imagecopyresampled($targetImage, $sourceImage, $margin, $margin, $additionalWhitespace, $additionalWhitespace, $size, $size, $size - 2 * $additionalWhitespace, $size - 2 * $additionalWhitespace); + + return $targetImage; + } + + /** + * @param resource $image + * @param int[] $foregroundColor + * @return int + */ + protected function calculateAdditionalWhiteSpace($image, array $foregroundColor) + { + $width = imagesx($image); + $height = imagesy($image); + + $foregroundColor = imagecolorallocate($image, $foregroundColor['r'], $foregroundColor['g'], $foregroundColor['b']); + + $whitespace = $width; + for ($y = 0; $y < $height; $y++) { + for ($x = 0; $x < $width; $x++) { + $color = imagecolorat($image, $x, $y); + if ($color == $foregroundColor || $x == $whitespace) { + $whitespace = min($whitespace, $x); + break; + } + } + } + + return $whitespace; + } + + /** + * @param resource $sourceImage + * @param string $logoPath + * @param int $logoWidth + * @return resource + */ + protected function addLogo($sourceImage, $logoPath, $logoWidth = null) + { + $logoImage = imagecreatefromstring(file_get_contents($logoPath)); + $logoSourceWidth = imagesx($logoImage); + $logoSourceHeight = imagesy($logoImage); + $logoTargetWidth = $logoWidth; + + if ($logoTargetWidth === null) { + $logoTargetWidth = $logoSourceWidth; + $logoTargetHeight = $logoSourceHeight; + } else { + $scale = $logoTargetWidth / $logoSourceWidth; + $logoTargetHeight = intval($scale * imagesy($logoImage)); + } + + $logoX = imagesx($sourceImage) / 2 - $logoTargetWidth / 2; + $logoY = imagesy($sourceImage) / 2 - $logoTargetHeight / 2; + imagecopyresampled($sourceImage, $logoImage, $logoX, $logoY, 0, 0, $logoTargetWidth, $logoTargetHeight, $logoSourceWidth, $logoSourceHeight); + + return $sourceImage; + } + + /** + * @param resource $sourceImage + * @param string $label + * @param string $labelFontPath + * @param int $labelFontSize + * @param string $labelAlignment + * @param int[] $labelMargin + * @param int[] $foregroundColor + * @param int[] $backgroundColor + * @return resource + * @throws MissingFunctionException + */ + protected function addLabel($sourceImage, $label, $labelFontPath, $labelFontSize, $labelAlignment, $labelMargin, array $foregroundColor, array $backgroundColor) + { + if (!function_exists('imagettfbbox')) { + throw new MissingFunctionException('Missing function "imagettfbbox". Did you install the FreeType library?'); + } + + $labelBox = imagettfbbox($labelFontSize, 0, $labelFontPath, $label); + $labelBoxWidth = intval($labelBox[2] - $labelBox[0]); + $labelBoxHeight = intval($labelBox[0] - $labelBox[7]); + + $sourceWidth = imagesx($sourceImage); + $sourceHeight = imagesy($sourceImage); + $targetWidth = $sourceWidth; + $targetHeight = $sourceHeight + $labelBoxHeight + $labelMargin['t'] + $labelMargin['b']; + + // Create empty target image + $targetImage = imagecreatetruecolor($targetWidth, $targetHeight); + $foregroundColor = imagecolorallocate($targetImage, $foregroundColor['r'], $foregroundColor['g'], $foregroundColor['b']); + $backgroundColor = imagecolorallocate($targetImage, $backgroundColor['r'], $backgroundColor['g'], $backgroundColor['b']); + imagefill($targetImage, 0, 0, $backgroundColor); + + // Copy source image to target image + imagecopyresampled($targetImage, $sourceImage, 0, 0, 0, 0, $sourceWidth, $sourceHeight, $sourceWidth, $sourceHeight); + + switch ($labelAlignment) { + case LabelAlignment::LEFT: + $labelX = $labelMargin['l']; + break; + case LabelAlignment::RIGHT: + $labelX = $targetWidth - $labelBoxWidth - $labelMargin['r']; + break; + default: + $labelX = intval($targetWidth / 2 - $labelBoxWidth / 2); + break; + } + + $labelY = $targetHeight - $labelMargin['b']; + imagettftext($targetImage, $labelFontSize, 0, $labelX, $labelY, $foregroundColor, $labelFontPath, $label); + + return $targetImage; + } + + /** + * @param resource $image + * @return string + */ + protected function imageToString($image) + { + ob_start(); + imagepng($image); + $string = ob_get_clean(); + + return $string; + } + + /** + * {@inheritdoc} + */ + public static function getContentType() + { + return 'image/png'; + } + + /** + * {@inheritdoc} + */ + public static function getSupportedExtensions() + { + return ['png']; + } +} diff --git a/vendor/endroid/qrcode/src/Writer/SvgWriter.php b/vendor/endroid/qrcode/src/Writer/SvgWriter.php new file mode 100644 index 0000000..d6b3ff5 --- /dev/null +++ b/vendor/endroid/qrcode/src/Writer/SvgWriter.php @@ -0,0 +1,91 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Writer; + +use BaconQrCode\Renderer\Image\Svg; +use BaconQrCode\Writer; +use Endroid\QrCode\QrCodeInterface; +use SimpleXMLElement; + +class SvgWriter extends AbstractBaconWriter +{ + /** + * {@inheritdoc} + */ + public function writeString(QrCodeInterface $qrCode) + { + $renderer = new Svg(); + $renderer->setWidth($qrCode->getSize()); + $renderer->setHeight($qrCode->getSize()); + $renderer->setMargin(0); + $renderer->setForegroundColor($this->convertColor($qrCode->getForegroundColor())); + $renderer->setBackgroundColor($this->convertColor($qrCode->getBackgroundColor())); + + $writer = new Writer($renderer); + $string = $writer->writeString($qrCode->getText(), $qrCode->getEncoding(), $this->convertErrorCorrectionLevel($qrCode->getErrorCorrectionLevel())); + + $string = $this->addMargin($string, $qrCode->getMargin(), $qrCode->getSize()); + + return $string; + } + + /** + * @param string $string + * @param int $margin + * @param int $size + * @return string + */ + protected function addMargin($string, $margin, $size) + { + $targetSize = $size + $margin * 2; + + $xml = new SimpleXMLElement($string); + $xml['width'] = $targetSize; + $xml['height'] = $targetSize; + $xml['viewBox'] = '0 0 '.$targetSize.' '.$targetSize; + $xml->rect['width'] = $targetSize; + $xml->rect['height'] = $targetSize; + + $additionalWhitespace = $targetSize; + foreach ($xml->use as $block) { + $additionalWhitespace = min($additionalWhitespace, (int) $block['x']); + } + + $sourceBlockSize = (int) $xml->defs->rect['width']; + $blockCount = ($size - 2 * $additionalWhitespace) / $sourceBlockSize; + $targetBlockSize = $size / $blockCount; + + $xml->defs->rect['width'] = $targetBlockSize; + $xml->defs->rect['height'] = $targetBlockSize; + + foreach ($xml->use as $block) { + $block['x'] = $margin + $targetBlockSize * ($block['x'] - $additionalWhitespace) / $sourceBlockSize; + $block['y'] = $margin + $targetBlockSize * ($block['y'] - $additionalWhitespace) / $sourceBlockSize; + } + + return $xml->asXML(); + } + + /** + * {@inheritdoc} + */ + public static function getContentType() + { + return 'image/svg+xml'; + } + + /** + * {@inheritdoc} + */ + public static function getSupportedExtensions() + { + return ['svg']; + } +} diff --git a/vendor/endroid/qrcode/src/Writer/WriterInterface.php b/vendor/endroid/qrcode/src/Writer/WriterInterface.php new file mode 100644 index 0000000..2f9e498 --- /dev/null +++ b/vendor/endroid/qrcode/src/Writer/WriterInterface.php @@ -0,0 +1,54 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Writer; + +use Endroid\QrCode\QrCodeInterface; + +interface WriterInterface +{ + /** + * @param QrCodeInterface $qrCode + * @return string + */ + public function writeString(QrCodeInterface $qrCode); + + /** + * @param QrCodeInterface $qrCode + * @return string + */ + public function writeDataUri(QrCodeInterface $qrCode); + + /** + * @param QrCodeInterface $qrCode + * @param string $path + */ + public function writeFile(QrCodeInterface $qrCode, $path); + + /** + * @return string + */ + public static function getContentType(); + + /** + * @param string $extension + * @return bool + */ + public static function supportsExtension($extension); + + /** + * @return string[] + */ + public static function getSupportedExtensions(); + + /** + * @return string + */ + public function getName(); +} diff --git a/vendor/endroid/qrcode/src/WriterRegistry.php b/vendor/endroid/qrcode/src/WriterRegistry.php new file mode 100644 index 0000000..032c92f --- /dev/null +++ b/vendor/endroid/qrcode/src/WriterRegistry.php @@ -0,0 +1,86 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode; + +use Endroid\QrCode\Exception\InvalidWriterException; +use Endroid\QrCode\Writer\WriterInterface; + +class WriterRegistry implements WriterRegistryInterface +{ + /** + * @var WriterInterface[] + */ + protected $writers; + + /** + * @var WriterInterface + */ + protected $defaultWriter; + + public function __construct() + { + $this->writers = []; + } + + /** + * {@inheritdoc} + */ + public function addWriter(WriterInterface $writer, $setAsDefault = false) + { + $this->writers[$writer->getName()] = $writer; + + if ($setAsDefault || count($this->writers) === 1) { + $this->defaultWriter = $writer; + } + } + + /** + * @param $name + * @return WriterInterface + */ + public function getWriter($name) + { + $this->assertValidWriter($name); + + return $this->writers[$name]; + } + + /** + * @return WriterInterface + * @throws InvalidWriterException + */ + public function getDefaultWriter() + { + if ($this->defaultWriter instanceof WriterInterface) { + return $this->defaultWriter; + } + + throw new InvalidWriterException('Please set the default writer via the second argument of addWriter'); + } + + /** + * @return WriterInterface[] + */ + public function getWriters() + { + return $this->writers; + } + + /** + * @param string $writer + * @throws InvalidWriterException + */ + protected function assertValidWriter($writer) + { + if (!isset($this->writers[$writer])) { + throw new InvalidWriterException('Invalid writer "'.$writer.'"'); + } + } +} diff --git a/vendor/endroid/qrcode/src/WriterRegistryInterface.php b/vendor/endroid/qrcode/src/WriterRegistryInterface.php new file mode 100644 index 0000000..87cca6a --- /dev/null +++ b/vendor/endroid/qrcode/src/WriterRegistryInterface.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode; + +use Endroid\QrCode\Writer\WriterInterface; + +interface WriterRegistryInterface +{ + /** + * @param WriterInterface $writer + * @return $this + */ + public function addWriter(WriterInterface $writer); + + /** + * @param $name + * @return WriterInterface + */ + public function getWriter($name); + + /** + * @return WriterInterface[] + */ + public function getWriters(); +} diff --git a/vendor/endroid/qrcode/tests/Bundle/Controller/QrCodeControllerTest.php b/vendor/endroid/qrcode/tests/Bundle/Controller/QrCodeControllerTest.php new file mode 100644 index 0000000..cd3fe8f --- /dev/null +++ b/vendor/endroid/qrcode/tests/Bundle/Controller/QrCodeControllerTest.php @@ -0,0 +1,45 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Tests\Bundle\Controller; + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\HttpFoundation\Response; + +class QrCodeControllerTest extends WebTestCase +{ + public function testGenerateAction() + { + $client = static::createClient(); + $client->request('GET', $client->getContainer()->get('router')->generate('endroid_qrcode_generate', [ + 'text' => 'Life is too short to be generating QR codes', + 'extension' => 'png', + 'size' => 200, + 'margin' => 10, + 'label' => 'Scan the code', + 'label_font_size' => 16, + ])); + + $response = $client->getResponse(); + $image = imagecreatefromstring($response->getContent()); + + $this->assertTrue(imagesx($image) == 220); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + public function testTwigFunctionsAction() + { + $client = static::createClient(); + $client->request('GET', $client->getContainer()->get('router')->generate('endroid_qrcode_twig_functions')); + + $response = $client->getResponse(); + + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } +} diff --git a/vendor/endroid/qrcode/tests/Bundle/EndroidQrCodeBundleTest.php b/vendor/endroid/qrcode/tests/Bundle/EndroidQrCodeBundleTest.php new file mode 100644 index 0000000..f0a1544 --- /dev/null +++ b/vendor/endroid/qrcode/tests/Bundle/EndroidQrCodeBundleTest.php @@ -0,0 +1,20 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Tests\Bundle; + +use PHPUnit\Framework\TestCase; + +class EndroidQrCodeBundleTest extends TestCase +{ + public function testNoTestsYet() + { + $this->assertTrue(true); + } +} diff --git a/vendor/endroid/qrcode/tests/Bundle/app/.gitignore b/vendor/endroid/qrcode/tests/Bundle/app/.gitignore new file mode 100644 index 0000000..6dd26c5 --- /dev/null +++ b/vendor/endroid/qrcode/tests/Bundle/app/.gitignore @@ -0,0 +1,2 @@ +/cache +/logs diff --git a/vendor/endroid/qrcode/tests/Bundle/app/AppKernel.php b/vendor/endroid/qrcode/tests/Bundle/app/AppKernel.php new file mode 100644 index 0000000..2722c2b --- /dev/null +++ b/vendor/endroid/qrcode/tests/Bundle/app/AppKernel.php @@ -0,0 +1,37 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\HttpKernel\Kernel; + +class AppKernel extends Kernel +{ + /** + * {@inheritdoc} + */ + public function registerBundles() + { + $bundles = [ + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\TwigBundle\TwigBundle(), + new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), + new Endroid\QrCode\Bundle\EndroidQrCodeBundle(), + ]; + + return $bundles; + } + + /** + * {@inheritdoc} + */ + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(__DIR__.'/config/config.yml'); + } +} diff --git a/vendor/endroid/qrcode/tests/Bundle/app/bootstrap.php b/vendor/endroid/qrcode/tests/Bundle/app/bootstrap.php new file mode 100644 index 0000000..ff87382 --- /dev/null +++ b/vendor/endroid/qrcode/tests/Bundle/app/bootstrap.php @@ -0,0 +1,7 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Endroid\QrCode\Tests; + +use Endroid\QrCode\Factory\QrCodeFactory; +use Endroid\QrCode\QrCode; +use Endroid\QrCode\Writer\BinaryWriter; +use Endroid\QrCode\Writer\DebugWriter; +use Endroid\QrCode\Writer\EpsWriter; +use Endroid\QrCode\Writer\PngWriter; +use Endroid\QrCode\Writer\SvgWriter; +use PHPUnit\Framework\TestCase; + +class QrCodeTest extends TestCase +{ + public function testReadable() + { + $messages = [ + 'Tiny', + 'This one has spaces', + 'd2llMS9uU01BVmlvalM2YU9BUFBPTTdQMmJabHpqdndt', + 'http://this.is.an/url?with=query&string=attached', + '11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111', + '{"i":"serialized.data","v":1,"t":1,"d":"4AEPc9XuIQ0OjsZoSRWp9DRWlN6UyDvuMlyOYy8XjOw="}', + 'Spëci&al ch@ract3rs', + '有限公司' + ]; + + foreach ($messages as $message) { + $qrCode = new QrCode($message); + $qrCode->setSize(300); + $qrCode->setValidateResult(true); + $pngData = $qrCode->writeString(); + $this->assertTrue(is_string($pngData)); + } + } + + public function testFactory() + { + $qrCodeFactory = new QrCodeFactory(); + $qrCode = $qrCodeFactory->create('QR Code', [ + 'writer' => 'png', + 'size' => 300, + 'margin' => 10 + ]); + + $pngData = $qrCode->writeString(); + $this->assertTrue(is_string($pngData)); + } + + public function testWriteQrCode() + { + $qrCode = new QrCode('QrCode'); + + $qrCode->setWriterByName('binary'); + $binData = $qrCode->writeString(); + $this->assertTrue(is_string($binData)); + + $qrCode->setWriterByName('debug'); + $debugData = $qrCode->writeString(); + $this->assertTrue(is_string($debugData)); + + $qrCode->setWriterByName('eps'); + $epsData = $qrCode->writeString(); + $this->assertTrue(is_string($epsData)); + + $qrCode->setWriterByName('png'); + $pngData = $qrCode->writeString(); + $this->assertTrue(is_string($pngData)); + $pngDataUriData = $qrCode->writeDataUri(); + $this->assertTrue(strpos($pngDataUriData, 'data:image/png;base64') === 0); + + $qrCode->setWriterByName('svg'); + $svgData = $qrCode->writeString(); + $this->assertTrue(is_string($svgData)); + $svgDataUriData = $qrCode->writeDataUri(); + $this->assertTrue(strpos($svgDataUriData, 'data:image/svg+xml;base64') === 0); + } + + public function testSetSize() + { + $size = 400; + $margin = 10; + + $qrCode = new QrCode('QrCode'); + $qrCode->setSize($size); + $qrCode->setMargin($margin); + + $pngData = $qrCode->writeString(); + $image = imagecreatefromstring($pngData); + + $this->assertTrue(imagesx($image) === $size + 2 * $margin); + $this->assertTrue(imagesy($image) === $size + 2 * $margin); + } + + public function testSetLabel() + { + $qrCode = new QrCode('QrCode'); + $qrCode + ->setSize(300) + ->setLabel('Scan the code', 15) + ; + + $pngData = $qrCode->writeString(); + $this->assertTrue(is_string($pngData)); + } + + public function testSetLogo() + { + $qrCode = new QrCode('QrCode'); + $qrCode + ->setSize(400) + ->setLogoPath(__DIR__.'/../assets/symfony.png') + ->setLogoWidth(150) + ->setValidateResult(true); + ; + + $pngData = $qrCode->writeString(); + $this->assertTrue(is_string($pngData)); + } +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/.gitignore b/vendor/khanamiryan/qrcode-detector-decoder/.gitignore new file mode 100644 index 0000000..ff72e2d --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/.gitignore @@ -0,0 +1,2 @@ +/composer.lock +/vendor diff --git a/vendor/khanamiryan/qrcode-detector-decoder/README.md b/vendor/khanamiryan/qrcode-detector-decoder/README.md new file mode 100644 index 0000000..f4eda03 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/README.md @@ -0,0 +1,37 @@ +# QR code decoder / reader for PHP +This is a PHP library to detect and decode QR-codes.
    This is first and only QR code reader that works without extensions.
    +Ported from [ZXing library](https://github.com/zxing/zxing) + + +## Installation +The recommended method of installing this library is via [Composer](https://getcomposer.org/). + +Run the following command from your project root: + +```bash +$ composer require khanamiryan/qrcode-detector-decoder +``` + + +## Usage +```php +require __DIR__ . "/vendor/autoload.php"; +$qrcode = new QrReader('path/to_image'); +$text = $qrcode->text(); //return decoded text from QR Code +``` + +## Requirements +* PHP >= 5.6 +* GD Library + + +## Contributing + +You can help the project by adding features, cleaning the code, adding composer and other. + + +1. Fork it +2. Create your feature branch: `git checkout -b my-new-feature` +3. Commit your changes: `git commit -am 'Add some feature'` +4. Push to the branch: `git push origin my-new-feature` +5. Submit a pull request diff --git a/vendor/khanamiryan/qrcode-detector-decoder/composer.json b/vendor/khanamiryan/qrcode-detector-decoder/composer.json new file mode 100644 index 0000000..53267b8 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/composer.json @@ -0,0 +1,24 @@ +{ + "name": "khanamiryan/qrcode-detector-decoder", + "type": "library", + "description": "QR code decoder / reader", + "keywords": ["qr", "zxing", "barcode"], + "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder", + "license": "MIT", + "authors": [{ + "name": "Ashot Khanamiryan", + "email": "a.khanamiryan@gmail.com", + "homepage": "https://github.com/khanamiryan", + "role": "Developer" + }], + "require": { + "php": "^5.6|^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7" + }, + "autoload": { + "classmap": ["lib/"], + "files": ["lib/common/customFunctions.php"] + } +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/Binarizer.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/Binarizer.php new file mode 100644 index 0000000..462113b --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/Binarizer.php @@ -0,0 +1,89 @@ +source = $source; + } + + public final function getLuminanceSource() { + return $this->source; + } + + /** + * Converts one row of luminance data to 1 bit data. May actually do the conversion, or return + * cached data. Callers should assume this method is expensive and call it as seldom as possible. + * This method is intended for decoding 1D barcodes and may choose to apply sharpening. + * For callers which only examine one row of pixels at a time, the same BitArray should be reused + * and passed in with each call for performance. However it is legal to keep more than one row + * at a time if needed. + * + * @param y The row to fetch, which must be in [0, bitmap height) + * @param row An optional preallocated array. If null or too small, it will be ignored. + * If used, the Binarizer will call BitArray.clear(). Always use the returned object. + * @return The array of bits for this row (true means black). + * @throws NotFoundException if row can't be binarized + */ + public abstract function getBlackRow($y, $row); + + /** + * Converts a 2D array of luminance data to 1 bit data. As above, assume this method is expensive + * and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or + * may not apply sharpening. Therefore, a row from this matrix may not be identical to one + * fetched using getBlackRow(), so don't mix and match between them. + * + * @return The 2D array of bits for the image (true means black). + * @throws NotFoundException if image can't be binarized to make a matrix + */ + public abstract function getBlackMatrix(); + + /** + * Creates a new object with the same type as this Binarizer implementation, but with pristine + * state. This is needed because Binarizer implementations may be stateful, e.g. keeping a cache + * of 1 bit data. See Effective Java for why we can't use Java's clone() method. + * + * @param source The LuminanceSource this Binarizer will operate on. + * @return A new concrete Binarizer implementation object. + */ + public abstract function createBinarizer($source); + + public final function getWidth() { + return $this->source->getWidth(); + } + + public final function getHeight() { + return $this->source->getHeight(); + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/BinaryBitmap.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/BinaryBitmap.php new file mode 100644 index 0000000..5b19b43 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/BinaryBitmap.php @@ -0,0 +1,152 @@ +binarizer = $binarizer; + } + + /** + * @return The width of the bitmap. + */ + public function getWidth() { + return $this->binarizer->getWidth(); + } + + /** + * @return The height of the bitmap. + */ + public function getHeight() { + return $this->binarizer->getHeight(); + } + + /** + * Converts one row of luminance data to 1 bit data. May actually do the conversion, or return + * cached data. Callers should assume this method is expensive and call it as seldom as possible. + * This method is intended for decoding 1D barcodes and may choose to apply sharpening. + * + * @param y The row to fetch, which must be in [0, bitmap height) + * @param row An optional preallocated array. If null or too small, it will be ignored. + * If used, the Binarizer will call BitArray.clear(). Always use the returned object. + * @return The array of bits for this row (true means black). + * @throws NotFoundException if row can't be binarized + */ + public function getBlackRow($y, $row) { + return $this->binarizer->getBlackRow($y, $row); + } + + /** + * Converts a 2D array of luminance data to 1 bit. As above, assume this method is expensive + * and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or + * may not apply sharpening. Therefore, a row from this matrix may not be identical to one + * fetched using getBlackRow(), so don't mix and match between them. + * + * @return The 2D array of bits for the image (true means black). + * @throws NotFoundException if image can't be binarized to make a matrix + */ + public function getBlackMatrix(){ +// The matrix is created on demand the first time it is requested, then cached. There are two +// reasons for this: +// 1. This work will never be done if the caller only installs 1D Reader objects, or if a +// 1D Reader finds a barcode before the 2D Readers run. +// 2. This work will only be done once even if the caller installs multiple 2D Readers. + if ($this->matrix == null) { + $this->matrix = $this->binarizer->getBlackMatrix(); + } + return $this->matrix; + } + + /** + * @return Whether this bitmap can be cropped. + */ + public function isCropSupported() { + return $this->binarizer->getLuminanceSource()->isCropSupported(); + } + + /** + * Returns a new object with cropped image data. Implementations may keep a reference to the + * original data rather than a copy. Only callable if isCropSupported() is true. + * + * @param left The left coordinate, which must be in [0,getWidth()) + * @param top The top coordinate, which must be in [0,getHeight()) + * @param width The width of the rectangle to crop. + * @param height The height of the rectangle to crop. + * @return A cropped version of this object. + */ + public function crop($left, $top, $width, $height) { + $newSource = $this->binarizer->getLuminanceSource()->crop($left, $top, $width, $height); + return new BinaryBitmap($this->binarizer->createBinarizer($newSource)); + } + + /** + * @return Whether this bitmap supports counter-clockwise rotation. + */ + public function isRotateSupported() { + return $this->binarizer->getLuminanceSource()->isRotateSupported(); + } + + /** + * Returns a new object with rotated image data by 90 degrees counterclockwise. + * Only callable if {@link #isRotateSupported()} is true. + * + * @return A rotated version of this object. + */ + public function rotateCounterClockwise() { + $newSource = $this->binarizer->getLuminanceSource()->rotateCounterClockwise(); + return new BinaryBitmap($this->binarizer->createBinarizer($newSource)); + } + + /** + * Returns a new object with rotated image data by 45 degrees counterclockwise. + * Only callable if {@link #isRotateSupported()} is true. + * + * @return A rotated version of this object. + */ + public function rotateCounterClockwise45() { + $newSource = $this->binarizer->getLuminanceSource()->rotateCounterClockwise45(); + return new BinaryBitmap($this->binarizer->createBinarizer($newSource)); + } + +//@Override + public function toString() { + try { + return $this->getBlackMatrix()->toString(); + } catch (NotFoundException $e) { + return ""; + } + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/ChecksumException.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/ChecksumException.php new file mode 100644 index 0000000..72446e1 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/ChecksumException.php @@ -0,0 +1,44 @@ +GDLuminanceSource($gdImage,$dataWidth,$dataHeight); + return; + } + parent::__construct($width, $height); + if ($left + $width > $dataWidth || $top + $height > $dataHeight) { + throw new \InvalidArgumentException("Crop rectangle does not fit within image data."); + } + $this->luminances = $gdImage; + $this->dataWidth = $dataWidth; + $this->dataHeight = $dataHeight; + $this->left = $left; + $this->top = $top; + } + + public function GDLuminanceSource($gdImage, $width, $height) + { + parent::__construct($width, $height); + + $this->dataWidth = $width; + $this->dataHeight = $height; + $this->left = 0; + $this->top = 0; + $this->$gdImage = $gdImage; + + +// In order to measure pure decoding speed, we convert the entire image to a greyscale array +// up front, which is the same as the Y channel of the YUVLuminanceSource in the real app. + $this->luminances = array(); + //$this->luminances = $this->grayScaleToBitmap($this->grayscale()); + + $array = array(); + $rgb = array(); + +for($j=0;$j<$height;$j++){ + for($i=0;$i<$width;$i++){ + $argb = imagecolorat($this->$gdImage, $i, $j); + $pixel = imagecolorsforindex($this->$gdImage, $argb); + $r = $pixel['red']; + $g = $pixel['green']; + $b = $pixel['blue']; + if ($r == $g && $g == $b) { +// Image is already greyscale, so pick any channel. + + $this->luminances[] = $r;//(($r + 128) % 256) - 128; + } else { +// Calculate luminance cheaply, favoring green. + $this->luminances[] = ($r+2*$g+$b)/4;//(((($r + 2 * $g + $b) / 4) + 128) % 256) - 128; + } + } +} + + + + /* + + for ($y = 0; $y < $height; $y++) { + $offset = $y * $width; + for ($x = 0; $x < $width; $x++) { + $pixel = $pixels[$offset + $x]; + $r = ($pixel >> 16) & 0xff; + $g = ($pixel >> 8) & 0xff; + $b = $pixel & 0xff; + if ($r == $g && $g == $b) { +// Image is already greyscale, so pick any channel. + + $this->luminances[intval($offset + $x)] = (($r+128) % 256) - 128; + } else { +// Calculate luminance cheaply, favoring green. + $this->luminances[intval($offset + $x)] = (((($r + 2 * $g + $b) / 4)+128)%256) - 128; + } + + + + } + */ + //} + // $this->luminances = $this->grayScaleToBitmap($this->luminances); + + } + +//@Override + public function getRow($y, $row=null) { + if ($y < 0 || $y >= $this->getHeight()) { + throw new \InvalidArgumentException("Requested row is outside the image: " + y); + } + $width = $this->getWidth(); + if ($row == null || count($row) < $width) { + $row = array(); + } + $offset = ($y + $this->top) * $this->dataWidth + $this->left; + $row = arraycopy($this->luminances,$offset, $row, 0, $width); + return $row; + } + +//@Override + public function getMatrix() { + $width = $this->getWidth(); + $height = $this->getHeight(); + +// If the caller asks for the entire underlying image, save the copy and give them the +// original data. The docs specifically warn that result.length must be ignored. + if ($width == $this->dataWidth && $height == $this->dataHeight) { + return $this->luminances; + } + + $area = $width * $height; + $matrix = array(); + $inputOffset = $this->top * $this->dataWidth + $this->left; + +// If the width matches the full width of the underlying data, perform a single copy. + if ($width == $this->dataWidth) { + $matrix = arraycopy($this->luminances, $inputOffset, $matrix, 0, $area); + return $matrix; + } + +// Otherwise copy one cropped row at a time. + $rgb = $this->luminances; + for ($y = 0; $y < $height; $y++) { + $outputOffset = $y * $width; + $matrix = arraycopy($rgb, $inputOffset, $matrix, $outputOffset, $width); + $inputOffset += $this->dataWidth; + } + return $matrix; + } + +//@Override + public function isCropSupported() { + return true; + } + +//@Override + public function crop($left, $top, $width, $height) { + return new GDLuminanceSource($this->luminances, + $this->dataWidth, + $this->dataHeight, + $this->left + $left, + $this->top + $top, + $width, + $height); + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/IMagickLuminanceSource.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/IMagickLuminanceSource.php new file mode 100644 index 0000000..3f0a26e --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/IMagickLuminanceSource.php @@ -0,0 +1,149 @@ +_IMagickLuminanceSource($image,$dataWidth,$dataHeight); + return; + } + parent::__construct($width, $height); + if ($left + $width > $dataWidth || $top + $height > $dataHeight) { + throw new \InvalidArgumentException("Crop rectangle does not fit within image data."); + } + $this->luminances = $image; + $this->dataWidth = $dataWidth; + $this->dataHeight = $dataHeight; + $this->left = $left; + $this->top = $top; + } + + public function _IMagickLuminanceSource($image, $width, $height) + { + parent::__construct($width, $height); + + $this->dataWidth = $width; + $this->dataHeight = $height; + $this->left = 0; + $this->top = 0; + $this->image = $image; + + +// In order to measure pure decoding speed, we convert the entire image to a greyscale array +// up front, which is the same as the Y channel of the YUVLuminanceSource in the real app. + $this->luminances = array(); + + $image->setImageColorspace (\Imagick::COLORSPACE_GRAY); + // $image->newPseudoImage(0, 0, "magick:rose"); + $pixels = $image->exportImagePixels(1, 1, $width, $height, "RGB", \Imagick::COLORSPACE_RGB); + + $array = array(); + $rgb = array(); + + + for($i=0;$iluminances[] = $r;//(($r + 128) % 256) - 128; + } else { +// Calculate luminance cheaply, favoring green. + $this->luminances[] = ($r+2*$g+$b)/4;//(((($r + 2 * $g + $b) / 4) + 128) % 256) - 128; + } + } + + + + } + +//@Override + public function getRow($y, $row=null) { + if ($y < 0 || $y >= $this->getHeight()) { + throw new \InvalidArgumentException("Requested row is outside the image: " + y); + } + $width = $this->getWidth(); + if ($row == null || count($row) < $width) { + $row = array(); + } + $offset = ($y + $this->top) * $this->dataWidth + $this->left; + $row = arraycopy($this->luminances,$offset, $row, 0, $width); + return $row; + } + +//@Override + public function getMatrix() { + $width = $this->getWidth(); + $height = $this->getHeight(); + +// If the caller asks for the entire underlying image, save the copy and give them the +// original data. The docs specifically warn that result.length must be ignored. + if ($width == $this->dataWidth && $height == $this->dataHeight) { + return $this->luminances; + } + + $area = $width * $height; + $matrix = array(); + $inputOffset = $this->top * $this->dataWidth + $this->left; + +// If the width matches the full width of the underlying data, perform a single copy. + if ($width == $this->dataWidth) { + $matrix = arraycopy($this->luminances, $inputOffset, $matrix, 0, $area); + return $matrix; + } + +// Otherwise copy one cropped row at a time. + $rgb = $this->luminances; + for ($y = 0; $y < $height; $y++) { + $outputOffset = $y * $width; + $matrix = arraycopy($rgb, $inputOffset, $matrix, $outputOffset, $width); + $inputOffset += $this->dataWidth; + } + return $matrix; + } + +//@Override + public function isCropSupported() { + return true; + } + +//@Override + public function crop($left, $top, $width, $height) { + return new GDLuminanceSource($this->luminances, + $this->dataWidth, + $this->dataHeight, + $this->left + $left, + $this->top + $top, + $width, + $height); + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/LuminanceSource.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/LuminanceSource.php new file mode 100644 index 0000000..ebe99c7 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/LuminanceSource.php @@ -0,0 +1,159 @@ +width = $width; + $this->height = $height; + } + + /** + * Fetches one row of luminance data from the underlying platform's bitmap. Values range from + * 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have + * to bitwise and with 0xff for each value. It is preferable for implementations of this method + * to only fetch this row rather than the whole image, since no 2D Readers may be installed and + * getMatrix() may never be called. + * + * @param $y; The row to fetch, which must be in [0,getHeight()) + * @param $row; An optional preallocated array. If null or too small, it will be ignored. + * Always use the returned object, and ignore the .length of the array. + * @return array + * An array containing the luminance data. + */ + public abstract function getRow($y, $row); + + /** + * Fetches luminance data for the underlying bitmap. Values should be fetched using: + * {@code int luminance = array[y * width + x] & 0xff} + * + * @return A row-major 2D array of luminance values. Do not use result.length as it may be + * larger than width * height bytes on some platforms. Do not modify the contents + * of the result. + */ + public abstract function getMatrix(); + + /** + * @return The width of the bitmap. + */ + public final function getWidth() { + return $this->width; + } + + /** + * @return The height of the bitmap. + */ + public final function getHeight() { + return $this->height; + } + + /** + * @return Whether this subclass supports cropping. + */ + public function isCropSupported() { + return false; + } + + /** + * Returns a new object with cropped image data. Implementations may keep a reference to the + * original data rather than a copy. Only callable if isCropSupported() is true. + * + * @param left The left coordinate, which must be in [0,getWidth()) + * @param top The top coordinate, which must be in [0,getHeight()) + * @param width The width of the rectangle to crop. + * @param height The height of the rectangle to crop. + * @return A cropped version of this object. + */ + public function crop($left, $top, $width, $height) { + throw new \Exception("This luminance source does not support cropping."); + } + + /** + * @return Whether this subclass supports counter-clockwise rotation. + */ + public function isRotateSupported() { + return false; + } + + /** + * @return a wrapper of this {@code LuminanceSource} which inverts the luminances it returns -- black becomes + * white and vice versa, and each value becomes (255-value). + */ + public function invert() { + return new InvertedLuminanceSource($this); + } + + /** + * Returns a new object with rotated image data by 90 degrees counterclockwise. + * Only callable if {@link #isRotateSupported()} is true. + * + * @return A rotated version of this object. + */ + public function rotateCounterClockwise() { + throw new \Exception("This luminance source does not support rotation by 90 degrees."); + } + + /** + * Returns a new object with rotated image data by 45 degrees counterclockwise. + * Only callable if {@link #isRotateSupported()} is true. + * + * @return A rotated version of this object. + */ + public function rotateCounterClockwise45() { + throw new \Exception("This luminance source does not support rotation by 45 degrees."); + } + +//@Override + public final function toString() { + $row = array(); + $result = ''; + for ($y = 0;$y < $this->height; $y++) { + $row = $this->getRow($y, $row); + for ($x = 0; $x < $this->width; $x++) { + $luminance = $row[$x] & 0xFF; + $c=''; + if ($luminance < 0x40) { + $c = '#'; + } else if ($luminance < 0x80) { + $c = '+'; + } else if ($luminance < 0xC0) { + $c = '.'; + } else { + $c = ' '; + } + $result.=($c); + } + $result.=('\n'); + } + return $result; + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/NotFoundException.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/NotFoundException.php new file mode 100644 index 0000000..572b4dc --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/NotFoundException.php @@ -0,0 +1,38 @@ + $dataWidth || $top + $height > $dataHeight) { +throw new IllegalArgumentException("Crop rectangle does not fit within image data."); +} + + $this->yuvData = $yuvData; + $this->dataWidth = $dataWidth; + $this->dataHeight = $dataHeight; + $this->left = $left; + $this->top = $top; + if ($reverseHorizontal) { + $this->reverseHorizontal($width, $height); + } + } + + //@Override + public function getRow($y, $row=null) { + if ($y < 0 || $y >= getHeight()) { + throw new IllegalArgumentException("Requested row is outside the image: " + y); + } + $width = $this->getWidth(); + if ($row == null || count($row) < $width) { + $row = array();//new byte[width]; + } + $offset = ($y + $this->top) * $this->dataWidth + $this->left; + $row = arraycopy($this->yuvData, $offset, $row, 0, $width); + return $row; + } + + //@Override + public function getMatrix() { + $width = $this->getWidth(); + $height = $this->getHeight(); + + // If the caller asks for the entire underlying image, save the copy and give them the + // original data. The docs specifically warn that result.length must be ignored. + if ($width == $this->dataWidth && $height == $this->dataHeight) { + return $this->yuvData; + } + + $area = $width * $height; + $matrix = array();//new byte[area]; + $inputOffset = $this->top * $this->dataWidth + $this->left; + + // If the width matches the full width of the underlying data, perform a single copy. + if ($width == $this->dataWidth) { + $matrix = arraycopy($this->yuvData, $inputOffset, $matrix, 0, $area); + return $matrix; + } + + // Otherwise copy one cropped row at a time. + $yuv = $this->yuvData; + for ($y = 0; $y < $height; $y++) { + $outputOffset = $y * $width; + $matrix = arraycopy($this->yuvData, $inputOffset, $matrix, $outputOffset, $width); + $inputOffset += $this->dataWidth; + } + return $matrix; + } + + // @Override + public function isCropSupported() { + return true; + } + + // @Override + public function crop($left, $top, $width, $height) { + return new PlanarYUVLuminanceSource($this->yuvData, + $this->dataWidth, + $this->dataHeight, + $this->left + $left, + $this->top + $top, + $width, + $height, + false); +} + + public function renderThumbnail() { +$width = intval($this->getWidth() / self::$THUMBNAIL_SCALE_FACTOR); + $height = intval($this->getHeight() / self::$THUMBNAIL_SCALE_FACTOR); + $pixels = array();//new int[width * height]; + $yuv = $this->yuvData; + $inputOffset = $this->top * $this->dataWidth + $this->left; + + for ($y = 0; $y < $height; $y++) { + $outputOffset = $y * $width; + for ($x = 0; $x < $width; $x++) { + $grey = intval32bits($yuv[$inputOffset + $x * self::$THUMBNAIL_SCALE_FACTOR] & 0xff); + $pixels[$outputOffset + $x] = intval32bits(0xFF000000 | ($grey * 0x00010101)); + } + $inputOffset += $this->dataWidth * self::$THUMBNAIL_SCALE_FACTOR; + } + return $pixels; + } + + /** + * @return width of image from {@link #renderThumbnail()} + */ + /* + public int getThumbnailWidth() { + return getWidth() / THUMBNAIL_SCALE_FACTOR; + }*/ + + /** + * @return height of image from {@link #renderThumbnail()} + */ + /* + public int getThumbnailHeight() { + return getHeight() / THUMBNAIL_SCALE_FACTOR; + } + + private void reverseHorizontal(int width, int height) { + byte[] yuvData = this.yuvData; + for (int y = 0, rowStart = top * dataWidth + left; y < height; y++, rowStart += dataWidth) { + int middle = rowStart + width / 2; + for (int x1 = rowStart, x2 = rowStart + width - 1; x1 < middle; x1++, x2--) { + byte temp = yuvData[x1]; + yuvData[x1] = yuvData[x2]; + yuvData[x2] = temp; + } + } + } +*/ +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/QrReader.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/QrReader.php new file mode 100644 index 0000000..c6c8052 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/QrReader.php @@ -0,0 +1,84 @@ +readImage($imgsource); + }else { + $image = file_get_contents($imgsource); + $im = imagecreatefromstring($image); + } + + break; + + case QrReader::SOURCE_TYPE_BLOB: + if($isUseImagickIfAvailable && extension_loaded('imagick')) { + $im = new Imagick(); + $im->readimageblob($imgsource); + }else { + $im = imagecreatefromstring($imgsource); + } + + break; + + case QrReader::SOURCE_TYPE_RESOURCE: + $im = $imgsource; + if($isUseImagickIfAvailable && extension_loaded('imagick')) { + $isUseImagickIfAvailable = true; + }else { + $isUseImagickIfAvailable = false; + } + + break; + } + + if($isUseImagickIfAvailable && extension_loaded('imagick')) { + $width = $im->getImageWidth(); + $height = $im->getImageHeight(); + $source = new \Zxing\IMagickLuminanceSource($im, $width, $height); + }else { + $width = imagesx($im); + $height = imagesy($im); + $source = new \Zxing\GDLuminanceSource($im, $width, $height); + } + $histo = new \Zxing\Common\HybridBinarizer($source); + $bitmap = new \Zxing\BinaryBitmap($histo); + $reader = new \Zxing\Qrcode\QRCodeReader(); + + $this->result = $reader->decode($bitmap); + }catch (\Zxing\NotFoundException $er){ + $this->result = false; + }catch( \Zxing\FormatException $er){ + $this->result = false; + }catch( \Zxing\ChecksumException $er){ + $this->result = false; + } + } + + public function text() + { + if(method_exists($this->result,'toString')) { + return ($this->result->toString()); + }else{ + return $this->result; + } + } + + public function decode() + { + return $this->text(); + } +} + diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/RGBLuminanceSource.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/RGBLuminanceSource.php new file mode 100644 index 0000000..7c09952 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/RGBLuminanceSource.php @@ -0,0 +1,310 @@ +RGBLuminanceSource_($pixels,$dataWidth,$dataHeight); + return; + } + parent::__construct($width, $height); + if ($left + $width > $dataWidth || $top + $height > $dataHeight) { + throw new \InvalidArgumentException("Crop rectangle does not fit within image data."); + } + $this->luminances = $pixels; + $this->dataWidth = $dataWidth; + $this->dataHeight = $dataHeight; + $this->left = $left; + $this->top = $top; + } + + public function RGBLuminanceSource_($width, $height, $pixels) + { + parent::__construct($width, $height); + + $this->dataWidth = $width; + $this->dataHeight = $height; + $this->left = 0; + $this->top = 0; + $this->pixels = $pixels; + + +// In order to measure pure decoding speed, we convert the entire image to a greyscale array +// up front, which is the same as the Y channel of the YUVLuminanceSource in the real app. + $this->luminances = array(); + //$this->luminances = $this->grayScaleToBitmap($this->grayscale()); + + foreach ($pixels as $key => $pixel) { + $r = $pixel['red']; + $g = $pixel['green']; + $b = $pixel['blue']; + + /* if (($pixel & 0xFF000000) == 0) { + $pixel = 0xFFFFFFFF; // = white + } + + // .229R + 0.587G + 0.114B (YUV/YIQ for PAL and NTSC) + + $this->luminances[$key] = + (306 * (($pixel >> 16) & 0xFF) + + 601 * (($pixel >> 8) & 0xFF) + + 117 * ($pixel & 0xFF) + + 0x200) >> 10; + + */ + //$r = ($pixel >> 16) & 0xff; + //$g = ($pixel >> 8) & 0xff; + //$b = $pixel & 0xff; + if ($r == $g && $g == $b) { +// Image is already greyscale, so pick any channel. + + $this->luminances[$key] = $r;//(($r + 128) % 256) - 128; + } else { +// Calculate luminance cheaply, favoring green. + $this->luminances[$key] = ($r+2*$g+$b)/4;//(((($r + 2 * $g + $b) / 4) + 128) % 256) - 128; + } + + } + + /* + + for ($y = 0; $y < $height; $y++) { + $offset = $y * $width; + for ($x = 0; $x < $width; $x++) { + $pixel = $pixels[$offset + $x]; + $r = ($pixel >> 16) & 0xff; + $g = ($pixel >> 8) & 0xff; + $b = $pixel & 0xff; + if ($r == $g && $g == $b) { +// Image is already greyscale, so pick any channel. + + $this->luminances[intval($offset + $x)] = (($r+128) % 256) - 128; + } else { +// Calculate luminance cheaply, favoring green. + $this->luminances[intval($offset + $x)] = (((($r + 2 * $g + $b) / 4)+128)%256) - 128; + } + + + + } + */ + //} + // $this->luminances = $this->grayScaleToBitmap($this->luminances); + + } + function grayscale(){ + $width = $this->dataWidth; + $height = $this->dataHeight; + + $ret = fill_array(0, $width*$height,0); + for ($y = 0; $y < $height; $y++) + { + for ($x = 0; $x < $width; $x++) + { + $gray = $this->getPixel($x, $y,$width,$height); + + $ret[$x+$y*$width] = $gray; + } + } + return $ret; + } + function getPixel($x,$y,$width,$height){ + $image = $this->pixels; + if ($width < $x) { + die('error'); + } + if ($height < $y) { + die('error'); + } + $point = ($x) + ($y * $width); + + $r = $image[$point]['red'];//($image[$point] >> 16) & 0xff; + $g = $image[$point]['green'];//($image[$point] >> 8) & 0xff; + $b = $image[$point]['blue'];//$image[$point] & 0xff; + + $p = intval(($r*33 +$g*34 + $b*33)/100); + + + return $p; + + } + + +function getMiddleBrightnessPerArea($image) +{ + $numSqrtArea = 4; + //obtain middle brightness((min + max) / 2) per area + $areaWidth = floor($this->dataWidth / $numSqrtArea); + $areaHeight = floor($this->dataHeight / $numSqrtArea); + $minmax = fill_array(0,$numSqrtArea,0); + for ($i = 0; $i < $numSqrtArea; $i++) + { + $minmax[$i] = fill_array(0,$numSqrtArea,0); + for ($i2 = 0; $i2 < $numSqrtArea; $i2++) + { + $minmax[$i][$i2] = array(0,0); + } + } + for ($ay = 0; $ay < $numSqrtArea; $ay++) + { + for ($ax = 0; $ax < $numSqrtArea; $ax++) + { + $minmax[$ax][$ay][0] = 0xFF; + for ($dy = 0; $dy < $areaHeight; $dy++) + { + for ($dx = 0; $dx < $areaWidth; $dx++) + { + $target = $image[intval($areaWidth * $ax + $dx+($areaHeight * $ay + $dy)*$this->dataWidth)]; + if ($target < $minmax[$ax][$ay][0]) + $minmax[$ax][$ay][0] = $target; + if ($target > $minmax[$ax][$ay][1]) + $minmax[$ax][$ay][1] = $target; + } + } + //minmax[ax][ay][0] = (minmax[ax][ay][0] + minmax[ax][ay][1]) / 2; + } + } + $middle = array(); + for ($i3 = 0; $i3 < $numSqrtArea; $i3++) + { + $middle[$i3] = array(); + } + for ($ay = 0; $ay < $numSqrtArea; $ay++) + { + for ($ax = 0; $ax < $numSqrtArea; $ax++) + { + $middle[$ax][$ay] = floor(($minmax[$ax][$ay][0] + $minmax[$ax][$ay][1]) / 2); + //Console.out.print(middle[ax][ay] + ","); + } + //Console.out.println(""); + } + //Console.out.println(""); + + return $middle; +} + +function grayScaleToBitmap ($grayScale) +{ + $middle = $this->getMiddleBrightnessPerArea($grayScale); + $sqrtNumArea = count($middle); + $areaWidth = floor($this->dataWidth/ $sqrtNumArea); + $areaHeight = floor($this->dataHeight / $sqrtNumArea); + $bitmap = fill_array(0,$this->dataWidth*$this->dataHeight,0); + + for ($ay = 0; $ay < $sqrtNumArea; $ay++) + { + for ($ax = 0; $ax < $sqrtNumArea; $ax++) + { + for ($dy = 0; $dy < $areaHeight; $dy++) + { + for ($dx = 0; $dx < $areaWidth; $dx++) + { + $bitmap[intval($areaWidth * $ax + $dx+ ($areaHeight * $ay + $dy)*$this->dataWidth)] = ($grayScale[intval($areaWidth * $ax + $dx+ ($areaHeight * $ay + $dy)*$this->dataWidth)] < $middle[$ax][$ay])?0:255; + } + } + } + } + return $bitmap; +} + +//@Override + public function getRow($y, $row=null) { + if ($y < 0 || $y >= $this->getHeight()) { + throw new \InvalidArgumentException("Requested row is outside the image: " + y); + } + $width = $this->getWidth(); + if ($row == null || count($row) < $width) { + $row = array(); + } + $offset = ($y + $this->top) * $this->dataWidth + $this->left; + $row = arraycopy($this->luminances,$offset, $row, 0, $width); + return $row; + } + +//@Override + public function getMatrix() { + $width = $this->getWidth(); + $height = $this->getHeight(); + +// If the caller asks for the entire underlying image, save the copy and give them the +// original data. The docs specifically warn that result.length must be ignored. + if ($width == $this->dataWidth && $height == $this->dataHeight) { + return $this->luminances; + } + + $area = $width * $height; + $matrix = array(); + $inputOffset = $this->top * $this->dataWidth + $this->left; + +// If the width matches the full width of the underlying data, perform a single copy. + if ($width == $this->dataWidth) { + $matrix = arraycopy($this->luminances, $inputOffset, $matrix, 0, $area); + return $matrix; + } + +// Otherwise copy one cropped row at a time. + $rgb = $this->luminances; + for ($y = 0; $y < $height; $y++) { + $outputOffset = $y * $width; + $matrix = arraycopy($rgb, $inputOffset, $matrix, $outputOffset, $width); + $inputOffset += $this->dataWidth; + } + return $matrix; + } + +//@Override + public function isCropSupported() { + return true; + } + +//@Override + public function crop($left, $top, $width, $height) { + return new RGBLuminanceSource($this->luminances, + $this->dataWidth, + $this->dataHeight, + $this->left + $left, + $this->top + $top, + $width, + $height); + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/Reader.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/Reader.php new file mode 100644 index 0000000..9d700c8 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/Reader.php @@ -0,0 +1,13 @@ +Encapsulates the result of decoding a barcode within an image.

    + * + * @author Sean Owen + */ +final class Result { + +private $text; +private $rawBytes; +private $resultPoints; +private $format; +private $resultMetadata; +private $timestamp; + + + +public function __construct($text, + $rawBytes, + $resultPoints, + $format, + $timestamp = '') { + + $this->text = $text; + $this->rawBytes = $rawBytes; + $this->resultPoints = $resultPoints; + $this->format = $format; + $this->resultMetadata = null; + $this->timestamp = $timestamp?$timestamp:time(); +} + + /** + * @return raw text encoded by the barcode + */ + public function getText() { + return $this->text; + } + + /** + * @return raw bytes encoded by the barcode, if applicable, otherwise {@code null} + */ + public function getRawBytes() { + return $this->rawBytes; + } + + /** + * @return points related to the barcode in the image. These are typically points + * identifying finder patterns or the corners of the barcode. The exact meaning is + * specific to the type of barcode that was decoded. + */ + public function getResultPoints() { + return $this->resultPoints; + } + + /** + * @return {@link BarcodeFormat} representing the format of the barcode that was decoded + */ + public function getBarcodeFormat() { + return $this->format; + } + + /** + * @return {@link Map} mapping {@link ResultMetadataType} keys to values. May be + * {@code null}. This contains optional metadata about what was detected about the barcode, + * like orientation. + */ + public function getResultMetadata() { + return $this->resultMetadata; + } + + public function putMetadata($type, $value) { + if ($this->resultMetadata == null) { + $this->resultMetadata = array(); + } + $resultMetadata[$type] = $value; +} + + public function putAllMetadata($metadata) { + if ($metadata != null) { + if ($this->resultMetadata == null) { + $this->resultMetadata = $metadata; + } else { + $this->resultMetadata = array_merge($this->resultMetadata, $metadata); + } + } + } + + public function addResultPoints($newPoints) { + $oldPoints = $this->resultPoints; + if ($oldPoints == null) { + $this->resultPoints = $newPoints; + } else if ($newPoints != null && count($newPoints) > 0) { + $allPoints = fill_array(0,count($oldPoints)+count($newPoints),0); + $allPoints = arraycopy($oldPoints, 0, $allPoints, 0, count($oldPoints)); + $allPoints = arraycopy($newPoints, 0, $allPoints, count($oldPoints), count($newPoints)); + $this->resultPoints = $allPoints; + } + } + + public function getTimestamp() { + return $this->timestamp; + } + + //@Override + public function toString() { + return $this->text; + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/ResultPoint.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/ResultPoint.php new file mode 100644 index 0000000..0702004 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/ResultPoint.php @@ -0,0 +1,140 @@ +Encapsulates a point of interest in an image containing a barcode. Typically, this + * would be the location of a finder pattern or the corner of the barcode, for example.

    + * + * @author Sean Owen + */ +class ResultPoint { + + private $x; + private $y; + + public function __construct($x, $y) { + $this->x = (float)($x); + $this->y = (float)($y); + } + + public final function getX() { + return (float)($this->x); + } + + public final function getY() { + return (float)($this->y); + } + +//@Override + public final function equals($other) { + if ($other instanceof ResultPoint) { + $otherPoint = $other; + return $this->x == $otherPoint->x && $this->y == $otherPoint->y; + } + return false; + } + +//@Override + public final function hashCode() { + return 31 * floatToIntBits($this->x) + floatToIntBits($this->y); + } + +//@Override + public final function toString() { + $result = ''; + $result.= ('('); + $result.=($this->x); + $result.=(','); + $result.=($this->y); + $result.=(')'); + return $result; + } + + /** + * Orders an array of three ResultPoints in an order [A,B,C] such that AB is less than AC + * and BC is less than AC, and the angle between BC and BA is less than 180 degrees. + * + * @param patterns array of three {@code ResultPoint} to order + */ + public static function orderBestPatterns($patterns) { + +// Find distances between pattern centers + $zeroOneDistance = self::distance($patterns[0], $patterns[1]); + $oneTwoDistance = self::distance($patterns[1], $patterns[2]); + $zeroTwoDistance = self::distance($patterns[0], $patterns[2]); + + $pointA=''; + $pointB=''; + $pointC=''; +// Assume one closest to other two is B; A and C will just be guesses at first + if ($oneTwoDistance >= $zeroOneDistance && $oneTwoDistance >= $zeroTwoDistance) { + $pointB = $patterns[0]; + $pointA = $patterns[1]; + $pointC = $patterns[2]; + } else if ($zeroTwoDistance >= $oneTwoDistance && $zeroTwoDistance >= $zeroOneDistance) { + $pointB = $patterns[1]; + $pointA = $patterns[0]; + $pointC = $patterns[2]; + } else { + $pointB = $patterns[2]; + $pointA = $patterns[0]; + $pointC = $patterns[1]; + } + +// Use cross product to figure out whether A and C are correct or flipped. +// This asks whether BC x BA has a positive z component, which is the arrangement +// we want for A, B, C. If it's negative, then we've got it flipped around and +// should swap A and C. + if (self::crossProductZ($pointA, $pointB, $pointC) < 0.0) { + $temp = $pointA; + $pointA = $pointC; + $pointC = $temp; + } + + $patterns[0] = $pointA; + $patterns[1] = $pointB; + $patterns[2] = $pointC; + return $patterns; + } + + + /** + * @param pattern1 first pattern + * @param pattern2 second pattern + * @return distance between two points + */ + public static function distance($pattern1, $pattern2) { + return MathUtils::distance($pattern1->x, $pattern1->y, $pattern2->x, $pattern2->y); + } + + /** + * Returns the z component of the cross product between vectors BC and BA. + */ + private static function crossProductZ($pointA, + $pointB, + $pointC) { + $bX = $pointB->x; + $bY = $pointB->y; + return (($pointC->x - $bX) * ($pointA->y - $bY)) - (($pointC->y - $bY) * ($pointA->x - $bX)); + } + + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/AbstractEnum.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/AbstractEnum.php new file mode 100644 index 0000000..b6e81b9 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/AbstractEnum.php @@ -0,0 +1,95 @@ +strict = $strict; + $this->change($initialValue); + } + /** + * Changes the value of the enum. + * + * @param mixed $value + * @return void + */ + public function change($value) + { + if (!in_array($value, $this->getConstList(), $this->strict)) { + throw new Exception\UnexpectedValueException('Value not a const in enum ' . get_class($this)); + } + $this->value = $value; + } + /** + * Gets current value. + * + * @return mixed + */ + public function get() + { + return $this->value; + } + /** + * Gets all constants (possible values) as an array. + * + * @param boolean $includeDefault + * @return array + */ + public function getConstList($includeDefault = true) + { + if ($this->constants === null) { + $reflection = new ReflectionClass($this); + $this->constants = $reflection->getConstants(); + } + if ($includeDefault) { + return $this->constants; + } + $constants = $this->constants; + unset($constants['__default']); + return $constants; + } + /** + * Gets the name of the enum. + * + * @return string + */ + public function __toString() + { + return array_search($this->value, $this->getConstList()); + } +} \ No newline at end of file diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/BitArray.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/BitArray.php new file mode 100644 index 0000000..45af51e --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/BitArray.php @@ -0,0 +1,394 @@ +A simple, fast array of bits, represented compactly by an array of ints internally.

    + * + * @author Sean Owen + */ + +final class BitArray { + + private $bits; + private $size; + + + + public function __construct($bits=array(),$size=0) { + + if(!$bits&&!$size){ + $this->$size = 0; + $this->bits = array(); + }elseif($bits&&!$size){ + $this->size = $bits; + $this->bits = $this->makeArray($bits); + }else{ + $this->bits = $bits; + $this->size = $size; + } + + } + + + + + public function getSize() { + return $this->size; + } + + public function getSizeInBytes() { + return ($this->size + 7) / 8; + } + + private function ensureCapacity($size) { + if ($size > count($this->bits) * 32) { + $newBits = $this->makeArray($size); + $newBits = arraycopy($this->bits, 0, $newBits, 0, count($this->bits)); + $this->bits = $newBits; + } + } + + /** + * @param $i; bit to get + * @return true iff bit i is set + */ + public function get($i) { + $key = intval($i / 32); + return intval32bits($this->bits[$key] & (1 << ($i & 0x1F))) != 0; + } + + /** + * Sets bit i. + * + * @param i bit to set + */ + public function set($i) { + $this->bits[intval($i / 32)] |= 1 << ($i & 0x1F); + $this->bits[intval($i / 32)] = overflow($this->bits[intval($i / 32)]); + } + + /** + * Flips bit i. + * + * @param i bit to set + */ + public function flip($i) { + $this->bits[intval($i / 32)] ^= 1 << ($i & 0x1F); + $this->bits[intval($i / 32)] = overflow32($this->bits[intval($i / 32)]); + } + + /** + * @param from first bit to check + * @return index of first bit that is set, starting from the given index, or size if none are set + * at or beyond this given index + * @see #getNextUnset(int) + */ + public function getNextSet($from) { + if ($from >= $this->size) { + return $this->size; + } + $bitsOffset = intval($from / 32); + $currentBits = (int)$this->bits[$bitsOffset]; + // mask off lesser bits first + $currentBits &= ~((1 << ($from & 0x1F)) - 1); + while ($currentBits == 0) { + if (++$bitsOffset == count($this->bits)) { + return $this->size; + } + $currentBits = $this->bits[$bitsOffset]; + } + $result = ($bitsOffset * 32) + numberOfTrailingZeros($currentBits); //numberOfTrailingZeros + return $result > $this->size ? $this->size : $result; + } + + /** + * @param from index to start looking for unset bit + * @return index of next unset bit, or {@code size} if none are unset until the end + * @see #getNextSet(int) + */ + public function getNextUnset($from) { + if ($from >= $this->size) { + return $this->size; + } + $bitsOffset = intval($from / 32); + $currentBits = ~$this->bits[$bitsOffset]; + // mask off lesser bits first + $currentBits &= ~((1 << ($from & 0x1F)) - 1); + while ($currentBits == 0) { + if (++$bitsOffset == count($this->bits)) { + return $this->size; + } + $currentBits = overflow32(~$this->bits[$bitsOffset]); + } + $result = ($bitsOffset * 32) + numberOfTrailingZeros($currentBits); + return $result > $this->size ? $this->size : $result; + } + + /** + * Sets a block of 32 bits, starting at bit i. + * + * @param i first bit to set + * @param newBits the new value of the next 32 bits. Note again that the least-significant bit + * corresponds to bit i, the next-least-significant to i+1, and so on. + */ + public function setBulk($i, $newBits) { + $this->bits[intval($i / 32)] = $newBits; + } + + /** + * Sets a range of bits. + * + * @param start start of range, inclusive. + * @param end end of range, exclusive + */ + public function setRange($start, $end) { + if ($end < $start) { + throw new \InvalidArgumentException(); + } + if ($end == $start) { + return; + } + $end--; // will be easier to treat this as the last actually set bit -- inclusive + $firstInt = intval($start / 32); + $lastInt = intval($end / 32); + for ($i = $firstInt; $i <= $lastInt; $i++) { + $firstBit = $i > $firstInt ? 0 : $start & 0x1F; + $lastBit = $i < $lastInt ? 31 : $end & 0x1F; + $mask = 0; + if ($firstBit == 0 && $lastBit == 31) { + $mask = -1; + } else { + $mask = 0; + for ($j = $firstBit; $j <= $lastBit; $j++) { + $mask |= 1 << $j; + } + } + $this->bits[$i] = overflow32($this->bits[$i]|$mask); + } + } + + /** + * Clears all bits (sets to false). + */ + public function clear() { + $max = count($this->bits); + for ($i = 0; $i < $max; $i++) { + $this->bits[$i] = 0; + } + } + + /** + * Efficient method to check if a range of bits is set, or not set. + * + * @param start start of range, inclusive. + * @param end end of range, exclusive + * @param value if true, checks that bits in range are set, otherwise checks that they are not set + * @return true iff all bits are set or not set in range, according to value argument + * @throws InvalidArgumentException if end is less than or equal to start + */ + public function isRange($start, $end, $value) { + if ($end < $start) { + throw new \InvalidArgumentException(); + } + if ($end == $start) { + return true; // empty range matches + } + $end--; // will be easier to treat this as the last actually set bit -- inclusive + $firstInt = intval($start / 32); + $lastInt = intval($end / 32); + for ($i = $firstInt; $i <= $lastInt; $i++) { + $firstBit = $i > $firstInt ? 0 : $start & 0x1F; + $lastBit = $i < $lastInt ? 31 :$end & 0x1F; + $mask = 0; + if ($firstBit == 0 && $lastBit == 31) { + $mask = -1; + } else { + $mask = 0; + for ($j = $firstBit; $j <= $lastBit; $j++) { + $mask = overflow32($mask|(1 << $j)); + } + } + + // Return false if we're looking for 1s and the masked bits[i] isn't all 1s (that is, + // equals the mask, or we're looking for 0s and the masked portion is not all 0s + if (($this->bits[$i] & $mask) != ($value ? $mask : 0)) { + return false; + } + } + return true; + } + + public function appendBit($bit) { + $this->ensureCapacity($this->size + 1); + if ($bit) { + $this->bits[intval($this->size / 32)] |= 1 << ($this->size & 0x1F); + } + $this->size++; + } + + /** + * Appends the least-significant bits, from value, in order from most-significant to + * least-significant. For example, appending 6 bits from 0x000001E will append the bits + * 0, 1, 1, 1, 1, 0 in that order. + * + * @param value {@code int} containing bits to append + * @param numBits bits from value to append + */ + public function appendBits($value, $numBits) { + if ($numBits < 0 || $numBits > 32) { + throw new \InvalidArgumentException("Num bits must be between 0 and 32"); + } + $this->ensureCapacity($this->size + $numBits); + for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) { + $this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) == 1); + } + } + + public function appendBitArray($other) { + $otherSize = $other->size; + $this->ensureCapacity($this->size + $otherSize); + for ($i = 0; $i < $otherSize; $i++) { + $this->appendBit($other->get($i)); + } + } + + public function _xor($other) { + if (count($this->bits) != count($other->bits)) { + throw new \InvalidArgumentException("Sizes don't match"); + } + for ($i = 0; $i < count($this->bits); $i++) { + // The last byte could be incomplete (i.e. not have 8 bits in + // it) but there is no problem since 0 XOR 0 == 0. + $this->bits[$i] ^= $other->bits[$i]; + } + } + + /** + * + * @param bitOffset first bit to start writing + * @param array array to write into. Bytes are written most-significant byte first. This is the opposite + * of the internal representation, which is exposed by {@link #getBitArray()} + * @param offset position in array to start writing + * @param numBytes how many bytes to write + */ + public function toBytes($bitOffset, &$array, $offset, $numBytes) { + for ($i = 0; $i < $numBytes; $i++) { + $theByte = 0; + for ($j = 0; $j < 8; $j++) { + if ($this->get($bitOffset)) { + $theByte |= 1 << (7 - $j); + } + $bitOffset++; + } + $array[(int)($offset + $i)] = $theByte; + } + } + + /** + * @return underlying array of ints. The first element holds the first 32 bits, and the least + * significant bit is bit 0. + */ + public function getBitArray() { + return $this->bits; + } + + /** + * Reverses all bits in the array. + */ + public function reverse() { + $newBits = array(); + // reverse all int's first + $len = (($this->size-1) / 32); + $oldBitsLen = $len + 1; + for ($i = 0; $i < $oldBitsLen; $i++) { + $x = $this->bits[$i];/* + $x = (($x >> 1) & 0x55555555L) | (($x & 0x55555555L) << 1); + $x = (($x >> 2) & 0x33333333L) | (($x & 0x33333333L) << 2); + $x = (($x >> 4) & 0x0f0f0f0fL) | (($x & 0x0f0f0f0fL) << 4); + $x = (($x >> 8) & 0x00ff00ffL) | (($x & 0x00ff00ffL) << 8); + $x = (($x >> 16) & 0x0000ffffL) | (($x & 0x0000ffffL) << 16);*/ + $x = (($x >> 1) & 0x55555555) | (($x & 0x55555555) << 1); + $x = (($x >> 2) & 0x33333333) | (($x & 0x33333333) << 2); + $x = (($x >> 4) & 0x0f0f0f0f) | (($x & 0x0f0f0f0f) << 4); + $x = (($x >> 8) & 0x00ff00ff) | (($x & 0x00ff00ff) << 8); + $x = (($x >> 16) & 0x0000ffff) | (($x & 0x0000ffff) << 16); + $newBits[(int)$len - $i] = (int) $x; + } + // now correct the int's if the bit size isn't a multiple of 32 + if ($this->size != $oldBitsLen * 32) { + $leftOffset = $oldBitsLen * 32 - $this->size; + $mask = 1; + for ($i = 0; $i < 31 - $leftOffset; $i++) { + $mask = ($mask << 1) | 1; + } + $currentInt = ($newBits[0] >> $leftOffset) & $mask; + for ($i = 1; $i < $oldBitsLen; $i++) { + $nextInt = $newBits[$i]; + $currentInt |= $nextInt << (32 - $leftOffset); + $newBits[intval($i) - 1] = $currentInt; + $currentInt = ($nextInt >> $leftOffset) & $mask; + } + $newBits[intval($oldBitsLen) - 1] = $currentInt; + } + $bits = $newBits; + } + + private static function makeArray($size) { + return array(); + } + + // @Override + public function equals($o) { + if (!($o instanceof BitArray)) { + return false; + } + $other = $o; + return $this->size == $other->size && $this->bits===$other->bits; + } + + //@Override + public function hashCode() { + return 31 * $this->size +hashCode($this->bits); + } + + // @Override + public function toString() { + $result = ''; + for ($i = 0; $i < $this->size; $i++) { + if (($i & 0x07) == 0) { + $result.=' '; + } + $result.= ($this->get($i) ? 'X' : '.'); + } + return (string) $result; + } + + // @Override + public function _clone() { + return new BitArray($this->bits, $this->size); + } + +} \ No newline at end of file diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/BitMatrix.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/BitMatrix.php new file mode 100644 index 0000000..bde9f49 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/BitMatrix.php @@ -0,0 +1,424 @@ +width = $width; + $this->height = $height; + $this->rowSize = $rowSize; + $this->bits = $bits; + } + public static function parse($stringRepresentation, $setString, $unsetString){ + if (!$stringRepresentation) { + throw new \InvalidArgumentException(); + } + $bits = array(); + $bitsPos = 0; + $rowStartPos = 0; + $rowLength = -1; + $nRows = 0; + $pos = 0; + while ($pos < strlen($stringRepresentation)) { + if ($stringRepresentation{$pos} == '\n' || + $stringRepresentation->{$pos} == '\r') { + if ($bitsPos > $rowStartPos) { + if($rowLength == -1) { + $rowLength = $bitsPos - $rowStartPos; + } + else if ($bitsPos - $rowStartPos != $rowLength) { + throw new \InvalidArgumentException("row lengths do not match"); + } + $rowStartPos = $bitsPos; + $nRows++; + } + $pos++; + } + else if (substr($stringRepresentation,$pos, strlen($setString))==$setString) { + $pos += strlen($setString); + $bits[$bitsPos] = true; + $bitsPos++; + } + else if (substr($stringRepresentation, $pos + strlen($unsetString))==$unsetString) { + $pos += strlen($unsetString); + $bits[$bitsPos] = false; + $bitsPos++; + } else { + throw new \InvalidArgumentException( + "illegal character encountered: " . substr($stringRepresentation,$pos)); + } + } + + // no EOL at end? + if ($bitsPos > $rowStartPos) { + if($rowLength == -1) { + $rowLength = $bitsPos - $rowStartPos; + } else if ($bitsPos - $rowStartPos != $rowLength) { + throw new \InvalidArgumentException("row lengths do not match"); + } + $nRows++; + } + + $matrix = new BitMatrix($rowLength, $nRows); + for ($i = 0; $i < $bitsPos; $i++) { + if ($bits[$i]) { + $matrix->set($i % $rowLength, $i / $rowLength); + } + } + return $matrix; + } + + /** + *

    Gets the requested bit, where true means black.

    + * + * @param $x; The horizontal component (i.e. which column) + * @param $y; The vertical component (i.e. which row) + * @return value of given bit in matrix + */ + public function get($x, $y) { + + $offset = intval($y * $this->rowSize + ($x / 32)); + if(!isset($this->bits[$offset])){ + $this->bits[$offset] = 0; + } + + // return (($this->bits[$offset] >> ($x & 0x1f)) & 1) != 0; + return (uRShift($this->bits[$offset],($x & 0x1f)) & 1) != 0;//было >>> вместо >>, не знаю как эмулировать беззнаковый сдвиг + } + + /** + *

    Sets the given bit to true.

    + * + * @param $x; The horizontal component (i.e. which column) + * @param $y; The vertical component (i.e. which row) + */ + public function set($x, $y) { + $offset = intval($y * $this->rowSize + ($x / 32)); + if(!isset($this->bits[$offset])){ + $this->bits[$offset] = 0; + } + //$this->bits[$offset] = $this->bits[$offset]; + + // if($this->bits[$offset]>200748364){ + //$this->bits= array(0,0,-16777216,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,-16777216,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,-16777216,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,-16777216,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,-16777216,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,-16777216,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,-16777216,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,-16777216,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,-16777216,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,-16777216,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,-16777216,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,-1090519040,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,1056964608,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,-1358954496,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,117440512,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,50331648,-1,-1,-1,-1,65535,0,0,0,0,0,0,0,33554432,-1,-1,536870911,-4096,65279,0,0,0,0,0,0,0,0,-1,-1,65535,-4096,65535,0,0,0,0,0,0,0,0,-193,536870911,0,-4096,65279,0,0,0,0,0,0,0,0,-254,32767,0,-4096,61951,0,0,0,0,0,0,0,0,20913920,0,0,-4096,50175,0,0,0,0,0,0,0,0,0,0,0,-4096,60159,0,0,0,0,0,0,0,0,0,0,0,-4096,64255,0,0,0,0,0,0,0,0,0,0,0,-8192,56319,0,0,0,0,0,0,0,0,0,0,0,-4096,16777215,0,0,0,0,0,0,0,0,0,0,0,-4096,16777215,0,0,0,0,0,0,0,0,0,0,0,-4096,16777215,0,0,0,0,0,0,0,0,0,0,0,-4096,16777215,0,0,0,0,0,0,0,0,0,0,0,-4096,16777215,0,0,0,0,0,0,0,0,0,0,0,-4096,16777215,0,0,0,0,0,0,0,0,0,0,0,-4096,16777215,0,0,0,0,0,0,0,0,0,0,0,-4096,16777215,0,0,0,0,0,0,0,251658240,0,0,0,-4096,-1,255,0,256,0,0,0,0,117440512,0,0,0,-4096,-1,255,0,512,0,0,0,0,117440512,0,0,0,-4096,-1,255,0,1024,0,0,0,0,117440512,0,0,0,-4096,-1,223,0,256,0,0,0,0,117440512,0,0,33030144,-4096,-1,191,0,256,0,0,0,0,117440512,0,0,33554428,-4096,-1,255,0,768,0,0,0,0,117440512,0,402849792,67108862,-8192,-1,255,0,768,0,0,0,0,117440512,0,470278396,63045630,-8192,-1,255,0,256,0,0,0,0,251658240,-8388608,470278399,58720286,-8192,-1,2686975,0,3842,0,0,0,0,251658240,-131072,1007149567,58720286,-8192,-1,2031615,0,3879,0,0,0,0,251658240,536739840,1007092192,58720286,-8192,-1,851967,0,3840,0,0,0,0,251658240,917504,1007092192,58720284,-8192,-1,2031615,0,3968,0,0,0,0,251658240,917504,1007092160,59244060,-8192,-1,65535,0,7936,0,0,0,0,251658240,917504,1009779136,59244060,-8192,-1,9371647,0,1792,0,0,0,0,251658240,917504,946921920,59244060,-8192,-1,8585215,0,1792,0,0,0,0,117440512,-15859712,477159875,59244060,-8192,-1,65535,0,12032,0,0,0,0,251658240,-15859712,52490691,59244060,-8192,-1,-1,0,65408,0,0,0,0,251658240,-15859712,58778051,59244060,-8192,-1,-1,0,65473,0,0,0,0,251658240,-15859712,125886915,59244060,-8192,-1,-1,0,65472,0,0,0,0,251658240,-15859712,58778051,59244060,-8192,-1,-1,0,65408,0,0,0,0,251658240,-15859712,8380867,59244060,-8192,-1,-1,0,65473,0,0,0,0,251658240,-15859712,8380867,59244060,-8192,-1,-1,0,131011,0,0,0,0,251658240,-15859712,8380867,58720284,-8192,-1,-1,0,130947,0,0,0,0,251658240,-15859712,2089411,58720284,-8192,-1,-1,0,130947,0,0,0,0,251658240,-32636928,449,58720284,-8192,-1,-1,33554431,131015,0,0,0,0,251658240,786432,448,62914588,-8192,-1,-1,16777215,131015,0,0,0,0,251658240,786432,448,67108860,-8192,-1,-1,553648127,131015,0,0,0,0,251658240,786432,946864576,67108860,-8192,-1,-1,32505855,131015,0,0,0,0,251658240,786432,946921976,8388604,-8192,-1,-1,8191999,131015,0,0,0,0,251658240,-262144,946921983,248,-8192,-1,-1,8126463,196551,0,0,0,0,251658240,-262144,7397887,0,-8192,-1,-1,16777215,262087,0,0,0,0,251658240,-262144,8257543,0,-8192,-1,-1,-2121269249,262095,0,0,0,0,520093696,0,8257536,0,-8192,-1,-1,-201326593,262095,0,0,0,0,520290304,0,8257536,117963776,-8192,-1,-1,-201326593,262095,0,0,0,0,520093696,0,-2140143616,118488579,-8192,-1,-1,-201326593,131023,0,0,0,0,520093696,0,-2131697280,118488579,-8192,-1,-1,-503316481,131023,0,0,0,0,520093696,2145386496,-2131631232,118484995,-16384,-1,-1,-469762049,262095,0,0,0,0,520093696,2147221504,552649600,118481344,-16384,-1,-1,-469762049,131023,0,0,0,0,520290304,2147221504,2029002240,118481344,-16384,-1,-1,-469762049,262031,0,0,0,0,520290304,-266600448,2029001791,125952960,-16384,-1,-1,-469762049,262031,0,0,0,0,1057423360,-266600448,2027953215,133177312,-16384,-1,-1,-134217729,262111,0,0,0,0,1058471936,-266600448,-119531393,133177343,-16384,-1,-1,-134217729,262111,0,0,0,0,1058471936,-2145648640,-253754369,66068479,-16384,-1,-1,-134217729,262111,0,0,0,0,1058471936,236716032,-253754369,15729663,-16384,-1,-1,-134217729,262095,0,0,0,0,1057947648,236716032,-253754369,6348807,-16384,-1,-1,-134217729,262095,0,0,0,0,524222464,236716032,-253690305,6348803,-16384,-1,-1,-134217729,262111,0,0,0,0,521076736,2115764224,-253625344,14737411,-16384,-1,-1,-134217729,262095,0,0,0,0,522125312,2115764224,-253625344,14743555,-16384,-1,-1,-134217729,262111,0,0,16772608,0,1073676288,-31719424,-2014283776,14810115,-16384,-1,-1,-1,262143,0,0,16776704,0,1065287680,-1642594304,-1879178880,14810115,-16384,-1,-1,-1,524287,0,0,16776192,0,2139029504,264241152,-2013396089,14809091,-16384,-1,-1,-1,262095,0,0,16776192,0,2139029504,264241152,-2080636025,14803335,-16384,-1,-1,-1,262087,0,0,16776192,0,2147418112,264241152,-2132803581,14803847,-16384,-1,-1,-402653185,524259,0,0,8386048,0,2147418112,0,-2132688896,123783,-16384,-1,-1,1207959551,262112,0,0,16775168,0,2147418112,0,14794752,1046535,-16384,-1,-1,268435455,262128,0,0,16775168,0,2147418112,0,14712832,1047615,-16384,-1,-1,536870911,524284,0,0,16776705,0,2147418112,0,14680832,1047615,-16384,-1,-1,-1,524287,0,0,16776704,0,2147418112,-1048576,14681087,1046591,-32768,-1,-1,-1,524287,0,0,16776704,0,2147418112,-524288,-2132802561,2080831,-32768,-1,-1,-1,524287,0,0,16776705,0,2147418112,-524288,-31718401,2080831,-32768,-1,-1,-1,1048575,0,0,16776193,0,2147418112,3670016,-31718528,2080831,-32768,-1,-1,-1,524287,0,0,16776195,0,2147418112,3670016,-31718528,134086719,-32768,-1,-1,-1,524287,0,0,16776195,0,2147418112,3670016,253494144,268173368,-32768,-1,-1,-1,524287,0,0,16775171,0,2147418112,3670016,268174208,268173368,-32768,-1,-1,-1,1048575,0,0,16771072,0,2147418112,-63438848,268174223,31457328,-32768,-1,-1,-1,1048575,0,0,10418176,0,-65536,-63438848,133957519,14807040,-32768,-1,-1,-1,2097151,0,0,15923200,0,2147418112,-63438848,1968015,14809095,-32768,-1,-1,-1,1048575,0,0,12808192,0,2147418112,-63438848,2082703,12711943,-32768,-1,-1,-1,2097151,0,0,6420480,0,2147418112,-63438848,2082703,14343,-32768,-1,-1,-1,2097151,0,0,15202304,0,-65536,-63438848,2082703,1849351,-32768,-1,-1,-1,2097151,0,0,15464448,0,-65536,-63438848,264472335,1849351,-32768,-1,-1,-1,4194303,0,0,16371712,0,-65536,-63438848,264472335,14343,-32768,-1,-1,-1,8388607,0,0,0,0,-65536,-63438848,532907791,235010048,-32768,-1,-1,-1,16777215,0,0,0,0,-65536,-63438848,-1603833,235010160,-32768,-1,-1,-1,16777215,0,0,0,0,-65536,3670016,-30976,67238000,-32768,-1,-1,-1,16777215,0,0,0,0,-65536,3670016,-30976,48,-32768,-1,-1,-1,16777215,0,0,0,0,-65536,3670016,-29391104,768,-32768,-1,-1,-1,16777215,0,0,0,0,-65536,3670016,-29391104,768,-32768,-1,-1,-1,16777215,0,0,0,0,-65536,-524287,-65042433,768,-32768,-1,-1,-1,16777215,0,0,0,0,-65536,-524287,2082441215,0,-65536,-1,-1,-1,16777215,0,0,0,0,-13697024,-524287,511,0,-65536,-1,-1,-1,16777215,0,0,0,0,-12648448,1,0,0,-65536,-1,-1,-1,14680063,0,0,0,0,-12648448,1,0,0,-65536,-1,-1,-1,16777215,0,0,0,0,-65536,1,0,0,-65536,-1,-1,-1,14680063,0,0,0,0,-8454144,1,0,0,-65536,-1,-1,-1,12582911,0,0,0,0,-12648448,1,0,0,-65536,-1,-1,-1,2097151,0,0,0,0,-12648448,1,0,0,-65536,-1,-1,-1,1048575,0,0,0,0,-14745600,1,0,0,-65536,-1,-1,-1,3145727,0,0,0,0,1056964608,1,0,0,-65536,-1,-1,-1,1048575,0,0,0,0,1056964608,1,0,0,-65536,-1,-1,-1,1048575,0,0,0,0,1056964608,1,0,0,-65536,-1,-1,-1,524287,0,0,0,0,2130706432,1,0,0,-65536,-1,-1,-1,1048575,0,0,0,0,1056964608,1,0,0,-65536,-1,-1,-1,524287,0,0,0,0,2130706432,1,0,0,-65536,-1,-1,-1,524287,0,0,0,0,2130706432,1,0,0,-65536,-1,-1,-1,1048575,0,0,0,0,2130706432,1,0,0,-65536,-1,-1,-1,1048575,0,0,0,0,50331648,1,0,0,-65536,-1,-1,-1,1048575,0,0,0,0,117440512,1,0,-268435456,-1,-1,-1,-1,524287,0,0,0,0,251658240,1,0,-320,-1,-1,-1,-1,262143,0,0,0,0,520093696,1,-2048,-1,-1,-1,-1,-1,262143,0,0,0,0,1056964608,-16777213,-1,-1,-1,-1,-1,-1,131071,0,0,0,0,-16777216,-121,-1,-1,-1,-1,-1,-1,131071,0,0,0,0,-16777216,-1,-1,-1,-1,-1,-1,-1,131071,0,0,0,0,-16777216,-1,-1,-1,-1,-1,-1,-1,131071,0,0,0,0,-16777216,-1,-1,-1,-1,-1,-1,-1,131071,0,0,0,0,-16777216,-1,-1,-1,-1,-1,-1,-1,65535,0,0,0,0,-16777216,-1,-1,-1,-1,-1,-1,-1,65535,0,0,0,0,-16777216,-1,-1,-1,-1,-1,-1,-1,131071,0,0,0,0,-16777216,-1,-1,-1,-1,-1,-1,-1,262143,0,0,0,0,-16777216,-1,-1,-1,-1,-1,-1,-1,524287,0,0,0,0,-16777216,-1,-1,-1,-1,-1,-1,-1,524287,0,0,0,0,-16777216,-1,-1,-1,-1,-1,-1,-1,589823,0,0,0,0,0,-1,-1,-1,-1,-1,-1,-1,8179,0,0,0,0,50331648,-1,-1,-1,-1,-1,-1,-1,4080,0,0,0,0,117440512,-1,-1,-1,-1,-1,-1,-1,1016,0,0,0,0,251658240,-1,-1,-1,-1,-1,-1,1073741823,1020,0,0,0,0,50331648,-1,-1,-1,-1,-1,-1,536870911,254,0,0,0,0,50331648,-1,-1,-1,-1,-1,-1,536870911,255,0,0,0,0,50331648,-1,-1,-1,-1,-1,-1,-1879048193,127,0,0,0,0,50331648,-1,-1,-1,-1,-1,-1,-469762049,63,0,0,0,0,1191182336,-1,-1,-1,-1,1023999,0,-520093712,15,0,0,0,0,-218103808,-1,-1,-1,-1,0,-8454144,-260046849,7,0,0,0,0,0,-193,-1,-1,-1057947649,-2147483648,-1,-58720257,1,0,0,0,0,0,-251,-1,-1,-1057423361,-2074,-1,-1,0,0,0,0,0,0,-59648,-1,-1,-1,-1,-1,1073741823,0,0,0,0,0,0,-65536,-1,-1,-1,-1,-1,268435455,0,0,0,0,0,0,-65536,-1,-1,-1,-1,-1,67108863,0,0,0,0,0,0,-65536,-1,-1,-1,-1,-1,8388607,0,0,0,0,0,0,0,-403603456,-1,-1,-1,-1,262143,0,0,0,8388656,0,0,0,-1891434496,-1,-1,-1,-1,16383,0,0,0,8388608,0,0,0,-1612513280,-1,-1,-1,-1,63,0,0,0,0,0,0,0,-24320,-1,-1,-1,8388607,0,0,0,0,0,0,0,0,-256,-1,-1,1073741823,1,0,0,0,0,0,0,0,1610612736,-15,-1,-1,16383,0,0,0,0,0,0,0,0,-16646144,-1,-1,251658239,0,0,0,0,0,0,0,0,0,-51200,-1,-1,40959,0,0,0,0,0,0,268419584,103809024,-12713984,-1,-2147483137,4194303,0,0,0,0,0,0,0,402620416,-2144010240,-13631487,-32513,3,20480,0,0,0,0,0,0,0,419299328,0,-262144,-1,0,0,0,0,0,0,0,0,0,0,0,-5832704,268049407,0,0,0,0,0,0,0,0,0,0,0,0,33030144,0,0,0,0,0,0,0,0,0,0,0,0,3670016,0,0,0,0,0,0,0,0,0,0,0,0,1572864,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,458752,0,0,0,0,0,0,0,0,0,0,0,0,229376,0,0,0,0,0,0,0,0,0,0,0,0,32768,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8192,0,0,0,0,0,0,0,0,0,0,0,0,8192,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31744,0,0,0,0,0,0,0,0,0,0,0,0,31744,0,0,0,0,0,0,0,0,0,0,0,0,64512,0,0,0,0,0,0,0,0,0,0,0,0,15872,0,0,0,0,0,0,0,0,0,0,0,0,3584,0,0,0,0,0,0,0,0,0,0,0,0,7680,0,0,0,0,0,0,0,0,0,0,0,0,512,0,0,0,0,0,0,0,0,0,0,0,0,3968,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3840,0,0,0,0,0,0,0,0,0,0,0,0,1855,0,0,0,0,0,0,0,0,0,0,0,0,63,0,0,0,0,0,0,0,0,0,0,0,0,15,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,134217728,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,0,0,0,-260046848,63,0,0,0,0,0,0,0,0,0,0,0,-17301504,127,0,0,0,0,0,0,0,0,0,0,0,-524288,127,0,0,0,0,0,0,0,0,0,0,0,-262144,127,0,0,0,0,0,0,0,0,0,0,0,-262144,63,0,0,0,0,0,0,0,0,0,0,0,-262144,63,0,0,0,0,0,0,0,0,0,0,0,-262144,63,0,0,0,0,0,0,0,0,0,0,0,-262144,31,0,0,0,0,0,0,0,0,0,0,0,-262144,63,0,0,0,0,3,0,0,0,0,0,0,-262144,63,0,0,0,0,7,0,0,0,0,0,0,-262144,63,0,0,0,0,63,0,0,0,0,0,0,-262144,63,0,0,0,0,511,0,0,0,0,0,0,-524288,31,0,0,0,0,8191,0,0,0,0,0,0,-1048576,63,0,0,0,0,131071,0,0,0,0,0,0,-524288,63,0,0,0,0,262143,0,0,0,0,0,0,-524288,63,0,0,0,0,131071,0,0,0,0,0,0,-1048576,63,0,0,0,0,262143,0,0,0,0,0,0,-1048576,63,0,0,0,0,262143,0,0,0,0,0,0,-1048576,63,0,0,0,0,262143,0,0,0,0,0,0,-1048576,63,0,0,0,0,262143,0,0,0,0,0,0,-2097152,127,0,0,0,0,262143,0,0,0,0,0,0,-2097152,127,0,0,0,0,262143,0,0,0,0,0,0,-1048576,127,0,0,0,0,262143,0,0,0,0,0,0,-1048576,127,0,0,0,0,262143,0,0,0,0,0,0,-2097152,255,0,0,0,0,262143,0,0,0,0,0,0,-2097152,255,0,0,0,0,262142,0,0,0,0,0,0,-2097152,255,0,0,0,0,262142,0,0,0,0,0,0,-2097152,255,0,0,0,0,262142,0,0,0,0,0,0,-2097152,255,0,0,0,0,262140,0,0,0,0,0,0,-2097152,255,0,0,0,0,131068,0,0,0,0,0,0,-4194304,255,0,0,0,0,131068,0,0,0,0,0,0,-4194304,255,0,0,0,0,65528,0,0,0,0,0,0,-8388608,255,0,0,0,0,65528,0,0,0,0,0,0,-8388608,255,0,0,0,0,65528,0,0,0,0,0,0,-8388608,255,0,0,0,0,32760,0,0,0,0,0,0,-8388608,255,0,0,0,0,32760,0,0,0,0,0,0,-16777216,255,0,0,-2147483648,255,16368,0,0,0,0,0,0,-16777216,255,0,0,-536870912,1023,16368,0,0,0,0,0,0,-33554432,255,0,0,-16777216,4095,16352,0,0,0,0,0,0,-33554432,255,0,0,-8388608,262143,16352,0,0,0,0,0,0,-33554432,255,0,0,-1048576,2097151,16352,0,0,0,0,0,0,-67108864,255,0,0,-524288,8388607,16352,0,0,0,0,0,0,-67108864,255,0,0,-262144,16777215,16320,0,0,0,0,0,0,-67108864,255,0,0,-131072,16777215,100679648,0,0,0,0,0,0,-67108864,255,0,0,-16384,16776959,125861824,0,0,0,0,0,0,-134217728,255,0,0,-4096,16773121,62930880,0,0,0,0,0,0,-134217728,127,0,0,2147482624,16252928,32704,0,0,0,0,0,0,-134217728,127,0,0,268435200,14680064,16320,0,0,0,0,0,0,-134217728,127,0,0,134217600,0,32704,0,0,0,0,0,0,-33554432,127,0,1056964608,67108736,0,32704,0,0,0,0,0,0,-33554432,127,0,2130706432,33554368,0,65408,0,0,0,0,0,0,-33554432,127,0,-16777216,8388576,0,32640,0,0,0,0,0,0,-134217728,127,0,-16777216,2097136,0,32640,0,0,0,0,0,0,-134217728,63,0,-16776960,1048573,0,32640,0,0,0,0,0,0,-536870912,63,0,-16776448,1048575,0,32640,0,0,0,0,0,0,-536870912,63,0,-33553664,6291455,66752,32640,0,0,0,0,0,0,-536870912,63,0,2013266688,2097148,229376,32640,0,0,0,0,0,0,-536870912,63,0,256,4194300,229376,32640,0,0,0,0,0,0,-536870912,63,0,0,524280,196608,32512,0,0,8,0,0,0,-1073741824,63,0,0,-200,15,65280,0,0,24,0,0,0,-1073741824,63,0,0,-1867768,127,32512,0,0,56,0,0,0,-1073741824,63,0,0,-1056768,4095,32512,0,0,124,0,0,0,-1073741824,63,0,0,-1050624,8191,32512,0,0,508,0,0,0,-2147483648,31,0,0,-7866368,8191,32512,0,0,1020,0,0,0,-2147483648,31,0,0,-33030656,8095,32512,0,0,2046,0,0,0,-2147483648,63,0,0,-66586624,771,32512,0,0,4094,0,0,0,0,63,0,0,-134184960,1,32256,0,0,8190,0,0,0,0,63,0,0,1610612736,0,32256,0,0,16382,0,0,0,-2147483648,63,0,0,0,0,15872,0,0,32767,0,0,0,-2147483648,31,0,0,0,0,15872,0,-2147483648,65535,0,0,0,-2147483648,31,0,0,0,0,7680,0,0,65535,0,0,0,-2147483648,31,0,0,134217728,0,7680,0,-2147483648,65535,0,0,0,-2147483648,31,0,0,0,0,7680,0,-2147483648,65535,0,0,0,-2147483648,31,0,0,0,0,7680,0,-1073741824,65535,0,0,0,-2147483648,31,0,0,0,0,3072,0,-1073741824,65535,0,0,0,-2147483648,31,0,0,0,0,3072,0,-1073741824,65535,0,0,0,-2147483648,31,0,0,0,0,0,0,-2147483648,65535,0,0,0,-2147483648,31,0,0,0,0,0,0,-1073741824,65535,0,0,0,0,31,0,0,0,0,0,0,-1073741824,65535,0,0,0,0,31,0,0,0,0,0,0,-2147483648,65535,0,0,0,0,30,0,0,0,0,0,0,-1073741824,65535,0,0,0,0,30,0,0,0,0,0,0,-2147483648,65535,0,0,0,0,30,0,0,0,0,0,0,0,65535,0,0,0,0,28,0,0,0,0,0,0,0,65535,0,0,0,0,28,0,0,0,0,0,0,0,65535,0,0,0,0,28,0,0,0,0,0,0,0,65535,0,0,0,0,24,0,0,0,0,0,0,-2147483648,65535,0,0,0,0,0,0,0,0,0,0,0,-536870912,65535);//[$offset] |= intval32bits(1 << ($x & 0x1f)); + $bob = $this->bits[$offset]; + $bob |= 1 << ($x & 0x1f); + $this->bits[$offset] |= overflow32($bob); + //$this->bits[$offset] = intval32bits($this->bits[$offset]); + + //} +//16777216 + } + + public function _unset($x, $y) {//было unset, php не позволяет использовать unset + $offset = intval($y * $this->rowSize + ($x / 32)); + $this->bits[$offset] &= ~(1 << ($x & 0x1f)); + } + + + /**1 << (249 & 0x1f) + *

    Flips the given bit.

    + * + * @param $x; The horizontal component (i.e. which column) + * @param $y; The vertical component (i.e. which row) + */ + public function flip($x, $y) { + $offset = $y * $this->rowSize + intval($x / 32); + + $this->bits[$offset] = overflow32($this->bits[$offset]^(1 << ($x & 0x1f))); + } + + /** + * Exclusive-or (XOR): Flip the bit in this {@code BitMatrix} if the corresponding + * mask bit is set. + * + * @param $mask; XOR mask + */ + public function _xor($mask) {//было xor, php не позволяет использовать xor + if ($this->width != $mask->getWidth() || $this->height != $mask->getHeight() + || $this->rowSize != $mask->getRowSize()) { + throw new \InvalidArgumentException("input matrix dimensions do not match"); + } + $rowArray = new BitArray($this->width / 32 + 1); + for ($y = 0; $y < $this->height; $y++) { + $offset = $y * $this->rowSize; + $row = $mask->getRow($y, $rowArray)->getBitArray(); + for ($x = 0; $x < $this->rowSize; $x++) { + $this->bits[$offset + $x] ^= $row[$x]; + } + } + } + + /** + * Clears all bits (sets to false). + */ + public function clear() { + $max = count($this->bits); + for ($i = 0; $i < $max; $i++) { + $this->bits[$i] = 0; + } + } + + /** + *

    Sets a square region of the bit matrix to true.

    + * + * @param $left; The horizontal position to begin at (inclusive) + * @param $top; The vertical position to begin at (inclusive) + * @param $width; The width of the region + * @param $height; The height of the region + */ + public function setRegion($left, $top, $width, $height) { + if ($top < 0 || $left < 0) { + throw new \InvalidArgumentException("Left and top must be nonnegative"); + } + if ($height < 1 || $width < 1) { + throw new \InvalidArgumentException("Height and width must be at least 1"); + } + $right = $left + $width; + $bottom = $top + $height; + if ($bottom > $this->height || $right > $this->width) { //> this.height || right > this.width + throw new \InvalidArgumentException("The region must fit inside the matrix"); + } + for ($y = $top; $y < $bottom; $y++) { + $offset = $y * $this->rowSize; + for ($x = $left; $x < $right; $x++) { + $this->bits[$offset + intval($x / 32)] = overflow32($this->bits[$offset + intval($x / 32)]|= 1 << ($x & 0x1f)); + } + } + } + + /** + * A fast method to retrieve one row of data from the matrix as a BitArray. + * + * @param $y; The row to retrieve + * @param $row; An optional caller-allocated BitArray, will be allocated if null or too small + * @return The resulting BitArray - this reference should always be used even when passing + * your own row + */ + public function getRow($y, $row) { + if ($row == null || $row->getSize() < $this->width) { + $row = new BitArray($this->width); + } else { + $row->clear(); + } + $offset = $y * $this->rowSize; + for ($x = 0; $x < $this->rowSize; $x++) { + $row->setBulk($x * 32, $this->bits[$offset + $x]); + } + return $row; + } + + /** + * @param $y; row to set + * @param $row; {@link BitArray} to copy from + */ + public function setRow($y, $row) { + $this->bits = arraycopy($row->getBitArray(), 0, $this->bits, $y * $this->rowSize, $this->rowSize); + } + + /** + * Modifies this {@code BitMatrix} to represent the same but rotated 180 degrees + */ + public function rotate180() { + $width = $this->getWidth(); + $height = $this-getHeight(); + $topRow = new BitArray($width); + $bottomRow = new BitArray($width); + for ($i = 0; $i < ($height+1) / 2; $i++) { + $topRow = $this->getRow($i, $topRow); + $bottomRow = $this->getRow($height - 1 - $i, $bottomRow); + $topRow->reverse(); + $bottomRow->reverse(); + $this->setRow($i, $bottomRow); + $this->setRow($height - 1 - $i, $topRow); + } + } + + /** + * This is useful in detecting the enclosing rectangle of a 'pure' barcode. + * + * @return {@code left,top,width,height} enclosing rectangle of all 1 bits, or null if it is all white + */ + public function getEnclosingRectangle() { + $left = $this->width; + $top = $this->height; + $right = -1; + $bottom = -1; + + for ($y = 0; $y < $this->height; $y++) { + for ($x32 = 0; $x32 < $this->rowSize; $x32++) { + $theBits = $this->bits[$y * $this->rowSize + $x32]; + if ($theBits != 0) { + if ($y < $top) { + $top = $y; + } + if ($y > $bottom) { + $bottom = $y; + } + if ($x32 * 32 < $left) { + $bit = 0; + while (($theBits << (31 - $bit)) == 0) { + $bit++; + } + if (($x32 * 32 + $bit) < $left) { + $left = $x32 * 32 + $bit; + } + } + if ($x32 * 32 + 31 > $right) { + $bit = 31; + while ((sdvig3($theBits, $bit)) == 0) {//>>> + $bit--; + } + if (($x32 * 32 + $bit) > $right) { + $right = $x32 * 32 + $bit; + } + } + } + } + } + + $width = $right - $left; + $height = $bottom - $top; + + if ($width < 0 || $height < 0) { + return null; + } + + return array($left, $top, $width, $height); + } + + /** + * This is useful in detecting a corner of a 'pure' barcode. + * + * @return {@code x,y} coordinate of top-left-most 1 bit, or null if it is all white + */ + public function getTopLeftOnBit() { + $bitsOffset = 0; + while ($bitsOffset < count($this->bits) && $this->bits[$bitsOffset] == 0) { + $bitsOffset++; + } + if ($bitsOffset == count($this->bits)) { + return null; + } + $y = $bitsOffset / $this->rowSize; + $x = ($bitsOffset % $this->rowSize) * 32; + + $theBits = $this->bits[$bitsOffset]; + $bit = 0; + while (($theBits << (31-$bit)) == 0) { + $bit++; + } + $x += $bit; + return array($x, $y); + } + + public function getBottomRightOnBit() { + $bitsOffset = count($this->bits) - 1; + while ($bitsOffset >= 0 && $this->bits[$bitsOffset] == 0) { + $bitsOffset--; + } + if ($bitsOffset < 0) { + return null; + } + + $y = $bitsOffset / $this->rowSize; + $x = ($bitsOffset % $this->rowSize) * 32; + + $theBits = $this->bits[$bitsOffset]; + $bit = 31; + while ((sdvig3($theBits, $bit)) == 0) {//>>> + $bit--; + } + $x += $bit; + + return array($x, $y); + } + + /** + * @return The width of the matrix + */ + public function getWidth() { + return $this->width; + } + + /** + * @return The height of the matrix + */ + public function getHeight() { + return $this->height; + } + + /** + * @return The row size of the matrix + */ + public function getRowSize() { + return $this->rowSize; + } + + //@Override + public function equals($o) { + if (!($o instanceof BitMatrix)) { + return false; + } + $other = $o; + return $this->width == $other->width && $this->height == $other->height && $this->rowSize == $other->rowSize && + $this->bits===$other->bits; + } + + //@Override + public function hashCode() { + $hash = $this->width; + $hash = 31 * $hash + $this->width; + $hash = 31 * $hash + $this->height; + $hash = 31 * $hash + $this->rowSize; + $hash = 31 * $hash + hashCode($this->bits); + return $hash; + } + + //@Override + public function toString($setString='', $unsetString='',$lineSeparator='') { + if(!$setString||!$unsetString){ + return (string)"X "." "; + } + if($lineSeparator&&$lineSeparator!=="\n"){ + return $this->toString_($setString, $unsetString, $lineSeparator); + } + return (string)($setString. $unsetString. "\n"); + } + + /** + * @deprecated call {@link #toString(String,String)} only, which uses \n line separator always + */ + // @Deprecated + public function toString_($setString, $unsetString, $lineSeparator) { + //$result = new StringBuilder(height * (width + 1)); + $result = ''; + for ($y = 0; $y < $this->height; $y++) { + for ($x = 0; $x < $this->width; $x++) { + $result .= ($this->get($x, $y) ? $setString : $unsetString); + } + $result .= ($lineSeparator); + } + return (string)$result; + } + +// @Override + public function _clone() {//clone() + return new BitMatrix($this->width, $this->height, $this->rowSize, $this->bits); + } + +} \ No newline at end of file diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/BitSource.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/BitSource.php new file mode 100644 index 0000000..0585506 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/BitSource.php @@ -0,0 +1,112 @@ +This provides an easy abstraction to read bits at a time from a sequence of bytes, where the + * number of bits read is not often a multiple of 8.

    + * + *

    This class is thread-safe but not reentrant -- unless the caller modifies the bytes array + * it passed in, in which case all bets are off.

    + * + * @author Sean Owen + */ +final class BitSource { + + private $bytes; + private $byteOffset = 0; + private $bitOffset = 0; + + /** + * @param bytes bytes from which this will read bits. Bits will be read from the first byte first. + * Bits are read within a byte from most-significant to least-significant bit. + */ + public function __construct($bytes) { + $this->bytes = $bytes; + } + + /** + * @return index of next bit in current byte which would be read by the next call to {@link #readBits(int)}. + */ + public function getBitOffset() { + return $this->bitOffset; + } + + /** + * @return index of next byte in input byte array which would be read by the next call to {@link #readBits(int)}. + */ + public function getByteOffset() { + return $this->byteOffset; + } + + /** + * @param numBits number of bits to read + * @return int representing the bits read. The bits will appear as the least-significant + * bits of the int + * @throws IllegalArgumentException if numBits isn't in [1,32] or more than is available + */ + public function readBits($numBits) { + if ($numBits < 1 || $numBits > 32 || $numBits > $this->available()) { + throw new \InvalidArgumentException(strval($numBits)); + } + + $result = 0; + + // First, read remainder from current byte + if ($this->bitOffset > 0) { + $bitsLeft = 8 - $this->bitOffset; + $toRead = $numBits < $bitsLeft ? $numBits : $bitsLeft; + $bitsToNotRead = $bitsLeft - $toRead; + $mask = (0xFF >> (8 - $toRead)) << $bitsToNotRead; + $result = ($this->bytes[$this->byteOffset] & $mask) >> $bitsToNotRead; + $numBits -= $toRead; + $this->bitOffset += $toRead; + if ($this->bitOffset == 8) { + $this->bitOffset = 0; + $this->byteOffset++; + } + } + + // Next read whole bytes + if ($numBits > 0) { + while ($numBits >= 8) { + $result = ($result << 8) | ($this->bytes[$this->byteOffset] & 0xFF); + $this->byteOffset++; + $numBits -= 8; + } + + // Finally read a partial byte + if ($numBits > 0) { + $bitsToNotRead = 8 - $numBits; + $mask = (0xFF >> $bitsToNotRead) << $bitsToNotRead; + $result = ($result << $numBits) | (($this->bytes[$this->byteOffset] & $mask) >> $bitsToNotRead); + $this->bitOffset += $numBits; + } + } + + return $result; + } + + /** + * @return number of bits that can be read successfully + */ + public function available() { + return 8 * (count($this->bytes) - $this->byteOffset) - $this->bitOffset; + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/CharacterSetEci.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/CharacterSetEci.php new file mode 100644 index 0000000..b440747 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/CharacterSetEci.php @@ -0,0 +1,154 @@ + self::ISO8859_1, + 'ISO-8859-2' => self::ISO8859_2, + 'ISO-8859-3' => self::ISO8859_3, + 'ISO-8859-4' => self::ISO8859_4, + 'ISO-8859-5' => self::ISO8859_5, + 'ISO-8859-6' => self::ISO8859_6, + 'ISO-8859-7' => self::ISO8859_7, + 'ISO-8859-8' => self::ISO8859_8, + 'ISO-8859-9' => self::ISO8859_9, + 'ISO-8859-10' => self::ISO8859_10, + 'ISO-8859-11' => self::ISO8859_11, + 'ISO-8859-12' => self::ISO8859_12, + 'ISO-8859-13' => self::ISO8859_13, + 'ISO-8859-14' => self::ISO8859_14, + 'ISO-8859-15' => self::ISO8859_15, + 'ISO-8859-16' => self::ISO8859_16, + 'SHIFT-JIS' => self::SJIS, + 'WINDOWS-1250' => self::CP1250, + 'WINDOWS-1251' => self::CP1251, + 'WINDOWS-1252' => self::CP1252, + 'WINDOWS-1256' => self::CP1256, + 'UTF-16BE' => self::UNICODE_BIG_UNMARKED, + 'UTF-8' => self::UTF8, + 'ASCII' => self::ASCII, + 'GBK' => self::GB18030, + 'EUC-KR' => self::EUC_KR, + ); + /** + * Additional possible values for character sets. + * + * @var array + */ + protected static $additionalValues = array( + self::CP437 => 2, + self::ASCII => 170, + ); + /** + * Gets character set ECI by value. + * + * @param string $name + * @return CharacterSetEci|null + */ + public static function getCharacterSetECIByValue($value) + { + if ($value < 0 || $value >= 900) { + throw new Exception\InvalidArgumentException('Value must be between 0 and 900'); + } + if (false !== ($key = array_search($value, self::$additionalValues))) { + $value = $key; + } + array_search($value, self::$nameToEci); + try + { + self::setName($value); + return new self($value); + } catch (Exception\UnexpectedValueException $e) { + return null; + } + } + + private static function setName($value) + { + foreach (self::$nameToEci as $name => $key) { + if($key == $value) + { + self::$name = $name; + return true; + } + } + if(self::$name == null) + { + foreach (self::$additionalValues as $name => $key) { + if($key == $value) + { + self::$name = $name; + return true; + } + } + } + } + /** + * Gets character set ECI name. + * + * @return character set ECI name|null + */ + public static function name() + { + return self::$name; + } + /** + * Gets character set ECI by name. + * + * @param string $name + * @return CharacterSetEci|null + */ + public static function getCharacterSetECIByName($name) + { + $name = strtoupper($name); + if (isset(self::$nameToEci[$name])) { + return new self(self::$nameToEci[$name]); + } + return null; + } +} \ No newline at end of file diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/DecoderResult.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/DecoderResult.php new file mode 100644 index 0000000..5568f90 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/DecoderResult.php @@ -0,0 +1,109 @@ +Encapsulates the result of decoding a matrix of bits. This typically + * applies to 2D barcode formats. For now it contains the raw bytes obtained, + * as well as a String interpretation of those bytes, if applicable.

    + * + * @author Sean Owen + */ +final class DecoderResult { + + private $rawBytes; + private $text; + private $byteSegments; + private $ecLevel; + private $errorsCorrected; + private $erasures; + private $other; + private $structuredAppendParity; + private $structuredAppendSequenceNumber; + + + + public function __construct($rawBytes, + $text, + $byteSegments, + $ecLevel, + $saSequence = -1, + $saParity = -1) { + $this->rawBytes = $rawBytes; + $this->text = $text; + $this->byteSegments = $byteSegments; + $this->ecLevel = $ecLevel; + $this->structuredAppendParity = $saParity; + $this->structuredAppendSequenceNumber = $saSequence; + } + + public function getRawBytes() { + return $this->rawBytes; + } + + public function getText() { + return $this->text; + } + + public function getByteSegments() { + return $this->byteSegments; + } + + public function getECLevel() { + return $this->ecLevel; + } + + public function getErrorsCorrected() { + return $this->errorsCorrected; + } + + public function setErrorsCorrected($errorsCorrected) { + $this->errorsCorrected = $errorsCorrected; + } + + public function getErasures() { + return $this->erasures; + } + + public function setErasures($erasures) { + $this->erasures = $erasures; + } + + public function getOther() { + return $this->other; + } + + public function setOther($other) { + $this->other = $other; + } + + public function hasStructuredAppend() { + return $this->structuredAppendParity >= 0 && $this->structuredAppendSequenceNumber >= 0; + } + + public function getStructuredAppendParity() { + return $this->structuredAppendParity; + } + + public function getStructuredAppendSequenceNumber() { + return $this->structuredAppendSequenceNumber; + } + +} \ No newline at end of file diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/DefaultGridSampler.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/DefaultGridSampler.php new file mode 100644 index 0000000..1e790a8 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/DefaultGridSampler.php @@ -0,0 +1,89 @@ +sampleGrid_($image, $dimensionX, $dimensionY, $transform); + } + +//@Override + public function sampleGrid_($image, + $dimensionX, + $dimensionY, + $transform) { + if ($dimensionX <= 0 || $dimensionY <= 0) { + throw NotFoundException::getNotFoundInstance(); + } + $bits = new BitMatrix($dimensionX, $dimensionY); + $points = fill_array(0,2 * $dimensionX,0.0); + for ($y = 0; $y < $dimensionY; $y++) { + $max = count($points); + $iValue = (float) $y + 0.5; + for ($x = 0; $x < $max; $x += 2) { + $points[$x] = (float) ($x / 2) + 0.5; + $points[$x + 1] = $iValue; + } + $transform->transformPoints($points); +// Quick check to see if points transformed to something inside the image; +// sufficient to check the endpoints + $this->checkAndNudgePoints($image, $points); + try { + for ($x = 0; $x < $max; $x += 2) { + if ($image->get((int) $points[$x], (int) $points[$x + 1])) { +// Black(-ish) pixel + $bits->set($x / 2, $y); + } + } + } catch (\Exception $aioobe) {//ArrayIndexOutOfBoundsException +// This feels wrong, but, sometimes if the finder patterns are misidentified, the resulting +// transform gets "twisted" such that it maps a straight line of points to a set of points +// whose endpoints are in bounds, but others are not. There is probably some mathematical +// way to detect this about the transformation that I don't know yet. +// This results in an ugly runtime exception despite our clever checks above -- can't have +// that. We could check each point's coordinates but that feels duplicative. We settle for +// catching and wrapping ArrayIndexOutOfBoundsException. + throw NotFoundException::getNotFoundInstance(); + } + } + return $bits; + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/DetectorResult.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/DetectorResult.php new file mode 100644 index 0000000..97f0567 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/DetectorResult.php @@ -0,0 +1,47 @@ +Encapsulates the result of detecting a barcode in an image. This includes the raw + * matrix of black/white pixels corresponding to the barcode, and possibly points of interest + * in the image, like the location of finder patterns or corners of the barcode in the image.

    + * + * @author Sean Owen + */ +class DetectorResult { + + private $bits; + private $points; + + public function __construct($bits, $points) { + $this->bits = $bits; + $this->points = $points; + } + + public final function getBits() { + return $this->bits; + } + + public final function getPoints() { + return $this->points; + } + +} \ No newline at end of file diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/GlobalHistogramBinarizer.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/GlobalHistogramBinarizer.php new file mode 100644 index 0000000..2df580f --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/GlobalHistogramBinarizer.php @@ -0,0 +1,206 @@ +luminances = self::$EMPTY; + $this->buckets = fill_array(0, self::$LUMINANCE_BUCKETS,0); + $this->source = $source; + } + +// Applies simple sharpening to the row data to improve performance of the 1D Readers. +//@Override + public function getBlackRow($y, $row=null) { + $this->source = $this->getLuminanceSource(); + $width = $this->source->getWidth(); + if ($row == null || $row->getSize() < $width) { + $row = new BitArray($width); + } else { + $row->clear(); + } + + $this->initArrays($width); + $localLuminances = $this->source->getRow($y, $this->luminances); + $localBuckets = $this->buckets; + for ($x = 0; $x < $width; $x++) { + $pixel = $localLuminances[$x] & 0xff; + $localBuckets[$pixel >> self::$LUMINANCE_SHIFT]++; + } + $blackPoint = $this->estimateBlackPoint($localBuckets); + + $left = $localLuminances[0] & 0xff; + $center = $localLuminances[1] & 0xff; + for ($x = 1; $x < $width - 1; $x++) { + $right = $localLuminances[$x + 1] & 0xff; +// A simple -1 4 -1 box filter with a weight of 2. + $luminance = (($center * 4) - $left - $right) / 2; + if ($luminance < $blackPoint) { + $row->set($x); + } + $left = $center; + $center = $right; + } + return $row; + } + +// Does not sharpen the data, as this call is intended to only be used by 2D Readers. +//@Override + public function getBlackMatrix(){ + $source = $this->getLuminanceSource(); + $width = $source->getWidth(); + $height = $source->getHeight(); + $matrix = new BitMatrix($width, $height); + +// Quickly calculates the histogram by sampling four rows from the image. This proved to be +// more robust on the blackbox tests than sampling a diagonal as we used to do. + $this->initArrays($width); + $localBuckets = $this->buckets; + for ($y = 1; $y < 5; $y++) { + $row = intval($height * $y / 5); + $localLuminances = $source->getRow($row, $this->luminances); + $right = intval(($width * 4) / 5); + for ($x = intval($width / 5); $x < $right; $x++) { + $pixel = intval32bits($localLuminances[intval($x)] & 0xff); + $localBuckets[intval32bits($pixel >> self::$LUMINANCE_SHIFT)]++; + } + } + $blackPoint = $this->estimateBlackPoint($localBuckets); + +// We delay reading the entire image luminance until the black point estimation succeeds. +// Although we end up reading four rows twice, it is consistent with our motto of +// "fail quickly" which is necessary for continuous scanning. + $localLuminances = $source->getMatrix(); + for ($y = 0; $y < $height; $y++) { + $offset = $y * $width; + for ($x = 0; $x< $width; $x++) { + $pixel = intval($localLuminances[$offset + $x] & 0xff); + if ($pixel < $blackPoint) { + $matrix->set($x, $y); + } + } + } + + return $matrix; + } + +//@Override + public function createBinarizer($source) { + return new GlobalHistogramBinarizer($source); + } + + private function initArrays($luminanceSize) { + if (count($this->luminances) < $luminanceSize) { + $this->luminances = array(); + } + for ($x = 0; $x < self::$LUMINANCE_BUCKETS; $x++) { + $this->buckets[$x] = 0; + } + } + + private static function estimateBlackPoint($buckets){ +// Find the tallest peak in the histogram. + $numBuckets = count($buckets); + $maxBucketCount = 0; + $firstPeak = 0; + $firstPeakSize = 0; + for ($x = 0; $x < $numBuckets; $x++) { + if ($buckets[$x] > $firstPeakSize) { + $firstPeak = $x; + $firstPeakSize = $buckets[$x]; + } + if ($buckets[$x] > $maxBucketCount) { + $maxBucketCount = $buckets[$x]; + } + } + +// Find the second-tallest peak which is somewhat far from the tallest peak. + $secondPeak = 0; + $secondPeakScore = 0; + for ($x = 0; $x < $numBuckets; $x++) { + $distanceToBiggest = $x - $firstPeak; +// Encourage more distant second peaks by multiplying by square of distance. + $score = $buckets[$x] * $distanceToBiggest * $distanceToBiggest; + if ($score > $secondPeakScore) { + $secondPeak = $x; + $secondPeakScore = $score; + } + } + +// Make sure firstPeak corresponds to the black peak. + if ($firstPeak > $secondPeak) { + $temp = $firstPeak; + $firstPeak = $secondPeak; + $secondPeak = $temp; + } + +// If there is too little contrast in the image to pick a meaningful black point, throw rather +// than waste time trying to decode the image, and risk false positives. + if ($secondPeak - $firstPeak <= $numBuckets / 16) { + throw NotFoundException::getNotFoundInstance(); + } + +// Find a valley between them that is low and closer to the white peak. + $bestValley = $secondPeak - 1; + $bestValleyScore = -1; + for ($x = $secondPeak - 1; $x > $firstPeak; $x--) { + $fromFirst = $x - $firstPeak; + $score = $fromFirst * $fromFirst * ($secondPeak - $x) * ($maxBucketCount - $buckets[$x]); + if ($score > $bestValleyScore) { + $bestValley = $x; + $bestValleyScore = $score; + } + } + + return intval32bits($bestValley << self::$LUMINANCE_SHIFT); + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/GridSampler.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/GridSampler.php new file mode 100644 index 0000000..30c3aee --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/GridSampler.php @@ -0,0 +1,177 @@ +Checks a set of points that have been transformed to sample points on an image against + * the image's dimensions to see if the point are even within the image.

    + * + *

    This method will actually "nudge" the endpoints back onto the image if they are found to be + * barely (less than 1 pixel) off the image. This accounts for imperfect detection of finder + * patterns in an image where the QR Code runs all the way to the image border.

    + * + *

    For efficiency, the method will check points from either end of the line until one is found + * to be within the image. Because the set of points are assumed to be linear, this is valid.

    + * + * @param image image into which the points should map + * @param points actual points in x1,y1,...,xn,yn form + * @throws NotFoundException if an endpoint is lies outside the image boundaries + */ + protected static function checkAndNudgePoints($image, + $points) { + $width = $image->getWidth(); + $height = $image->getHeight(); +// Check and nudge points from start until we see some that are OK: + $nudged = true; + for ($offset = 0; $offset < count($points) && $nudged; $offset += 2) { + $x = (int) $points[$offset]; + $y = (int) $points[$offset + 1]; + if ($x < -1 || $x > $width || $y < -1 || $y > $height) { + throw NotFoundException::getNotFoundInstance(); + } + $nudged = false; + if ($x == -1) { + $points[$offset] = 0.0; + $nudged = true; + } else if ($x == $width) { + $points[$offset] = $width - 1; + $nudged = true; + } + if ($y == -1) { + $points[$offset + 1] = 0.0; + $nudged = true; + } else if ($y == $height) { + $points[$offset + 1] = $height - 1; + $nudged = true; + } + } +// Check and nudge points from end: + $nudged = true; + for ($offset = count($points) - 2; $offset >= 0 && $nudged; $offset -= 2) { + $x = (int) $points[$offset]; + $y = (int) $points[$offset + 1]; + if ($x < -1 || $x > $width || $y < -1 || $y > $height) { + throw NotFoundException::getNotFoundInstance(); + } + $nudged = false; + if ($x == -1) { + $points[$offset] = 0.0; + $nudged = true; + } else if ($x == $width) { + $points[$offset] = $width - 1; + $nudged = true; + } + if ($y == -1) { + $points[$offset + 1] = 0.0; + $nudged = true; + } else if ($y == $height) { + $points[$offset + 1] = $height - 1; + $nudged = true; + } + } + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/HybridBinarizer.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/HybridBinarizer.php new file mode 100644 index 0000000..670562c --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/HybridBinarizer.php @@ -0,0 +1,259 @@ +matrix != null) { + return $this->matrix; + } + $source = $this->getLuminanceSource(); + $width = $source->getWidth(); + $height = $source->getHeight(); + if ($width >= self::$MINIMUM_DIMENSION && $height >= self::$MINIMUM_DIMENSION) { + $luminances = $source->getMatrix(); + $subWidth = $width >> self::$BLOCK_SIZE_POWER; + if (($width & self::$BLOCK_SIZE_MASK) != 0) { + $subWidth++; + } + $subHeight = $height >> self::$BLOCK_SIZE_POWER; + if (($height & self::$BLOCK_SIZE_MASK) != 0) { + $subHeight++; + } + $blackPoints = $this->calculateBlackPoints($luminances, $subWidth, $subHeight, $width, $height); + + $newMatrix = new BitMatrix($width, $height); + $this->calculateThresholdForBlock($luminances, $subWidth, $subHeight, $width, $height, $blackPoints, $newMatrix); + $this->matrix = $newMatrix; + } else { +// If the image is too small, fall back to the global histogram approach. + $this->matrix = parent::getBlackMatrix(); + } + return $this->matrix; + } + +//@Override + public function createBinarizer($source) { + return new HybridBinarizer($source); + } + + /** + * For each block in the image, calculate the average black point using a 5x5 grid + * of the blocks around it. Also handles the corner cases (fractional blocks are computed based + * on the last pixels in the row/column which are also used in the previous block). + */ + private static function calculateThresholdForBlock($luminances, + $subWidth, + $subHeight, + $width, + $height, + $blackPoints, + $matrix) { + for ($y = 0; $y < $subHeight; $y++) { + $yoffset = intval32bits($y << self::$BLOCK_SIZE_POWER); + $maxYOffset = $height - self::$BLOCK_SIZE; + if ($yoffset > $maxYOffset) { + $yoffset = $maxYOffset; + } + for ($x = 0; $x < $subWidth; $x++) { + $xoffset = intval32bits($x << self::$BLOCK_SIZE_POWER); + $maxXOffset = $width - self::$BLOCK_SIZE; + if ($xoffset > $maxXOffset) { + $xoffset = $maxXOffset; + } + $left = self::cap($x, 2, $subWidth - 3); + $top = self::cap($y, 2, $subHeight - 3); + $sum = 0; + for ($z = -2; $z <= 2; $z++) { + $blackRow = $blackPoints[$top + $z]; + $sum += $blackRow[$left - 2] + $blackRow[$left - 1] + $blackRow[$left] + $blackRow[$left + 1] + $blackRow[$left + 2]; + } + $average = intval($sum / 25); + + self::thresholdBlock($luminances, $xoffset, $yoffset, $average, $width, $matrix); + } + } + } + + private static function cap($value, $min, $max) { + if($value<$min){ + return $min; + }elseif($value>$max){ + return $max; + }else{ + return $value; + } + + + + } + + /** + * Applies a single threshold to a block of pixels. + */ + private static function thresholdBlock($luminances, + $xoffset, + $yoffset, + $threshold, + $stride, + $matrix) { + + for ($y = 0, $offset = $yoffset * $stride + $xoffset; $y < self::$BLOCK_SIZE; $y++, $offset += $stride) { + for ($x = 0; $x < self::$BLOCK_SIZE; $x++) { +// Comparison needs to be <= so that black == 0 pixels are black even if the threshold is 0. + if (($luminances[$offset + $x] & 0xFF) <= $threshold) { + $matrix->set($xoffset + $x, $yoffset + $y); + } + } + } + } + + /** + * Calculates a single black point for each block of pixels and saves it away. + * See the following thread for a discussion of this algorithm: + * http://groups.google.com/group/zxing/browse_thread/thread/d06efa2c35a7ddc0 + */ + private static function calculateBlackPoints($luminances, + $subWidth, + $subHeight, + $width, + $height) { + $blackPoints = fill_array(0,$subHeight,0); + foreach($blackPoints as $key=>$point){ + $blackPoints[$key] = fill_array(0,$subWidth,0); + } + for ($y = 0; $y < $subHeight; $y++) { + $yoffset = intval32bits($y << self::$BLOCK_SIZE_POWER); + $maxYOffset = $height - self::$BLOCK_SIZE; + if ($yoffset > $maxYOffset) { + $yoffset = $maxYOffset; + } + for ($x = 0; $x < $subWidth; $x++) { + $xoffset = intval32bits($x << self::$BLOCK_SIZE_POWER); + $maxXOffset = $width - self::$BLOCK_SIZE; + if ($xoffset > $maxXOffset) { + $xoffset = $maxXOffset; + } + $sum = 0; + $min = 0xFF; + $max = 0; + for ($yy = 0, $offset = $yoffset * $width + $xoffset; $yy < self::$BLOCK_SIZE; $yy++, $offset += $width) { + for ($xx = 0; $xx < self::$BLOCK_SIZE; $xx++) { + $pixel = intval32bits(intval($luminances[intval($offset +$xx)]) & 0xFF); + $sum += $pixel; +// still looking for good contrast + if ($pixel < $min) { + $min = $pixel; + } + if ($pixel > $max) { + $max = $pixel; + } + } +// short-circuit min/max tests once dynamic range is met + if ($max - $min > self::$MIN_DYNAMIC_RANGE) { +// finish the rest of the rows quickly + for ($yy++, $offset += $width; $yy < self::$BLOCK_SIZE; $yy++, $offset += $width) { + for ($xx = 0; $xx < self::$BLOCK_SIZE; $xx++) { + $sum += intval32bits($luminances[$offset +$xx] & 0xFF); + } + } + } + } + +// The default estimate is the average of the values in the block. + $average = intval32bits($sum >> (self::$BLOCK_SIZE_POWER * 2)); + if ($max - $min <= self::$MIN_DYNAMIC_RANGE) { +// If variation within the block is low, assume this is a block with only light or only +// dark pixels. In that case we do not want to use the average, as it would divide this +// low contrast area into black and white pixels, essentially creating data out of noise. +// +// The default assumption is that the block is light/background. Since no estimate for +// the level of dark pixels exists locally, use half the min for the block. + $average = intval($min / 2); + + if ($y > 0 && $x > 0) { +// Correct the "white background" assumption for blocks that have neighbors by comparing +// the pixels in this block to the previously calculated black points. This is based on +// the fact that dark barcode symbology is always surrounded by some amount of light +// background for which reasonable black point estimates were made. The bp estimated at +// the boundaries is used for the interior. + +// The (min < bp) is arbitrary but works better than other heuristics that were tried. + $averageNeighborBlackPoint = + intval(($blackPoints[$y - 1][$x] + (2 * $blackPoints[$y][$x - 1]) + $blackPoints[$y - 1][$x - 1]) / 4); + if ($min < $averageNeighborBlackPoint) { + $average = $averageNeighborBlackPoint; + } + } + } + $blackPoints[$y][$x] = intval($average); + } + } + return $blackPoints; + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/PerspectiveTransform.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/PerspectiveTransform.php new file mode 100644 index 0000000..7c046a8 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/PerspectiveTransform.php @@ -0,0 +1,161 @@ +This class implements a perspective transform in two dimensions. Given four source and four + * destination points, it will compute the transformation implied between them. The code is based + * directly upon section 3.4.2 of George Wolberg's "Digital Image Warping"; see pages 54-56.

    + * + * @author Sean Owen + */ +final class PerspectiveTransform { + + private $a11; + private $a12; + private $a13; + private $a21; + private $a22; + private $a23; + private $a31; + private $a32; + private $a33; + + private function __construct($a11, $a21, $a31, + $a12, $a22, $a32, + $a13, $a23, $a33) { + $this->a11 = $a11; + $this->a12 = $a12; + $this->a13 = $a13; + $this->a21 = $a21; + $this->a22 = $a22; + $this->a23 = $a23; + $this->a31 = $a31; + $this->a32 = $a32; + $this->a33 = $a33; + } + + public static function quadrilateralToQuadrilateral($x0, $y0, + $x1, $y1, + $x2, $y2, + $x3, $y3, + $x0p, $y0p, + $x1p, $y1p, + $x2p, $y2p, + $x3p, $y3p) { + + $qToS = self::quadrilateralToSquare($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3); + $sToQ = self::squareToQuadrilateral($x0p, $y0p, $x1p, $y1p, $x2p, $y2p, $x3p, $y3p); + return $sToQ->times($qToS); + } + + public function transformPoints(&$points,&$yValues=0) { + if($yValues) { + $this->transformPoints_($points,$yValues); + return; + } + $max =count($points); + $a11 = $this->a11; + $a12 = $this->a12; + $a13 = $this->a13; + $a21 = $this->a21; + $a22 = $this->a22; + $a23 = $this->a23; + $a31 = $this->a31; + $a32 = $this->a32; + $a33 = $this->a33; + for ($i = 0; $i < $max; $i += 2) { + $x = $points[$i]; + $y = $points[$i + 1]; + $denominator = $a13 * $x + $a23 * $y + $a33; + $points[$i] = ($a11 * $x + $a21 * $y + $a31) / $denominator; + $points[$i + 1] = ($a12 * $x + $a22 * $y +$a32) / $denominator; + } + } + + public function transformPoints_(&$xValues, &$yValues) { + $n = count($xValues); + for ($i = 0; $i < $n; $i ++) { + $x = $xValues[$i]; + $y = $yValues[$i]; + $denominator = $this->a13 * $x + $this->a23 * $y + $this->a33; + $xValues[$i] = ($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator; + $yValues[$i] = ($this->a12 * $x + $this->a22 *$y + $this->a32) / $denominator; + } + } + + public static function squareToQuadrilateral($x0, $y0, + $x1, $y1, + $x2, $y2, + $x3, $y3) { + $dx3 = $x0 - $x1 + $x2 - $x3; + $dy3 = $y0 - $y1 + $y2 - $y3; + if ($dx3 == 0.0 && $dy3 == 0.0) { +// Affine + return new PerspectiveTransform($x1 - $x0, $x2 - $x1, $x0, + $y1 - $y0, $y2 - $y1, $y0, + 0.0, 0.0, 1.0); + } else { + $dx1 = $x1 - $x2; + $dx2 = $x3 - $x2; + $dy1 = $y1 - $y2; + $dy2 = $y3 - $y2; + $denominator = $dx1 * $dy2 - $dx2 * $dy1; + $a13 = ($dx3 * $dy2 - $dx2 * $dy3) / $denominator; + $a23 = ($dx1 * $dy3 - $dx3 * $dy1) / $denominator; + return new PerspectiveTransform($x1 - $x0 + $a13 * $x1, $x3 - $x0 + $a23 * $x3, $x0, + $y1 - $y0 + $a13 * $y1, $y3 - $y0 + $a23 * $y3, $y0, + $a13, $a23, 1.0); + } + } + + public static function quadrilateralToSquare($x0, $y0, + $x1, $y1, + $x2, $y2, + $x3, $y3) { +// Here, the adjoint serves as the inverse: + return self::squareToQuadrilateral($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)->buildAdjoint(); + } + + function buildAdjoint() { +// Adjoint is the transpose of the cofactor matrix: + return new PerspectiveTransform($this->a22 * $this->a33 - $this->a23 * $this->a32, + $this->a23 * $this->a31 - $this->a21 * $this->a33, + $this->a21 * $this->a32 - $this->a22 * $this->a31, + $this->a13 * $this->a32 - $this->a12 * $this->a33, + $this->a11 * $this->a33 - $this->a13 * $this->a31, + $this->a12 * $this->a31 - $this->a11 * $this->a32, + $this->a12 * $this->a23 - $this->a13 * $this->a22, + $this->a13 * $this->a21 - $this->a11 * $this->a23, + $this->a11 * $this->a22 - $this->a12 * $this->a21); + } + + function times($other) { + return new PerspectiveTransform($this->a11 * $other->a11 + $this->a21 * $other->a12 + $this->a31 * $other->a13, + $this->a11 * $other->a21 + $this->a21 * $other->a22 + $this->a31 * $other->a23, + $this->a11 * $other->a31 + $this->a21 * $other->a32 + $this->a31 * $other->a33, + $this->a12 * $other->a11 + $this->a22 * $other->a12 + $this->a32 * $other->a13, + $this->a12 * $other->a21 + $this->a22 * $other->a22 + $this->a32 * $other->a23, + $this->a12 * $other->a31 + $this->a22 * $other->a32 + $this->a32 * $other->a33, + $this->a13 * $other->a11 + $this->a23 * $other->a12 + $this->a33 * $other->a13, + $this->a13 * $other->a21 + $this->a23 * $other->a22 + $this->a33 * $other->a23, + $this->a13 * $other->a31 + $this->a23 * $other->a32 + $this->a33 * $other->a33); + + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/customFunctions.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/customFunctions.php new file mode 100644 index 0000000..6ae71cc --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/customFunctions.php @@ -0,0 +1,93 @@ +>= 1; + $num++; + } + return $num; +} +function intval32bits($value) +{ + $value = ($value & 0xFFFFFFFF); + + if ($value & 0x80000000) + $value = -((~$value & 0xFFFFFFFF) + 1); + + return $value; +} + +function uRShift($a, $b) +{ + + if($b == 0) return $a; + return ($a >> $b) & ~(1<<(8*PHP_INT_SIZE-1)>>($b-1)); +} +/* +function sdvig3($num,$count=1){//>>> 32 bit + $s = decbin($num); + + $sarray = str_split($s,1); + $sarray = array_slice($sarray,-32);//32bit + + for($i=0;$i<=1;$i++) { + array_pop($sarray); + array_unshift($sarray, '0'); + } + return bindec(implode($sarray)); +} +*/ + +function sdvig3($a,$b) { + + if ($a >= 0) { + return bindec(decbin($a>>$b)); //simply right shift for positive number + } + + $bin = decbin($a>>$b); + + $bin = substr($bin, $b); // zero fill on the left side + + $o = bindec($bin); + return $o; +} + +function floatToIntBits($float_val) +{ + $int = unpack('i', pack('f', $float_val)); + return $int[1]; +} + +function fill_array($index,$count,$value){ + if($count<=0){ + return array(0); + }else { + return array_fill($index, $count, $value); + } +} \ No newline at end of file diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/detector/MathUtils.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/detector/MathUtils.php new file mode 100644 index 0000000..1d3de23 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/detector/MathUtils.php @@ -0,0 +1,46 @@ +A somewhat generic detector that looks for a barcode-like rectangular region within an image. + * It looks within a mostly white region of an image for a region of black and white, but mostly + * black. It returns the four corners of the region, as best it can determine.

    + * + * @author Sean Owen + * @port Ashot Khanamiryan + */ +class MonochromeRectangleDetector { + private static $MAX_MODULES = 32; + private $image; + function __construct($image){ + $this->image = $image; + + } + + /** + *

    Detects a rectangular region of black and white -- mostly black -- with a region of mostly + * white, in an image.

    + * + * @return {@link ResultPoint}[] describing the corners of the rectangular region. The first and + * last points are opposed on the diagonal, as are the second and third. The first point will be + * the topmost point and the last, the bottommost. The second point will be leftmost and the + * third, the rightmost + * @throws NotFoundException if no Data Matrix Code can be found + */ + public function detect(){ + + $height = $this->image->getHeight(); + $width = $this->image->getWidth(); + $halfHeight = $height / 2; + $halfWidth = $width / 2; + + $deltaY = max(1, $height / (self::$MAX_MODULES * 8)); + $deltaX = max(1, $width / (self::$MAX_MODULES * 8)); + + + $top = 0; + $bottom = $height; + $left = 0; + $right = $width; + $pointA = $this->findCornerFromCenter($halfWidth, 0, $left, $right, + $halfHeight, -$deltaY, $top, $bottom, $halfWidth / 2); + $top = (int) $pointA->getY() - 1; + $pointB = $this->findCornerFromCenter($halfWidth, -$deltaX, $left,$right, + $halfHeight, 0, $top, $bottom, $halfHeight / 2); + $left = (int) $pointB->getX() - 1; + $pointC = $this->findCornerFromCenter($halfWidth, $deltaX, $left, $right, + $halfHeight, 0, $top, $bottom, $halfHeight / 2); + $right = (int) $pointC->getX() + 1; + $pointD = $this->findCornerFromCenter($halfWidth, 0, $left, $right, + $halfHeight, $deltaY, $top, $bottom, $halfWidth / 2); + $bottom = (int) $pointD->getY() + 1; + + // Go try to find po$A again with better information -- might have been off at first. + $pointA = $this->findCornerFromCenter($halfWidth, 0, $left, $right, + $halfHeight, -$deltaY, $top, $bottom, $halfWidth / 4); + + return new ResultPoint( $pointA, $pointB, $pointC, $pointD ); + + } + + + + /** + * Attempts to locate a corner of the barcode by scanning up, down, left or right from a center + * point which should be within the barcode. + * + * @param centerX center's x component (horizontal) + * @param deltaX same as deltaY but change in x per step instead + * @param left minimum value of x + * @param right maximum value of x + * @param centerY center's y component (vertical) + * @param deltaY change in y per step. If scanning up this is negative; down, positive; + * left or right, 0 + * @param top minimum value of y to search through (meaningless when di == 0) + * @param bottom maximum value of y + * @param maxWhiteRun maximum run of white pixels that can still be considered to be within + * the barcode + * @return a {@link com.google.zxing.ResultPoint} encapsulating the corner that was found + * @throws NotFoundException if such a point cannot be found + */ + private function findCornerFromCenter($centerX, + $deltaX, + $left, + $right, + $centerY, + $deltaY, + $top, + $bottom, + $maxWhiteRun){ + $lastRange = null; + for ($y = $centerY, $x = $centerX; + $y < $bottom && $y >= $top && $x < $right && $x >= $left; + $y += $deltaY, $x += $deltaX) { + $range = 0; + if ($deltaX == 0) { + // horizontal slices, up and down + $range = $this->blackWhiteRange($y, $maxWhiteRun, $left, $right, true); + } else { + // vertical slices, left and right + $range = $this->blackWhiteRange($x, $maxWhiteRun, $top, $bottom, false); + } + if ($range == null) { + if ($lastRange == null) { + throw NotFoundException::getNotFoundInstance(); + } + // lastRange was found + if ($deltaX == 0) { + $lastY = $y - $deltaY; + if ($lastRange[0] < $centerX) { + if ($lastRange[1] > $centerX) { + // straddle, choose one or the other based on direction + return new ResultPoint($deltaY > 0 ? $lastRange[0] : $lastRange[1], $lastY); + } + return new ResultPoint($lastRange[0], $lastY); + } else { + return new ResultPoint($lastRange[1], $lastY); + } + } else { + $lastX = $x - $deltaX; + if ($lastRange[0] < $centerY) { + if ($lastRange[1] > $centerY) { + return new ResultPoint($lastX, $deltaX < 0 ? $lastRange[0] : $lastRange[1]); + } + return new ResultPoint($lastX, $lastRange[0]); + } else { + return new ResultPoint($lastX, $lastRange[1]); + } + } + } + $lastRange = $range; + } + throw NotFoundException::getNotFoundInstance(); + } + + + + /** + * Computes the start and end of a region of pixels, either horizontally or vertically, that could + * be part of a Data Matrix barcode. + * + * @param fixedDimension if scanning horizontally, this is the row (the fixed vertical location) + * where we are scanning. If scanning vertically it's the column, the fixed horizontal location + * @param maxWhiteRun largest run of white pixels that can still be considered part of the + * barcode region + * @param minDim minimum pixel location, horizontally or vertically, to consider + * @param maxDim maximum pixel location, horizontally or vertically, to consider + * @param horizontal if true, we're scanning left-right, instead of up-down + * @return int[] with start and end of found range, or null if no such range is found + * (e.g. only white was found) + */ + + private function blackWhiteRange($fixedDimension, $maxWhiteRun, $minDim, $maxDim, $horizontal){ + $center = ($minDim + $maxDim) / 2; + + // Scan left/up first + $start = $center; + while ($start >= $minDim) { + if ($horizontal ? $this->image->get($start, $fixedDimension) : $this->image->get($fixedDimension, $start)) { + $start--; + } else { + $whiteRunStart = $start; + do { + $start--; + } while ($start >= $minDim && !($horizontal ? $this->image->get($start, $fixedDimension) : + $this->image->get($fixedDimension, $start))); + $whiteRunSize = $whiteRunStart - $start; + if ($start < $minDim || $whiteRunSize > $maxWhiteRun) { + $start = $whiteRunStart; + break; + } + } + } + $start++; + + // Then try right/down + $end = $center; + while ($end < $maxDim) { + if ($horizontal ? $this->image->get($end, $fixedDimension) : $this->image->get($fixedDimension, $end)) { + $end++; + } else { + $whiteRunStart = $end; + do { + $end++; + } while ($end < $maxDim && !($horizontal ? $this->image->get($end, $fixedDimension) : + $this->image->get($fixedDimension, $end))); + $whiteRunSize = $end - $whiteRunStart; + if ($end >= $maxDim || $whiteRunSize > $maxWhiteRun) { + $end = $whiteRunStart; + break; + } + } + } + $end--; + + return $end > $start ? array($start, $end) : null; + } +} \ No newline at end of file diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/GenericGF.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/GenericGF.php new file mode 100644 index 0000000..ea9d7b9 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/GenericGF.php @@ -0,0 +1,181 @@ +This class contains utility methods for performing mathematical operations over + * the Galois Fields. Operations use a given primitive polynomial in calculations.

    + * + *

    Throughout this package, elements of the GF are represented as an {@code int} + * for convenience and speed (but at the cost of memory). + *

    + * + * @author Sean Owen + * @author David Olivier + */ +final class GenericGF { + + public static $AZTEC_DATA_12; + public static $AZTEC_DATA_10; + public static $AZTEC_DATA_6; + public static $AZTEC_PARAM; + public static $QR_CODE_FIELD_256; + public static $DATA_MATRIX_FIELD_256; + public static $AZTEC_DATA_8; + public static $MAXICODE_FIELD_64; + + private $expTable; + private $logTable; + private $zero; + private $one; + private $size; + private $primitive; + private $generatorBase; + + + public static function Init(){ + self::$AZTEC_DATA_12 = new GenericGF(0x1069, 4096, 1); // x^12 + x^6 + x^5 + x^3 + 1 + self::$AZTEC_DATA_10 = new GenericGF(0x409, 1024, 1); // x^10 + x^3 + 1 + self::$AZTEC_DATA_6 = new GenericGF(0x43, 64, 1); // x^6 + x + 1 + self::$AZTEC_PARAM = new GenericGF(0x13, 16, 1); // x^4 + x + 1 + self::$QR_CODE_FIELD_256 = new GenericGF(0x011D, 256, 0); // x^8 + x^4 + x^3 + x^2 + 1 + self::$DATA_MATRIX_FIELD_256 = new GenericGF(0x012D, 256, 1); // x^8 + x^5 + x^3 + x^2 + 1 + self::$AZTEC_DATA_8 = self::$DATA_MATRIX_FIELD_256; + self::$MAXICODE_FIELD_64 = self::$AZTEC_DATA_6; + } + + + /** + * Create a representation of GF(size) using the given primitive polynomial. + * + * @param primitive irreducible polynomial whose coefficients are represented by + * the bits of an int, where the least-significant bit represents the constant + * coefficient + * @param size the size of the field + * @param b the factor b in the generator polynomial can be 0- or 1-based + * (g(x) = (x+a^b)(x+a^(b+1))...(x+a^(b+2t-1))). + * In most cases it should be 1, but for QR code it is 0. + */ + public function __construct($primitive, $size, $b) { + $this->primitive = $primitive; + $this->size = $size; + $this->generatorBase = $b; + + $this->expTable = array(); + $this->logTable =array(); + $x = 1; + for ($i = 0; $i < $size; $i++) { + $this->expTable[$i] = $x; + $x *= 2; // we're assuming the generator alpha is 2 + if ($x >= $size) { + $x ^= $primitive; + $x &= $size-1; + } + } + for ($i = 0; $i < $size-1; $i++) { + $this->logTable[$this->expTable[$i]] = $i; + } + // logTable[0] == 0 but this should never be used + $this->zero = new GenericGFPoly($this, array(0)); + $this->one = new GenericGFPoly($this, array(1)); + } + + function getZero() { + return $this->zero; + } + + function getOne() { + return $this->one; + } + + /** + * @return the monomial representing coefficient * x^degree + */ + function buildMonomial($degree, $coefficient) { + if ($degree < 0) { + throw new \InvalidArgumentException(); + } + if ($coefficient == 0) { + return $this->zero; + } + $coefficients = fill_array(0,$degree+1,0);//new int[degree + 1]; + $coefficients[0] = $coefficient; + return new GenericGFPoly($this, $coefficients); + } + + /** + * Implements both addition and subtraction -- they are the same in GF(size). + * + * @return sum/difference of a and b + */ + static function addOrSubtract($a, $b) { + return $a ^ $b; + } + + /** + * @return 2 to the power of a in GF(size) + */ + function exp($a) { + return $this->expTable[$a]; + } + + /** + * @return base 2 log of a in GF(size) + */ + function log($a) { + if ($a == 0) { + throw new \InvalidArgumentException(); + } + return $this->logTable[$a]; + } + + /** + * @return multiplicative inverse of a + */ + function inverse($a) { + if ($a == 0) { + throw new Exception(); + } + return $this->expTable[$this->size - $this->logTable[$a] - 1]; + } + + /** + * @return product of a and b in GF(size) + */ + function multiply($a, $b) { + if ($a == 0 || $b == 0) { + return 0; + } + return $this->expTable[($this->logTable[$a] + $this->logTable[$b]) % ($this->size - 1)]; + } + + public function getSize() { + return $this->size; + } + + public function getGeneratorBase() { + return $this->generatorBase; + } + + // @Override + public function toString() { + return "GF(0x" . dechex(intval($this->primitive)) . ',' . $this->size . ')'; + } + +} +GenericGF::Init(); \ No newline at end of file diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/GenericGFPoly.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/GenericGFPoly.php new file mode 100644 index 0000000..9e9aa33 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/GenericGFPoly.php @@ -0,0 +1,268 @@ +Represents a polynomial whose coefficients are elements of a GF. + * Instances of this class are immutable.

    + * + *

    Much credit is due to William Rucklidge since portions of this code are an indirect + * port of his C++ Reed-Solomon implementation.

    + * + * @author Sean Owen + */ +final class GenericGFPoly { + + private $field; + private $coefficients; + + /** + * @param field the {@link GenericGF} instance representing the field to use + * to perform computations + * @param coefficients array coefficients as ints representing elements of GF(size), arranged + * from most significant (highest-power term) coefficient to least significant + * @throws IllegalArgumentException if argument is null or empty, + * or if leading coefficient is 0 and this is not a + * constant polynomial (that is, it is not the monomial "0") + */ + function __construct($field, $coefficients) { + if (count($coefficients) == 0) { + throw new \InvalidArgumentException(); + } + $this->field = $field; + $coefficientsLength = count($coefficients); + if ($coefficientsLength > 1 && $coefficients[0] == 0) { + // Leading term must be non-zero for anything except the constant polynomial "0" + $firstNonZero = 1; + while ($firstNonZero < $coefficientsLength && $coefficients[$firstNonZero] == 0) { + $firstNonZero++; + } + if ($firstNonZero == $coefficientsLength) { + $this->coefficients = array(0); + } else { + $this->coefficients = fill_array(0,$coefficientsLength - $firstNonZero,0); + $this->coefficients = arraycopy($coefficients, + $firstNonZero, + $this->coefficients, + 0, + count($this->coefficients)); + } + } else { + $this->coefficients = $coefficients; + } + } + + function getCoefficients() { + return $this->coefficients; + } + + /** + * @return degree of this polynomial + */ + function getDegree() { + return count($this->coefficients) - 1; + } + + /** + * @return true iff this polynomial is the monomial "0" + */ + function isZero() { + return $this->coefficients[0] == 0; + } + + /** + * @return coefficient of x^degree term in this polynomial + */ + function getCoefficient($degree) { + return $this->coefficients[count($this->coefficients) - 1 - $degree]; + } + + /** + * @return evaluation of this polynomial at a given point + */ + function evaluateAt($a) { + if ($a == 0) { + // Just return the x^0 coefficient + return $this->getCoefficient(0); + } + $size = count($this->coefficients); + if ($a == 1) { + // Just the sum of the coefficients + $result = 0; + foreach ($this->coefficients as $coefficient ) { + $result = GenericGF::addOrSubtract($result, $coefficient); + } + return $result; + } + $result = $this->coefficients[0]; + for ($i = 1; $i < $size; $i++) { + $result = GenericGF::addOrSubtract($this->field->multiply($a, $result), $this->coefficients[$i]); + } + return $result; + } + + function addOrSubtract($other) { + if ($this->field !== $other->field) { + throw new \InvalidArgumentException("GenericGFPolys do not have same GenericGF field"); + } + if ($this->isZero()) { + return $other; + } + if ($other->isZero()) { + return $this; + } + + $smallerCoefficients = $this->coefficients; + $largerCoefficients = $other->coefficients; + if (count($smallerCoefficients) > count($largerCoefficients)) { + $temp = $smallerCoefficients; + $smallerCoefficients = $largerCoefficients; + $largerCoefficients = $temp; + } + $sumDiff = fill_array(0,count($largerCoefficients),0); + $lengthDiff = count($largerCoefficients) - count($smallerCoefficients); + // Copy high-order terms only found in higher-degree polynomial's coefficients + $sumDiff = arraycopy($largerCoefficients, 0, $sumDiff, 0, $lengthDiff); + + for ($i = $lengthDiff; $i < count($largerCoefficients); $i++) { + $sumDiff[$i] = GenericGF::addOrSubtract($smallerCoefficients[$i - $lengthDiff], $largerCoefficients[$i]); + } + + return new GenericGFPoly($this->field, $sumDiff); + } + + function multiply($other) { + if(is_int($other)){ + return $this->multiply_($other); + } + if ($this->field !== $other->field) { + throw new \InvalidArgumentException("GenericGFPolys do not have same GenericGF field"); + } + if ($this->isZero() || $other->isZero()) { + return $this->field->getZero(); + } + $aCoefficients = $this->coefficients; + $aLength = count($aCoefficients); + $bCoefficients = $other->coefficients; + $bLength = count($bCoefficients); + $product = fill_array(0,$aLength + $bLength - 1,0); + for ($i = 0; $i < $aLength; $i++) { + $aCoeff = $aCoefficients[$i]; + for ($j = 0; $j < $bLength; $j++) { + $product[$i + $j] = GenericGF::addOrSubtract($product[$i + $j], + $this->field->multiply($aCoeff, $bCoefficients[$j])); + } + } + return new GenericGFPoly($this->field, $product); + } + + function multiply_($scalar) { + if ($scalar == 0) { + return $this->field->getZero(); + } + if ($scalar == 1) { + return $this; + } + $size = count($this->coefficients); + $product = fill_array(0,$size,0); + for ($i = 0; $i < $size; $i++) { + $product[$i] = $this->field->multiply($this->coefficients[$i], $scalar); + } + return new GenericGFPoly($this->field, $product); + } + + function multiplyByMonomial($degree, $coefficient) { + if ($degree < 0) { + throw new \InvalidArgumentException(); + } + if ($coefficient == 0) { + return $this->field->getZero(); + } + $size = count($this->coefficients); + $product = fill_array(0,$size + $degree,0); + for ($i = 0; $i < $size; $i++) { + $product[$i] = $this->field->multiply($this->coefficients[$i], $coefficient); + } + return new GenericGFPoly($this->field, $product); + } + + function divide($other) { + if ($this->field !==$other->field) { + throw new \InvalidArgumentException("GenericGFPolys do not have same GenericGF field"); + } + if ($other->isZero()) { + throw new \InvalidArgumentException("Divide by 0"); + } + + $quotient = $this->field->getZero(); + $remainder = $this; + + $denominatorLeadingTerm = $other->getCoefficient($other->getDegree()); + $inverseDenominatorLeadingTerm = $this->field->inverse($denominatorLeadingTerm); + + while ($remainder->getDegree() >= $other->getDegree() && !$remainder->isZero()) { + $degreeDifference = $remainder->getDegree() - $other->getDegree(); + $scale = $this->field->multiply($remainder->getCoefficient($remainder->getDegree()), $inverseDenominatorLeadingTerm); + $term = $other->multiplyByMonomial($degreeDifference, $scale); + $iterationQuotient = $this->field->buildMonomial($degreeDifference, $scale); + $quotient = $quotient->addOrSubtract($iterationQuotient); + $remainder = $remainder->addOrSubtract($term); + } + + return array($quotient, $remainder ); + } + + //@Override + public function toString() { + $result = ''; + for ($degree = $this->getDegree(); $degree >= 0; $degree--) { + $coefficient = $this->getCoefficient($degree); + if ($coefficient != 0) { + if ($coefficient < 0) { + $result.=" - "; + $coefficient = -$coefficient; + } else { + if (strlen($result) > 0) { + $result .= " + "; + } + } + if ($degree == 0 || $coefficient != 1) { + $alphaPower = $this->field->log($coefficient); + if ($alphaPower == 0) { + $result.='1'; + } else if ($alphaPower == 1) { + $result.='a'; + } else { + $result.="a^"; + $result.=($alphaPower); + } + } + if ($degree != 0) { + if ($degree == 1) { + $result.='x'; + } else { + $result.="x^"; + $result.= $degree; + } + } + } + } + return $result; + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/ReedSolomonDecoder.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/ReedSolomonDecoder.php new file mode 100644 index 0000000..3fb1c2e --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/ReedSolomonDecoder.php @@ -0,0 +1,192 @@ +Implements Reed-Solomon decoding, as the name implies.

    + * + *

    The algorithm will not be explained here, but the following references were helpful + * in creating this implementation:

    + * + * + * + *

    Much credit is due to William Rucklidge since portions of this code are an indirect + * port of his C++ Reed-Solomon implementation.

    + * + * @author Sean Owen + * @author William Rucklidge + * @author sanfordsquires + */ +final class ReedSolomonDecoder { + + private $field; + + public function __construct($field) { + $this->field = $field; + } + + /** + *

    Decodes given set of received codewords, which include both data and error-correction + * codewords. Really, this means it uses Reed-Solomon to detect and correct errors, in-place, + * in the input.

    + * + * @param received data and error-correction codewords + * @param twoS number of error-correction codewords available + * @throws ReedSolomonException if decoding fails for any reason + */ + public function decode(&$received, $twoS) { + $poly = new GenericGFPoly($this->field, $received); + $syndromeCoefficients = fill_array(0,$twoS,0); + $noError = true; + for ($i = 0; $i < $twoS; $i++) { + $eval = $poly->evaluateAt($this->field->exp($i + $this->field->getGeneratorBase())); + $syndromeCoefficients[count($syndromeCoefficients) - 1 - $i] = $eval; + if ($eval != 0) { + $noError = false; + } + } + if ($noError) { + return; + } + $syndrome = new GenericGFPoly($this->field, $syndromeCoefficients); + $sigmaOmega = + $this->runEuclideanAlgorithm($this->field->buildMonomial($twoS, 1), $syndrome, $twoS); + $sigma = $sigmaOmega[0]; + $omega = $sigmaOmega[1]; + $errorLocations = $this->findErrorLocations($sigma); + $errorMagnitudes = $this->findErrorMagnitudes($omega, $errorLocations); + for ($i = 0; $i < count($errorLocations); $i++) { + $position = count($received) - 1 - $this->field->log($errorLocations[$i]); + if ($position < 0) { + throw new ReedSolomonException("Bad error location"); + } + $received[$position] = GenericGF::addOrSubtract($received[$position], $errorMagnitudes[$i]); + } + + } + + private function runEuclideanAlgorithm($a, $b, $R) + { + // Assume a's degree is >= b's + if ($a->getDegree() < $b->getDegree()) { + $temp = $a; + $a = $b; + $b = $temp; + } + + $rLast = $a; + $r = $b; + $tLast = $this->field->getZero(); + $t = $this->field->getOne(); + + // Run Euclidean algorithm until r's degree is less than R/2 + while ($r->getDegree() >= $R / 2) { + $rLastLast = $rLast; + $tLastLast = $tLast; + $rLast = $r; + $tLast = $t; + + // Divide rLastLast by rLast, with quotient in q and remainder in r + if ($rLast->isZero()) { + // Oops, Euclidean algorithm already terminated? + throw new ReedSolomonException("r_{i-1} was zero"); + } + $r = $rLastLast; + $q = $this->field->getZero(); + $denominatorLeadingTerm = $rLast->getCoefficient($rLast->getDegree()); + $dltInverse = $this->field->inverse($denominatorLeadingTerm); + while ($r->getDegree() >= $rLast->getDegree() && !$r->isZero()) { + $degreeDiff = $r->getDegree() - $rLast->getDegree(); + $scale = $this->field->multiply($r->getCoefficient($r->getDegree()), $dltInverse); + $q = $q->addOrSubtract($this->field->buildMonomial($degreeDiff, $scale)); + $r = $r->addOrSubtract($rLast->multiplyByMonomial($degreeDiff, $scale)); + } + + $t = $q->multiply($tLast)->addOrSubtract($tLastLast); + + if ($r->getDegree() >= $rLast->getDegree()) { + throw new IllegalStateException("Division algorithm failed to reduce polynomial?"); + } + } + + $sigmaTildeAtZero = $t->getCoefficient(0); + if ($sigmaTildeAtZero == 0) { + throw new ReedSolomonException("sigmaTilde(0) was zero"); + } + + $inverse = $this->field->inverse($sigmaTildeAtZero); + $sigma = $t->multiply($inverse); + $omega = $r->multiply($inverse); + return array($sigma, $omega); + } + + private function findErrorLocations($errorLocator) { + // This is a direct application of Chien's search + $numErrors = $errorLocator->getDegree(); + if ($numErrors == 1) { // shortcut + return array($errorLocator->getCoefficient(1) ); + } + $result = fill_array(0,$numErrors,0); + $e = 0; + for ($i = 1; $i < $this->field->getSize() && $e < $numErrors; $i++) { + if ($errorLocator->evaluateAt($i) == 0) { + $result[$e] = $this->field->inverse($i); + $e++; + } + } + if ($e != $numErrors) { + throw new ReedSolomonException("Error locator degree does not match number of roots"); + } + return $result; + } + + private function findErrorMagnitudes($errorEvaluator, $errorLocations) { + // This is directly applying Forney's Formula + $s = count($errorLocations); + $result = fill_array(0,$s,0); + for ($i = 0; $i < $s; $i++) { + $xiInverse = $this->field->inverse($errorLocations[$i]); + $denominator = 1; + for ($j = 0; $j < $s; $j++) { + if ($i != $j) { + //denominator = field.multiply(denominator, + // GenericGF.addOrSubtract(1, field.multiply(errorLocations[j], xiInverse))); + // Above should work but fails on some Apple and Linux JDKs due to a Hotspot bug. + // Below is a funny-looking workaround from Steven Parkes + $term = $this->field->multiply($errorLocations[$j], $xiInverse); + $termPlus1 = ($term & 0x1) == 0 ? $term | 1 : $term & ~1; + $denominator = $this->field->multiply($denominator, $termPlus1); + } + } + $result[$i] = $this->field->multiply($errorEvaluator->evaluateAt($xiInverse), + $this->field->inverse($denominator)); + if ($this->field->getGeneratorBase() != 0) { + $result[$i] = $this->field->multiply($result[$i], $xiInverse); + } + } + return $result; + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/ReedSolomonException.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/ReedSolomonException.php new file mode 100644 index 0000000..10ac55e --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/common/reedsolomon/ReedSolomonException.php @@ -0,0 +1,31 @@ +Thrown when an exception occurs during Reed-Solomon decoding, such as when + * there are too many errors to correct.

    +* +* @author Sean Owen +*/ +final class ReedSolomonException extends \Exception { + + +} + +?> \ No newline at end of file diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/QRCodeReader.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/QRCodeReader.php new file mode 100644 index 0000000..dafdbd4 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/QRCodeReader.php @@ -0,0 +1,222 @@ +decoder = new Decoder(); + + } + + protected final function getDecoder() { + return $this->decoder; + } + + /** + * Locates and decodes a QR code in an image. + * + * @return a String representing the content encoded by the QR code + * @throws NotFoundException if a QR code cannot be found + * @throws FormatException if a QR code cannot be decoded + * @throws ChecksumException if error correction fails + */ + //@Override + + + // @Override + public function decode($image, $hints=null){/* Map hints*/ + $decoderResult = null; + $points = array(); + if ($hints != null && $hints['PURE_BARCODE']) {//hints.containsKey(DecodeHintType.PURE_BARCODE)) { + $bits = $this->extractPureBits($image->getBlackMatrix()); + $decoderResult = $this->decoder->decode($bits, $hints); + $points = self::$NO_POINTS; + } else { + $detector = new Detector($image->getBlackMatrix()); + $detectorResult = $detector->detect($hints); + + $decoderResult = $this->decoder->decode($detectorResult->getBits(), $hints); + $points = $detectorResult->getPoints(); + } + + // If the code was mirrored: swap the bottom-left and the top-right points. + if ($decoderResult->getOther() instanceof QRCodeDecoderMetaData) { + $decoderResult->getOther()->applyMirroredCorrection($points); + } + + $result = new Result($decoderResult->getText(), $decoderResult->getRawBytes(), $points, 'QR_CODE');//BarcodeFormat.QR_CODE + $byteSegments = $decoderResult->getByteSegments(); + if ($byteSegments != null) { + $result->putMetadata('BYTE_SEGMENTS', $byteSegments);//ResultMetadataType.BYTE_SEGMENTS + } + $ecLevel = $decoderResult->getECLevel(); + if ($ecLevel != null) { + $result->putMetadata('ERROR_CORRECTION_LEVEL', $ecLevel);//ResultMetadataType.ERROR_CORRECTION_LEVEL + } + if ($decoderResult->hasStructuredAppend()) { + $result->putMetadata('STRUCTURED_APPEND_SEQUENCE',//ResultMetadataType.STRUCTURED_APPEND_SEQUENCE + $decoderResult->getStructuredAppendSequenceNumber()); + $result->putMetadata('STRUCTURED_APPEND_PARITY',//ResultMetadataType.STRUCTURED_APPEND_PARITY + $decoderResult->getStructuredAppendParity()); + } + return $result; + } + + //@Override + public function reset() { + // do nothing + } + + /** + * This method detects a code in a "pure" image -- that is, pure monochrome image + * which contains only an unrotated, unskewed, image of a code, with some white border + * around it. This is a specialized method that works exceptionally fast in this special + * case. + * + * @see com.google.zxing.datamatrix.DataMatrixReader#extractPureBits(BitMatrix) + */ + private static function extractPureBits($image) { + + $leftTopBlack = $image->getTopLeftOnBit(); + $rightBottomBlack = $image->getBottomRightOnBit(); + if ($leftTopBlack == null || $rightBottomBlack == null) { + throw NotFoundException::getNotFoundInstance(); + } + + $moduleSize = self::moduleSize($leftTopBlack, $image); + + $top = $leftTopBlack[1]; + $bottom = $rightBottomBlack[1]; + $left = $leftTopBlack[0]; + $right = $rightBottomBlack[0]; + + // Sanity check! + if ($left >= $right || $top >= $bottom) { + throw NotFoundException::getNotFoundInstance(); + } + + if ($bottom - $top != $right - $left) { + // Special case, where bottom-right module wasn't black so we found something else in the last row + // Assume it's a square, so use height as the width + $right = $left + ($bottom - $top); + } + + $matrixWidth = round(($right - $left + 1) / $moduleSize); + $matrixHeight = round(($bottom - $top + 1) / $moduleSize); + if ($matrixWidth <= 0 || $matrixHeight <= 0) { + throw NotFoundException::getNotFoundInstance(); + } + if ($matrixHeight != $matrixWidth) { + // Only possibly decode square regions + throw NotFoundException::getNotFoundInstance(); + } + + // Push in the "border" by half the module width so that we start + // sampling in the middle of the module. Just in case the image is a + // little off, this will help recover. + $nudge = (int) ($moduleSize / 2.0);// $nudge = (int) ($moduleSize / 2.0f); + $top += $nudge; + $left += $nudge; + + // But careful that this does not sample off the edge + // "right" is the farthest-right valid pixel location -- right+1 is not necessarily + // This is positive by how much the inner x loop below would be too large + $nudgedTooFarRight = $left + (int) (($matrixWidth - 1) * $moduleSize) - $right; + if ($nudgedTooFarRight > 0) { + if ($nudgedTooFarRight > $nudge) { + // Neither way fits; abort + throw NotFoundException::getNotFoundInstance(); + } + $left -= $nudgedTooFarRight; + } + // See logic above + $nudgedTooFarDown = $top + (int) (($matrixHeight - 1) * $moduleSize) - $bottom; + if ($nudgedTooFarDown > 0) { + if ($nudgedTooFarDown > $nudge) { + // Neither way fits; abort + throw NotFoundException::getNotFoundInstance(); + } + $top -= $nudgedTooFarDown; + } + + // Now just read off the bits + $bits = new BitMatrix($matrixWidth, $matrixHeight); + for ($y = 0; $y < $matrixHeight; $y++) { + $iOffset = $top + (int) ($y * $moduleSize); + for ($x = 0; $x < $matrixWidth; $x++) { + if ($image->get($left + (int) ($x * $moduleSize), $iOffset)) { + $bits->set($x, $y); + } + } + } + return $bits; + } + + private static function moduleSize($leftTopBlack, $image) { + $height = $image->getHeight(); + $width = $image->getWidth(); + $x = $leftTopBlack[0]; + $y = $leftTopBlack[1]; + $inBlack = true; + $transitions = 0; + while ($x < $width && $y < $height) { + if ($inBlack != $image->get($x, $y)) { + if (++$transitions == 5) { + break; + } + $inBlack = !$inBlack; + } + $x++; + $y++; + } + if ($x == $width || $y == $height) { + throw NotFoundException::getNotFoundInstance(); + } + return ($x - $leftTopBlack[0]) / 7.0; //return ($x - $leftTopBlack[0]) / 7.0f; + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/BitMatrixParser.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/BitMatrixParser.php new file mode 100644 index 0000000..510cfa8 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/BitMatrixParser.php @@ -0,0 +1,250 @@ += 21 and 1 mod 4 + */ + function __construct($bitMatrix) { + $dimension = $bitMatrix->getHeight(); + if ($dimension < 21 || ($dimension & 0x03) != 1) { + throw FormatException::getFormatInstance(); + } + $this->bitMatrix = $bitMatrix; + } + + /** + *

    Reads format information from one of its two locations within the QR Code.

    + * + * @return {@link FormatInformation} encapsulating the QR Code's format info + * @throws FormatException if both format information locations cannot be parsed as + * the valid encoding of format information + */ + function readFormatInformation() { + + if ($this->parsedFormatInfo != null) { + return $this->parsedFormatInfo; + } + + // Read top-left format info bits + $formatInfoBits1 = 0; + for ($i = 0; $i < 6; $i++) { + $formatInfoBits1 = $this->copyBit($i, 8, $formatInfoBits1); + } + // .. and skip a bit in the timing pattern ... + $formatInfoBits1 = $this->copyBit(7, 8, $formatInfoBits1); + $formatInfoBits1 = $this->copyBit(8, 8, $formatInfoBits1); + $formatInfoBits1 = $this->copyBit(8, 7, $formatInfoBits1); + // .. and skip a bit in the timing pattern ... + for ($j = 5; $j >= 0; $j--) { + $formatInfoBits1 = $this->copyBit(8, $j, $formatInfoBits1); + } + + // Read the top-right/bottom-left pattern too + $dimension = $this->bitMatrix->getHeight(); + $formatInfoBits2 = 0; + $jMin = $dimension - 7; + for ($j = $dimension - 1; $j >= $jMin; $j--) { + $formatInfoBits2 = $this->copyBit(8, $j, $formatInfoBits2); + } + for ($i = $dimension - 8; $i < $dimension; $i++) { + $formatInfoBits2 = $this->copyBit($i, 8, $formatInfoBits2); + } + + $parsedFormatInfo = FormatInformation::decodeFormatInformation($formatInfoBits1, $formatInfoBits2); + if ($parsedFormatInfo != null) { + return $parsedFormatInfo; + } + throw FormatException::getFormatInstance(); + } + + /** + *

    Reads version information from one of its two locations within the QR Code.

    + * + * @return {@link Version} encapsulating the QR Code's version + * @throws FormatException if both version information locations cannot be parsed as + * the valid encoding of version information + */ + function readVersion(){ + + if ($this->parsedVersion != null) { + return $this->parsedVersion; + } + + $dimension = $this->bitMatrix->getHeight(); + + $provisionalVersion = ($dimension - 17) / 4; + if ($provisionalVersion <= 6) { + return Version::getVersionForNumber($provisionalVersion); + } + + // Read top-right version info: 3 wide by 6 tall + $versionBits = 0; + $ijMin = $dimension - 11; + for ($j = 5; $j >= 0; $j--) { + for ($i = $dimension - 9; $i >= $ijMin; $i--) { + $versionBits = $this->copyBit($i, $j, $versionBits); + } + } + + $theParsedVersion = Version::decodeVersionInformation($versionBits); + if ($theParsedVersion != null && $theParsedVersion->getDimensionForVersion() == $dimension) { + $this->parsedVersion = $theParsedVersion; + return $theParsedVersion; + } + + // Hmm, failed. Try bottom left: 6 wide by 3 tall + $versionBits = 0; + for ($i = 5; $i >= 0; $i--) { + for ($j = $dimension - 9; $j >=$ijMin; $j--) { + $versionBits = $this->copyBit($i, $j, $versionBits); + } + } + + $theParsedVersion = Version::decodeVersionInformation($versionBits); + if ($theParsedVersion != null && $theParsedVersion->getDimensionForVersion() == $dimension) { + $this->parsedVersion = $theParsedVersion; + return $theParsedVersion; + } + throw FormatException::getFormatInstance(); + } + + private function copyBit($i, $j, $versionBits) { + $bit = $this->mirror ? $this->bitMatrix->get($j, $i) : $this->bitMatrix->get($i, $j); + return $bit ? ($versionBits << 1) | 0x1 : $versionBits << 1; + } + + /** + *

    Reads the bits in the {@link BitMatrix} representing the finder pattern in the + * correct order in order to reconstruct the codewords bytes contained within the + * QR Code.

    + * + * @return bytes encoded within the QR Code + * @throws FormatException if the exact number of bytes expected is not read + */ + function readCodewords(){ + + $formatInfo = $this->readFormatInformation(); + $version = $this->readVersion(); + + // Get the data mask for the format used in this QR Code. This will exclude + // some bits from reading as we wind through the bit matrix. + $dataMask = DataMask::forReference($formatInfo->getDataMask()); + $dimension = $this->bitMatrix->getHeight(); + $dataMask->unmaskBitMatrix($this->bitMatrix, $dimension); + + $functionPattern = $version->buildFunctionPattern(); + + $readingUp = true; + if($version->getTotalCodewords()) { + $result = fill_array(0, $version->getTotalCodewords(), 0); + }else{ + $result = array(); + } + $resultOffset = 0; + $currentByte = 0; + $bitsRead = 0; + // Read columns in pairs, from right to left + for ($j = $dimension - 1; $j > 0; $j -= 2) { + if ($j == 6) { + // Skip whole column with vertical alignment pattern; + // saves time and makes the other code proceed more cleanly + $j--; + } + // Read alternatingly from bottom to top then top to bottom + for ($count = 0; $count < $dimension; $count++) { + $i = $readingUp ? $dimension - 1 - $count : $count; + for ($col = 0; $col < 2; $col++) { + // Ignore bits covered by the function pattern + if (!$functionPattern->get($j - $col, $i)) { + // Read a bit + $bitsRead++; + $currentByte <<= 1; + if ($this->bitMatrix->get($j - $col, $i)) { + $currentByte |= 1; + } + // If we've made a whole byte, save it off + if ($bitsRead == 8) { + $result[$resultOffset++] = $currentByte; //(byte) + $bitsRead = 0; + $currentByte = 0; + } + } + } + } + $readingUp ^= true; // readingUp = !readingUp; // switch directions + } + if ($resultOffset != $version->getTotalCodewords()) { + throw FormatException::getFormatInstance(); + } + return $result; + } + + /** + * Revert the mask removal done while reading the code words. The bit matrix should revert to its original state. + */ + function remask() { + if ($this->parsedFormatInfo == null) { + return; // We have no format information, and have no data mask + } + $dataMask = DataMask::forReference($this->parsedFormatInfo->getDataMask()); + $dimension = $this->bitMatrix->getHeight(); + $dataMask->unmaskBitMatrix($this->bitMatrix, $dimension); + } + + /** + * Prepare the parser for a mirrored operation. + * This flag has effect only on the {@link #readFormatInformation()} and the + * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the + * {@link #mirror()} method should be called. + * + * @param mirror Whether to read version and format information mirrored. + */ + function setMirror($mirror) { + $parsedVersion = null; + $parsedFormatInfo = null; + $this->mirror = $mirror; + } + + /** Mirror the bit matrix in order to attempt a second reading. */ + function mirror() { + for ($x = 0; $x < $this->bitMatrix->getWidth(); $x++) { + for ($y = $x + 1; $y < $this->bitMatrix->getHeight(); $y++) { + if ($this->bitMatrix->get($x, $y) != $this->bitMatrix->get($y, $x)) { + $this->bitMatrix->flip($y, $x); + $this->bitMatrix->flip($x, $y); + } + } + } + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataBlock.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataBlock.php new file mode 100644 index 0000000..803c14f --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataBlock.php @@ -0,0 +1,123 @@ +Encapsulates a block of data within a QR Code. QR Codes may split their data into + * multiple blocks, each of which is a unit of data and error-correction codewords. Each + * is represented by an instance of this class.

    + * + * @author Sean Owen + */ +final class DataBlock { + + private $numDataCodewords; + private $codewords; //byte[] + + private function __construct($numDataCodewords, $codewords) { + $this->numDataCodewords = $numDataCodewords; + $this->codewords = $codewords; + } + + /** + *

    When QR Codes use multiple data blocks, they are actually interleaved. + * That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This + * method will separate the data into original blocks.

    + * + * @param rawCodewords bytes as read directly from the QR Code + * @param version version of the QR Code + * @param ecLevel error-correction level of the QR Code + * @return DataBlocks containing original bytes, "de-interleaved" from representation in the + * QR Code + */ + static function getDataBlocks($rawCodewords, + $version, + $ecLevel) { + + if (count($rawCodewords) != $version->getTotalCodewords()) { + throw new \InvalidArgumentException(); + } + + // Figure out the number and size of data blocks used by this version and + // error correction level + $ecBlocks = $version->getECBlocksForLevel($ecLevel); + + // First count the total number of data blocks + $totalBlocks = 0; + $ecBlockArray = $ecBlocks->getECBlocks(); + foreach ($ecBlockArray as $ecBlock) { + $totalBlocks += $ecBlock->getCount(); + } + + // Now establish DataBlocks of the appropriate size and number of data codewords + $result = array();//new DataBlock[$totalBlocks]; + $numResultBlocks = 0; + foreach ($ecBlockArray as $ecBlock) { + for ($i = 0; $i < $ecBlock->getCount(); $i++) { + $numDataCodewords = $ecBlock->getDataCodewords(); + $numBlockCodewords = $ecBlocks->getECCodewordsPerBlock() + $numDataCodewords; + $result[$numResultBlocks++] = new DataBlock($numDataCodewords, fill_array(0,$numBlockCodewords,0)); + } + } + + // All blocks have the same amount of data, except that the last n + // (where n may be 0) have 1 more byte. Figure out where these start. + $shorterBlocksTotalCodewords = count($result[0]->codewords); + $longerBlocksStartAt = count($result) - 1; + while ($longerBlocksStartAt >= 0) { + $numCodewords = count($result[$longerBlocksStartAt]->codewords); + if ($numCodewords == $shorterBlocksTotalCodewords) { + break; + } + $longerBlocksStartAt--; + } + $longerBlocksStartAt++; + + $shorterBlocksNumDataCodewords = $shorterBlocksTotalCodewords - $ecBlocks->getECCodewordsPerBlock(); + // The last elements of result may be 1 element longer; + // first fill out as many elements as all of them have + $rawCodewordsOffset = 0; + for ($i = 0; $i < $shorterBlocksNumDataCodewords; $i++) { + for ($j = 0; $j < $numResultBlocks; $j++) { + $result[$j]->codewords[$i] = $rawCodewords[$rawCodewordsOffset++]; + } + } + // Fill out the last data block in the longer ones + for ($j = $longerBlocksStartAt; $j < $numResultBlocks; $j++) { + $result[$j]->codewords[$shorterBlocksNumDataCodewords] = $rawCodewords[$rawCodewordsOffset++]; + } + // Now add in error correction blocks + $max = count($result[0]->codewords); + for ($i = $shorterBlocksNumDataCodewords; $i < $max; $i++) { + for ($j = 0; $j < $numResultBlocks; $j++) { + $iOffset = $j < $longerBlocksStartAt ? $i : $i + 1; + $result[$j]->codewords[$iOffset] = $rawCodewords[$rawCodewordsOffset++]; + } + } + return $result; + } + + function getNumDataCodewords() { + return $this->numDataCodewords; + } + + function getCodewords() { + return $this->codewords; + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php new file mode 100644 index 0000000..852aab7 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DataMask.php @@ -0,0 +1,175 @@ +Encapsulates data masks for the data bits in a QR code, per ISO 18004:2006 6.8. Implementations + * of this class can un-mask a raw BitMatrix. For simplicity, they will unmask the entire BitMatrix, + * including areas used for finder patterns, timing patterns, etc. These areas should be unused + * after the point they are unmasked anyway.

    + * + *

    Note that the diagram in section 6.8.1 is misleading since it indicates that i is column position + * and j is row position. In fact, as the text says, i is row position and j is column position.

    + * + * @author Sean Owen + */ +abstract class DataMask +{ + + /** + * See ISO 18004:2006 6.8.1 + */ + private static $DATA_MASKS = array(); + + static function Init() + { + self::$DATA_MASKS = array( + new DataMask000(), + new DataMask001(), + new DataMask010(), + new DataMask011(), + new DataMask100(), + new DataMask101(), + new DataMask110(), + new DataMask111(), + ); + } + + function __construct() + { + + } + + /** + *

    Implementations of this method reverse the data masking process applied to a QR Code and + * make its bits ready to read.

    + * + * @param bits representation of QR Code bits + * @param dimension dimension of QR Code, represented by bits, being unmasked + */ + final function unmaskBitMatrix($bits, $dimension) + { + for ($i = 0; $i < $dimension; $i++) { + for ($j = 0; $j < $dimension; $j++) { + if ($this->isMasked($i, $j)) { + $bits->flip($j, $i); + } + } + } + } + + abstract function isMasked($i, $j); + + /** + * @param reference a value between 0 and 7 indicating one of the eight possible + * data mask patterns a QR Code may use + * @return DataMask encapsulating the data mask pattern + */ + static function forReference($reference) + { + if ($reference < 0 || $reference > 7) { + throw new \InvalidArgumentException(); + } + return self::$DATA_MASKS[$reference]; + } +} +DataMask::Init(); +/** + * 000: mask bits for which (x + y) mod 2 == 0 + */ +final class DataMask000 extends DataMask { + // @Override + function isMasked($i, $j) { + return (($i + $j) & 0x01) == 0; + } +} + +/** + * 001: mask bits for which x mod 2 == 0 + */ +final class DataMask001 extends DataMask { + //@Override + function isMasked($i, $j) { + return ($i & 0x01) == 0; + } +} + +/** + * 010: mask bits for which y mod 3 == 0 + */ +final class DataMask010 extends DataMask { + //@Override + function isMasked($i, $j) { + return $j % 3 == 0; + } +} + +/** + * 011: mask bits for which (x + y) mod 3 == 0 + */ +final class DataMask011 extends DataMask { + //@Override + function isMasked($i, $j) { + return ($i + $j) % 3 == 0; + } +} + +/** + * 100: mask bits for which (x/2 + y/3) mod 2 == 0 + */ +final class DataMask100 extends DataMask { + //@Override + function isMasked($i, $j) { + return intval((intval($i / 2) + intval($j /3)) & 0x01) == 0; + } +} + +/** + * 101: mask bits for which xy mod 2 + xy mod 3 == 0 + */ +final class DataMask101 extends DataMask { + //@Override + function isMasked($i, $j) { + $temp = $i * $j; + return ($temp & 0x01) + ($temp % 3) == 0; + } +} + +/** + * 110: mask bits for which (xy mod 2 + xy mod 3) mod 2 == 0 + */ +final class DataMask110 extends DataMask { + //@Override + function isMasked($i, $j) { + $temp = $i * $j; + return ((($temp & 0x01) + ($temp % 3)) & 0x01) == 0; + } +} + +/** + * 111: mask bits for which ((x+y)mod 2 + xy mod 3) mod 2 == 0 + */ +final class DataMask111 extends DataMask { + //@Override + function isMasked($i, $j) { + return (((($i + $j) & 0x01) + (($i * $j) % 3)) & 0x01) == 0; + } +} + diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DecodedBitStreamParser.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DecodedBitStreamParser.php new file mode 100644 index 0000000..ec4e667 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/DecodedBitStreamParser.php @@ -0,0 +1,359 @@ +QR Codes can encode text as bits in one of several modes, and can use multiple modes + * in one QR Code. This class decodes the bits back into text.

    + * + *

    See ISO 18004:2006, 6.4.3 - 6.4.7

    + * + * @author Sean Owen + */ +final class DecodedBitStreamParser { + + /** + * See ISO 18004:2006, 6.4.4 Table 5 + */ + private static $ALPHANUMERIC_CHARS = array( + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', + 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + ' ', '$', '%', '*', '+', '-', '.', '/', ':' + ); + private static $GB2312_SUBSET = 1; + + + private function DecodedBitStreamParser() { + + + } + + static function decode($bytes, + $version, + $ecLevel, + $hints) { + $bits = new BitSource($bytes); + $result = '';//new StringBuilder(50); + $byteSegments = array(); + $symbolSequence = -1; + $parityData = -1; + + try { + $currentCharacterSetECI = null; + $fc1InEffect = false; + $mode=''; + do { + // While still another segment to read... + if ($bits->available() < 4) { + // OK, assume we're done. Really, a TERMINATOR mode should have been recorded here + $mode = Mode::$TERMINATOR; + } else { + $mode = Mode::forBits($bits->readBits(4)); // mode is encoded by 4 bits + } + if ($mode != Mode::$TERMINATOR) { + if ($mode == Mode::$FNC1_FIRST_POSITION || $mode == Mode::$FNC1_SECOND_POSITION) { + // We do little with FNC1 except alter the parsed result a bit according to the spec + $fc1InEffect = true; + } else if ($mode == Mode::$STRUCTURED_APPEND) { + if ($bits->available() < 16) { + throw FormatException::getFormatInstance(); + } + // sequence number and parity is added later to the result metadata + // Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue + $symbolSequence = $bits->readBits(8); + $parityData = $bits->readBits(8); + } else if ($mode == Mode::$ECI) { + // Count doesn't apply to ECI + $value = self::parseECIValue($bits); + $currentCharacterSetECI = CharacterSetECI::getCharacterSetECIByValue($value); + if ($currentCharacterSetECI == null) { + throw FormatException::getFormatInstance(); + } + } else { + // First handle Hanzi mode which does not start with character count + if ($mode == Mode::$HANZI) { + //chinese mode contains a sub set indicator right after mode indicator + $subset = $bits->readBits(4); + $countHanzi = $bits->readBits($mode->getCharacterCountBits($version)); + if ($subset == self::$GB2312_SUBSET) { + self::decodeHanziSegment($bits, $result, $countHanzi); + } + } else { + // "Normal" QR code modes: + // How many characters will follow, encoded in this mode? + $count = $bits->readBits($mode->getCharacterCountBits($version)); + if ($mode == Mode::$NUMERIC) { + self::decodeNumericSegment($bits, $result, $count); + } else if ($mode == Mode::$ALPHANUMERIC) { + self::decodeAlphanumericSegment($bits, $result, $count, $fc1InEffect); + } else if ($mode == Mode::$BYTE) { + self::decodeByteSegment($bits, $result, $count, $currentCharacterSetECI, $byteSegments, $hints); + } else if ($mode == Mode::$KANJI) { + self::decodeKanjiSegment($bits, $result, $count); + } else { + throw FormatException::getFormatInstance(); + } + } + } + } + } while ($mode != Mode::$TERMINATOR); + } catch (IllegalArgumentException $iae) { + // from readBits() calls + throw FormatException::getFormatInstance(); + } + + return new DecoderResult($bytes, + $result, + empty($byteSegments) ? null : $byteSegments, + $ecLevel == null ? null : 'L',//ErrorCorrectionLevel::toString($ecLevel), + $symbolSequence, + $parityData); + } + + /** + * See specification GBT 18284-2000 + */ + private static function decodeHanziSegment($bits, + &$result, + $count) { + // Don't crash trying to read more bits than we have available. + if ($count * 13 > $bits->available()) { + throw FormatException::getFormatInstance(); + } + + // Each character will require 2 bytes. Read the characters as 2-byte pairs + // and decode as GB2312 afterwards + $buffer = fill_array(0,2 * $count,0); + $offset = 0; + while ($count > 0) { + // Each 13 bits encodes a 2-byte character + $twoBytes = $bits->readBits(13); + $assembledTwoBytes = (($twoBytes / 0x060) << 8) | ($twoBytes % 0x060); + if ($assembledTwoBytes < 0x003BF) { + // In the 0xA1A1 to 0xAAFE range + $assembledTwoBytes += 0x0A1A1; + } else { + // In the 0xB0A1 to 0xFAFE range + $assembledTwoBytes += 0x0A6A1; + } + $buffer[$offset] = (($assembledTwoBytes >> 8) & 0xFF);//(byte) + $buffer[$offset + 1] = ($assembledTwoBytes & 0xFF);//(byte) + $offset += 2; + $count--; + } + + try { + $result .= iconv('GB2312', 'UTF-8', implode($buffer)); + } catch (UnsupportedEncodingException $ignored) { + throw FormatException::getFormatInstance(); + } + } + + private static function decodeKanjiSegment($bits, + &$result, + $count) { + // Don't crash trying to read more bits than we have available. + if ($count * 13 > $bits->available()) { + throw FormatException::getFormatInstance(); + } + + // Each character will require 2 bytes. Read the characters as 2-byte pairs + // and decode as Shift_JIS afterwards + $buffer = array(0,2 * $count,0); + $offset = 0; + while ($count > 0) { + // Each 13 bits encodes a 2-byte character + $twoBytes = $bits->readBits(13); + $assembledTwoBytes = (($twoBytes / 0x0C0) << 8) | ($twoBytes % 0x0C0); + if ($assembledTwoBytes < 0x01F00) { + // In the 0x8140 to 0x9FFC range + $assembledTwoBytes += 0x08140; + } else { + // In the 0xE040 to 0xEBBF range + $assembledTwoBytes += 0x0C140; + } + $buffer[$offset] = ($assembledTwoBytes >> 8);//(byte) + $buffer[$offset + 1] = $assembledTwoBytes; //(byte) + $offset += 2; + $count--; + } + // Shift_JIS may not be supported in some environments: + try { + $result .= iconv('shift-jis','utf-8',implode($buffer)); + + + } catch (UnsupportedEncodingException $ignored) { + throw FormatException::getFormatInstance(); + } + } + + private static function decodeByteSegment($bits, + &$result, + $count, + $currentCharacterSetECI, + &$byteSegments, + $hints) { + // Don't crash trying to read more bits than we have available. + if (8 * $count > $bits->available()) { + throw FormatException::getFormatInstance(); + } + + $readBytes = fill_array(0,$count,0); + for ($i = 0; $i < $count; $i++) { + $readBytes[$i] = $bits->readBits(8);//(byte) + } + $text = implode(array_map('chr',$readBytes)); + $encoding = ''; + if ($currentCharacterSetECI == null) { + // The spec isn't clear on this mode; see + // section 6.4.5: t does not say which encoding to assuming + // upon decoding. I have seen ISO-8859-1 used as well as + // Shift_JIS -- without anything like an ECI designator to + // give a hint. + + $encoding = mb_detect_encoding($text, $hints); + } else { + $encoding = $currentCharacterSetECI->name(); + } + try { + // $result.= mb_convert_encoding($text ,$encoding);//(new String(readBytes, encoding)); + $result.= $text;//(new String(readBytes, encoding)); + } catch (UnsupportedEncodingException $ignored) { + throw FormatException::getFormatInstance(); + } + $byteSegments = array_merge($byteSegments, $readBytes); + } + + private static function toAlphaNumericChar($value) { + if ($value >= count(self::$ALPHANUMERIC_CHARS)) { + throw FormatException::getFormatInstance(); + } + return self::$ALPHANUMERIC_CHARS[$value]; + } + + private static function decodeAlphanumericSegment($bits, + &$result, + $count, + $fc1InEffect) { + // Read two characters at a time + $start = strlen($result); + while ($count > 1) { + if ($bits->available() < 11) { + throw FormatException::getFormatInstance(); + } + $nextTwoCharsBits = $bits->readBits(11); + $result.=(self::toAlphaNumericChar($nextTwoCharsBits / 45)); + $result.=(self::toAlphaNumericChar($nextTwoCharsBits % 45)); + $count -= 2; + } + if ($count == 1) { + // special case: one character left + if ($bits->available() < 6) { + throw FormatException::getFormatInstance(); + } + $result.=self::toAlphaNumericChar($bits->readBits(6)); + } + // See section 6.4.8.1, 6.4.8.2 + if ($fc1InEffect) { + // We need to massage the result a bit if in an FNC1 mode: + for ($i = $start; $i < strlen($result); $i++) { + if ($result{$i} == '%') { + if ($i < strlen($result) - 1 && $result{$i + 1} == '%') { + // %% is rendered as % + $result = substr_replace($result,'',$i + 1,1);//deleteCharAt(i + 1); + } else { + // In alpha mode, % should be converted to FNC1 separator 0x1D + $result.setCharAt($i, chr(0x1D)); + } + } + } + } + } + + private static function decodeNumericSegment($bits, + &$result, + $count) { + // Read three digits at a time + while ($count >= 3) { + // Each 10 bits encodes three digits + if ($bits->available() < 10) { + throw FormatException::getFormatInstance(); + } + $threeDigitsBits = $bits->readBits(10); + if ($threeDigitsBits >= 1000) { + throw FormatException::getFormatInstance(); + } + $result.=(self::toAlphaNumericChar($threeDigitsBits / 100)); + $result.=(self::toAlphaNumericChar(($threeDigitsBits / 10) % 10)); + $result.=(self::toAlphaNumericChar($threeDigitsBits % 10)); + $count -= 3; + } + if ($count == 2) { + // Two digits left over to read, encoded in 7 bits + if ($bits->available() < 7) { + throw FormatException::getFormatInstance(); + } + $twoDigitsBits = $bits->readBits(7); + if ($twoDigitsBits >= 100) { + throw FormatException::getFormatInstance(); + } + $result.=(self::toAlphaNumericChar($twoDigitsBits / 10)); + $result.=(self::toAlphaNumericChar($twoDigitsBits % 10)); + } else if ($count == 1) { + // One digit left over to read + if ($bits->available() < 4) { + throw FormatException::getFormatInstance(); + } + $digitBits = $bits->readBits(4); + if ($digitBits >= 10) { + throw FormatException::getFormatInstance(); + } + $result.=(self::toAlphaNumericChar($digitBits)); + } + } + + private static function parseECIValue($bits) { + $firstByte = $bits->readBits(8); + if (($firstByte & 0x80) == 0) { + // just one byte + return $firstByte & 0x7F; + } + if (($firstByte & 0xC0) == 0x80) { + // two bytes + $secondByte = $bits->readBits(8); + return (($firstByte & 0x3F) << 8) | $secondByte; + } + if (($firstByte & 0xE0) == 0xC0) { + // three bytes + $secondThirdBytes = $bits->readBits(16); + return (($firstByte & 0x1F) << 16) | $secondThirdBytes; + } + throw FormatException::getFormatInstance(); + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Decoder.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Decoder.php new file mode 100644 index 0000000..1859fc7 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Decoder.php @@ -0,0 +1,214 @@ +The main class which implements QR Code decoding -- as opposed to locating and extracting + * the QR Code from an image.

    + * + * @author Sean Owen + */ +final class Decoder { + + private $rsDecoder; + + public function __construct() { + $this->rsDecoder = new ReedSolomonDecoder(GenericGF::$QR_CODE_FIELD_256); + } + + + function decode($variable, $hints=null){ + if(is_array($variable)){ + return $this->decodeImage($variable,$hints); + }elseif(is_object($variable)&&$variable instanceof BitMatrix){ + return $this->decodeBits($variable,$hints); + }elseif(is_object($variable)&&$variable instanceof BitMatrixParser){ + return $this->decodeParser($variable,$hints); + }else{ + die('decode error Decoder.php'); + } + + + } + + /** + *

    Convenience method that can decode a QR Code represented as a 2D array of booleans. + * "true" is taken to mean a black module.

    + * + * @param image booleans representing white/black QR Code modules + * @param hints decoding hints that should be used to influence decoding + * @return text and bytes encoded within the QR Code + * @throws FormatException if the QR Code cannot be decoded + * @throws ChecksumException if error correction fails + */ + public function decodeImage($image, $hints=null) + { + $dimension = count($image); + $bits = new BitMatrix($dimension); + for ($i = 0; $i < $dimension; $i++) { + for ($j = 0; $j < $dimension; $j++) { + if ($image[$i][$j]) { + $bits->set($j, $i); + } + } + } + return $this->decode($bits, $hints); + } + + + + /** + *

    Decodes a QR Code represented as a {@link BitMatrix}. A 1 or "true" is taken to mean a black module.

    + * + * @param bits booleans representing white/black QR Code modules + * @param hints decoding hints that should be used to influence decoding + * @return text and bytes encoded within the QR Code + * @throws FormatException if the QR Code cannot be decoded + * @throws ChecksumException if error correction fails + */ + public function decodeBits($bits, $hints=null) + { + +// Construct a parser and read version, error-correction level + $parser = new BitMatrixParser($bits); + $fe = null; + $ce = null; + try { + return $this->decode($parser, $hints); + } catch (FormatException $e) { + $fe = $e; + } catch (ChecksumException $e) { + $ce = $e; + } + + try { + +// Revert the bit matrix + $parser->remask(); + +// Will be attempting a mirrored reading of the version and format info. + $parser->setMirror(true); + +// Preemptively read the version. + $parser->readVersion(); + +// Preemptively read the format information. + $parser->readFormatInformation(); + + /* + * Since we're here, this means we have successfully detected some kind + * of version and format information when mirrored. This is a good sign, + * that the QR code may be mirrored, and we should try once more with a + * mirrored content. + */ +// Prepare for a mirrored reading. + $parser->mirror(); + + $result = $this->decode($parser, $hints); + +// Success! Notify the caller that the code was mirrored. + $result->setOther(new QRCodeDecoderMetaData(true)); + + return $result; + + } catch (FormatException $e ) {// catch (FormatException | ChecksumException e) { +// Throw the exception from the original reading + if ($fe != null) { + throw $fe; + } + if ($ce != null) { + throw $ce; + } + throw $e; + + } + } + + private function decodeParser($parser,$hints=null) + { + $version = $parser->readVersion(); + $ecLevel = $parser->readFormatInformation()->getErrorCorrectionLevel(); + +// Read codewords + $codewords = $parser->readCodewords(); +// Separate into data blocks + $dataBlocks = DataBlock::getDataBlocks($codewords, $version, $ecLevel); + +// Count total number of data bytes + $totalBytes = 0; + foreach ($dataBlocks as $dataBlock) { + $totalBytes += $dataBlock->getNumDataCodewords(); + } + $resultBytes = fill_array(0,$totalBytes,0); + $resultOffset = 0; + +// Error-correct and copy data blocks together into a stream of bytes + foreach ($dataBlocks as $dataBlock) { + $codewordBytes = $dataBlock->getCodewords(); + $numDataCodewords = $dataBlock->getNumDataCodewords(); + $this->correctErrors($codewordBytes, $numDataCodewords); + for ($i = 0; $i < $numDataCodewords; $i++) { + $resultBytes[$resultOffset++] = $codewordBytes[$i]; + } + } + +// Decode the contents of that stream of bytes + return DecodedBitStreamParser::decode($resultBytes, $version, $ecLevel, $hints); + } + + /** + *

    Given data and error-correction codewords received, possibly corrupted by errors, attempts to + * correct the errors in-place using Reed-Solomon error correction.

    + * + * @param codewordBytes data and error correction codewords + * @param numDataCodewords number of codewords that are data bytes + * @throws ChecksumException if error correction fails + */ + private function correctErrors(&$codewordBytes, $numDataCodewords){ + $numCodewords = count($codewordBytes); +// First read into an array of ints + $codewordsInts =fill_array(0,$numCodewords,0); + for ($i = 0; $i < $numCodewords; $i++) { + $codewordsInts[$i] = $codewordBytes[$i] & 0xFF; + } + $numECCodewords = count($codewordBytes)- $numDataCodewords; + try { + $this->rsDecoder->decode($codewordsInts, $numECCodewords); + } catch (ReedSolomonException $ignored) { + throw ChecksumException::getChecksumInstance(); + } +// Copy back into array of bytes -- only need to worry about the bytes that were data +// We don't care about errors in the error-correction codewords + for ($i = 0; $i < $numDataCodewords; $i++) { + $codewordBytes[$i] = $codewordsInts[$i]; + } + + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/ErrorCorrectionLevel.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/ErrorCorrectionLevel.php new file mode 100644 index 0000000..5f61b5c --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/ErrorCorrectionLevel.php @@ -0,0 +1,86 @@ +See ISO 18004:2006, 6.5.1. This enum encapsulates the four error correction levels + * defined by the QR code standard.

    + * + * @author Sean Owen + */ +class ErrorCorrectionLevel { + + + private static $FOR_BITS; + + + private $bits; + private $ordinal; + + function __construct($bits,$ordinal=0) { + $this->bits = $bits; + $this->ordinal = $ordinal; + } + + public static function Init(){ + self::$FOR_BITS = array( + + + new ErrorCorrectionLevel(0x00,1), //M + new ErrorCorrectionLevel(0x01,0), //L + new ErrorCorrectionLevel(0x02,3), //H + new ErrorCorrectionLevel(0x03,2), //Q + + ); + } + /** L = ~7% correction */ + // self::$L = new ErrorCorrectionLevel(0x01); + /** M = ~15% correction */ + //self::$M = new ErrorCorrectionLevel(0x00); + /** Q = ~25% correction */ + //self::$Q = new ErrorCorrectionLevel(0x03); + /** H = ~30% correction */ + //self::$H = new ErrorCorrectionLevel(0x02); + + + public function getBits() { + return $this->bits; + } + public function toString() { + return $this->bits; + } + public function getOrdinal() { + return $this->ordinal; + } + + /** + * @param bits int containing the two bits encoding a QR Code's error correction level + * @return ErrorCorrectionLevel representing the encoded error correction level + */ + public static function forBits($bits) { + if ($bits < 0 || $bits >= count(self::$FOR_BITS)) { + throw new \InvalidArgumentException(); + } + $level = self::$FOR_BITS[$bits]; + // $lev = self::$$bit; + return $level; + } + + +} +ErrorCorrectionLevel::Init(); diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/FormatInformation.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/FormatInformation.php new file mode 100644 index 0000000..ac802fd --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/FormatInformation.php @@ -0,0 +1,179 @@ +Encapsulates a QR Code's format information, including the data mask used and + * error correction level.

    + * + * @author Sean Owen + * @see DataMask + * @see ErrorCorrectionLevel + */ +final class FormatInformation { + + public static $FORMAT_INFO_MASK_QR; + + /** + * See ISO 18004:2006, Annex C, Table C.1 + */ + public static $FORMAT_INFO_DECODE_LOOKUP; + /** + * Offset i holds the number of 1 bits in the binary representation of i + */ + private static $BITS_SET_IN_HALF_BYTE; + + private $errorCorrectionLevel; + private $dataMask; + + public static function Init(){ + + self::$FORMAT_INFO_MASK_QR= 0x5412; + self::$BITS_SET_IN_HALF_BYTE = array(0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4); + self::$FORMAT_INFO_DECODE_LOOKUP = array( + array(0x5412, 0x00), + array (0x5125, 0x01), + array(0x5E7C, 0x02), + array(0x5B4B, 0x03), + array(0x45F9, 0x04), + array(0x40CE, 0x05), + array(0x4F97, 0x06), + array(0x4AA0, 0x07), + array(0x77C4, 0x08), + array(0x72F3, 0x09), + array(0x7DAA, 0x0A), + array(0x789D, 0x0B), + array(0x662F, 0x0C), + array(0x6318, 0x0D), + array(0x6C41, 0x0E), + array(0x6976, 0x0F), + array(0x1689, 0x10), + array(0x13BE, 0x11), + array(0x1CE7, 0x12), + array(0x19D0, 0x13), + array(0x0762, 0x14), + array(0x0255, 0x15), + array(0x0D0C, 0x16), + array(0x083B, 0x17), + array(0x355F, 0x18), + array(0x3068, 0x19), + array(0x3F31, 0x1A), + array(0x3A06, 0x1B), + array(0x24B4, 0x1C), + array(0x2183, 0x1D), + array(0x2EDA, 0x1E), + array(0x2BED, 0x1F), + ); + + } + private function __construct($formatInfo) { + // Bits 3,4 + $this->errorCorrectionLevel = ErrorCorrectionLevel::forBits(($formatInfo >> 3) & 0x03); + // Bottom 3 bits + $this->dataMask = ($formatInfo & 0x07);//(byte) + } + + static function numBitsDiffering($a, $b) { + $a ^= $b; // a now has a 1 bit exactly where its bit differs with b's + // Count bits set quickly with a series of lookups: + return self::$BITS_SET_IN_HALF_BYTE[$a & 0x0F] + + self::$BITS_SET_IN_HALF_BYTE[intval(uRShift($a, 4) & 0x0F)] + + self::$BITS_SET_IN_HALF_BYTE[(uRShift($a ,8) & 0x0F)] + + self::$BITS_SET_IN_HALF_BYTE[(uRShift($a , 12) & 0x0F)] + + self::$BITS_SET_IN_HALF_BYTE[(uRShift($a, 16) & 0x0F)] + + self::$BITS_SET_IN_HALF_BYTE[(uRShift($a , 20) & 0x0F)] + + self::$BITS_SET_IN_HALF_BYTE[(uRShift($a, 24) & 0x0F)] + + self::$BITS_SET_IN_HALF_BYTE[(uRShift($a ,28) & 0x0F)]; + } + + /** + * @param maskedFormatInfo1; format info indicator, with mask still applied + * @param maskedFormatInfo2; second copy of same info; both are checked at the same time + * to establish best match + * @return information about the format it specifies, or {@code null} + * if doesn't seem to match any known pattern + */ + static function decodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2) { + $formatInfo = self::doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2); + if ($formatInfo != null) { + return $formatInfo; + } + // Should return null, but, some QR codes apparently + // do not mask this info. Try again by actually masking the pattern + // first + return self::doDecodeFormatInformation($maskedFormatInfo1 ^ self::$FORMAT_INFO_MASK_QR, + $maskedFormatInfo2 ^ self::$FORMAT_INFO_MASK_QR); + } + + private static function doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2) { + // Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing + $bestDifference = PHP_INT_MAX; + $bestFormatInfo = 0; + foreach (self::$FORMAT_INFO_DECODE_LOOKUP as $decodeInfo ) { + $targetInfo = $decodeInfo[0]; + if ($targetInfo == $maskedFormatInfo1 || $targetInfo == $maskedFormatInfo2) { + // Found an exact match + return new FormatInformation($decodeInfo[1]); + } + $bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $targetInfo); + if ($bitsDifference < $bestDifference) { + $bestFormatInfo = $decodeInfo[1]; + $bestDifference = $bitsDifference; + } + if ($maskedFormatInfo1 != $maskedFormatInfo2) { + // also try the other option + $bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $targetInfo); + if ($bitsDifference < $bestDifference) { + $bestFormatInfo = $decodeInfo[1]; + $bestDifference = $bitsDifference; + } + } + } + // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits + // differing means we found a match + if ($bestDifference <= 3) { + return new FormatInformation($bestFormatInfo); + } + return null; + } + + function getErrorCorrectionLevel() { + return $this->errorCorrectionLevel; + } + + function getDataMask() { + return $this->dataMask; + } + + //@Override + public function hashCode() { + return ($this->errorCorrectionLevel->ordinal() << 3) | intval($this->dataMask); + } + + //@Override + public function equals($o) { + if (!($o instanceof FormatInformation)) { + return false; + } + $other =$o; + return $this->errorCorrectionLevel == $other->errorCorrectionLevel && + $this->dataMask == $other->dataMask; + } + +} +FormatInformation::Init(); diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Mode.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Mode.php new file mode 100644 index 0000000..30421ad --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Mode.php @@ -0,0 +1,118 @@ +See ISO 18004:2006, 6.4.1, Tables 2 and 3. This enum encapsulates the various modes in which + * data can be encoded to bits in the QR code standard.

    + * + * @author Sean Owen + */ +class Mode { + static $TERMINATOR; + static $NUMERIC; + static $ALPHANUMERIC; + static $STRUCTURED_APPEND; + static $BYTE; + static $ECI; + static $KANJI; + static $FNC1_FIRST_POSITION; + static $FNC1_SECOND_POSITION; + static $HANZI; + + + private $characterCountBitsForVersions; + private $bits; + + function __construct($characterCountBitsForVersions, $bits) { + $this->characterCountBitsForVersions = $characterCountBitsForVersions; + $this->bits = $bits; + } + static function Init() + { + + + self::$TERMINATOR = new Mode(array(0, 0, 0), 0x00); // Not really a mode... + self::$NUMERIC = new Mode(array(10, 12, 14), 0x01); + self::$ALPHANUMERIC = new Mode(array(9, 11, 13), 0x02); + self::$STRUCTURED_APPEND = new Mode(array(0, 0, 0), 0x03); // Not supported + self::$BYTE = new Mode(array(8, 16, 16), 0x04); + self::$ECI = new Mode(array(0, 0, 0), 0x07); // character counts don't apply + self::$KANJI = new Mode(array(8, 10, 12), 0x08); + self::$FNC1_FIRST_POSITION = new Mode(array(0, 0, 0), 0x05); + self::$FNC1_SECOND_POSITION =new Mode(array(0, 0, 0), 0x09); + /** See GBT 18284-2000; "Hanzi" is a transliteration of this mode name. */ + self::$HANZI = new Mode(array(8, 10, 12), 0x0D); + } + /** + * @param bits four bits encoding a QR Code data mode + * @return Mode encoded by these bits + * @throws IllegalArgumentException if bits do not correspond to a known mode + */ + public static function forBits($bits) { + switch ($bits) { + case 0x0: + return self::$TERMINATOR; + case 0x1: + return self::$NUMERIC; + case 0x2: + return self::$ALPHANUMERIC; + case 0x3: + return self::$STRUCTURED_APPEND; + case 0x4: + return self::$BYTE; + case 0x5: + return self::$FNC1_FIRST_POSITION; + case 0x7: + return self::$ECI; + case 0x8: + return self::$KANJI; + case 0x9: + return self::$FNC1_SECOND_POSITION; + case 0xD: + // 0xD is defined in GBT 18284-2000, may not be supported in foreign country + return self::$HANZI; + default: + throw new \InvalidArgumentException(); + } + } + + /** + * @param version version in question + * @return number of bits used, in this QR Code symbol {@link Version}, to encode the + * count of characters that will follow encoded in this Mode + */ + public function getCharacterCountBits($version) { + $number = $version->getVersionNumber(); + $offset = 0; + if ($number <= 9) { + $offset = 0; + } else if ($number <= 26) { + $offset = 1; + } else { + $offset = 2; + } + return $this->characterCountBitsForVersions[$offset]; + } + + public function getBits() { + return $this->bits; + } + +} +Mode::Init(); \ No newline at end of file diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Version.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Version.php new file mode 100644 index 0000000..a3cc03f --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/decoder/Version.php @@ -0,0 +1,619 @@ +versionNumber = $versionNumber; + $this->alignmentPatternCenters = $alignmentPatternCenters; + $this->ecBlocks = $ecBlocks; + $total = 0; + if(is_array($ecBlocks)) { + $ecCodewords = $ecBlocks[0]->getECCodewordsPerBlock(); + $ecbArray = $ecBlocks[0]->getECBlocks(); + }else{ + $ecCodewords = $ecBlocks->getECCodewordsPerBlock(); + $ecbArray = $ecBlocks->getECBlocks(); + } + foreach ($ecbArray as $ecBlock) { + $total += $ecBlock->getCount() * ($ecBlock->getDataCodewords() + $ecCodewords); + } + $this->totalCodewords = $total; + } + public function getVersionNumber() + { + return $this->versionNumber; + } + + public function getAlignmentPatternCenters() + { + return $this->alignmentPatternCenters; + } + + public function getTotalCodewords() + { + return $this->totalCodewords; + } + + public function getDimensionForVersion() + { + return 17 + 4 * $this->versionNumber; + } + + public function getECBlocksForLevel($ecLevel) + { + return $this->ecBlocks[$ecLevel->getOrdinal()]; + } + + /** + *

    Deduces version information purely from QR Code dimensions.

    + * + * @param dimension dimension in modules + * @return Version for a QR Code of that dimension + * @throws FormatException if dimension is not 1 mod 4 + */ + public static function getProvisionalVersionForDimension($dimension) + { + if ($dimension % 4 != 1) { + throw FormatException::getFormatInstance(); + } + try { + return self::getVersionForNumber(($dimension - 17) / 4); + } catch (InvalidArgumentException $ignored) { + throw FormatException::getFormatInstance(); + } + } + + public static function getVersionForNumber($versionNumber) + { + if ($versionNumber < 1 || $versionNumber > 40) { + throw new \InvalidArgumentException(); + } + if(!self::$VERSIONS){ + + self::$VERSIONS = self::buildVersions(); + + } + return self::$VERSIONS[$versionNumber - 1]; + } + + static function decodeVersionInformation($versionBits) + { + $bestDifference = PHP_INT_MAX; + $bestVersion = 0; + for ($i = 0; $i < count(self::$VERSION_DECODE_INFO); $i++) { + $targetVersion = self::$VERSION_DECODE_INFO[$i]; +// Do the version info bits match exactly? done. + if ($targetVersion == $versionBits) { + return self::getVersionForNumber($i + 7); + } +// Otherwise see if this is the closest to a real version info bit string +// we have seen so far + $bitsDifference = FormatInformation::numBitsDiffering($versionBits, $targetVersion); + if ($bitsDifference < $bestDifference) { + $bestVersion = $i + 7; + $bestDifference = $bitsDifference; + } + } +// We can tolerate up to 3 bits of error since no two version info codewords will +// differ in less than 8 bits. + if ($bestDifference <= 3) { + return self::getVersionForNumber($bestVersion); + } +// If we didn't find a close enough match, fail + return null; + } + + /** + * See ISO 18004:2006 Annex E + */ + function buildFunctionPattern() + { + $dimension = self::getDimensionForVersion(); + $bitMatrix = new BitMatrix($dimension); + +// Top left finder pattern + separator + format + $bitMatrix->setRegion(0, 0, 9, 9); +// Top right finder pattern + separator + format + $bitMatrix->setRegion($dimension - 8, 0, 8, 9); +// Bottom left finder pattern + separator + format + $bitMatrix->setRegion(0, $dimension - 8, 9, 8); + +// Alignment patterns + $max = count($this->alignmentPatternCenters); + for ($x = 0; $x < $max; $x++) { + $i = $this->alignmentPatternCenters[$x] - 2; + for ($y = 0; $y < $max; $y++) { + if (($x == 0 && ($y == 0 || $y == $max - 1)) || ($x == $max - 1 && $y == 0)) { +// No alignment patterns near the three finder paterns + continue; + } + $bitMatrix->setRegion($this->alignmentPatternCenters[$y] - 2, $i, 5, 5); + } + } + +// Vertical timing pattern + $bitMatrix->setRegion(6, 9, 1, $dimension - 17); +// Horizontal timing pattern + $bitMatrix->setRegion(9, 6, $dimension - 17, 1); + + if ($this->versionNumber > 6) { +// Version info, top right + $bitMatrix->setRegion($dimension - 11, 0, 3, 6); +// Version info, bottom left + $bitMatrix->setRegion(0, $dimension - 11, 6, 3); + } + + return $bitMatrix; + } + /** + * See ISO 18004:2006 6.5.1 Table 9 + */ + private static function buildVersions() + { + + + return array( + new Version(1, array(), + array(new ECBlocks(7, array(new ECB(1, 19))), + new ECBlocks(10, array(new ECB(1, 16))), + new ECBlocks(13, array(new ECB(1, 13))), + new ECBlocks(17, array(new ECB(1, 9))))), + new Version(2, array(6, 18), + array(new ECBlocks(10, array(new ECB(1, 34))), + new ECBlocks(16, array(new ECB(1, 28))), + new ECBlocks(22, array(new ECB(1, 22))), + new ECBlocks(28, array(new ECB(1, 16))))), + new Version(3, array(6, 22), + array( new ECBlocks(15, array(new ECB(1, 55))), + new ECBlocks(26, array(new ECB(1, 44))), + new ECBlocks(18, array(new ECB(2, 17))), + new ECBlocks(22, array(new ECB(2, 13))))), + new Version(4, array(6, 26), + array(new ECBlocks(20, array(new ECB(1, 80))), + new ECBlocks(18, array(new ECB(2, 32))), + new ECBlocks(26, array(new ECB(2, 24))), + new ECBlocks(16, array(new ECB(4, 9))))), + new Version(5, array(6, 30), + array(new ECBlocks(26, array(new ECB(1, 108))), + new ECBlocks(24, array(new ECB(2, 43))), + new ECBlocks(18, array(new ECB(2, 15), + new ECB(2, 16))), + new ECBlocks(22, array(new ECB(2, 11), + new ECB(2, 12))))), + new Version(6, array(6, 34), + array(new ECBlocks(18, array(new ECB(2, 68))), + new ECBlocks(16, array(new ECB(4, 27))), + new ECBlocks(24, array(new ECB(4, 19))), + new ECBlocks(28, array(new ECB(4, 15))))), + new Version(7, array(6, 22, 38), + array(new ECBlocks(20, array(new ECB(2, 78))), + new ECBlocks(18, array(new ECB(4, 31))), + new ECBlocks(18, array(new ECB(2, 14), + new ECB(4, 15))), + new ECBlocks(26, array(new ECB(4, 13), + new ECB(1, 14))))), + new Version(8, array(6, 24, 42), + array(new ECBlocks(24, array(new ECB(2, 97))), + new ECBlocks(22, array(new ECB(2, 38), + new ECB(2, 39))), + new ECBlocks(22, array(new ECB(4, 18), + new ECB(2, 19))), + new ECBlocks(26, array(new ECB(4, 14), + new ECB(2, 15))))), + new Version(9, array(6, 26, 46), + array(new ECBlocks(30, array(new ECB(2, 116))), + new ECBlocks(22, array(new ECB(3, 36), + new ECB(2, 37))), + new ECBlocks(20, array(new ECB(4, 16), + new ECB(4, 17))), + new ECBlocks(24, array(new ECB(4, 12), + new ECB(4, 13))))), + new Version(10, array(6, 28, 50), + array(new ECBlocks(18, array(new ECB(2, 68), + new ECB(2, 69))), + new ECBlocks(26, array(new ECB(4, 43), + new ECB(1, 44))), + new ECBlocks(24, array(new ECB(6, 19), + new ECB(2, 20))), + new ECBlocks(28, array(new ECB(6, 15), + new ECB(2, 16))))), + new Version(11, array(6, 30, 54), + array(new ECBlocks(20, array(new ECB(4, 81))), + new ECBlocks(30, array(new ECB(1, 50), + new ECB(4, 51))), + new ECBlocks(28, array(new ECB(4, 22), + new ECB(4, 23))), + new ECBlocks(24, array(new ECB(3, 12), + new ECB(8, 13))))), + new Version(12, array(6, 32, 58), + array(new ECBlocks(24, array(new ECB(2, 92), + new ECB(2, 93))), + new ECBlocks(22, array(new ECB(6, 36), + new ECB(2, 37))), + new ECBlocks(26, array(new ECB(4, 20), + new ECB(6, 21))), + new ECBlocks(28, array(new ECB(7, 14), + new ECB(4, 15))))), + new Version(13, array(6, 34, 62), + array(new ECBlocks(26, array(new ECB(4, 107))), + new ECBlocks(22, array(new ECB(8, 37), + new ECB(1, 38))), + new ECBlocks(24, array(new ECB(8, 20), + new ECB(4, 21))), + new ECBlocks(22, array(new ECB(12, 11), + new ECB(4, 12))))), + new Version(14, array(6, 26, 46, 66), + array(new ECBlocks(30, array(new ECB(3, 115), + new ECB(1, 116))), + new ECBlocks(24, array(new ECB(4, 40), + new ECB(5, 41))), + new ECBlocks(20, array(new ECB(11, 16), + new ECB(5, 17))), + new ECBlocks(24, array(new ECB(11, 12), + new ECB(5, 13))))), + new Version(15, array(6, 26, 48, 70), + array(new ECBlocks(22, array(new ECB(5, 87), + new ECB(1, 88))), + new ECBlocks(24, array(new ECB(5, 41), + new ECB(5, 42))), + new ECBlocks(30, array(new ECB(5, 24), + new ECB(7, 25))), + new ECBlocks(24, array(new ECB(11, 12), + new ECB(7, 13))))), + new Version(16, array(6, 26, 50, 74), + array(new ECBlocks(24, array(new ECB(5, 98), + new ECB(1, 99))), + new ECBlocks(28, array(new ECB(7, 45), + new ECB(3, 46))), + new ECBlocks(24, array(new ECB(15, 19), + new ECB(2, 20))), + new ECBlocks(30, array(new ECB(3, 15), + new ECB(13, 16))))), + new Version(17, array(6, 30, 54, 78), + array(new ECBlocks(28, array(new ECB(1, 107), + new ECB(5, 108))), + new ECBlocks(28, array(new ECB(10, 46), + new ECB(1, 47))), + new ECBlocks(28, array(new ECB(1, 22), + new ECB(15, 23))), + new ECBlocks(28, array(new ECB(2, 14), + new ECB(17, 15))))), + new Version(18, array(6, 30, 56, 82), + array(new ECBlocks(30, array(new ECB(5, 120), + new ECB(1, 121))), + new ECBlocks(26, array(new ECB(9, 43), + new ECB(4, 44))), + new ECBlocks(28, array(new ECB(17, 22), + new ECB(1, 23))), + new ECBlocks(28, array(new ECB(2, 14), + new ECB(19, 15))))), + new Version(19, array(6, 30, 58, 86), + array(new ECBlocks(28, array(new ECB(3, 113), + new ECB(4, 114))), + new ECBlocks(26, array(new ECB(3, 44), + new ECB(11, 45))), + new ECBlocks(26, array(new ECB(17, 21), + new ECB(4, 22))), + new ECBlocks(26, array(new ECB(9, 13), + new ECB(16, 14))))), + new Version(20, array(6, 34, 62, 90), + array(new ECBlocks(28, array(new ECB(3, 107), + new ECB(5, 108))), + new ECBlocks(26, array(new ECB(3, 41), + new ECB(13, 42))), + new ECBlocks(30, array(new ECB(15, 24), + new ECB(5, 25))), + new ECBlocks(28, array(new ECB(15, 15), + new ECB(10, 16))))), + new Version(21, array(6, 28, 50, 72, 94), + array( new ECBlocks(28, array(new ECB(4, 116), + new ECB(4, 117))), + new ECBlocks(26, array(new ECB(17, 42))), + new ECBlocks(28, array(new ECB(17, 22), + new ECB(6, 23))), + new ECBlocks(30, array(new ECB(19, 16), + new ECB(6, 17))))), + new Version(22, array(6, 26, 50, 74, 98), + array(new ECBlocks(28, array(new ECB(2, 111), + new ECB(7, 112))), + new ECBlocks(28, array(new ECB(17, 46))), + new ECBlocks(30, array(new ECB(7, 24), + new ECB(16, 25))), + new ECBlocks(24, array(new ECB(34, 13))))), + new Version(23, array(6, 30, 54, 78, 102), + new ECBlocks(30, array(new ECB(4, 121), + new ECB(5, 122))), + new ECBlocks(28, array(new ECB(4, 47), + new ECB(14, 48))), + new ECBlocks(30, array(new ECB(11, 24), + new ECB(14, 25))), + new ECBlocks(30, array(new ECB(16, 15), + new ECB(14, 16)))), + new Version(24, array(6, 28, 54, 80, 106), + array(new ECBlocks(30, array(new ECB(6, 117), + new ECB(4, 118))), + new ECBlocks(28, array(new ECB(6, 45), + new ECB(14, 46))), + new ECBlocks(30, array(new ECB(11, 24), + new ECB(16, 25))), + new ECBlocks(30, array(new ECB(30, 16), + new ECB(2, 17))))), + new Version(25, array(6, 32, 58, 84, 110), + array(new ECBlocks(26, array(new ECB(8, 106), + new ECB(4, 107))), + new ECBlocks(28, array(new ECB(8, 47), + new ECB(13, 48))), + new ECBlocks(30, array(new ECB(7, 24), + new ECB(22, 25))), + new ECBlocks(30, array(new ECB(22, 15), + new ECB(13, 16))))), + new Version(26, array(6, 30, 58, 86, 114), + array(new ECBlocks(28, array(new ECB(10, 114), + new ECB(2, 115))), + new ECBlocks(28, array(new ECB(19, 46), + new ECB(4, 47))), + new ECBlocks(28, array(new ECB(28, 22), + new ECB(6, 23))), + new ECBlocks(30, array(new ECB(33, 16), + new ECB(4, 17))))), + new Version(27, array(6, 34, 62, 90, 118), + array(new ECBlocks(30, array(new ECB(8, 122), + new ECB(4, 123))), + new ECBlocks(28, array(new ECB(22, 45), + new ECB(3, 46))), + new ECBlocks(30, array(new ECB(8, 23), + new ECB(26, 24))), + new ECBlocks(30, array(new ECB(12, 15), + new ECB(28, 16))))), + new Version(28, array(6, 26, 50, 74, 98, 122), + array(new ECBlocks(30, array(new ECB(3, 117), + new ECB(10, 118))), + new ECBlocks(28, array(new ECB(3, 45), + new ECB(23, 46))), + new ECBlocks(30, array(new ECB(4, 24), + new ECB(31, 25))), + new ECBlocks(30, array(new ECB(11, 15), + new ECB(31, 16))))), + new Version(29, array(6, 30, 54, 78, 102, 126), + array(new ECBlocks(30, array(new ECB(7, 116), + new ECB(7, 117))), + new ECBlocks(28, array(new ECB(21, 45), + new ECB(7, 46))), + new ECBlocks(30, array(new ECB(1, 23), + new ECB(37, 24))), + new ECBlocks(30, array(new ECB(19, 15), + new ECB(26, 16))))), + new Version(30, array(6, 26, 52, 78, 104, 130), + array(new ECBlocks(30, array(new ECB(5, 115), + new ECB(10, 116))), + new ECBlocks(28, array(new ECB(19, 47), + new ECB(10, 48))), + new ECBlocks(30, array(new ECB(15, 24), + new ECB(25, 25))), + new ECBlocks(30, array(new ECB(23, 15), + new ECB(25, 16))))), + new Version(31, array(6, 30, 56, 82, 108, 134), + array(new ECBlocks(30, array(new ECB(13, 115), + new ECB(3, 116))), + new ECBlocks(28, array(new ECB(2, 46), + new ECB(29, 47))), + new ECBlocks(30, array(new ECB(42, 24), + new ECB(1, 25))), + new ECBlocks(30, array(new ECB(23, 15), + new ECB(28, 16))))), + new Version(32, array(6, 34, 60, 86, 112, 138), + array(new ECBlocks(30, array(new ECB(17, 115))), + new ECBlocks(28, array(new ECB(10, 46), + new ECB(23, 47))), + new ECBlocks(30, array(new ECB(10, 24), + new ECB(35, 25))), + new ECBlocks(30, array(new ECB(19, 15), + new ECB(35, 16))))), + new Version(33, array(6, 30, 58, 86, 114, 142), + array(new ECBlocks(30, array(new ECB(17, 115), + new ECB(1, 116))), + new ECBlocks(28, array(new ECB(14, 46), + new ECB(21, 47))), + new ECBlocks(30, array(new ECB(29, 24), + new ECB(19, 25))), + new ECBlocks(30, array(new ECB(11, 15), + new ECB(46, 16))))), + new Version(34, array(6, 34, 62, 90, 118, 146), + array(new ECBlocks(30, array(new ECB(13, 115), + new ECB(6, 116))), + new ECBlocks(28, array(new ECB(14, 46), + new ECB(23, 47))), + new ECBlocks(30, array(new ECB(44, 24), + new ECB(7, 25))), + new ECBlocks(30, array(new ECB(59, 16), + new ECB(1, 17))))), + new Version(35, array(6, 30, 54, 78, 102, 126, 150), + array(new ECBlocks(30, array(new ECB(12, 121), + new ECB(7, 122))), + new ECBlocks(28, array(new ECB(12, 47), + new ECB(26, 48))), + new ECBlocks(30, array(new ECB(39, 24), + new ECB(14, 25))), + new ECBlocks(30, array(new ECB(22, 15), + new ECB(41, 16))))), + new Version(36, array(6, 24, 50, 76, 102, 128, 154), + array(new ECBlocks(30, array(new ECB(6, 121), + new ECB(14, 122))), + new ECBlocks(28, array(new ECB(6, 47), + new ECB(34, 48))), + new ECBlocks(30, array(new ECB(46, 24), + new ECB(10, 25))), + new ECBlocks(30, array(new ECB(2, 15), + new ECB(64, 16))))), + new Version(37, array(6, 28, 54, 80, 106, 132, 158), + array(new ECBlocks(30, array(new ECB(17, 122), + new ECB(4, 123))), + new ECBlocks(28, array(new ECB(29, 46), + new ECB(14, 47))), + new ECBlocks(30, array(new ECB(49, 24), + new ECB(10, 25))), + new ECBlocks(30, array(new ECB(24, 15), + new ECB(46, 16))))), + new Version(38, array(6, 32, 58, 84, 110, 136, 162), + array(new ECBlocks(30, array(new ECB(4, 122), + new ECB(18, 123))), + new ECBlocks(28, array(new ECB(13, 46), + new ECB(32, 47))), + new ECBlocks(30, array(new ECB(48, 24), + new ECB(14, 25))), + new ECBlocks(30, array(new ECB(42, 15), + new ECB(32, 16))))), + new Version(39, array(6, 26, 54, 82, 110, 138, 166), + array(new ECBlocks(30, array(new ECB(20, 117), + new ECB(4, 118))), + new ECBlocks(28, array(new ECB(40, 47), + new ECB(7, 48))), + new ECBlocks(30, array(new ECB(43, 24), + new ECB(22, 25))), + new ECBlocks(30, array(new ECB(10, 15), + new ECB(67, 16))))), + new Version(40, array(6, 30, 58, 86, 114, 142, 170), + array(new ECBlocks(30, array(new ECB(19, 118), + new ECB(6, 119))), + new ECBlocks(28, array(new ECB(18, 47), + new ECB(31, 48))), + new ECBlocks(30, array(new ECB(34, 24), + new ECB(34, 25))), + new ECBlocks(30, array(new ECB(20, 15), + new ECB(61, 16))))) + ); + } +} + +/** + *

    Encapsulates a set of error-correction blocks in one symbol version. Most versions will + * use blocks of differing sizes within one version, so, this encapsulates the parameters for + * each set of blocks. It also holds the number of error-correction codewords per block since it + * will be the same across all blocks within one version.

    + */ +final class ECBlocks +{ + private $ecCodewordsPerBlock; + private $ecBlocks; + + function __construct($ecCodewordsPerBlock, $ecBlocks) + { + $this->ecCodewordsPerBlock = $ecCodewordsPerBlock; + $this->ecBlocks = $ecBlocks; + } + + public function getECCodewordsPerBlock() + { + return $this->ecCodewordsPerBlock; + } + + public function getNumBlocks() + { + $total = 0; + foreach ($this->ecBlocks as $ecBlock) { + $total += $ecBlock->getCount(); + } + return $total; + } + + public function getTotalECCodewords() + { + return $this->ecCodewordsPerBlock * $this->getNumBlocks(); + } + + public function getECBlocks() + { + return $this->ecBlocks; + } +} + +/** + *

    Encapsualtes the parameters for one error-correction block in one symbol version. + * This includes the number of data codewords, and the number of times a block with these + * parameters is used consecutively in the QR code version's format.

    + */ +final class ECB +{ + private $count; + private $dataCodewords; + + function __construct($count, $dataCodewords) + { + $this->count = $count; + $this->dataCodewords = $dataCodewords; + } + + public function getCount() + { + return $this->count; + } + + public function getDataCodewords() + { + return $this->dataCodewords; + } + + +//@Override + public function toString() + { + die('Version ECB toString()'); + // return parent::$versionNumber; + } + + +} + + diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/AlignmentPattern.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/AlignmentPattern.php new file mode 100644 index 0000000..b6d4130 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/AlignmentPattern.php @@ -0,0 +1,60 @@ +Encapsulates an alignment pattern, which are the smaller square patterns found in + * all but the simplest QR Codes.

    + * + * @author Sean Owen + */ +final class AlignmentPattern extends ResultPoint { + +private $estimatedModuleSize; + +function __construct($posX, $posY, $estimatedModuleSize) { +parent::__construct($posX, $posY); +$this->estimatedModuleSize = $estimatedModuleSize; +} + + /** + *

    Determines if this alignment pattern "about equals" an alignment pattern at the stated + * position and size -- meaning, it is at nearly the same center with nearly the same size.

    + */ + function aboutEquals($moduleSize, $i, $j) { + if (abs($i - $this->getY()) <= $moduleSize && abs($j - $this->getX()) <= $moduleSize) { + $moduleSizeDiff = abs($moduleSize - $this->estimatedModuleSize); + return $moduleSizeDiff <= 1.0 || $moduleSizeDiff <= $this->estimatedModuleSize; + } + return false; +} + + /** + * Combines this object's current estimate of a finder pattern position and module size + * with a new estimate. It returns a new {@code FinderPattern} containing an average of the two. + */ + function combineEstimate($i, $j, $newModuleSize) { + $combinedX = ($this->getX() + $j) / 2.0; + $combinedY = ($this->getY() + $i) / 2.0; + $combinedModuleSize = ($this->estimatedModuleSize + $newModuleSize) / 2.0; + return new AlignmentPattern($combinedX, $combinedY, $combinedModuleSize); + } + +} \ No newline at end of file diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/AlignmentPatternFinder.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/AlignmentPatternFinder.php new file mode 100644 index 0000000..0484ffa --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/AlignmentPatternFinder.php @@ -0,0 +1,277 @@ +This class attempts to find alignment patterns in a QR Code. Alignment patterns look like finder + * patterns but are smaller and appear at regular intervals throughout the image.

    + * + *

    At the moment this only looks for the bottom-right alignment pattern.

    + * + *

    This is mostly a simplified copy of {@link FinderPatternFinder}. It is copied, + * pasted and stripped down here for maximum performance but does unfortunately duplicate + * some code.

    + * + *

    This class is thread-safe but not reentrant. Each thread must allocate its own object.

    + * + * @author Sean Owen + */ +final class AlignmentPatternFinder { + + private $image; + private $possibleCenters; + private $startX; + private $startY; + private $width; + private $height; + private $moduleSize; + private $crossCheckStateCount; + private $resultPointCallback; + + /** + *

    Creates a finder that will look in a portion of the whole image.

    + * + * @param image image to search + * @param startX left column from which to start searching + * @param startY top row from which to start searching + * @param width width of region to search + * @param height height of region to search + * @param moduleSize estimated module size so far + */ + function __construct($image, + $startX, + $startY, + $width, + $height, + $moduleSize, + $resultPointCallback) { + $this->image = $image; + $this->possibleCenters = array(); + $this->startX = $startX; + $this->startY = $startY; + $this->width = $width; + $this->height = $height; + $this->moduleSize = $moduleSize; + $this->crossCheckStateCount = array(); + $this->resultPointCallback = $resultPointCallback; + } + + /** + *

    This method attempts to find the bottom-right alignment pattern in the image. It is a bit messy since + * it's pretty performance-critical and so is written to be fast foremost.

    + * + * @return {@link AlignmentPattern} if found + * @throws NotFoundException if not found + */ + function find() { + $startX = $this->startX; + $height = $this->height; + $maxJ = $startX + $this->width; + $middleI = $this->startY + ($height / 2); + // We are looking for black/white/black modules in 1:1:1 ratio; + // this tracks the number of black/white/black modules seen so far + $stateCount = array(); + for ($iGen = 0; $iGen < $height; $iGen++) { + // Search from middle outwards + $i = $middleI + (($iGen & 0x01) == 0 ? ($iGen + 1) / 2 : -(($iGen + 1) / 2)); + $i = intval($i); + $stateCount[0] = 0; + $stateCount[1] = 0; + $stateCount[2] = 0; + $j = $startX; + // Burn off leading white pixels before anything else; if we start in the middle of + // a white run, it doesn't make sense to count its length, since we don't know if the + // white run continued to the left of the start point + while ($j < $maxJ && !$this->image->get($j, $i)) { + $j++; + } + $currentState = 0; + while ($j < $maxJ) { + if ($this->image->get($j, $i)) { + // Black pixel + if ($currentState == 1) { // Counting black pixels + $stateCount[$currentState]++; + } else { // Counting white pixels + if ($currentState == 2) { // A winner? + if ($this->foundPatternCross($stateCount)) { // Yes + $confirmed = $this->handlePossibleCenter($stateCount, $i, $j); + if ($confirmed != null) { + return $confirmed; + } + } + $stateCount[0] = $stateCount[2]; + $stateCount[1] = 1; + $stateCount[2] = 0; + $currentState = 1; + } else { + $stateCount[++$currentState]++; + } + } + } else { // White pixel + if ($currentState == 1) { // Counting black pixels + $currentState++; + } + $stateCount[$currentState]++; + } + $j++; + } + if ($this->foundPatternCross($stateCount)) { + $confirmed = $this->handlePossibleCenter($stateCount, $i, $maxJ); + if ($confirmed != null) { + return $confirmed; + } + } + + } + + // Hmm, nothing we saw was observed and confirmed twice. If we had + // any guess at all, return it. + if (count($this->possibleCenters)) { + return $this->possibleCenters[0]; + } + + throw NotFoundException::getNotFoundInstance(); + } + + /** + * Given a count of black/white/black pixels just seen and an end position, + * figures the location of the center of this black/white/black run. + */ + private static function centerFromEnd($stateCount, $end) { + return (float) ($end - $stateCount[2]) - $stateCount[1] / 2.0; + } + + /** + * @param stateCount count of black/white/black pixels just read + * @return true iff the proportions of the counts is close enough to the 1/1/1 ratios + * used by alignment patterns to be considered a match + */ + private function foundPatternCross($stateCount) { + $moduleSize = $this->moduleSize; + $maxVariance = $moduleSize / 2.0; + for ($i = 0; $i < 3; $i++) { + if (abs($moduleSize - $stateCount[$i]) >= $maxVariance) { + return false; + } + } + return true; + } + + /** + *

    After a horizontal scan finds a potential alignment pattern, this method + * "cross-checks" by scanning down vertically through the center of the possible + * alignment pattern to see if the same proportion is detected.

    + * + * @param startI row where an alignment pattern was detected + * @param centerJ center of the section that appears to cross an alignment pattern + * @param maxCount maximum reasonable number of modules that should be + * observed in any reading state, based on the results of the horizontal scan + * @return vertical center of alignment pattern, or {@link Float#NaN} if not found + */ + private function crossCheckVertical($startI, $centerJ, $maxCount, + $originalStateCountTotal) { + $image = $this->image; + + $maxI = $image->getHeight(); + $stateCount = $this->crossCheckStateCount; + $stateCount[0] = 0; + $stateCount[1] = 0; + $stateCount[2] = 0; + + // Start counting up from center + $i = $startI; + while ($i >= 0 && $image->get($centerJ, $i) && $stateCount[1] <= $maxCount) { + $stateCount[1]++; + $i--; + } + // If already too many modules in this state or ran off the edge: + if ($i < 0 || $stateCount[1] > $maxCount) { + return NAN; + } + while ($i >= 0 && !$image->get($centerJ, $i) && $stateCount[0] <= $maxCount) { + $stateCount[0]++; + $i--; + } + if ($stateCount[0] > $maxCount) { + return NAN; + } + + // Now also count down from center + $i = $startI + 1; + while ($i < $maxI && $image->get($centerJ, $i) && $stateCount[1] <= $maxCount) { + $stateCount[1]++; + $i++; + } + if ($i == $maxI || $stateCount[1] > $maxCount) { + return NAN; + } + while ($i < $maxI && !$image->get($centerJ, $i) && $stateCount[2] <= $maxCount) { + $stateCount[2]++; + $i++; + } + if ($stateCount[2] > $maxCount) { + return NAN; + } + + $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2]; + if (5 * abs($stateCountTotal - $originalStateCountTotal) >= 2 * $originalStateCountTotal) { + return NAN; + } + + return $this->foundPatternCross($stateCount) ? $this->centerFromEnd($stateCount, $i) : NAN; + } + + /** + *

    This is called when a horizontal scan finds a possible alignment pattern. It will + * cross check with a vertical scan, and if successful, will see if this pattern had been + * found on a previous horizontal scan. If so, we consider it confirmed and conclude we have + * found the alignment pattern.

    + * + * @param stateCount reading state module counts from horizontal scan + * @param i row where alignment pattern may be found + * @param j end of possible alignment pattern in row + * @return {@link AlignmentPattern} if we have found the same pattern twice, or null if not + */ + private function handlePossibleCenter($stateCount, $i, $j) { + $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2]; + $centerJ = $this->centerFromEnd($stateCount, $j); + $centerI = $this->crossCheckVertical($i, (int) $centerJ, 2 * $stateCount[1], $stateCountTotal); + if (!is_nan($centerI)) { + $estimatedModuleSize = (float) ($stateCount[0] + $stateCount[1] + $stateCount[2]) / 3.0; + foreach ($this->possibleCenters as $center) { + // Look for about the same center and module size: + if ($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)) { + return $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize); + } + } + // Hadn't found this before; save it + $point = new AlignmentPattern($centerJ, $centerI, $estimatedModuleSize); + $this->possibleCenters[] = $point; + if ($this->resultPointCallback != null) { + $this->resultPointCallback->foundPossibleResultPoint($point); + } + } + return null; + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/Detector.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/Detector.php new file mode 100644 index 0000000..2043a49 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/Detector.php @@ -0,0 +1,405 @@ +Encapsulates logic that can detect a QR Code in an image, even if the QR Code + * is rotated or skewed, or partially obscured.

    + * + * @author Sean Owen + */ +?> +image = $image; + } + + protected final function getImage() { + return $this->image; + } + + protected final function getResultPointCallback() { + return $this->resultPointCallback; + } + + /** + *

    Detects a QR Code in an image.

    + * + * @return {@link DetectorResult} encapsulating results of detecting a QR Code + * @throws NotFoundException if QR Code cannot be found + * @throws FormatException if a QR Code cannot be decoded + */ + + + /** + *

    Detects a QR Code in an image.

    + * + * @param hints optional hints to detector + * @return {@link DetectorResult} encapsulating results of detecting a QR Code + * @throws NotFoundException if QR Code cannot be found + * @throws FormatException if a QR Code cannot be decoded + */ + public final function detect($hints=null){/*Map*/ + + $resultPointCallback = $hints == null ? null : + $hints->get('NEED_RESULT_POINT_CALLBACK'); + /* resultPointCallback = hints == null ? null : + (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK);*/ + $finder = new FinderPatternFinder($this->image, $resultPointCallback); + $info = $finder->find($hints); + + return $this->processFinderPatternInfo($info); + } + + protected final function processFinderPatternInfo($info){ + + $topLeft = $info->getTopLeft(); + $topRight = $info->getTopRight(); + $bottomLeft = $info->getBottomLeft(); + + $moduleSize = (float) $this->calculateModuleSize($topLeft, $topRight, $bottomLeft); + if ($moduleSize < 1.0) { + throw NotFoundException::getNotFoundInstance(); + } + $dimension =(int) $this->computeDimension($topLeft, $topRight, $bottomLeft, $moduleSize); + $provisionalVersion = Version::getProvisionalVersionForDimension($dimension); + $modulesBetweenFPCenters = $provisionalVersion->getDimensionForVersion() - 7; + + $alignmentPattern = null; +// Anything above version 1 has an alignment pattern + if (count($provisionalVersion->getAlignmentPatternCenters())> 0) { + +// Guess where a "bottom right" finder pattern would have been + $bottomRightX = $topRight->getX() - $topLeft->getX() + $bottomLeft->getX(); + $bottomRightY = $topRight->getY() - $topLeft->getY() + $bottomLeft->getY(); + +// Estimate that alignment pattern is closer by 3 modules +// from "bottom right" to known top left location + $correctionToTopLeft = 1.0 - 3.0 / (float) $modulesBetweenFPCenters; + $estAlignmentX = (int) ($topLeft->getX() + $correctionToTopLeft * ($bottomRightX - $topLeft->getX())); + $estAlignmentY = (int) ($topLeft->getY() + $correctionToTopLeft * ($bottomRightY - $topLeft->getY())); + +// Kind of arbitrary -- expand search radius before giving up + for ($i = 4; $i <= 16; $i <<= 1) {//?????????? + try { + $alignmentPattern = $this->findAlignmentInRegion($moduleSize, + $estAlignmentX, + $estAlignmentY, + (float) $i); + break; + } catch (NotFoundException $re) { +// try next round + } + } +// If we didn't find alignment pattern... well try anyway without it + } + + $transform = + $this->createTransform($topLeft, $topRight, $bottomLeft, $alignmentPattern, $dimension); + + $bits = $this->sampleGrid($this->image, $transform, $dimension); + + $points = array(); + if ($alignmentPattern == null) { + $points = array($bottomLeft, $topLeft, $topRight); + } else { + // die('$points = new ResultPoint[]{bottomLeft, topLeft, topRight, alignmentPattern};'); +$points = array($bottomLeft, $topLeft, $topRight, $alignmentPattern); + } + return new DetectorResult($bits, $points); + } + + private static function createTransform($topLeft, + $topRight, + $bottomLeft, + $alignmentPattern, + $dimension) { + $dimMinusThree = (float) $dimension - 3.5; + $bottomRightX = 0.0; + $bottomRightY = 0.0; + $sourceBottomRightX = 0.0; + $sourceBottomRightY = 0.0; + if ($alignmentPattern != null) { + $bottomRightX = $alignmentPattern->getX(); + $bottomRightY = $alignmentPattern->getY(); + $sourceBottomRightX = $dimMinusThree - 3.0; + $sourceBottomRightY = $sourceBottomRightX; + } else { +// Don't have an alignment pattern, just make up the bottom-right point + $bottomRightX = ($topRight->getX() - $topLeft->getX()) + $bottomLeft->getX(); + $bottomRightY = ($topRight->getY() - $topLeft->getY()) + $bottomLeft->getY(); + $sourceBottomRightX = $dimMinusThree; + $sourceBottomRightY = $dimMinusThree; + } + + return PerspectiveTransform::quadrilateralToQuadrilateral( + 3.5, + 3.5, + $dimMinusThree, + 3.5, + $sourceBottomRightX, + $sourceBottomRightY, + 3.5, + $dimMinusThree, + $topLeft->getX(), + $topLeft->getY(), + $topRight->getX(), + $topRight->getY(), + $bottomRightX, + $bottomRightY, + $bottomLeft->getX(), + $bottomLeft->getY()); + } + + private static function sampleGrid($image, $transform, + $dimension) { + + $sampler = GridSampler::getInstance(); + return $sampler->sampleGrid_($image, $dimension, $dimension, $transform); + } + + /** + *

    Computes the dimension (number of modules on a size) of the QR Code based on the position + * of the finder patterns and estimated module size.

    + */ + private static function computeDimension($topLeft, + $topRight, + $bottomLeft, + $moduleSize) { + $tltrCentersDimension = MathUtils::round(ResultPoint::distance($topLeft, $topRight) / $moduleSize); + $tlblCentersDimension = MathUtils::round(ResultPoint::distance($topLeft, $bottomLeft) / $moduleSize); + $dimension = (($tltrCentersDimension + $tlblCentersDimension) / 2) + 7; + switch ($dimension & 0x03) { // mod 4 + case 0: + $dimension++; + break; +// 1? do nothing + case 2: + $dimension--; + break; + case 3: + throw NotFoundException::getNotFoundInstance(); + } + return $dimension; + } + + /** + *

    Computes an average estimated module size based on estimated derived from the positions + * of the three finder patterns.

    + * + * @param topLeft detected top-left finder pattern center + * @param topRight detected top-right finder pattern center + * @param bottomLeft detected bottom-left finder pattern center + * @return estimated module size + */ + protected final function calculateModuleSize($topLeft, + $topRight, + $bottomLeft) { +// Take the average + return ($this->calculateModuleSizeOneWay($topLeft, $topRight) + + $this->calculateModuleSizeOneWay($topLeft, $bottomLeft)) / 2.0; + } + + /** + *

    Estimates module size based on two finder patterns -- it uses + * {@link #sizeOfBlackWhiteBlackRunBothWays(int, int, int, int)} to figure the + * width of each, measuring along the axis between their centers.

    + */ + private function calculateModuleSizeOneWay($pattern, $otherPattern) { + $moduleSizeEst1 = $this->sizeOfBlackWhiteBlackRunBothWays($pattern->getX(), + (int) $pattern->getY(), + (int) $otherPattern->getX(), + (int) $otherPattern->getY()); + $moduleSizeEst2 = $this->sizeOfBlackWhiteBlackRunBothWays((int) $otherPattern->getX(), + (int) $otherPattern->getY(), + (int) $pattern->getX(), + (int) $pattern->getY()); + if (is_nan($moduleSizeEst1)) { + return $moduleSizeEst2 / 7.0; + } + if (is_nan($moduleSizeEst2)) { + return $moduleSizeEst1 / 7.0; + } +// Average them, and divide by 7 since we've counted the width of 3 black modules, +// and 1 white and 1 black module on either side. Ergo, divide sum by 14. + return ($moduleSizeEst1 + $moduleSizeEst2) / 14.0; + } + + /** + * See {@link #sizeOfBlackWhiteBlackRun(int, int, int, int)}; computes the total width of + * a finder pattern by looking for a black-white-black run from the center in the direction + * of another po$(another finder pattern center), and in the opposite direction too.

    + */ + private function sizeOfBlackWhiteBlackRunBothWays($fromX, $fromY, $toX, $toY) { + + $result = $this->sizeOfBlackWhiteBlackRun($fromX, $fromY, $toX, $toY); + +// Now count other way -- don't run off image though of course + $scale = 1.0; + $otherToX = $fromX - ($toX - $fromX); + if ($otherToX < 0) { + $scale = (float) $fromX / (float) ($fromX - $otherToX); + $otherToX = 0; + } else if ($otherToX >= $this->image->getWidth()) { + $scale = (float) ($this->image->getWidth() - 1 - $fromX) / (float) ($otherToX - $fromX); + $otherToX = $this->image->getWidth() - 1; + } + $otherToY = (int) ($fromY - ($toY - $fromY) * $scale); + + $scale = 1.0; + if ($otherToY < 0) { + $scale = (float) $fromY / (float) ($fromY - $otherToY); + $otherToY = 0; + } else if ($otherToY >= $this->image->getHeight()) { + $scale = (float) ($this->image->getHeight() - 1 - $fromY) / (float) ($otherToY - $fromY); + $otherToY = $this->image->getHeight() - 1; + } + $otherToX = (int) ($fromX + ($otherToX - $fromX) * $scale); + + $result += $this->sizeOfBlackWhiteBlackRun($fromX, $fromY, $otherToX, $otherToY); + +// Middle pixel is double-counted this way; subtract 1 + return $result - 1.0; + } + + /** + *

    This method traces a line from a po$in the image, in the direction towards another point. + * It begins in a black region, and keeps going until it finds white, then black, then white again. + * It reports the distance from the start to this point.

    + * + *

    This is used when figuring out how wide a finder pattern is, when the finder pattern + * may be skewed or rotated.

    + */ + private function sizeOfBlackWhiteBlackRun($fromX, $fromY, $toX, $toY) { +// Mild variant of Bresenham's algorithm; +// see http://en.wikipedia.org/wiki/Bresenham's_line_algorithm + $steep = abs($toY - $fromY) > abs($toX - $fromX); + if ($steep) { + $temp = $fromX; + $fromX = $fromY; + $fromY = $temp; + $temp = $toX; + $toX = $toY; + $toY = $temp; + } + + $dx = abs($toX - $fromX); + $dy = abs($toY - $fromY); + $error = -$dx / 2; + $xstep = $fromX < $toX ? 1 : -1; + $ystep = $fromY < $toY ? 1 : -1; + +// In black pixels, looking for white, first or second time. + $state = 0; +// Loop up until x == toX, but not beyond + $xLimit = $toX + $xstep; + for ($x = $fromX, $y = $fromY; $x != $xLimit; $x += $xstep) { + $realX = $steep ? $y : $x; + $realY = $steep ? $x : $y; + +// Does current pixel mean we have moved white to black or vice versa? +// Scanning black in state 0,2 and white in state 1, so if we find the wrong +// color, advance to next state or end if we are in state 2 already + if (($state == 1) == $this->image->get($realX, $realY)) { + if ($state == 2) { + return MathUtils::distance($x, $y, $fromX, $fromY); + } + $state++; + } + + $error += $dy; + if ($error > 0) { + if ($y == $toY) { + break; + } + $y += $ystep; + $error -= $dx; + } + } +// Found black-white-black; give the benefit of the doubt that the next pixel outside the image +// is "white" so this last po$at (toX+xStep,toY) is the right ending. This is really a +// small approximation; (toX+xStep,toY+yStep) might be really correct. Ignore this. + if ($state == 2) { + return MathUtils::distance($toX + $xstep, $toY, $fromX, $fromY); + } +// else we didn't find even black-white-black; no estimate is really possible + return NAN; + } + + /** + *

    Attempts to locate an alignment pattern in a limited region of the image, which is + * guessed to contain it. This method uses {@link AlignmentPattern}.

    + * + * @param overallEstModuleSize estimated module size so far + * @param estAlignmentX x coordinate of center of area probably containing alignment pattern + * @param estAlignmentY y coordinate of above + * @param allowanceFactor number of pixels in all directions to search from the center + * @return {@link AlignmentPattern} if found, or null otherwise + * @throws NotFoundException if an unexpected error occurs during detection + */ + protected final function findAlignmentInRegion($overallEstModuleSize, + $estAlignmentX, + $estAlignmentY, + $allowanceFactor) + { +// Look for an alignment pattern (3 modules in size) around where it +// should be + $allowance = (int) ($allowanceFactor * $overallEstModuleSize); + $alignmentAreaLeftX = max(0, $estAlignmentX - $allowance); + $alignmentAreaRightX = min($this->image->getWidth() - 1, $estAlignmentX + $allowance); + if ($alignmentAreaRightX - $alignmentAreaLeftX < $overallEstModuleSize * 3) { + throw NotFoundException::getNotFoundInstance(); + } + + $alignmentAreaTopY = max(0, $estAlignmentY - $allowance); + $alignmentAreaBottomY = min($this->image->getHeight() - 1, $estAlignmentY + $allowance); + if ($alignmentAreaBottomY - $alignmentAreaTopY < $overallEstModuleSize * 3) { + throw NotFoundException::getNotFoundInstance(); + } + + $alignmentFinder = + new AlignmentPatternFinder( + $this->image, + $alignmentAreaLeftX, + $alignmentAreaTopY, + $alignmentAreaRightX - $alignmentAreaLeftX, + $alignmentAreaBottomY - $alignmentAreaTopY, + $overallEstModuleSize, + $this->resultPointCallback); + return $alignmentFinder->find(); + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPattern.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPattern.php new file mode 100644 index 0000000..1900a78 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPattern.php @@ -0,0 +1,81 @@ +Encapsulates a finder pattern, which are the three square patterns found in + * the corners of QR Codes. It also encapsulates a count of similar finder patterns, + * as a convenience to the finder's bookkeeping.

    + * + * @author Sean Owen + */ +final class FinderPattern extends ResultPoint { + +private $estimatedModuleSize; +private $count; + + + + function __construct($posX, $posY, $estimatedModuleSize, $count=1) { + parent::__construct($posX, $posY); + $this->estimatedModuleSize = $estimatedModuleSize; + $this->count = $count; +} + + public function getEstimatedModuleSize() { + return $this->estimatedModuleSize; + } + + function getCount() { + return $this->count; + } + + /* + void incrementCount() { + this.count++; + } + */ + + /** + *

    Determines if this finder pattern "about equals" a finder pattern at the stated + * position and size -- meaning, it is at nearly the same center with nearly the same size.

    + */ + function aboutEquals($moduleSize, $i, $j) { + if (abs($i - $this->getY()) <= $moduleSize && abs($j - $this->getX()) <= $moduleSize) { + $moduleSizeDiff = abs($moduleSize - $this->estimatedModuleSize); + return $moduleSizeDiff <= 1.0 || $moduleSizeDiff <= $this->estimatedModuleSize; + } + return false; +} + + /** + * Combines this object's current estimate of a finder pattern position and module size + * with a new estimate. It returns a new {@code FinderPattern} containing a weighted average + * based on count. + */ + function combineEstimate($i, $j, $newModuleSize) { + $combinedCount = $this->count + 1; + $combinedX = ($this->count * $this->getX() + $j) / $combinedCount; + $combinedY = ($this->count * $this->getY() + $i) / $combinedCount; + $combinedModuleSize = ($this->count * $this->estimatedModuleSize + $newModuleSize) / $combinedCount; + return new FinderPattern($combinedX, $combinedY, $combinedModuleSize, $combinedCount); + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPatternFinder.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPatternFinder.php new file mode 100644 index 0000000..d2fe473 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPatternFinder.php @@ -0,0 +1,705 @@ +This class attempts to find finder patterns in a QR Code. Finder patterns are the square + * markers at three corners of a QR Code.

    + * + *

    This class is thread-safe but not reentrant. Each thread must allocate its own object. + * + * @author Sean Owen + */ +class FinderPatternFinder +{ + + private static $CENTER_QUORUM = 2; + protected static $MIN_SKIP = 3; // 1 pixel/module times 3 modules/center + protected static $MAX_MODULES = 57; // support up to version 10 for mobile clients + + private $image; + private $average; + private $possibleCenters; //private final List possibleCenters; + private $hasSkipped = false; + private $crossCheckStateCount; + private $resultPointCallback; + + /** + *

    Creates a finder that will search the image for three finder patterns.

    + * + * @param image image to search + */ + public function __construct($image, $resultPointCallback = null) + { + $this->image = $image; + + + $this->possibleCenters = array();//new ArrayList<>(); + $this->crossCheckStateCount = fill_array(0,5,0); + $this->resultPointCallback = $resultPointCallback; + } + + protected final function getImage() + { + return $this->image; + } + + protected final function getPossibleCenters() + { //List getPossibleCenters() + return $this->possibleCenters; + } + + final function find($hints) + {/*final FinderPatternInfo find(Map hints) throws NotFoundException {*/ + $tryHarder = $hints != null && $hints['TRY_HARDER']; + $pureBarcode = $hints != null && $hints['PURE_BARCODE']; + $maxI = $this->image->getHeight(); + $maxJ = $this->image->getWidth(); + // We are looking for black/white/black/white/black modules in + // 1:1:3:1:1 ratio; this tracks the number of such modules seen so far + + // Let's assume that the maximum version QR Code we support takes up 1/4 the height of the + // image, and then account for the center being 3 modules in size. This gives the smallest + // number of pixels the center could be, so skip this often. When trying harder, look for all + // QR versions regardless of how dense they are. + $iSkip = intval((3 * $maxI) / (4 * self::$MAX_MODULES)); + if ($iSkip < self::$MIN_SKIP || $tryHarder) { + $iSkip = self::$MIN_SKIP; + } + + $done = false; + $stateCount = array(); + for ($i = $iSkip - 1; $i < $maxI && !$done; $i += $iSkip) { + // Get a row of black/white values + $stateCount[0] = 0; + $stateCount[1] = 0; + $stateCount[2] = 0; + $stateCount[3] = 0; + $stateCount[4] = 0; + $currentState = 0; + for ($j = 0; $j < $maxJ; $j++) { + if ($this->image->get($j, $i)) { + // Black pixel + if (($currentState & 1) == 1) { // Counting white pixels + $currentState++; + } + $stateCount[$currentState]++; + } else { // White pixel + if (($currentState & 1) == 0) { // Counting black pixels + if ($currentState == 4) { // A winner? + if ($this->foundPatternCross($stateCount)) { // Yes + $confirmed = $this->handlePossibleCenter($stateCount, $i, $j, $pureBarcode); + if ($confirmed) { + // Start examining every other line. Checking each line turned out to be too + // expensive and didn't improve performance. + $iSkip = 2; + if ($this->hasSkipped) { + $done = $this->haveMultiplyConfirmedCenters(); + } else { + $rowSkip = $this->findRowSkip(); + if ($rowSkip > $stateCount[2]) { + // Skip rows between row of lower confirmed center + // and top of presumed third confirmed center + // but back up a bit to get a full chance of detecting + // it, entire width of center of finder pattern + + // Skip by rowSkip, but back off by $stateCount[2] (size of last center + // of pattern we saw) to be conservative, and also back off by iSkip which + // is about to be re-added + $i += $rowSkip - $stateCount[2] - $iSkip; + $j = $maxJ - 1; + } + } + } else { + + + + $stateCount[0] = $stateCount[2]; + $stateCount[1] = $stateCount[3]; + $stateCount[2] = $stateCount[4]; + $stateCount[3] = 1; + $stateCount[4] = 0; + $currentState = 3; + continue; + } + // Clear state to start looking again + $currentState = 0; + $stateCount[0] = 0; + $stateCount[1] = 0; + $stateCount[2] = 0; + $stateCount[3] = 0; + $stateCount[4] = 0; + } else { // No, shift counts back by two + $stateCount[0] = $stateCount[2]; + $stateCount[1] = $stateCount[3]; + $stateCount[2] = $stateCount[4]; + $stateCount[3] = 1; + $stateCount[4] = 0; + $currentState = 3; + } + } else { + $stateCount[++$currentState]++; + } + } else { // Counting white pixels + $stateCount[$currentState]++; + } + } + } + if ($this->foundPatternCross($stateCount)) { + $confirmed = $this->handlePossibleCenter($stateCount, $i, $maxJ, $pureBarcode); + if ($confirmed) { + $iSkip = $stateCount[0]; + if ($this->hasSkipped) { + // Found a third one + $done = $this->haveMultiplyConfirmedCenters(); + } + } + } + } + + $patternInfo = $this->selectBestPatterns(); + $patternInfo = ResultPoint::orderBestPatterns($patternInfo); + + return new FinderPatternInfo($patternInfo); + } + + /** + * Given a count of black/white/black/white/black pixels just seen and an end position, + * figures the location of the center of this run. + */ + private static function centerFromEnd($stateCount, $end) + { + return (float)($end - $stateCount[4] - $stateCount[3]) - $stateCount[2] / 2.0; + } + + /** + * @param $stateCount ; count of black/white/black/white/black pixels just read + * @return true iff the proportions of the counts is close enough to the 1/1/3/1/1 ratios + * used by finder patterns to be considered a match + */ + protected static function foundPatternCross($stateCount) + { + $totalModuleSize = 0; + for ($i = 0; $i < 5; $i++) { + $count = $stateCount[$i]; + if ($count == 0) { + return false; + } + $totalModuleSize += $count; + } + if ($totalModuleSize < 7) { + return false; + } + $moduleSize = $totalModuleSize / 7.0; + $maxVariance = $moduleSize / 2.0; + // Allow less than 50% variance from 1-1-3-1-1 proportions + return + abs($moduleSize - $stateCount[0]) < $maxVariance && + abs($moduleSize - $stateCount[1]) < $maxVariance && + abs(3.0 * $moduleSize - $stateCount[2]) < 3 * $maxVariance && + abs($moduleSize - $stateCount[3]) < $maxVariance && + abs($moduleSize - $stateCount[4]) < $maxVariance; + } + + private function getCrossCheckStateCount() + { + $this->crossCheckStateCount[0] = 0; + $this->crossCheckStateCount[1] = 0; + $this->crossCheckStateCount[2] = 0; + $this->crossCheckStateCount[3] = 0; + $this->crossCheckStateCount[4] = 0; + return $this->crossCheckStateCount; + } + + /** + * After a vertical and horizontal scan finds a potential finder pattern, this method + * "cross-cross-cross-checks" by scanning down diagonally through the center of the possible + * finder pattern to see if the same proportion is detected. + * + * @param $startI ; row where a finder pattern was detected + * @param centerJ ; center of the section that appears to cross a finder pattern + * @param $maxCount ; maximum reasonable number of modules that should be + * observed in any reading state, based on the results of the horizontal scan + * @param originalStateCountTotal ; The original state count total. + * @return true if proportions are withing expected limits + */ + private function crossCheckDiagonal($startI, $centerJ, $maxCount, $originalStateCountTotal) + { + $stateCount = $this->getCrossCheckStateCount(); + + // Start counting up, left from center finding black center mass + $i = 0; + $startI = intval($startI); + $centerJ = intval($centerJ); + while ($startI >= $i && $centerJ >= $i && $this->image->get($centerJ - $i, $startI - $i)) { + $stateCount[2]++; + $i++; + } + + if ($startI < $i || $centerJ < $i) { + return false; + } + + // Continue up, left finding white space + while ($startI >= $i && $centerJ >= $i && !$this->image->get($centerJ - $i, $startI - $i) && + $stateCount[1] <= $maxCount) { + $stateCount[1]++; + $i++; + } + + // If already too many modules in this state or ran off the edge: + if ($startI < $i || $centerJ < $i || $stateCount[1] > $maxCount) { + return false; + } + + // Continue up, left finding black border + while ($startI >= $i && $centerJ >= $i && $this->image->get($centerJ - $i, $startI - $i) && + $stateCount[0] <= $maxCount) { + $stateCount[0]++; + $i++; + } + if ($stateCount[0] > $maxCount) { + return false; + } + + $maxI = $this->image->getHeight(); + $maxJ = $this->image->getWidth(); + + // Now also count down, right from center + $i = 1; + while ($startI + $i < $maxI && $centerJ + $i < $maxJ && $this->image->get($centerJ + $i, $startI + $i)) { + $stateCount[2]++; + $i++; + } + + // Ran off the edge? + if ($startI + $i >= $maxI || $centerJ + $i >= $maxJ) { + return false; + } + + while ($startI + $i < $maxI && $centerJ + $i < $maxJ && !$this->image->get($centerJ + $i, $startI + $i) && + $stateCount[3] < $maxCount) { + $stateCount[3]++; + $i++; + } + + if ($startI + $i >= $maxI || $centerJ + $i >= $maxJ || $stateCount[3] >= $maxCount) { + return false; + } + + while ($startI + $i < $maxI && $centerJ + $i < $maxJ && $this->image->get($centerJ + $i, $startI + $i) && + $stateCount[4] < $maxCount) { + $stateCount[4]++; + $i++; + } + + if ($stateCount[4] >= $maxCount) { + return false; + } + + // If we found a finder-pattern-like section, but its size is more than 100% different than + // the original, assume it's a false positive + $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4]; + return + abs($stateCountTotal - $originalStateCountTotal) < 2 * $originalStateCountTotal && + $this->foundPatternCross($stateCount); + } + + /** + *

    After a horizontal scan finds a potential finder pattern, this method + * "cross-checks" by scanning down vertically through the center of the possible + * finder pattern to see if the same proportion is detected.

    + * + * @param $startI ; row where a finder pattern was detected + * @param centerJ ; center of the section that appears to cross a finder pattern + * @param $maxCount ; maximum reasonable number of modules that should be + * observed in any reading state, based on the results of the horizontal scan + * @return vertical center of finder pattern, or {@link Float#NaN} if not found + */ + private function crossCheckVertical($startI, $centerJ, $maxCount, + $originalStateCountTotal) + { + $image = $this->image; + + $maxI = $image->getHeight(); + $stateCount = $this->getCrossCheckStateCount(); + + // Start counting up from center + $i = $startI; + while ($i >= 0 && $image->get($centerJ, $i)) { + $stateCount[2]++; + $i--; + } + if ($i < 0) { + return NAN; + } + while ($i >= 0 && !$image->get($centerJ, $i) && $stateCount[1] <= $maxCount) { + $stateCount[1]++; + $i--; + } + // If already too many modules in this state or ran off the edge: + if ($i < 0 || $stateCount[1] > $maxCount) { + return NAN; + } + while ($i >= 0 && $image->get($centerJ, $i) && $stateCount[0] <= $maxCount) { + $stateCount[0]++; + $i--; + } + if ($stateCount[0] > $maxCount) { + return NAN; + } + + // Now also count down from center + $i = $startI + 1; + while ($i < $maxI && $image->get($centerJ, $i)) { + $stateCount[2]++; + $i++; + } + if ($i == $maxI) { + return NAN; + } + while ($i < $maxI && !$image->get($centerJ, $i) && $stateCount[3] < $maxCount) { + $stateCount[3]++; + $i++; + } + if ($i == $maxI || $stateCount[3] >= $maxCount) { + return NAN; + } + while ($i < $maxI && $image->get($centerJ, $i) && $stateCount[4] < $maxCount) { + $stateCount[4]++; + $i++; + } + if ($stateCount[4] >= $maxCount) { + return NAN; + } + + // If we found a finder-pattern-like section, but its size is more than 40% different than + // the original, assume it's a false positive + $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + + $stateCount[4]; + if (5 * abs($stateCountTotal - $originalStateCountTotal) >= 2 * $originalStateCountTotal) { + return NAN; + } + + return $this->foundPatternCross($stateCount) ? $this->centerFromEnd($stateCount, $i) : NAN; + } + + /** + *

    Like {@link #crossCheckVertical(int, int, int, int)}, and in fact is basically identical, + * except it reads horizontally instead of vertically. This is used to cross-cross + * check a vertical cross check and locate the real center of the alignment pattern.

    + */ + private function crossCheckHorizontal($startJ, $centerI, $maxCount, + $originalStateCountTotal) + { + $image = $this->image; + + $maxJ = $this->image->getWidth(); + $stateCount = $this->getCrossCheckStateCount(); + + $j = $startJ; + while ($j >= 0 && $image->get($j, $centerI)) { + $stateCount[2]++; + $j--; + } + if ($j < 0) { + return NAN; + } + while ($j >= 0 && !$image->get($j, $centerI) && $stateCount[1] <= $maxCount) { + $stateCount[1]++; + $j--; + } + if ($j < 0 || $stateCount[1] > $maxCount) { + return NAN; + } + while ($j >= 0 && $image->get($j, $centerI) && $stateCount[0] <= $maxCount) { + $stateCount[0]++; + $j--; + } + if ($stateCount[0] > $maxCount) { + return NAN; + } + + $j = $startJ + 1; + while ($j < $maxJ && $image->get($j, $centerI)) { + $stateCount[2]++; + $j++; + } + if ($j == $maxJ) { + return NAN; + } + while ($j < $maxJ && !$image->get($j, $centerI) && $stateCount[3] < $maxCount) { + $stateCount[3]++; + $j++; + } + if ($j == $maxJ || $stateCount[3] >= $maxCount) { + return NAN; + } + while ($j < $maxJ && $this->image->get($j, $centerI) && $stateCount[4] < $maxCount) { + $stateCount[4]++; + $j++; + } + if ($stateCount[4] >= $maxCount) { + return NAN; + } + + // If we found a finder-pattern-like section, but its size is significantly different than + // the original, assume it's a false positive + $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + + $stateCount[4]; + if (5 * abs($stateCountTotal - $originalStateCountTotal) >= $originalStateCountTotal) { + return NAN; + } + + return $this->foundPatternCross($stateCount) ? $this->centerFromEnd($stateCount, $j) : NAN; + } + + /** + *

    This is called when a horizontal scan finds a possible alignment pattern. It will + * cross check with a vertical scan, and if successful, will, ah, cross-cross-check + * with another horizontal scan. This is needed primarily to locate the real horizontal + * center of the pattern in cases of extreme skew. + * And then we cross-cross-cross check with another diagonal scan.

    + * + *

    If that succeeds the finder pattern location is added to a list that tracks + * the number of times each location has been nearly-matched as a finder pattern. + * Each additional find is more evidence that the location is in fact a finder + * pattern center + * + * @param $stateCount reading state module counts from horizontal scan + * @param i row where finder pattern may be found + * @param j end of possible finder pattern in row + * @param pureBarcode true if in "pure barcode" mode + * @return true if a finder pattern candidate was found this time + */ + protected final function handlePossibleCenter($stateCount, $i, $j, $pureBarcode) + { + $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + + $stateCount[4]; + $centerJ = $this->centerFromEnd($stateCount, $j); + $centerI = $this->crossCheckVertical($i, intval($centerJ), $stateCount[2], $stateCountTotal); + if (!is_nan($centerI)) { + // Re-cross check + $centerJ = $this->crossCheckHorizontal(intval($centerJ), intval($centerI), $stateCount[2], $stateCountTotal); + if (!is_nan($centerJ) && + (!$pureBarcode || $this->crossCheckDiagonal(intval($centerI), intval($centerJ), $stateCount[2], $stateCountTotal)) + ) { + $estimatedModuleSize = (float)$stateCountTotal / 7.0; + $found = false; + for ($index = 0; $index < count($this->possibleCenters); $index++) { + $center = $this->possibleCenters[$index]; + // Look for about the same center and module size: + if ($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)) { + $this->possibleCenters[$index] = $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize); + $found = true; + break; + } + } + if (!$found) { + $point = new FinderPattern($centerJ, $centerI, $estimatedModuleSize); + $this->possibleCenters[] = $point; + if ($this->resultPointCallback != null) { + $this->resultPointCallback->foundPossibleResultPoint($point); + } + } + return true; + } + } + return false; + } + + /** + * @return number of rows we could safely skip during scanning, based on the first + * two finder patterns that have been located. In some cases their position will + * allow us to infer that the third pattern must lie below a certain point farther + * down in the image. + */ + private function findRowSkip() + { + $max = count($this->possibleCenters); + if ($max <= 1) { + return 0; + } + $firstConfirmedCenter = null; + foreach ($this->possibleCenters as $center) { + + + if ($center->getCount() >= self::$CENTER_QUORUM) { + if ($firstConfirmedCenter == null) { + $firstConfirmedCenter = $center; + } else { + // We have two confirmed centers + // How far down can we skip before resuming looking for the next + // pattern? In the worst case, only the difference between the + // difference in the x / y coordinates of the two centers. + // This is the case where you find top left last. + $this->hasSkipped = true; + return intval((abs($firstConfirmedCenter->getX() - $center->getX()) - + abs($firstConfirmedCenter->getY() - $center->getY())) / 2); + } + } + } + return 0; + } + + /** + * @return true iff we have found at least 3 finder patterns that have been detected + * at least {@link #CENTER_QUORUM} times each, and, the estimated module size of the + * candidates is "pretty similar" + */ + private function haveMultiplyConfirmedCenters() + { + $confirmedCount = 0; + $totalModuleSize = 0.0; + $max = count($this->possibleCenters); + foreach ($this->possibleCenters as $pattern) { + if ($pattern->getCount() >= self::$CENTER_QUORUM) { + $confirmedCount++; + $totalModuleSize += $pattern->getEstimatedModuleSize(); + } + } + if ($confirmedCount < 3) { + return false; + } + // OK, we have at least 3 confirmed centers, but, it's possible that one is a "false positive" + // and that we need to keep looking. We detect this by asking if the estimated module sizes + // vary too much. We arbitrarily say that when the total deviation from average exceeds + // 5% of the total module size estimates, it's too much. + $average = $totalModuleSize / (float)$max; + $totalDeviation = 0.0; + foreach ($this->possibleCenters as $pattern) { + $totalDeviation += abs($pattern->getEstimatedModuleSize() - $average); + } + return $totalDeviation <= 0.05 * $totalModuleSize; + } + + /** + * @return the 3 best {@link FinderPattern}s from our list of candidates. The "best" are + * those that have been detected at least {@link #CENTER_QUORUM} times, and whose module + * size differs from the average among those patterns the least + * @throws NotFoundException if 3 such finder patterns do not exist + */ + private function selectBestPatterns() + { + $startSize = count($this->possibleCenters); + if ($startSize < 3) { + // Couldn't find enough finder patterns + throw new NotFoundException; + } + + // Filter outlier possibilities whose module size is too different + if ($startSize > 3) { + // But we can only afford to do so if we have at least 4 possibilities to choose from + $totalModuleSize = 0.0; + $square = 0.0; + foreach ($this->possibleCenters as $center) { + $size = $center->getEstimatedModuleSize(); + $totalModuleSize += $size; + $square += $size * $size; + } + $this->average = $totalModuleSize / (float)$startSize; + $stdDev = (float)sqrt($square / $startSize - $this->average * $this->average); + + usort($this->possibleCenters, array($this,'FurthestFromAverageComparator')); + + $limit = max(0.2 * $this->average, $stdDev); + + for ($i = 0; $i < count($this->possibleCenters) && count($this->possibleCenters) > 3; $i++) { + $pattern = $this->possibleCenters[$i]; + if (abs($pattern->getEstimatedModuleSize() - $this->average) > $limit) { + unset($this->possibleCenters[$i]);//возможно что ключи меняются в java при вызове .remove(i) ??? + $this->possibleCenters = array_values($this->possibleCenters); + $i--; + } + } + } + + if (count($this->possibleCenters) > 3) { + // Throw away all but those first size candidate points we found. + + $totalModuleSize = 0.0; + foreach ($this->possibleCenters as $possibleCenter) { + $totalModuleSize += $possibleCenter->getEstimatedModuleSize(); + } + + $this->average = $totalModuleSize / (float)count($this->possibleCenters); + + usort($this->possibleCenters, array($this,'CenterComparator')); + + array_slice($this->possibleCenters, 3, count($this->possibleCenters) - 3); + + + } + + return array($this->possibleCenters[0], $this->possibleCenters[1], $this->possibleCenters[2]); + + + } + /** + *

    Orders by furthest from average

    + */ +public function FurthestFromAverageComparator($center1, $center2) +{ + + $dA = abs($center2->getEstimatedModuleSize() - $this->average); + $dB = abs($center1->getEstimatedModuleSize() - $this->average); + if ($dA < $dB) { + return -1; + } elseif ($dA == $dB) { + return 0; + } else { + return 1; + } +} + /** + *

    Orders by {@link FinderPattern#getCount()}, descending.

    + */ + + //@Override + public function CenterComparator($center1, $center2) { + if ($center2->getCount() == $center1->getCount()) { + $dA = abs($center2->getEstimatedModuleSize() - $this->average); + $dB = abs($center1->getEstimatedModuleSize() - $this->average); + if($dA < $dB){ + return 1; + }elseif( $dA == $dB){ + return 0; + }else{ + return -1; + } + + } else { + return $center2->getCount() - $center1->getCount(); + } + + } +} + + + + + + diff --git a/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPatternInfo.php b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPatternInfo.php new file mode 100644 index 0000000..801b7df --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/lib/qrcode/detector/FinderPatternInfo.php @@ -0,0 +1,50 @@ +Encapsulates information about finder patterns in an image, including the location of + * the three finder patterns, and their estimated module size.

    + * + * @author Sean Owen + */ + final class FinderPatternInfo { + +private $bottomLeft; +private $topLeft; +private $topRight; + +public function __construct($patternCenters) { +$this->bottomLeft = $patternCenters[0]; +$this->topLeft = $patternCenters[1]; +$this->topRight = $patternCenters[2]; +} + + public function getBottomLeft() { + return $this->bottomLeft; + } + + public function getTopLeft() { + return $this->topLeft; + } + + public function getTopRight() { + return $this->topRight; + } + +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/phpunit.xml.dist b/vendor/khanamiryan/qrcode-detector-decoder/phpunit.xml.dist new file mode 100644 index 0000000..7c557c8 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/phpunit.xml.dist @@ -0,0 +1,10 @@ + + + tests + + + + src + + + diff --git a/vendor/khanamiryan/qrcode-detector-decoder/tests/QrReaderTest.php b/vendor/khanamiryan/qrcode-detector-decoder/tests/QrReaderTest.php new file mode 100644 index 0000000..0521ec3 --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/tests/QrReaderTest.php @@ -0,0 +1,15 @@ +assertSame("Hello world!", $qrcode->text()); + } +} diff --git a/vendor/khanamiryan/qrcode-detector-decoder/tests/bootstrap.php b/vendor/khanamiryan/qrcode-detector-decoder/tests/bootstrap.php new file mode 100644 index 0000000..a3f233f --- /dev/null +++ b/vendor/khanamiryan/qrcode-detector-decoder/tests/bootstrap.php @@ -0,0 +1,3 @@ +=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|^5.7|^6.0", + "squizlabs/php_codesniffer": "1.*" + } +} diff --git a/vendor/myclabs/php-enum/src/Enum.php b/vendor/myclabs/php-enum/src/Enum.php new file mode 100644 index 0000000..140e722 --- /dev/null +++ b/vendor/myclabs/php-enum/src/Enum.php @@ -0,0 +1,186 @@ + + * @author Daniel Costa + * @author Mirosław Filip + */ +abstract class Enum +{ + /** + * Enum value + * + * @var mixed + */ + protected $value; + + /** + * Store existing constants in a static cache per object. + * + * @var array + */ + protected static $cache = array(); + + /** + * Creates a new value of some type + * + * @param mixed $value + * + * @throws \UnexpectedValueException if incompatible type is given. + */ + public function __construct($value) + { + if (!$this->isValid($value)) { + throw new \UnexpectedValueException("Value '$value' is not part of the enum " . get_called_class()); + } + + $this->value = $value; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * Returns the enum key (i.e. the constant name). + * + * @return mixed + */ + public function getKey() + { + return static::search($this->value); + } + + /** + * @return string + */ + public function __toString() + { + return (string)$this->value; + } + + /** + * Compares one Enum with another. + * + * This method is final, for more information read https://github.com/myclabs/php-enum/issues/4 + * + * @return bool True if Enums are equal, false if not equal + */ + final public function equals(Enum $enum) + { + return $this->getValue() === $enum->getValue() && get_called_class() == get_class($enum); + } + + /** + * Returns the names (keys) of all constants in the Enum class + * + * @return array + */ + public static function keys() + { + return array_keys(static::toArray()); + } + + /** + * Returns instances of the Enum class of all Enum constants + * + * @return static[] Constant name in key, Enum instance in value + */ + public static function values() + { + $values = array(); + + foreach (static::toArray() as $key => $value) { + $values[$key] = new static($value); + } + + return $values; + } + + /** + * Returns all possible values as an array + * + * @return array Constant name in key, constant value in value + */ + public static function toArray() + { + $class = get_called_class(); + if (!array_key_exists($class, static::$cache)) { + $reflection = new \ReflectionClass($class); + static::$cache[$class] = $reflection->getConstants(); + } + + return static::$cache[$class]; + } + + /** + * Check if is valid enum value + * + * @param $value + * + * @return bool + */ + public static function isValid($value) + { + return in_array($value, static::toArray(), true); + } + + /** + * Check if is valid enum key + * + * @param $key + * + * @return bool + */ + public static function isValidKey($key) + { + $array = static::toArray(); + + return isset($array[$key]); + } + + /** + * Return key for value + * + * @param $value + * + * @return mixed + */ + public static function search($value) + { + return array_search($value, static::toArray(), true); + } + + /** + * Returns a value when called statically like so: MyEnum::SOME_VALUE() given SOME_VALUE is a class constant + * + * @param string $name + * @param array $arguments + * + * @return static + * @throws \BadMethodCallException + */ + public static function __callStatic($name, $arguments) + { + $array = static::toArray(); + if (isset($array[$name])) { + return new static($array[$name]); + } + + throw new \BadMethodCallException("No static method or enum constant '$name' in class " . get_called_class()); + } +} diff --git a/vendor/paragonie/random_compat/LICENSE b/vendor/paragonie/random_compat/LICENSE new file mode 100644 index 0000000..45c7017 --- /dev/null +++ b/vendor/paragonie/random_compat/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Paragon Initiative Enterprises + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/paragonie/random_compat/build-phar.sh b/vendor/paragonie/random_compat/build-phar.sh new file mode 100644 index 0000000..b4a5ba3 --- /dev/null +++ b/vendor/paragonie/random_compat/build-phar.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +basedir=$( dirname $( readlink -f ${BASH_SOURCE[0]} ) ) + +php -dphar.readonly=0 "$basedir/other/build_phar.php" $* \ No newline at end of file diff --git a/vendor/paragonie/random_compat/composer.json b/vendor/paragonie/random_compat/composer.json new file mode 100644 index 0000000..1c5978c --- /dev/null +++ b/vendor/paragonie/random_compat/composer.json @@ -0,0 +1,37 @@ +{ + "name": "paragonie/random_compat", + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "random", + "pseudorandom" + ], + "license": "MIT", + "type": "library", + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "support": { + "issues": "https://github.com/paragonie/random_compat/issues", + "email": "info@paragonie.com", + "source": "https://github.com/paragonie/random_compat" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "autoload": { + "files": [ + "lib/random.php" + ] + } +} diff --git a/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey b/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey new file mode 100644 index 0000000..eb50ebf --- /dev/null +++ b/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEEd+wCqJDrx5B4OldM0dQE0ZMX+lx1ZWm +pui0SUqD4G29L3NGsz9UhJ/0HjBdbnkhIK5xviT0X5vtjacF6ajgcCArbTB+ds+p ++h7Q084NuSuIpNb6YPfoUFgC/CL9kAoc +-----END PUBLIC KEY----- diff --git a/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc b/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc new file mode 100644 index 0000000..6a1d7f3 --- /dev/null +++ b/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v2.0.22 (MingW32) + +iQEcBAABAgAGBQJWtW1hAAoJEGuXocKCZATaJf0H+wbZGgskK1dcRTsuVJl9IWip +QwGw/qIKI280SD6/ckoUMxKDCJiFuPR14zmqnS36k7N5UNPnpdTJTS8T11jttSpg +1LCmgpbEIpgaTah+cELDqFCav99fS+bEiAL5lWDAHBTE/XPjGVCqeehyPYref4IW +NDBIEsvnHPHPLsn6X5jq4+Yj5oUixgxaMPiR+bcO4Sh+RzOVB6i2D0upWfRXBFXA +NNnsg9/zjvoC7ZW73y9uSH+dPJTt/Vgfeiv52/v41XliyzbUyLalf02GNPY+9goV +JHG1ulEEBJOCiUD9cE1PUIJwHA/HqyhHIvV350YoEFiHl8iSwm7SiZu5kPjaq74= +=B6+8 +-----END PGP SIGNATURE----- diff --git a/vendor/paragonie/random_compat/lib/byte_safe_strings.php b/vendor/paragonie/random_compat/lib/byte_safe_strings.php new file mode 100644 index 0000000..3de86b2 --- /dev/null +++ b/vendor/paragonie/random_compat/lib/byte_safe_strings.php @@ -0,0 +1,181 @@ + RandomCompat_strlen($binary_string)) { + return ''; + } + + return (string) mb_substr($binary_string, $start, $length, '8bit'); + } + + } else { + + /** + * substr() implementation that isn't brittle to mbstring.func_overload + * + * This version just uses the default substr() + * + * @param string $binary_string + * @param int $start + * @param int $length (optional) + * + * @throws TypeError + * + * @return string + */ + function RandomCompat_substr($binary_string, $start, $length = null) + { + if (!is_string($binary_string)) { + throw new TypeError( + 'RandomCompat_substr(): First argument should be a string' + ); + } + + if (!is_int($start)) { + throw new TypeError( + 'RandomCompat_substr(): Second argument should be an integer' + ); + } + + if ($length !== null) { + if (!is_int($length)) { + throw new TypeError( + 'RandomCompat_substr(): Third argument should be an integer, or omitted' + ); + } + + return (string) substr($binary_string, $start, $length); + } + + return (string) substr($binary_string, $start); + } + } +} diff --git a/vendor/paragonie/random_compat/lib/cast_to_int.php b/vendor/paragonie/random_compat/lib/cast_to_int.php new file mode 100644 index 0000000..be7388d --- /dev/null +++ b/vendor/paragonie/random_compat/lib/cast_to_int.php @@ -0,0 +1,74 @@ + operators might accidentally let a float + * through. + * + * @param int|float $number The number we want to convert to an int + * @param boolean $fail_open Set to true to not throw an exception + * + * @return float|int + * + * @throws TypeError + */ + function RandomCompat_intval($number, $fail_open = false) + { + if (is_int($number) || is_float($number)) { + $number += 0; + } elseif (is_numeric($number)) { + $number += 0; + } + + if ( + is_float($number) + && + $number > ~PHP_INT_MAX + && + $number < PHP_INT_MAX + ) { + $number = (int) $number; + } + + if (is_int($number)) { + return (int) $number; + } elseif (!$fail_open) { + throw new TypeError( + 'Expected an integer.' + ); + } + return $number; + } +} diff --git a/vendor/paragonie/random_compat/lib/error_polyfill.php b/vendor/paragonie/random_compat/lib/error_polyfill.php new file mode 100644 index 0000000..6a91990 --- /dev/null +++ b/vendor/paragonie/random_compat/lib/error_polyfill.php @@ -0,0 +1,49 @@ += 70000) { + return; +} + +if (!defined('RANDOM_COMPAT_READ_BUFFER')) { + define('RANDOM_COMPAT_READ_BUFFER', 8); +} + +$RandomCompatDIR = dirname(__FILE__); + +require_once $RandomCompatDIR . '/byte_safe_strings.php'; +require_once $RandomCompatDIR . '/cast_to_int.php'; +require_once $RandomCompatDIR . '/error_polyfill.php'; + +if (!is_callable('random_bytes')) { + /** + * PHP 5.2.0 - 5.6.x way to implement random_bytes() + * + * We use conditional statements here to define the function in accordance + * to the operating environment. It's a micro-optimization. + * + * In order of preference: + * 1. Use libsodium if available. + * 2. fread() /dev/urandom if available (never on Windows) + * 3. mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM) + * 4. COM('CAPICOM.Utilities.1')->GetRandom() + * + * See RATIONALE.md for our reasoning behind this particular order + */ + if (extension_loaded('libsodium')) { + // See random_bytes_libsodium.php + if (PHP_VERSION_ID >= 50300 && is_callable('\\Sodium\\randombytes_buf')) { + require_once $RandomCompatDIR . '/random_bytes_libsodium.php'; + } elseif (method_exists('Sodium', 'randombytes_buf')) { + require_once $RandomCompatDIR . '/random_bytes_libsodium_legacy.php'; + } + } + + /** + * Reading directly from /dev/urandom: + */ + if (DIRECTORY_SEPARATOR === '/') { + // DIRECTORY_SEPARATOR === '/' on Unix-like OSes -- this is a fast + // way to exclude Windows. + $RandomCompatUrandom = true; + $RandomCompat_basedir = ini_get('open_basedir'); + + if (!empty($RandomCompat_basedir)) { + $RandomCompat_open_basedir = explode( + PATH_SEPARATOR, + strtolower($RandomCompat_basedir) + ); + $RandomCompatUrandom = (array() !== array_intersect( + array('/dev', '/dev/', '/dev/urandom'), + $RandomCompat_open_basedir + )); + $RandomCompat_open_basedir = null; + } + + if ( + !is_callable('random_bytes') + && + $RandomCompatUrandom + && + @is_readable('/dev/urandom') + ) { + // Error suppression on is_readable() in case of an open_basedir + // or safe_mode failure. All we care about is whether or not we + // can read it at this point. If the PHP environment is going to + // panic over trying to see if the file can be read in the first + // place, that is not helpful to us here. + + // See random_bytes_dev_urandom.php + require_once $RandomCompatDIR . '/random_bytes_dev_urandom.php'; + } + // Unset variables after use + $RandomCompat_basedir = null; + } else { + $RandomCompatUrandom = false; + } + + /** + * mcrypt_create_iv() + * + * We only want to use mcypt_create_iv() if: + * + * - random_bytes() hasn't already been defined + * - the mcrypt extensions is loaded + * - One of these two conditions is true: + * - We're on Windows (DIRECTORY_SEPARATOR !== '/') + * - We're not on Windows and /dev/urandom is readabale + * (i.e. we're not in a chroot jail) + * - Special case: + * - If we're not on Windows, but the PHP version is between + * 5.6.10 and 5.6.12, we don't want to use mcrypt. It will + * hang indefinitely. This is bad. + * - If we're on Windows, we want to use PHP >= 5.3.7 or else + * we get insufficient entropy errors. + */ + if ( + !is_callable('random_bytes') + && + // Windows on PHP < 5.3.7 is broken, but non-Windows is not known to be. + (DIRECTORY_SEPARATOR === '/' || PHP_VERSION_ID >= 50307) + && + // Prevent this code from hanging indefinitely on non-Windows; + // see https://bugs.php.net/bug.php?id=69833 + ( + DIRECTORY_SEPARATOR !== '/' || + (PHP_VERSION_ID <= 50609 || PHP_VERSION_ID >= 50613) + ) + && + extension_loaded('mcrypt') + ) { + // See random_bytes_mcrypt.php + require_once $RandomCompatDIR . '/random_bytes_mcrypt.php'; + } + $RandomCompatUrandom = null; + + /** + * This is a Windows-specific fallback, for when the mcrypt extension + * isn't loaded. + */ + if ( + !is_callable('random_bytes') + && + extension_loaded('com_dotnet') + && + class_exists('COM') + ) { + $RandomCompat_disabled_classes = preg_split( + '#\s*,\s*#', + strtolower(ini_get('disable_classes')) + ); + + if (!in_array('com', $RandomCompat_disabled_classes)) { + try { + $RandomCompatCOMtest = new COM('CAPICOM.Utilities.1'); + if (method_exists($RandomCompatCOMtest, 'GetRandom')) { + // See random_bytes_com_dotnet.php + require_once $RandomCompatDIR . '/random_bytes_com_dotnet.php'; + } + } catch (com_exception $e) { + // Don't try to use it. + } + } + $RandomCompat_disabled_classes = null; + $RandomCompatCOMtest = null; + } + + /** + * throw new Exception + */ + if (!is_callable('random_bytes')) { + /** + * We don't have any more options, so let's throw an exception right now + * and hope the developer won't let it fail silently. + * + * @param mixed $length + * @return void + * @throws Exception + */ + function random_bytes($length) + { + unset($length); // Suppress "variable not used" warnings. + throw new Exception( + 'There is no suitable CSPRNG installed on your system' + ); + } + } +} + +if (!is_callable('random_int')) { + require_once $RandomCompatDIR . '/random_int.php'; +} + +$RandomCompatDIR = null; diff --git a/vendor/paragonie/random_compat/lib/random_bytes_com_dotnet.php b/vendor/paragonie/random_compat/lib/random_bytes_com_dotnet.php new file mode 100644 index 0000000..fc1926e --- /dev/null +++ b/vendor/paragonie/random_compat/lib/random_bytes_com_dotnet.php @@ -0,0 +1,88 @@ +GetRandom($bytes, 0)); + if (RandomCompat_strlen($buf) >= $bytes) { + /** + * Return our random entropy buffer here: + */ + return RandomCompat_substr($buf, 0, $bytes); + } + ++$execCount; + } while ($execCount < $bytes); + + /** + * If we reach here, PHP has failed us. + */ + throw new Exception( + 'Could not gather sufficient random data' + ); + } +} \ No newline at end of file diff --git a/vendor/paragonie/random_compat/lib/random_bytes_dev_urandom.php b/vendor/paragonie/random_compat/lib/random_bytes_dev_urandom.php new file mode 100644 index 0000000..df5b915 --- /dev/null +++ b/vendor/paragonie/random_compat/lib/random_bytes_dev_urandom.php @@ -0,0 +1,167 @@ + 0); + + /** + * Is our result valid? + */ + if (is_string($buf)) { + if (RandomCompat_strlen($buf) === $bytes) { + /** + * Return our random entropy buffer here: + */ + return $buf; + } + } + } + + /** + * If we reach here, PHP has failed us. + */ + throw new Exception( + 'Error reading from source device' + ); + } +} diff --git a/vendor/paragonie/random_compat/lib/random_bytes_libsodium.php b/vendor/paragonie/random_compat/lib/random_bytes_libsodium.php new file mode 100644 index 0000000..4af1a24 --- /dev/null +++ b/vendor/paragonie/random_compat/lib/random_bytes_libsodium.php @@ -0,0 +1,88 @@ + 2147483647) { + $buf = ''; + for ($i = 0; $i < $bytes; $i += 1073741824) { + $n = ($bytes - $i) > 1073741824 + ? 1073741824 + : $bytes - $i; + $buf .= \Sodium\randombytes_buf($n); + } + } else { + $buf = \Sodium\randombytes_buf($bytes); + } + + if ($buf !== false) { + if (RandomCompat_strlen($buf) === $bytes) { + return $buf; + } + } + + /** + * If we reach here, PHP has failed us. + */ + throw new Exception( + 'Could not gather sufficient random data' + ); + } +} diff --git a/vendor/paragonie/random_compat/lib/random_bytes_libsodium_legacy.php b/vendor/paragonie/random_compat/lib/random_bytes_libsodium_legacy.php new file mode 100644 index 0000000..02160b9 --- /dev/null +++ b/vendor/paragonie/random_compat/lib/random_bytes_libsodium_legacy.php @@ -0,0 +1,92 @@ + 2147483647) { + for ($i = 0; $i < $bytes; $i += 1073741824) { + $n = ($bytes - $i) > 1073741824 + ? 1073741824 + : $bytes - $i; + $buf .= Sodium::randombytes_buf($n); + } + } else { + $buf .= Sodium::randombytes_buf($bytes); + } + + if (is_string($buf)) { + if (RandomCompat_strlen($buf) === $bytes) { + return $buf; + } + } + + /** + * If we reach here, PHP has failed us. + */ + throw new Exception( + 'Could not gather sufficient random data' + ); + } +} diff --git a/vendor/paragonie/random_compat/lib/random_bytes_mcrypt.php b/vendor/paragonie/random_compat/lib/random_bytes_mcrypt.php new file mode 100644 index 0000000..aac9c01 --- /dev/null +++ b/vendor/paragonie/random_compat/lib/random_bytes_mcrypt.php @@ -0,0 +1,77 @@ + operators might accidentally let a float + * through. + */ + + try { + $min = RandomCompat_intval($min); + } catch (TypeError $ex) { + throw new TypeError( + 'random_int(): $min must be an integer' + ); + } + + try { + $max = RandomCompat_intval($max); + } catch (TypeError $ex) { + throw new TypeError( + 'random_int(): $max must be an integer' + ); + } + + /** + * Now that we've verified our weak typing system has given us an integer, + * let's validate the logic then we can move forward with generating random + * integers along a given range. + */ + if ($min > $max) { + throw new Error( + 'Minimum value must be less than or equal to the maximum value' + ); + } + + if ($max === $min) { + return $min; + } + + /** + * Initialize variables to 0 + * + * We want to store: + * $bytes => the number of random bytes we need + * $mask => an integer bitmask (for use with the &) operator + * so we can minimize the number of discards + */ + $attempts = $bits = $bytes = $mask = $valueShift = 0; + + /** + * At this point, $range is a positive number greater than 0. It might + * overflow, however, if $max - $min > PHP_INT_MAX. PHP will cast it to + * a float and we will lose some precision. + */ + $range = $max - $min; + + /** + * Test for integer overflow: + */ + if (!is_int($range)) { + + /** + * Still safely calculate wider ranges. + * Provided by @CodesInChaos, @oittaa + * + * @ref https://gist.github.com/CodesInChaos/03f9ea0b58e8b2b8d435 + * + * We use ~0 as a mask in this case because it generates all 1s + * + * @ref https://eval.in/400356 (32-bit) + * @ref http://3v4l.org/XX9r5 (64-bit) + */ + $bytes = PHP_INT_SIZE; + $mask = ~0; + + } else { + + /** + * $bits is effectively ceil(log($range, 2)) without dealing with + * type juggling + */ + while ($range > 0) { + if ($bits % 8 === 0) { + ++$bytes; + } + ++$bits; + $range >>= 1; + $mask = $mask << 1 | 1; + } + $valueShift = $min; + } + + $val = 0; + /** + * Now that we have our parameters set up, let's begin generating + * random integers until one falls between $min and $max + */ + do { + /** + * The rejection probability is at most 0.5, so this corresponds + * to a failure probability of 2^-128 for a working RNG + */ + if ($attempts > 128) { + throw new Exception( + 'random_int: RNG is broken - too many rejections' + ); + } + + /** + * Let's grab the necessary number of random bytes + */ + $randomByteString = random_bytes($bytes); + + /** + * Let's turn $randomByteString into an integer + * + * This uses bitwise operators (<< and |) to build an integer + * out of the values extracted from ord() + * + * Example: [9F] | [6D] | [32] | [0C] => + * 159 + 27904 + 3276800 + 201326592 => + * 204631455 + */ + $val &= 0; + for ($i = 0; $i < $bytes; ++$i) { + $val |= ord($randomByteString[$i]) << ($i * 8); + } + + /** + * Apply mask + */ + $val &= $mask; + $val += $valueShift; + + ++$attempts; + /** + * If $val overflows to a floating point number, + * ... or is larger than $max, + * ... or smaller than $min, + * then try again. + */ + } while (!is_int($val) || $val > $max || $val < $min); + + return (int)$val; + } +} diff --git a/vendor/paragonie/random_compat/other/build_phar.php b/vendor/paragonie/random_compat/other/build_phar.php new file mode 100644 index 0000000..70ef4b2 --- /dev/null +++ b/vendor/paragonie/random_compat/other/build_phar.php @@ -0,0 +1,57 @@ +buildFromDirectory(dirname(__DIR__).'/lib'); +rename( + dirname(__DIR__).'/lib/index.php', + dirname(__DIR__).'/lib/random.php' +); + +/** + * If we pass an (optional) path to a private key as a second argument, we will + * sign the Phar with OpenSSL. + * + * If you leave this out, it will produce an unsigned .phar! + */ +if ($argc > 1) { + if (!@is_readable($argv[1])) { + echo 'Could not read the private key file:', $argv[1], "\n"; + exit(255); + } + $pkeyFile = file_get_contents($argv[1]); + + $private = openssl_get_privatekey($pkeyFile); + if ($private !== false) { + $pkey = ''; + openssl_pkey_export($private, $pkey); + $phar->setSignatureAlgorithm(Phar::OPENSSL, $pkey); + + /** + * Save the corresponding public key to the file + */ + if (!@is_readable($dist.'/random_compat.phar.pubkey')) { + $details = openssl_pkey_get_details($private); + file_put_contents( + $dist.'/random_compat.phar.pubkey', + $details['key'] + ); + } + } else { + echo 'An error occurred reading the private key from OpenSSL.', "\n"; + exit(255); + } +} diff --git a/vendor/paragonie/random_compat/psalm-autoload.php b/vendor/paragonie/random_compat/psalm-autoload.php new file mode 100644 index 0000000..d71d1b8 --- /dev/null +++ b/vendor/paragonie/random_compat/psalm-autoload.php @@ -0,0 +1,9 @@ + + + + + + + + + + + diff --git a/vendor/symfony/inflector/Inflector.php b/vendor/symfony/inflector/Inflector.php new file mode 100644 index 0000000..fbe8cc8 --- /dev/null +++ b/vendor/symfony/inflector/Inflector.php @@ -0,0 +1,232 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Inflector; + +/** + * Converts words between singular and plural forms. + * + * @author Bernhard Schussek + * + * @internal + */ +final class Inflector +{ + /** + * Map English plural to singular suffixes. + * + * @var array + * + * @see http://english-zone.com/spelling/plurals.html + */ + private static $pluralMap = array( + // First entry: plural suffix, reversed + // Second entry: length of plural suffix + // Third entry: Whether the suffix may succeed a vocal + // Fourth entry: Whether the suffix may succeed a consonant + // Fifth entry: singular suffix, normal + + // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) + array('a', 1, true, true, array('on', 'um')), + + // nebulae (nebula) + array('ea', 2, true, true, 'a'), + + // services (service) + array('secivres', 8, true, true, 'service'), + + // mice (mouse), lice (louse) + array('eci', 3, false, true, 'ouse'), + + // geese (goose) + array('esee', 4, false, true, 'oose'), + + // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius) + array('i', 1, true, true, 'us'), + + // men (man), women (woman) + array('nem', 3, true, true, 'man'), + + // children (child) + array('nerdlihc', 8, true, true, 'child'), + + // oxen (ox) + array('nexo', 4, false, false, 'ox'), + + // indices (index), appendices (appendix), prices (price) + array('seci', 4, false, true, array('ex', 'ix', 'ice')), + + // selfies (selfie) + array('seifles', 7, true, true, 'selfie'), + + // movies (movie) + array('seivom', 6, true, true, 'movie'), + + // feet (foot) + array('teef', 4, true, true, 'foot'), + + // geese (goose) + array('eseeg', 5, true, true, 'goose'), + + // teeth (tooth) + array('hteet', 5, true, true, 'tooth'), + + // news (news) + array('swen', 4, true, true, 'news'), + + // series (series) + array('seires', 6, true, true, 'series'), + + // babies (baby) + array('sei', 3, false, true, 'y'), + + // accesses (access), addresses (address), kisses (kiss) + array('sess', 4, true, false, 'ss'), + + // analyses (analysis), ellipses (ellipsis), funguses (fungus), + // neuroses (neurosis), theses (thesis), emphases (emphasis), + // oases (oasis), crises (crisis), houses (house), bases (base), + // atlases (atlas) + array('ses', 3, true, true, array('s', 'se', 'sis')), + + // objectives (objective), alternative (alternatives) + array('sevit', 5, true, true, 'tive'), + + // drives (drive) + array('sevird', 6, false, true, 'drive'), + + // lives (life), wives (wife) + array('sevi', 4, false, true, 'ife'), + + // moves (move) + array('sevom', 5, true, true, 'move'), + + // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff) + array('sev', 3, true, true, array('f', 've', 'ff')), + + // axes (axis), axes (ax), axes (axe) + array('sexa', 4, false, false, array('ax', 'axe', 'axis')), + + // indexes (index), matrixes (matrix) + array('sex', 3, true, false, 'x'), + + // quizzes (quiz) + array('sezz', 4, true, false, 'z'), + + // bureaus (bureau) + array('suae', 4, false, true, 'eau'), + + // roses (rose), garages (garage), cassettes (cassette), + // waltzes (waltz), heroes (hero), bushes (bush), arches (arch), + // shoes (shoe) + array('se', 2, true, true, array('', 'e')), + + // tags (tag) + array('s', 1, true, true, ''), + + // chateaux (chateau) + array('xuae', 4, false, true, 'eau'), + + // people (person) + array('elpoep', 6, true, true, 'person'), + ); + + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Returns the singular form of a word. + * + * If the method can't determine the form with certainty, an array of the + * possible singulars is returned. + * + * @param string $plural A word in plural form + * + * @return string|array The singular form or an array of possible singular + * forms + * + * @internal + */ + public static function singularize($plural) + { + $pluralRev = strrev($plural); + $lowerPluralRev = strtolower($pluralRev); + $pluralLength = strlen($lowerPluralRev); + + // The outer loop iterates over the entries of the plural table + // The inner loop $j iterates over the characters of the plural suffix + // in the plural table to compare them with the characters of the actual + // given plural suffix + foreach (self::$pluralMap as $map) { + $suffix = $map[0]; + $suffixLength = $map[1]; + $j = 0; + + // Compare characters in the plural table and of the suffix of the + // given plural one by one + while ($suffix[$j] === $lowerPluralRev[$j]) { + // Let $j point to the next character + ++$j; + + // Successfully compared the last character + // Add an entry with the singular suffix to the singular array + if ($j === $suffixLength) { + // Is there any character preceding the suffix in the plural string? + if ($j < $pluralLength) { + $nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]); + + if (!$map[2] && $nextIsVocal) { + // suffix may not succeed a vocal but next char is one + break; + } + + if (!$map[3] && !$nextIsVocal) { + // suffix may not succeed a consonant but next char is one + break; + } + } + + $newBase = substr($plural, 0, $pluralLength - $suffixLength); + $newSuffix = $map[4]; + + // Check whether the first character in the plural suffix + // is uppercased. If yes, uppercase the first character in + // the singular suffix too + $firstUpper = ctype_upper($pluralRev[$j - 1]); + + if (is_array($newSuffix)) { + $singulars = array(); + + foreach ($newSuffix as $newSuffixEntry) { + $singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry); + } + + return $singulars; + } + + return $newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix); + } + + // Suffix is longer than word + if ($j === $pluralLength) { + break; + } + } + } + + // Assume that plural and singular is identical + return $plural; + } +} diff --git a/vendor/symfony/inflector/LICENSE b/vendor/symfony/inflector/LICENSE new file mode 100644 index 0000000..ac30964 --- /dev/null +++ b/vendor/symfony/inflector/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012-2017 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/inflector/README.md b/vendor/symfony/inflector/README.md new file mode 100644 index 0000000..8b81839 --- /dev/null +++ b/vendor/symfony/inflector/README.md @@ -0,0 +1,19 @@ +Inflector Component +=================== + +Inflector converts words between their singular and plural forms (English only). + +Disclaimer +---------- + +This component is currently marked as internal. Do not use it in your own code. +Breaking changes may be introduced in the next minor version of Symfony, or the +component itself might even be removed completely. + +Resources +--------- + + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/inflector/Tests/InflectorTest.php b/vendor/symfony/inflector/Tests/InflectorTest.php new file mode 100644 index 0000000..be78367 --- /dev/null +++ b/vendor/symfony/inflector/Tests/InflectorTest.php @@ -0,0 +1,172 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Inflector\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Inflector\Inflector; + +class InflectorTest extends TestCase +{ + public function singularizeProvider() + { + // see http://english-zone.com/spelling/plurals.html + // see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English + return array( + array('accesses', 'access'), + array('addresses', 'address'), + array('agendas', 'agenda'), + array('alumnae', 'alumna'), + array('alumni', 'alumnus'), + array('analyses', array('analys', 'analyse', 'analysis')), + array('antennae', 'antenna'), + array('antennas', 'antenna'), + array('appendices', array('appendex', 'appendix', 'appendice')), + array('arches', array('arch', 'arche')), + array('atlases', array('atlas', 'atlase', 'atlasis')), + array('axes', array('ax', 'axe', 'axis')), + array('babies', 'baby'), + array('bacteria', array('bacterion', 'bacterium')), + array('bases', array('bas', 'base', 'basis')), + array('batches', array('batch', 'batche')), + array('beaux', 'beau'), + array('bees', array('be', 'bee')), + array('boxes', 'box'), + array('boys', 'boy'), + array('bureaus', 'bureau'), + array('bureaux', 'bureau'), + array('buses', array('bus', 'buse', 'busis')), + array('bushes', array('bush', 'bushe')), + array('calves', array('calf', 'calve', 'calff')), + array('cars', 'car'), + array('cassettes', array('cassett', 'cassette')), + array('caves', array('caf', 'cave', 'caff')), + array('chateaux', 'chateau'), + array('cheeses', array('chees', 'cheese', 'cheesis')), + array('children', 'child'), + array('circuses', array('circus', 'circuse', 'circusis')), + array('cliffs', 'cliff'), + array('committee', 'committee'), + array('crises', array('cris', 'crise', 'crisis')), + array('criteria', array('criterion', 'criterium')), + array('cups', 'cup'), + array('data', array('daton', 'datum')), + array('days', 'day'), + array('discos', 'disco'), + array('devices', array('devex', 'devix', 'device')), + array('drives', 'drive'), + array('drivers', 'driver'), + array('dwarves', array('dwarf', 'dwarve', 'dwarff')), + array('echoes', array('echo', 'echoe')), + array('elves', array('elf', 'elve', 'elff')), + array('emphases', array('emphas', 'emphase', 'emphasis')), + array('faxes', 'fax'), + array('feet', 'foot'), + array('feedback', 'feedback'), + array('foci', 'focus'), + array('focuses', array('focus', 'focuse', 'focusis')), + array('formulae', 'formula'), + array('formulas', 'formula'), + array('fungi', 'fungus'), + array('funguses', array('fungus', 'funguse', 'fungusis')), + array('garages', array('garag', 'garage')), + array('geese', 'goose'), + array('halves', array('half', 'halve', 'halff')), + array('hats', 'hat'), + array('heroes', array('hero', 'heroe')), + array('hippopotamuses', array('hippopotamus', 'hippopotamuse', 'hippopotamusis')), //hippopotami + array('hoaxes', 'hoax'), + array('hooves', array('hoof', 'hoove', 'hooff')), + array('houses', array('hous', 'house', 'housis')), + array('indexes', 'index'), + array('indices', array('index', 'indix', 'indice')), + array('ions', 'ion'), + array('irises', array('iris', 'irise', 'irisis')), + array('kisses', 'kiss'), + array('knives', 'knife'), + array('lamps', 'lamp'), + array('leaves', array('leaf', 'leave', 'leaff')), + array('lice', 'louse'), + array('lives', 'life'), + array('matrices', array('matrex', 'matrix', 'matrice')), + array('matrixes', 'matrix'), + array('men', 'man'), + array('mice', 'mouse'), + array('moves', 'move'), + array('movies', 'movie'), + array('nebulae', 'nebula'), + array('neuroses', array('neuros', 'neurose', 'neurosis')), + array('news', 'news'), + array('oases', array('oas', 'oase', 'oasis')), + array('objectives', 'objective'), + array('oxen', 'ox'), + array('parties', 'party'), + array('people', 'person'), + array('persons', 'person'), + array('phenomena', array('phenomenon', 'phenomenum')), + array('photos', 'photo'), + array('pianos', 'piano'), + array('plateaux', 'plateau'), + array('poppies', 'poppy'), + array('prices', array('prex', 'prix', 'price')), + array('quizzes', 'quiz'), + array('radii', 'radius'), + array('roofs', 'roof'), + array('roses', array('ros', 'rose', 'rosis')), + array('sandwiches', array('sandwich', 'sandwiche')), + array('scarves', array('scarf', 'scarve', 'scarff')), + array('schemas', 'schema'), //schemata + array('selfies', 'selfie'), + array('series', 'series'), + array('services', 'service'), + array('sheriffs', 'sheriff'), + array('shoes', array('sho', 'shoe')), + array('spies', 'spy'), + array('staves', array('staf', 'stave', 'staff')), + array('stories', 'story'), + array('strata', array('straton', 'stratum')), + array('suitcases', array('suitcas', 'suitcase', 'suitcasis')), + array('syllabi', 'syllabus'), + array('tags', 'tag'), + array('teeth', 'tooth'), + array('theses', array('thes', 'these', 'thesis')), + array('thieves', array('thief', 'thieve', 'thieff')), + array('trees', array('tre', 'tree')), + array('waltzes', array('waltz', 'waltze')), + array('wives', 'wife'), + + // test casing: if the first letter was uppercase, it should remain so + array('Men', 'Man'), + array('GrandChildren', 'GrandChild'), + array('SubTrees', array('SubTre', 'SubTree')), + + // Known issues + //array('insignia', 'insigne'), + //array('insignias', 'insigne'), + //array('rattles', 'rattle'), + ); + } + + /** + * @dataProvider singularizeProvider + */ + public function testSingularize($plural, $singular) + { + $single = Inflector::singularize($plural); + if (is_string($singular) && is_array($single)) { + $this->fail("--- Expected\n`string`: ".$singular."\n+++ Actual\n`array`: ".implode(', ', $single)); + } elseif (is_array($singular) && is_string($single)) { + $this->fail("--- Expected\n`array`: ".implode(', ', $singular)."\n+++ Actual\n`string`: ".$single); + } + + $this->assertEquals($singular, $single); + } +} diff --git a/vendor/symfony/inflector/composer.json b/vendor/symfony/inflector/composer.json new file mode 100644 index 0000000..79f4ed7 --- /dev/null +++ b/vendor/symfony/inflector/composer.json @@ -0,0 +1,40 @@ +{ + "name": "symfony/inflector", + "type": "library", + "description": "Symfony Inflector Component", + "keywords": [ + "string", + "inflection", + "singularize", + "pluralize", + "words", + "symfony" + ], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.5.9" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Inflector\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + } +} diff --git a/vendor/symfony/inflector/phpunit.xml.dist b/vendor/symfony/inflector/phpunit.xml.dist new file mode 100644 index 0000000..0e953fc --- /dev/null +++ b/vendor/symfony/inflector/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/vendor/symfony/options-resolver/.gitignore b/vendor/symfony/options-resolver/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/vendor/symfony/options-resolver/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/vendor/symfony/options-resolver/CHANGELOG.md b/vendor/symfony/options-resolver/CHANGELOG.md new file mode 100644 index 0000000..5f6d15b --- /dev/null +++ b/vendor/symfony/options-resolver/CHANGELOG.md @@ -0,0 +1,46 @@ +CHANGELOG +========= + +2.6.0 +----- + + * deprecated OptionsResolverInterface + * [BC BREAK] removed "array" type hint from OptionsResolverInterface methods + setRequired(), setAllowedValues(), addAllowedValues(), setAllowedTypes() and + addAllowedTypes() + * added OptionsResolver::setDefault() + * added OptionsResolver::hasDefault() + * added OptionsResolver::setNormalizer() + * added OptionsResolver::isRequired() + * added OptionsResolver::getRequiredOptions() + * added OptionsResolver::isMissing() + * added OptionsResolver::getMissingOptions() + * added OptionsResolver::setDefined() + * added OptionsResolver::isDefined() + * added OptionsResolver::getDefinedOptions() + * added OptionsResolver::remove() + * added OptionsResolver::clear() + * deprecated OptionsResolver::replaceDefaults() + * deprecated OptionsResolver::setOptional() in favor of setDefined() + * deprecated OptionsResolver::isKnown() in favor of isDefined() + * [BC BREAK] OptionsResolver::isRequired() returns true now if a required + option has a default value set + * [BC BREAK] merged Options into OptionsResolver and turned Options into an + interface + * deprecated Options::overload() (now in OptionsResolver) + * deprecated Options::set() (now in OptionsResolver) + * deprecated Options::get() (now in OptionsResolver) + * deprecated Options::has() (now in OptionsResolver) + * deprecated Options::replace() (now in OptionsResolver) + * [BC BREAK] Options::get() (now in OptionsResolver) can only be used within + lazy option/normalizer closures now + * [BC BREAK] removed Traversable interface from Options since using within + lazy option/normalizer closures resulted in exceptions + * [BC BREAK] removed Options::all() since using within lazy option/normalizer + closures resulted in exceptions + * [BC BREAK] OptionDefinitionException now extends LogicException instead of + RuntimeException + * [BC BREAK] normalizers are not executed anymore for unset options + * normalizers are executed after validating the options now + * [BC BREAK] an UndefinedOptionsException is now thrown instead of an + InvalidOptionsException when non-existing options are passed diff --git a/vendor/symfony/options-resolver/Exception/AccessException.php b/vendor/symfony/options-resolver/Exception/AccessException.php new file mode 100644 index 0000000..c12b680 --- /dev/null +++ b/vendor/symfony/options-resolver/Exception/AccessException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when trying to read an option outside of or write it inside of + * {@link \Symfony\Component\OptionsResolver\Options::resolve()}. + * + * @author Bernhard Schussek + */ +class AccessException extends \LogicException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/options-resolver/Exception/ExceptionInterface.php b/vendor/symfony/options-resolver/Exception/ExceptionInterface.php new file mode 100644 index 0000000..b62bb51 --- /dev/null +++ b/vendor/symfony/options-resolver/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Marker interface for all exceptions thrown by the OptionsResolver component. + * + * @author Bernhard Schussek + */ +interface ExceptionInterface +{ +} diff --git a/vendor/symfony/options-resolver/Exception/InvalidArgumentException.php b/vendor/symfony/options-resolver/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..6d421d6 --- /dev/null +++ b/vendor/symfony/options-resolver/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when an argument is invalid. + * + * @author Bernhard Schussek + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/options-resolver/Exception/InvalidOptionsException.php b/vendor/symfony/options-resolver/Exception/InvalidOptionsException.php new file mode 100644 index 0000000..6fd4f12 --- /dev/null +++ b/vendor/symfony/options-resolver/Exception/InvalidOptionsException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when the value of an option does not match its validation rules. + * + * You should make sure a valid value is passed to the option. + * + * @author Bernhard Schussek + */ +class InvalidOptionsException extends InvalidArgumentException +{ +} diff --git a/vendor/symfony/options-resolver/Exception/MissingOptionsException.php b/vendor/symfony/options-resolver/Exception/MissingOptionsException.php new file mode 100644 index 0000000..faa487f --- /dev/null +++ b/vendor/symfony/options-resolver/Exception/MissingOptionsException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Exception thrown when a required option is missing. + * + * Add the option to the passed options array. + * + * @author Bernhard Schussek + */ +class MissingOptionsException extends InvalidArgumentException +{ +} diff --git a/vendor/symfony/options-resolver/Exception/NoSuchOptionException.php b/vendor/symfony/options-resolver/Exception/NoSuchOptionException.php new file mode 100644 index 0000000..4c3280f --- /dev/null +++ b/vendor/symfony/options-resolver/Exception/NoSuchOptionException.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when trying to read an option that has no value set. + * + * When accessing optional options from within a lazy option or normalizer you should first + * check whether the optional option is set. You can do this with `isset($options['optional'])`. + * In contrast to the {@link UndefinedOptionsException}, this is a runtime exception that can + * occur when evaluating lazy options. + * + * @author Tobias Schultze + */ +class NoSuchOptionException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/options-resolver/Exception/OptionDefinitionException.php b/vendor/symfony/options-resolver/Exception/OptionDefinitionException.php new file mode 100644 index 0000000..e8e339d --- /dev/null +++ b/vendor/symfony/options-resolver/Exception/OptionDefinitionException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when two lazy options have a cyclic dependency. + * + * @author Bernhard Schussek + */ +class OptionDefinitionException extends \LogicException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/options-resolver/Exception/UndefinedOptionsException.php b/vendor/symfony/options-resolver/Exception/UndefinedOptionsException.php new file mode 100644 index 0000000..6ca3fce --- /dev/null +++ b/vendor/symfony/options-resolver/Exception/UndefinedOptionsException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Exception thrown when an undefined option is passed. + * + * You should remove the options in question from your code or define them + * beforehand. + * + * @author Bernhard Schussek + */ +class UndefinedOptionsException extends InvalidArgumentException +{ +} diff --git a/vendor/symfony/options-resolver/LICENSE b/vendor/symfony/options-resolver/LICENSE new file mode 100644 index 0000000..17d16a1 --- /dev/null +++ b/vendor/symfony/options-resolver/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2017 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/options-resolver/Options.php b/vendor/symfony/options-resolver/Options.php new file mode 100644 index 0000000..d444ec4 --- /dev/null +++ b/vendor/symfony/options-resolver/Options.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +/** + * Contains resolved option values. + * + * @author Bernhard Schussek + * @author Tobias Schultze + */ +interface Options extends \ArrayAccess, \Countable +{ +} diff --git a/vendor/symfony/options-resolver/OptionsResolver.php b/vendor/symfony/options-resolver/OptionsResolver.php new file mode 100644 index 0000000..32ac566 --- /dev/null +++ b/vendor/symfony/options-resolver/OptionsResolver.php @@ -0,0 +1,1039 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +use Symfony\Component\OptionsResolver\Exception\AccessException; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException; +use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; + +/** + * Validates options and merges them with default values. + * + * @author Bernhard Schussek + * @author Tobias Schultze + */ +class OptionsResolver implements Options +{ + /** + * The names of all defined options. + * + * @var array + */ + private $defined = array(); + + /** + * The default option values. + * + * @var array + */ + private $defaults = array(); + + /** + * The names of required options. + * + * @var array + */ + private $required = array(); + + /** + * The resolved option values. + * + * @var array + */ + private $resolved = array(); + + /** + * A list of normalizer closures. + * + * @var \Closure[] + */ + private $normalizers = array(); + + /** + * A list of accepted values for each option. + * + * @var array + */ + private $allowedValues = array(); + + /** + * A list of accepted types for each option. + * + * @var array + */ + private $allowedTypes = array(); + + /** + * A list of closures for evaluating lazy options. + * + * @var array + */ + private $lazy = array(); + + /** + * A list of lazy options whose closure is currently being called. + * + * This list helps detecting circular dependencies between lazy options. + * + * @var array + */ + private $calling = array(); + + /** + * Whether the instance is locked for reading. + * + * Once locked, the options cannot be changed anymore. This is + * necessary in order to avoid inconsistencies during the resolving + * process. If any option is changed after being read, all evaluated + * lazy options that depend on this option would become invalid. + * + * @var bool + */ + private $locked = false; + + private static $typeAliases = array( + 'boolean' => 'bool', + 'integer' => 'int', + 'double' => 'float', + ); + + /** + * Sets the default value of a given option. + * + * If the default value should be set based on other options, you can pass + * a closure with the following signature: + * + * function (Options $options) { + * // ... + * } + * + * The closure will be evaluated when {@link resolve()} is called. The + * closure has access to the resolved values of other options through the + * passed {@link Options} instance: + * + * function (Options $options) { + * if (isset($options['port'])) { + * // ... + * } + * } + * + * If you want to access the previously set default value, add a second + * argument to the closure's signature: + * + * $options->setDefault('name', 'Default Name'); + * + * $options->setDefault('name', function (Options $options, $previousValue) { + * // 'Default Name' === $previousValue + * }); + * + * This is mostly useful if the configuration of the {@link Options} object + * is spread across different locations of your code, such as base and + * sub-classes. + * + * @param string $option The name of the option + * @param mixed $value The default value of the option + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefault($option, $value) + { + // Setting is not possible once resolving starts, because then lazy + // options could manipulate the state of the object, leading to + // inconsistent results. + if ($this->locked) { + throw new AccessException('Default values cannot be set from a lazy option or normalizer.'); + } + + // If an option is a closure that should be evaluated lazily, store it + // in the "lazy" property. + if ($value instanceof \Closure) { + $reflClosure = new \ReflectionFunction($value); + $params = $reflClosure->getParameters(); + + if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && Options::class === $class->name) { + // Initialize the option if no previous value exists + if (!isset($this->defaults[$option])) { + $this->defaults[$option] = null; + } + + // Ignore previous lazy options if the closure has no second parameter + if (!isset($this->lazy[$option]) || !isset($params[1])) { + $this->lazy[$option] = array(); + } + + // Store closure for later evaluation + $this->lazy[$option][] = $value; + $this->defined[$option] = true; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + } + + // This option is not lazy anymore + unset($this->lazy[$option]); + + // Yet undefined options can be marked as resolved, because we only need + // to resolve options with lazy closures, normalizers or validation + // rules, none of which can exist for undefined options + // If the option was resolved before, update the resolved value + if (!isset($this->defined[$option]) || array_key_exists($option, $this->resolved)) { + $this->resolved[$option] = $value; + } + + $this->defaults[$option] = $value; + $this->defined[$option] = true; + + return $this; + } + + /** + * Sets a list of default values. + * + * @param array $defaults The default values to set + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefaults(array $defaults) + { + foreach ($defaults as $option => $value) { + $this->setDefault($option, $value); + } + + return $this; + } + + /** + * Returns whether a default value is set for an option. + * + * Returns true if {@link setDefault()} was called for this option. + * An option is also considered set if it was set to null. + * + * @param string $option The option name + * + * @return bool Whether a default value is set + */ + public function hasDefault($option) + { + return array_key_exists($option, $this->defaults); + } + + /** + * Marks one or more options as required. + * + * @param string|string[] $optionNames One or more option names + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setRequired($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be made required from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + $this->defined[$option] = true; + $this->required[$option] = true; + } + + return $this; + } + + /** + * Returns whether an option is required. + * + * An option is required if it was passed to {@link setRequired()}. + * + * @param string $option The name of the option + * + * @return bool Whether the option is required + */ + public function isRequired($option) + { + return isset($this->required[$option]); + } + + /** + * Returns the names of all required options. + * + * @return string[] The names of the required options + * + * @see isRequired() + */ + public function getRequiredOptions() + { + return array_keys($this->required); + } + + /** + * Returns whether an option is missing a default value. + * + * An option is missing if it was passed to {@link setRequired()}, but not + * to {@link setDefault()}. This option must be passed explicitly to + * {@link resolve()}, otherwise an exception will be thrown. + * + * @param string $option The name of the option + * + * @return bool Whether the option is missing + */ + public function isMissing($option) + { + return isset($this->required[$option]) && !array_key_exists($option, $this->defaults); + } + + /** + * Returns the names of all options missing a default value. + * + * @return string[] The names of the missing options + * + * @see isMissing() + */ + public function getMissingOptions() + { + return array_keys(array_diff_key($this->required, $this->defaults)); + } + + /** + * Defines a valid option name. + * + * Defines an option name without setting a default value. The option will + * be accepted when passed to {@link resolve()}. When not passed, the + * option will not be included in the resolved options. + * + * @param string|string[] $optionNames One or more option names + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefined($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be defined from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + $this->defined[$option] = true; + } + + return $this; + } + + /** + * Returns whether an option is defined. + * + * Returns true for any option passed to {@link setDefault()}, + * {@link setRequired()} or {@link setDefined()}. + * + * @param string $option The option name + * + * @return bool Whether the option is defined + */ + public function isDefined($option) + { + return isset($this->defined[$option]); + } + + /** + * Returns the names of all defined options. + * + * @return string[] The names of the defined options + * + * @see isDefined() + */ + public function getDefinedOptions() + { + return array_keys($this->defined); + } + + /** + * Sets the normalizer for an option. + * + * The normalizer should be a closure with the following signature: + * + * ```php + * function (Options $options, $value) { + * // ... + * } + * ``` + * + * The closure is invoked when {@link resolve()} is called. The closure + * has access to the resolved values of other options through the passed + * {@link Options} instance. + * + * The second parameter passed to the closure is the value of + * the option. + * + * The resolved option value is set to the return value of the closure. + * + * @param string $option The option name + * @param \Closure $normalizer The normalizer + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setNormalizer($option, \Closure $normalizer) + { + if ($this->locked) { + throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + $this->normalizers[$option] = $normalizer; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Sets allowed values for an option. + * + * Instead of passing values, you may also pass a closures with the + * following signature: + * + * function ($value) { + * // return true or false + * } + * + * The closure receives the value as argument and should return true to + * accept the value and false to reject the value. + * + * @param string $option The option name + * @param mixed $allowedValues One or more acceptable values/closures + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setAllowedValues($option, $allowedValues) + { + if ($this->locked) { + throw new AccessException('Allowed values cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + $this->allowedValues[$option] = is_array($allowedValues) ? $allowedValues : array($allowedValues); + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds allowed values for an option. + * + * The values are merged with the allowed values defined previously. + * + * Instead of passing values, you may also pass a closures with the + * following signature: + * + * function ($value) { + * // return true or false + * } + * + * The closure receives the value as argument and should return true to + * accept the value and false to reject the value. + * + * @param string $option The option name + * @param mixed $allowedValues One or more acceptable values/closures + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addAllowedValues($option, $allowedValues) + { + if ($this->locked) { + throw new AccessException('Allowed values cannot be added from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + if (!is_array($allowedValues)) { + $allowedValues = array($allowedValues); + } + + if (!isset($this->allowedValues[$option])) { + $this->allowedValues[$option] = $allowedValues; + } else { + $this->allowedValues[$option] = array_merge($this->allowedValues[$option], $allowedValues); + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Sets allowed types for an option. + * + * Any type for which a corresponding is_() function exists is + * acceptable. Additionally, fully-qualified class or interface names may + * be passed. + * + * @param string $option The option name + * @param string|string[] $allowedTypes One or more accepted types + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setAllowedTypes($option, $allowedTypes) + { + if ($this->locked) { + throw new AccessException('Allowed types cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + $this->allowedTypes[$option] = (array) $allowedTypes; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds allowed types for an option. + * + * The types are merged with the allowed types defined previously. + * + * Any type for which a corresponding is_() function exists is + * acceptable. Additionally, fully-qualified class or interface names may + * be passed. + * + * @param string $option The option name + * @param string|string[] $allowedTypes One or more accepted types + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addAllowedTypes($option, $allowedTypes) + { + if ($this->locked) { + throw new AccessException('Allowed types cannot be added from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + if (!isset($this->allowedTypes[$option])) { + $this->allowedTypes[$option] = (array) $allowedTypes; + } else { + $this->allowedTypes[$option] = array_merge($this->allowedTypes[$option], (array) $allowedTypes); + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Removes the option with the given name. + * + * Undefined options are ignored. + * + * @param string|string[] $optionNames One or more option names + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function remove($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be removed from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]); + unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option]); + } + + return $this; + } + + /** + * Removes all options. + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function clear() + { + if ($this->locked) { + throw new AccessException('Options cannot be cleared from a lazy option or normalizer.'); + } + + $this->defined = array(); + $this->defaults = array(); + $this->required = array(); + $this->resolved = array(); + $this->lazy = array(); + $this->normalizers = array(); + $this->allowedTypes = array(); + $this->allowedValues = array(); + + return $this; + } + + /** + * Merges options with the default values stored in the container and + * validates them. + * + * Exceptions are thrown if: + * + * - Undefined options are passed; + * - Required options are missing; + * - Options have invalid types; + * - Options have invalid values. + * + * @param array $options A map of option names to values + * + * @return array The merged and validated options + * + * @throws UndefinedOptionsException If an option name is undefined + * @throws InvalidOptionsException If an option doesn't fulfill the + * specified validation rules + * @throws MissingOptionsException If a required option is missing + * @throws OptionDefinitionException If there is a cyclic dependency between + * lazy options and/or normalizers + * @throws NoSuchOptionException If a lazy option reads an unavailable option + * @throws AccessException If called from a lazy option or normalizer + */ + public function resolve(array $options = array()) + { + if ($this->locked) { + throw new AccessException('Options cannot be resolved from a lazy option or normalizer.'); + } + + // Allow this method to be called multiple times + $clone = clone $this; + + // Make sure that no unknown options are passed + $diff = array_diff_key($options, $clone->defined); + + if (count($diff) > 0) { + ksort($clone->defined); + ksort($diff); + + throw new UndefinedOptionsException(sprintf( + (count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Defined options are: "%s".', + implode('", "', array_keys($diff)), + implode('", "', array_keys($clone->defined)) + )); + } + + // Override options set by the user + foreach ($options as $option => $value) { + $clone->defaults[$option] = $value; + unset($clone->resolved[$option], $clone->lazy[$option]); + } + + // Check whether any required option is missing + $diff = array_diff_key($clone->required, $clone->defaults); + + if (count($diff) > 0) { + ksort($diff); + + throw new MissingOptionsException(sprintf( + count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', + implode('", "', array_keys($diff)) + )); + } + + // Lock the container + $clone->locked = true; + + // Now process the individual options. Use offsetGet(), which resolves + // the option itself and any options that the option depends on + foreach ($clone->defaults as $option => $_) { + $clone->offsetGet($option); + } + + return $clone->resolved; + } + + /** + * Returns the resolved value of an option. + * + * @param string $option The option name + * + * @return mixed The option value + * + * @throws AccessException If accessing this method outside of + * {@link resolve()} + * @throws NoSuchOptionException If the option is not set + * @throws InvalidOptionsException If the option doesn't fulfill the + * specified validation rules + * @throws OptionDefinitionException If there is a cyclic dependency between + * lazy options and/or normalizers + */ + public function offsetGet($option) + { + if (!$this->locked) { + throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); + } + + // Shortcut for resolved options + if (array_key_exists($option, $this->resolved)) { + return $this->resolved[$option]; + } + + // Check whether the option is set at all + if (!array_key_exists($option, $this->defaults)) { + if (!isset($this->defined[$option])) { + throw new NoSuchOptionException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + throw new NoSuchOptionException(sprintf( + 'The optional option "%s" has no value set. You should make sure it is set with "isset" before reading it.', + $option + )); + } + + $value = $this->defaults[$option]; + + // Resolve the option if the default value is lazily evaluated + if (isset($this->lazy[$option])) { + // If the closure is already being called, we have a cyclic + // dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(sprintf( + 'The options "%s" have a cyclic dependency.', + implode('", "', array_keys($this->calling)) + )); + } + + // The following section must be protected from cyclic + // calls. Set $calling for the current $option to detect a cyclic + // dependency + // BEGIN + $this->calling[$option] = true; + try { + foreach ($this->lazy[$option] as $closure) { + $value = $closure($this, $value); + } + } finally { + unset($this->calling[$option]); + } + // END + } + + // Validate the type of the resolved option + if (isset($this->allowedTypes[$option])) { + $valid = false; + + foreach ($this->allowedTypes[$option] as $type) { + $type = isset(self::$typeAliases[$type]) ? self::$typeAliases[$type] : $type; + + if (function_exists($isFunction = 'is_'.$type)) { + if ($isFunction($value)) { + $valid = true; + break; + } + + continue; + } + + if ($value instanceof $type) { + $valid = true; + break; + } + } + + if (!$valid) { + throw new InvalidOptionsException(sprintf( + 'The option "%s" with value %s is expected to be of type '. + '"%s", but is of type "%s".', + $option, + $this->formatValue($value), + implode('" or "', $this->allowedTypes[$option]), + $this->formatTypeOf($value) + )); + } + } + + // Validate the value of the resolved option + if (isset($this->allowedValues[$option])) { + $success = false; + $printableAllowedValues = array(); + + foreach ($this->allowedValues[$option] as $allowedValue) { + if ($allowedValue instanceof \Closure) { + if ($allowedValue($value)) { + $success = true; + break; + } + + // Don't include closures in the exception message + continue; + } elseif ($value === $allowedValue) { + $success = true; + break; + } + + $printableAllowedValues[] = $allowedValue; + } + + if (!$success) { + $message = sprintf( + 'The option "%s" with value %s is invalid.', + $option, + $this->formatValue($value) + ); + + if (count($printableAllowedValues) > 0) { + $message .= sprintf( + ' Accepted values are: %s.', + $this->formatValues($printableAllowedValues) + ); + } + + throw new InvalidOptionsException($message); + } + } + + // Normalize the validated option + if (isset($this->normalizers[$option])) { + // If the closure is already being called, we have a cyclic + // dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(sprintf( + 'The options "%s" have a cyclic dependency.', + implode('", "', array_keys($this->calling)) + )); + } + + $normalizer = $this->normalizers[$option]; + + // The following section must be protected from cyclic + // calls. Set $calling for the current $option to detect a cyclic + // dependency + // BEGIN + $this->calling[$option] = true; + try { + $value = $normalizer($this, $value); + } finally { + unset($this->calling[$option]); + } + // END + } + + // Mark as resolved + $this->resolved[$option] = $value; + + return $value; + } + + /** + * Returns whether a resolved option with the given name exists. + * + * @param string $option The option name + * + * @return bool Whether the option is set + * + * @throws AccessException If accessing this method outside of {@link resolve()} + * + * @see \ArrayAccess::offsetExists() + */ + public function offsetExists($option) + { + if (!$this->locked) { + throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); + } + + return array_key_exists($option, $this->defaults); + } + + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetSet($option, $value) + { + throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.'); + } + + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetUnset($option) + { + throw new AccessException('Removing options via array access is not supported. Use remove() instead.'); + } + + /** + * Returns the number of set options. + * + * This may be only a subset of the defined options. + * + * @return int Number of options + * + * @throws AccessException If accessing this method outside of {@link resolve()} + * + * @see \Countable::count() + */ + public function count() + { + if (!$this->locked) { + throw new AccessException('Counting is only supported within closures of lazy options and normalizers.'); + } + + return count($this->defaults); + } + + /** + * Returns a string representation of the type of the value. + * + * This method should be used if you pass the type of a value as + * message parameter to a constraint violation. Note that such + * parameters should usually not be included in messages aimed at + * non-technical people. + * + * @param mixed $value The value to return the type of + * + * @return string The type of the value + */ + private function formatTypeOf($value) + { + return is_object($value) ? get_class($value) : gettype($value); + } + + /** + * Returns a string representation of the value. + * + * This method returns the equivalent PHP tokens for most scalar types + * (i.e. "false" for false, "1" for 1 etc.). Strings are always wrapped + * in double quotes ("). + * + * @param mixed $value The value to format as string + * + * @return string The string representation of the passed value + */ + private function formatValue($value) + { + if (is_object($value)) { + return get_class($value); + } + + if (is_array($value)) { + return 'array'; + } + + if (is_string($value)) { + return '"'.$value.'"'; + } + + if (is_resource($value)) { + return 'resource'; + } + + if (null === $value) { + return 'null'; + } + + if (false === $value) { + return 'false'; + } + + if (true === $value) { + return 'true'; + } + + return (string) $value; + } + + /** + * Returns a string representation of a list of values. + * + * Each of the values is converted to a string using + * {@link formatValue()}. The values are then concatenated with commas. + * + * @param array $values A list of values + * + * @return string The string representation of the value list + * + * @see formatValue() + */ + private function formatValues(array $values) + { + foreach ($values as $key => $value) { + $values[$key] = $this->formatValue($value); + } + + return implode(', ', $values); + } +} diff --git a/vendor/symfony/options-resolver/README.md b/vendor/symfony/options-resolver/README.md new file mode 100644 index 0000000..245e69b --- /dev/null +++ b/vendor/symfony/options-resolver/README.md @@ -0,0 +1,15 @@ +OptionsResolver Component +========================= + +The OptionsResolver component is `array_replace` on steroids. It allows you to +create an options system with required options, defaults, validation (type, +value), normalization and more. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/options_resolver.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/options-resolver/Tests/OptionsResolverTest.php b/vendor/symfony/options-resolver/Tests/OptionsResolverTest.php new file mode 100644 index 0000000..d09dece --- /dev/null +++ b/vendor/symfony/options-resolver/Tests/OptionsResolverTest.php @@ -0,0 +1,1559 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Tests; + +use PHPUnit\Framework\Assert; +use PHPUnit\Framework\TestCase; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class OptionsResolverTest extends TestCase +{ + /** + * @var OptionsResolver + */ + private $resolver; + + protected function setUp() + { + $this->resolver = new OptionsResolver(); + } + + //////////////////////////////////////////////////////////////////////////// + // resolve() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The option "foo" does not exist. Defined options are: "a", "z". + */ + public function testResolveFailsIfNonExistingOption() + { + $this->resolver->setDefault('z', '1'); + $this->resolver->setDefault('a', '2'); + + $this->resolver->resolve(array('foo' => 'bar')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The options "baz", "foo", "ping" do not exist. Defined options are: "a", "z". + */ + public function testResolveFailsIfMultipleNonExistingOptions() + { + $this->resolver->setDefault('z', '1'); + $this->resolver->setDefault('a', '2'); + + $this->resolver->resolve(array('ping' => 'pong', 'foo' => 'bar', 'baz' => 'bam')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testResolveFailsFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->resolve(array()); + }); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // setDefault()/hasDefault() + //////////////////////////////////////////////////////////////////////////// + + public function testSetDefaultReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setDefault('foo', 'bar')); + } + + public function testSetDefault() + { + $this->resolver->setDefault('one', '1'); + $this->resolver->setDefault('two', '20'); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '20', + ), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetDefaultFromLazyOption() + { + $this->resolver->setDefault('lazy', function (Options $options) { + $options->setDefault('default', 42); + }); + + $this->resolver->resolve(); + } + + public function testHasDefault() + { + $this->assertFalse($this->resolver->hasDefault('foo')); + $this->resolver->setDefault('foo', 42); + $this->assertTrue($this->resolver->hasDefault('foo')); + } + + public function testHasDefaultWithNullValue() + { + $this->assertFalse($this->resolver->hasDefault('foo')); + $this->resolver->setDefault('foo', null); + $this->assertTrue($this->resolver->hasDefault('foo')); + } + + //////////////////////////////////////////////////////////////////////////// + // lazy setDefault() + //////////////////////////////////////////////////////////////////////////// + + public function testSetLazyReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setDefault('foo', function (Options $options) {})); + } + + public function testSetLazyClosure() + { + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testClosureWithoutTypeHintNotInvoked() + { + $closure = function ($options) { + Assert::fail('Should not be called'); + }; + + $this->resolver->setDefault('foo', $closure); + + $this->assertSame(array('foo' => $closure), $this->resolver->resolve()); + } + + public function testClosureWithoutParametersNotInvoked() + { + $closure = function () { + Assert::fail('Should not be called'); + }; + + $this->resolver->setDefault('foo', $closure); + + $this->assertSame(array('foo' => $closure), $this->resolver->resolve()); + } + + public function testAccessPreviousDefaultValue() + { + // defined by superclass + $this->resolver->setDefault('foo', 'bar'); + + // defined by subclass + $this->resolver->setDefault('foo', function (Options $options, $previousValue) { + Assert::assertEquals('bar', $previousValue); + + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testAccessPreviousLazyDefaultValue() + { + // defined by superclass + $this->resolver->setDefault('foo', function (Options $options) { + return 'bar'; + }); + + // defined by subclass + $this->resolver->setDefault('foo', function (Options $options, $previousValue) { + Assert::assertEquals('bar', $previousValue); + + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testPreviousValueIsNotEvaluatedIfNoSecondArgument() + { + // defined by superclass + $this->resolver->setDefault('foo', function () { + Assert::fail('Should not be called'); + }); + + // defined by subclass, no $previousValue argument defined! + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testOverwrittenLazyOptionNotEvaluated() + { + $this->resolver->setDefault('foo', function (Options $options) { + Assert::fail('Should not be called'); + }); + + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testInvokeEachLazyOptionOnlyOnce() + { + $calls = 0; + + $this->resolver->setDefault('lazy1', function (Options $options) use (&$calls) { + Assert::assertSame(1, ++$calls); + + $options['lazy2']; + }); + + $this->resolver->setDefault('lazy2', function (Options $options) use (&$calls) { + Assert::assertSame(2, ++$calls); + }); + + $this->resolver->resolve(); + + $this->assertSame(2, $calls); + } + + //////////////////////////////////////////////////////////////////////////// + // setRequired()/isRequired()/getRequiredOptions() + //////////////////////////////////////////////////////////////////////////// + + public function testSetRequiredReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setRequired('foo')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetRequiredFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setRequired('bar'); + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException + */ + public function testResolveFailsIfRequiredOptionMissing() + { + $this->resolver->setRequired('foo'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfRequiredOptionSet() + { + $this->resolver->setRequired('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + public function testResolveSucceedsIfRequiredOptionPassed() + { + $this->resolver->setRequired('foo'); + + $this->assertNotEmpty($this->resolver->resolve(array('foo' => 'bar'))); + } + + public function testIsRequired() + { + $this->assertFalse($this->resolver->isRequired('foo')); + $this->resolver->setRequired('foo'); + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testRequiredIfSetBefore() + { + $this->assertFalse($this->resolver->isRequired('foo')); + + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setRequired('foo'); + + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testStillRequiredAfterSet() + { + $this->assertFalse($this->resolver->isRequired('foo')); + + $this->resolver->setRequired('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testIsNotRequiredAfterRemove() + { + $this->assertFalse($this->resolver->isRequired('foo')); + $this->resolver->setRequired('foo'); + $this->resolver->remove('foo'); + $this->assertFalse($this->resolver->isRequired('foo')); + } + + public function testIsNotRequiredAfterClear() + { + $this->assertFalse($this->resolver->isRequired('foo')); + $this->resolver->setRequired('foo'); + $this->resolver->clear(); + $this->assertFalse($this->resolver->isRequired('foo')); + } + + public function testGetRequiredOptions() + { + $this->resolver->setRequired(array('foo', 'bar')); + $this->resolver->setDefault('bam', 'baz'); + $this->resolver->setDefault('foo', 'boo'); + + $this->assertSame(array('foo', 'bar'), $this->resolver->getRequiredOptions()); + } + + //////////////////////////////////////////////////////////////////////////// + // isMissing()/getMissingOptions() + //////////////////////////////////////////////////////////////////////////// + + public function testIsMissingIfNotSet() + { + $this->assertFalse($this->resolver->isMissing('foo')); + $this->resolver->setRequired('foo'); + $this->assertTrue($this->resolver->isMissing('foo')); + } + + public function testIsNotMissingIfSet() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->assertFalse($this->resolver->isMissing('foo')); + $this->resolver->setRequired('foo'); + $this->assertFalse($this->resolver->isMissing('foo')); + } + + public function testIsNotMissingAfterRemove() + { + $this->resolver->setRequired('foo'); + $this->resolver->remove('foo'); + $this->assertFalse($this->resolver->isMissing('foo')); + } + + public function testIsNotMissingAfterClear() + { + $this->resolver->setRequired('foo'); + $this->resolver->clear(); + $this->assertFalse($this->resolver->isRequired('foo')); + } + + public function testGetMissingOptions() + { + $this->resolver->setRequired(array('foo', 'bar')); + $this->resolver->setDefault('bam', 'baz'); + $this->resolver->setDefault('foo', 'boo'); + + $this->assertSame(array('bar'), $this->resolver->getMissingOptions()); + } + + //////////////////////////////////////////////////////////////////////////// + // setDefined()/isDefined()/getDefinedOptions() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetDefinedFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setDefined('bar'); + }); + + $this->resolver->resolve(); + } + + public function testDefinedOptionsNotIncludedInResolvedOptions() + { + $this->resolver->setDefined('foo'); + + $this->assertSame(array(), $this->resolver->resolve()); + } + + public function testDefinedOptionsIncludedIfDefaultSetBefore() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setDefined('foo'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testDefinedOptionsIncludedIfDefaultSetAfter() + { + $this->resolver->setDefined('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testDefinedOptionsIncludedIfPassedToResolve() + { + $this->resolver->setDefined('foo'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve(array('foo' => 'bar'))); + } + + public function testIsDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefined('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testLazyOptionsAreDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefault('foo', function (Options $options) {}); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testRequiredOptionsAreDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setRequired('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testSetOptionsAreDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefault('foo', 'bar'); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testGetDefinedOptions() + { + $this->resolver->setDefined(array('foo', 'bar')); + $this->resolver->setDefault('baz', 'bam'); + $this->resolver->setRequired('boo'); + + $this->assertSame(array('foo', 'bar', 'baz', 'boo'), $this->resolver->getDefinedOptions()); + } + + public function testRemovedOptionsAreNotDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefined('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + $this->resolver->remove('foo'); + $this->assertFalse($this->resolver->isDefined('foo')); + } + + public function testClearedOptionsAreNotDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefined('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + $this->resolver->clear(); + $this->assertFalse($this->resolver->isDefined('foo')); + } + + //////////////////////////////////////////////////////////////////////////// + // setAllowedTypes() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testSetAllowedTypesFailsIfUnknownOption() + { + $this->resolver->setAllowedTypes('foo', 'string'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetAllowedTypesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setAllowedTypes('bar', 'string'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @dataProvider provideInvalidTypes + */ + public function testResolveFailsIfInvalidType($actualType, $allowedType, $exceptionMessage) + { + $this->resolver->setDefined('option'); + $this->resolver->setAllowedTypes('option', $allowedType); + + if (method_exists($this, 'expectException')) { + $this->expectException('Symfony\Component\OptionsResolver\Exception\InvalidOptionsException'); + $this->expectExceptionMessage($exceptionMessage); + } else { + $this->setExpectedException('Symfony\Component\OptionsResolver\Exception\InvalidOptionsException', $exceptionMessage); + } + + $this->resolver->resolve(array('option' => $actualType)); + } + + public function provideInvalidTypes() + { + return array( + array(true, 'string', 'The option "option" with value true is expected to be of type "string", but is of type "boolean".'), + array(false, 'string', 'The option "option" with value false is expected to be of type "string", but is of type "boolean".'), + array(fopen(__FILE__, 'r'), 'string', 'The option "option" with value resource is expected to be of type "string", but is of type "resource".'), + array(array(), 'string', 'The option "option" with value array is expected to be of type "string", but is of type "array".'), + array(new OptionsResolver(), 'string', 'The option "option" with value Symfony\Component\OptionsResolver\OptionsResolver is expected to be of type "string", but is of type "Symfony\Component\OptionsResolver\OptionsResolver".'), + array(42, 'string', 'The option "option" with value 42 is expected to be of type "string", but is of type "integer".'), + array(null, 'string', 'The option "option" with value null is expected to be of type "string", but is of type "NULL".'), + array('bar', '\stdClass', 'The option "option" with value "bar" is expected to be of type "\stdClass", but is of type "string".'), + ); + } + + public function testResolveSucceedsIfValidType() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'string'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value 42 is expected to be of type "string" or "bool", but is of type "integer". + */ + public function testResolveFailsIfInvalidTypeMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedTypes('foo', array('string', 'bool')); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidTypeMultiple() + { + $this->resolver->setDefault('foo', true); + $this->resolver->setAllowedTypes('foo', array('string', 'bool')); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + public function testResolveSucceedsIfInstanceOfClass() + { + $this->resolver->setDefault('foo', new \stdClass()); + $this->resolver->setAllowedTypes('foo', '\stdClass'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // addAllowedTypes() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testAddAllowedTypesFailsIfUnknownOption() + { + $this->resolver->addAllowedTypes('foo', 'string'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfAddAllowedTypesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->addAllowedTypes('bar', 'string'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedType() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedTypes('foo', 'string'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedType() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedTypes('foo', 'string'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedTypeMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedTypes('foo', array('string', 'bool')); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedTypeMultiple() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedTypes('foo', array('string', 'bool')); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + public function testAddAllowedTypesDoesNotOverwrite() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'string'); + $this->resolver->addAllowedTypes('foo', 'bool'); + + $this->resolver->setDefault('foo', 'bar'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + public function testAddAllowedTypesDoesNotOverwrite2() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'string'); + $this->resolver->addAllowedTypes('foo', 'bool'); + + $this->resolver->setDefault('foo', false); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // setAllowedValues() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testSetAllowedValuesFailsIfUnknownOption() + { + $this->resolver->setAllowedValues('foo', 'bar'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetAllowedValuesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setAllowedValues('bar', 'baz'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value 42 is invalid. Accepted values are: "bar". + */ + public function testResolveFailsIfInvalidValue() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedValues('foo', 'bar'); + + $this->resolver->resolve(array('foo' => 42)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value null is invalid. Accepted values are: "bar". + */ + public function testResolveFailsIfInvalidValueIsNull() + { + $this->resolver->setDefault('foo', null); + $this->resolver->setAllowedValues('foo', 'bar'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidValueStrict() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', '42'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidValue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', 'bar'); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testResolveSucceedsIfValidValueIsNull() + { + $this->resolver->setDefault('foo', null); + $this->resolver->setAllowedValues('foo', null); + + $this->assertEquals(array('foo' => null), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value 42 is invalid. Accepted values are: "bar", false, null. + */ + public function testResolveFailsIfInvalidValueMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', array('bar', false, null)); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidValueMultiple() + { + $this->resolver->setDefault('foo', 'baz'); + $this->resolver->setAllowedValues('foo', array('bar', 'baz')); + + $this->assertEquals(array('foo' => 'baz'), $this->resolver->resolve()); + } + + public function testResolveFailsIfClosureReturnsFalse() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', function ($value) use (&$passedValue) { + $passedValue = $value; + + return false; + }); + + try { + $this->resolver->resolve(); + $this->fail('Should fail'); + } catch (InvalidOptionsException $e) { + } + + $this->assertSame(42, $passedValue); + } + + public function testResolveSucceedsIfClosureReturnsTrue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', function ($value) use (&$passedValue) { + $passedValue = $value; + + return true; + }); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + $this->assertSame('bar', $passedValue); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfAllClosuresReturnFalse() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', array( + function () { return false; }, + function () { return false; }, + function () { return false; }, + )); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfAnyClosureReturnsTrue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', array( + function () { return false; }, + function () { return true; }, + function () { return false; }, + )); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // addAllowedValues() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testAddAllowedValuesFailsIfUnknownOption() + { + $this->resolver->addAllowedValues('foo', 'bar'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfAddAllowedValuesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->addAllowedValues('bar', 'baz'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedValue() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedValues('foo', 'bar'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedValue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedValues('foo', 'bar'); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testResolveSucceedsIfValidAddedValueIsNull() + { + $this->resolver->setDefault('foo', null); + $this->resolver->addAllowedValues('foo', null); + + $this->assertEquals(array('foo' => null), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedValueMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedValues('foo', array('bar', 'baz')); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedValueMultiple() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedValues('foo', array('bar', 'baz')); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testAddAllowedValuesDoesNotOverwrite() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', 'bar'); + $this->resolver->addAllowedValues('foo', 'baz'); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testAddAllowedValuesDoesNotOverwrite2() + { + $this->resolver->setDefault('foo', 'baz'); + $this->resolver->setAllowedValues('foo', 'bar'); + $this->resolver->addAllowedValues('foo', 'baz'); + + $this->assertEquals(array('foo' => 'baz'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfAllAddedClosuresReturnFalse() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', function () { return false; }); + $this->resolver->addAllowedValues('foo', function () { return false; }); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfAnyAddedClosureReturnsTrue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', function () { return false; }); + $this->resolver->addAllowedValues('foo', function () { return true; }); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testResolveSucceedsIfAnyAddedClosureReturnsTrue2() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', function () { return true; }); + $this->resolver->addAllowedValues('foo', function () { return false; }); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // setNormalizer() + //////////////////////////////////////////////////////////////////////////// + + public function testSetNormalizerReturnsThis() + { + $this->resolver->setDefault('foo', 'bar'); + $this->assertSame($this->resolver, $this->resolver->setNormalizer('foo', function () {})); + } + + public function testSetNormalizerClosure() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function () { + return 'normalized'; + }); + + $this->assertEquals(array('foo' => 'normalized'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testSetNormalizerFailsIfUnknownOption() + { + $this->resolver->setNormalizer('foo', function () {}); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetNormalizerFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setNormalizer('foo', function () {}); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + public function testNormalizerReceivesSetOption() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized['.$value.']'; + }); + + $this->assertEquals(array('foo' => 'normalized[bar]'), $this->resolver->resolve()); + } + + public function testNormalizerReceivesPassedOption() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized['.$value.']'; + }); + + $resolved = $this->resolver->resolve(array('foo' => 'baz')); + + $this->assertEquals(array('foo' => 'normalized[baz]'), $resolved); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testValidateTypeBeforeNormalization() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setAllowedTypes('foo', 'int'); + + $this->resolver->setNormalizer('foo', function () { + Assert::fail('Should not be called.'); + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testValidateValueBeforeNormalization() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setAllowedValues('foo', 'baz'); + + $this->resolver->setNormalizer('foo', function () { + Assert::fail('Should not be called.'); + }); + + $this->resolver->resolve(); + } + + public function testNormalizerCanAccessOtherOptions() + { + $this->resolver->setDefault('default', 'bar'); + $this->resolver->setDefault('norm', 'baz'); + + $this->resolver->setNormalizer('norm', function (Options $options) { + /* @var TestCase $test */ + Assert::assertSame('bar', $options['default']); + + return 'normalized'; + }); + + $this->assertEquals(array( + 'default' => 'bar', + 'norm' => 'normalized', + ), $this->resolver->resolve()); + } + + public function testNormalizerCanAccessLazyOptions() + { + $this->resolver->setDefault('lazy', function (Options $options) { + return 'bar'; + }); + $this->resolver->setDefault('norm', 'baz'); + + $this->resolver->setNormalizer('norm', function (Options $options) { + /* @var TestCase $test */ + Assert::assertEquals('bar', $options['lazy']); + + return 'normalized'; + }); + + $this->assertEquals(array( + 'lazy' => 'bar', + 'norm' => 'normalized', + ), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailIfCyclicDependencyBetweenNormalizers() + { + $this->resolver->setDefault('norm1', 'bar'); + $this->resolver->setDefault('norm2', 'baz'); + + $this->resolver->setNormalizer('norm1', function (Options $options) { + $options['norm2']; + }); + + $this->resolver->setNormalizer('norm2', function (Options $options) { + $options['norm1']; + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailIfCyclicDependencyBetweenNormalizerAndLazyOption() + { + $this->resolver->setDefault('lazy', function (Options $options) { + $options['norm']; + }); + + $this->resolver->setDefault('norm', 'baz'); + + $this->resolver->setNormalizer('norm', function (Options $options) { + $options['lazy']; + }); + + $this->resolver->resolve(); + } + + public function testCaughtExceptionFromNormalizerDoesNotCrashOptionResolver() + { + $throw = true; + + $this->resolver->setDefaults(array('catcher' => null, 'thrower' => null)); + + $this->resolver->setNormalizer('catcher', function (Options $options) { + try { + return $options['thrower']; + } catch (\Exception $e) { + return false; + } + }); + + $this->resolver->setNormalizer('thrower', function () use (&$throw) { + if ($throw) { + $throw = false; + throw new \UnexpectedValueException('throwing'); + } + + return true; + }); + + $this->assertSame(array('catcher' => false, 'thrower' => true), $this->resolver->resolve()); + } + + public function testCaughtExceptionFromLazyDoesNotCrashOptionResolver() + { + $throw = true; + + $this->resolver->setDefault('catcher', function (Options $options) { + try { + return $options['thrower']; + } catch (\Exception $e) { + return false; + } + }); + + $this->resolver->setDefault('thrower', function (Options $options) use (&$throw) { + if ($throw) { + $throw = false; + throw new \UnexpectedValueException('throwing'); + } + + return true; + }); + + $this->assertSame(array('catcher' => false, 'thrower' => true), $this->resolver->resolve()); + } + + public function testInvokeEachNormalizerOnlyOnce() + { + $calls = 0; + + $this->resolver->setDefault('norm1', 'bar'); + $this->resolver->setDefault('norm2', 'baz'); + + $this->resolver->setNormalizer('norm1', function ($options) use (&$calls) { + Assert::assertSame(1, ++$calls); + + $options['norm2']; + }); + $this->resolver->setNormalizer('norm2', function () use (&$calls) { + Assert::assertSame(2, ++$calls); + }); + + $this->resolver->resolve(); + + $this->assertSame(2, $calls); + } + + public function testNormalizerNotCalledForUnsetOptions() + { + $this->resolver->setDefined('norm'); + + $this->resolver->setNormalizer('norm', function () { + Assert::fail('Should not be called.'); + }); + + $this->assertEmpty($this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // setDefaults() + //////////////////////////////////////////////////////////////////////////// + + public function testSetDefaultsReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setDefaults(array('foo', 'bar'))); + } + + public function testSetDefaults() + { + $this->resolver->setDefault('one', '1'); + $this->resolver->setDefault('two', 'bar'); + + $this->resolver->setDefaults(array( + 'two' => '2', + 'three' => '3', + )); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '2', + 'three' => '3', + ), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetDefaultsFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setDefaults(array('two' => '2')); + }); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // remove() + //////////////////////////////////////////////////////////////////////////// + + public function testRemoveReturnsThis() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame($this->resolver, $this->resolver->remove('foo')); + } + + public function testRemoveSingleOption() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setDefault('baz', 'boo'); + $this->resolver->remove('foo'); + + $this->assertSame(array('baz' => 'boo'), $this->resolver->resolve()); + } + + public function testRemoveMultipleOptions() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setDefault('baz', 'boo'); + $this->resolver->setDefault('doo', 'dam'); + + $this->resolver->remove(array('foo', 'doo')); + + $this->assertSame(array('baz' => 'boo'), $this->resolver->resolve()); + } + + public function testRemoveLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + $this->resolver->remove('foo'); + + $this->assertSame(array(), $this->resolver->resolve()); + } + + public function testRemoveNormalizer() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized'; + }); + $this->resolver->remove('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testRemoveAllowedTypes() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'int'); + $this->resolver->remove('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testRemoveAllowedValues() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', array('baz', 'boo')); + $this->resolver->remove('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfRemoveFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->remove('bar'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + public function testRemoveUnknownOptionIgnored() + { + $this->assertNotNull($this->resolver->remove('foo')); + } + + //////////////////////////////////////////////////////////////////////////// + // clear() + //////////////////////////////////////////////////////////////////////////// + + public function testClearReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->clear()); + } + + public function testClearRemovesAllOptions() + { + $this->resolver->setDefault('one', 1); + $this->resolver->setDefault('two', 2); + + $this->resolver->clear(); + + $this->assertEmpty($this->resolver->resolve()); + } + + public function testClearLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + $this->resolver->clear(); + + $this->assertSame(array(), $this->resolver->resolve()); + } + + public function testClearNormalizer() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized'; + }); + $this->resolver->clear(); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testClearAllowedTypes() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'int'); + $this->resolver->clear(); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testClearAllowedValues() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', 'baz'); + $this->resolver->clear(); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfClearFromLazyption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->clear(); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + public function testClearOptionAndNormalizer() + { + $this->resolver->setDefault('foo1', 'bar'); + $this->resolver->setNormalizer('foo1', function (Options $options) { + return ''; + }); + $this->resolver->setDefault('foo2', 'bar'); + $this->resolver->setNormalizer('foo2', function (Options $options) { + return ''; + }); + + $this->resolver->clear(); + $this->assertEmpty($this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // ArrayAccess + //////////////////////////////////////////////////////////////////////////// + + public function testArrayAccess() + { + $this->resolver->setDefault('default1', 0); + $this->resolver->setDefault('default2', 1); + $this->resolver->setRequired('required'); + $this->resolver->setDefined('defined'); + $this->resolver->setDefault('lazy1', function (Options $options) { + return 'lazy'; + }); + + $this->resolver->setDefault('lazy2', function (Options $options) { + Assert::assertTrue(isset($options['default1'])); + Assert::assertTrue(isset($options['default2'])); + Assert::assertTrue(isset($options['required'])); + Assert::assertTrue(isset($options['lazy1'])); + Assert::assertTrue(isset($options['lazy2'])); + Assert::assertFalse(isset($options['defined'])); + + Assert::assertSame(0, $options['default1']); + Assert::assertSame(42, $options['default2']); + Assert::assertSame('value', $options['required']); + Assert::assertSame('lazy', $options['lazy1']); + + // Obviously $options['lazy'] and $options['defined'] cannot be + // accessed + }); + + $this->resolver->resolve(array('default2' => 42, 'required' => 'value')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessGetFailsOutsideResolve() + { + $this->resolver->setDefault('default', 0); + + $this->resolver['default']; + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessExistsFailsOutsideResolve() + { + $this->resolver->setDefault('default', 0); + + isset($this->resolver['default']); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessSetNotSupported() + { + $this->resolver['default'] = 0; + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessUnsetNotSupported() + { + $this->resolver->setDefault('default', 0); + + unset($this->resolver['default']); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException + * @expectedExceptionMessage The option "undefined" does not exist. Defined options are: "foo", "lazy". + */ + public function testFailIfGetNonExisting() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setDefault('lazy', function (Options $options) { + $options['undefined']; + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException + * @expectedExceptionMessage The optional option "defined" has no value set. You should make sure it is set with "isset" before reading it. + */ + public function testFailIfGetDefinedButUnset() + { + $this->resolver->setDefined('defined'); + + $this->resolver->setDefault('lazy', function (Options $options) { + $options['defined']; + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailIfCyclicDependency() + { + $this->resolver->setDefault('lazy1', function (Options $options) { + $options['lazy2']; + }); + + $this->resolver->setDefault('lazy2', function (Options $options) { + $options['lazy1']; + }); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // Countable + //////////////////////////////////////////////////////////////////////////// + + public function testCount() + { + $this->resolver->setDefault('default', 0); + $this->resolver->setRequired('required'); + $this->resolver->setDefined('defined'); + $this->resolver->setDefault('lazy1', function () {}); + + $this->resolver->setDefault('lazy2', function (Options $options) { + Assert::assertCount(4, $options); + }); + + $this->assertCount(4, $this->resolver->resolve(array('required' => 'value'))); + } + + /** + * In resolve() we count the options that are actually set (which may be + * only a subset of the defined options). Outside of resolve(), it's not + * clear what is counted. + * + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testCountFailsOutsideResolve() + { + $this->resolver->setDefault('foo', 0); + $this->resolver->setRequired('bar'); + $this->resolver->setDefined('bar'); + $this->resolver->setDefault('lazy1', function () {}); + + count($this->resolver); + } +} diff --git a/vendor/symfony/options-resolver/composer.json b/vendor/symfony/options-resolver/composer.json new file mode 100644 index 0000000..a751730 --- /dev/null +++ b/vendor/symfony/options-resolver/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/options-resolver", + "type": "library", + "description": "Symfony OptionsResolver Component", + "keywords": ["options", "config", "configuration"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.5.9" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\OptionsResolver\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + } +} diff --git a/vendor/symfony/options-resolver/phpunit.xml.dist b/vendor/symfony/options-resolver/phpunit.xml.dist new file mode 100644 index 0000000..7e04e60 --- /dev/null +++ b/vendor/symfony/options-resolver/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/vendor/symfony/polyfill-php70/LICENSE b/vendor/symfony/polyfill-php70/LICENSE new file mode 100644 index 0000000..39fa189 --- /dev/null +++ b/vendor/symfony/polyfill-php70/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014-2016 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/polyfill-php70/Php70.php b/vendor/symfony/polyfill-php70/Php70.php new file mode 100644 index 0000000..e7ff0b2 --- /dev/null +++ b/vendor/symfony/polyfill-php70/Php70.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php70; + +/** + * @author Nicolas Grekas + * + * @internal + */ +final class Php70 +{ + public static function intdiv($dividend, $divisor) + { + $dividend = self::intArg($dividend, __FUNCTION__, 1); + $divisor = self::intArg($divisor, __FUNCTION__, 2); + + if (0 === $divisor) { + throw new \DivisionByZeroError('Division by zero'); + } + if (-1 === $divisor && ~PHP_INT_MAX === $dividend) { + throw new \ArithmeticError('Division of PHP_INT_MIN by -1 is not an integer'); + } + + return ($dividend - ($dividend % $divisor)) / $divisor; + } + + public static function preg_replace_callback_array(array $patterns, $subject, $limit = -1, &$count = 0) + { + $count = 0; + $result = ''.$subject; + if (0 === $limit = self::intArg($limit, __FUNCTION__, 3)) { + return $result; + } + + foreach ($patterns as $pattern => $callback) { + $result = preg_replace_callback($pattern, $callback, $result, $limit, $c); + $count += $c; + } + + return $result; + } + + public static function error_clear_last() + { + static $handler; + if (!$handler) { + $handler = function() { return false; }; + } + set_error_handler($handler); + @trigger_error(''); + restore_error_handler(); + } + + public static function intArg($value, $caller, $pos) + { + if (is_int($value)) { + return $value; + } + if (!is_numeric($value) || PHP_INT_MAX <= ($value += 0) || ~PHP_INT_MAX >= $value) { + throw new \TypeError(sprintf('%s() expects parameter %d to be integer, %s given', $caller, $pos, gettype($value))); + } + + return (int) $value; + } +} diff --git a/vendor/symfony/polyfill-php70/README.md b/vendor/symfony/polyfill-php70/README.md new file mode 100644 index 0000000..d3a3a53 --- /dev/null +++ b/vendor/symfony/polyfill-php70/README.md @@ -0,0 +1,26 @@ +Symfony Polyfill / Php70 +======================== + +This component provides functions unavailable in releases prior to PHP 7.0: + +- [`intdiv`](http://php.net/intdiv) +- [`preg_replace_callback_array`](http://php.net/preg_replace_callback_array) +- [`error_clear_last`](http://php.net/error_clear_last) +- `random_bytes` and `random_int` (from [paragonie/random_compat](https://github.com/paragonie/random_compat)) +- [`*Error` throwable classes](http://php.net/Error) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md). + +Compatibility notes +=================== + +To write portable code between PHP5 and PHP7, some care must be taken: +- `\*Error` exceptions must be caught before `\Exception`; +- after calling `error_clear_last()`, the result of `$e = error_get_last()` must be + verified using `isset($e['message'][0])` instead of `null === $e`. + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-php70/Resources/stubs/ArithmeticError.php b/vendor/symfony/polyfill-php70/Resources/stubs/ArithmeticError.php new file mode 100644 index 0000000..6819124 --- /dev/null +++ b/vendor/symfony/polyfill-php70/Resources/stubs/ArithmeticError.php @@ -0,0 +1,5 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php70 as p; + +if (PHP_VERSION_ID < 70000) { + if (!defined('PHP_INT_MIN')) { + define('PHP_INT_MIN', ~PHP_INT_MAX); + } + if (!function_exists('intdiv')) { + function intdiv($dividend, $divisor) { return p\Php70::intdiv($dividend, $divisor); } + } + if (!function_exists('preg_replace_callback_array')) { + function preg_replace_callback_array(array $patterns, $subject, $limit = -1, &$count = 0) { return p\Php70::preg_replace_callback_array($patterns, $subject, $limit, $count); } + } + if (!function_exists('error_clear_last')) { + function error_clear_last() { return p\Php70::error_clear_last(); } + } +} diff --git a/vendor/symfony/polyfill-php70/composer.json b/vendor/symfony/polyfill-php70/composer.json new file mode 100644 index 0000000..3ce13ad --- /dev/null +++ b/vendor/symfony/polyfill-php70/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/polyfill-php70", + "type": "library", + "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.3", + "paragonie/random_compat": "~1.0|~2.0" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php70\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + } +} diff --git a/vendor/symfony/property-access/.gitignore b/vendor/symfony/property-access/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/vendor/symfony/property-access/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/vendor/symfony/property-access/CHANGELOG.md b/vendor/symfony/property-access/CHANGELOG.md new file mode 100644 index 0000000..416287e --- /dev/null +++ b/vendor/symfony/property-access/CHANGELOG.md @@ -0,0 +1,35 @@ +CHANGELOG +========= + +3.1.0 +----- + + * deprecated the `StringUtil` class, use `Symfony\Component\Inflector\Inflector` + instead + +2.7.0 +------ + + * `UnexpectedTypeException` now expects three constructor arguments: The invalid property value, + the `PropertyPathInterface` object and the current index of the property path. + +2.5.0 +------ + + * allowed non alpha numeric characters in second level and deeper object properties names + * [BC BREAK] when accessing an index on an object that does not implement + ArrayAccess, a NoSuchIndexException is now thrown instead of the + semantically wrong NoSuchPropertyException + * [BC BREAK] added isReadable() and isWritable() to PropertyAccessorInterface + +2.3.0 +------ + + * added PropertyAccessorBuilder, to enable or disable the support of "__call" + * added support for "__call" in the PropertyAccessor (disabled by default) + * [BC BREAK] changed PropertyAccessor to continue its search for a property or + method even if a non-public match was found. Before, a PropertyAccessDeniedException + was thrown in this case. Class PropertyAccessDeniedException was removed + now. + * deprecated PropertyAccess::getPropertyAccessor + * added PropertyAccess::createPropertyAccessor and PropertyAccess::createPropertyAccessorBuilder diff --git a/vendor/symfony/property-access/Exception/AccessException.php b/vendor/symfony/property-access/Exception/AccessException.php new file mode 100644 index 0000000..b3a8546 --- /dev/null +++ b/vendor/symfony/property-access/Exception/AccessException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when a property path is not available. + * + * @author Stéphane Escandell + */ +class AccessException extends RuntimeException +{ +} diff --git a/vendor/symfony/property-access/Exception/ExceptionInterface.php b/vendor/symfony/property-access/Exception/ExceptionInterface.php new file mode 100644 index 0000000..d1fcdac --- /dev/null +++ b/vendor/symfony/property-access/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Marker interface for the PropertyAccess component. + * + * @author Bernhard Schussek + */ +interface ExceptionInterface +{ +} diff --git a/vendor/symfony/property-access/Exception/InvalidArgumentException.php b/vendor/symfony/property-access/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..47bc7e1 --- /dev/null +++ b/vendor/symfony/property-access/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Base InvalidArgumentException for the PropertyAccess component. + * + * @author Bernhard Schussek + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/property-access/Exception/InvalidPropertyPathException.php b/vendor/symfony/property-access/Exception/InvalidPropertyPathException.php new file mode 100644 index 0000000..69de31c --- /dev/null +++ b/vendor/symfony/property-access/Exception/InvalidPropertyPathException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when a property path is malformed. + * + * @author Bernhard Schussek + */ +class InvalidPropertyPathException extends RuntimeException +{ +} diff --git a/vendor/symfony/property-access/Exception/NoSuchIndexException.php b/vendor/symfony/property-access/Exception/NoSuchIndexException.php new file mode 100644 index 0000000..597b990 --- /dev/null +++ b/vendor/symfony/property-access/Exception/NoSuchIndexException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when an index cannot be found. + * + * @author Stéphane Escandell + */ +class NoSuchIndexException extends AccessException +{ +} diff --git a/vendor/symfony/property-access/Exception/NoSuchPropertyException.php b/vendor/symfony/property-access/Exception/NoSuchPropertyException.php new file mode 100644 index 0000000..1c7eda5 --- /dev/null +++ b/vendor/symfony/property-access/Exception/NoSuchPropertyException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when a property cannot be found. + * + * @author Bernhard Schussek + */ +class NoSuchPropertyException extends AccessException +{ +} diff --git a/vendor/symfony/property-access/Exception/OutOfBoundsException.php b/vendor/symfony/property-access/Exception/OutOfBoundsException.php new file mode 100644 index 0000000..a3c4559 --- /dev/null +++ b/vendor/symfony/property-access/Exception/OutOfBoundsException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Base OutOfBoundsException for the PropertyAccess component. + * + * @author Bernhard Schussek + */ +class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/property-access/Exception/RuntimeException.php b/vendor/symfony/property-access/Exception/RuntimeException.php new file mode 100644 index 0000000..9fe843e --- /dev/null +++ b/vendor/symfony/property-access/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Base RuntimeException for the PropertyAccess component. + * + * @author Bernhard Schussek + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/property-access/Exception/UnexpectedTypeException.php b/vendor/symfony/property-access/Exception/UnexpectedTypeException.php new file mode 100644 index 0000000..d238d32 --- /dev/null +++ b/vendor/symfony/property-access/Exception/UnexpectedTypeException.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * Thrown when a value does not match an expected type. + * + * @author Bernhard Schussek + */ +class UnexpectedTypeException extends RuntimeException +{ + /** + * @param mixed $value The unexpected value found while traversing property path + * @param PropertyPathInterface $path The property path + * @param int $pathIndex The property path index when the unexpected value was found + */ + public function __construct($value, PropertyPathInterface $path, $pathIndex) + { + $message = sprintf( + 'PropertyAccessor requires a graph of objects or arrays to operate on, '. + 'but it found type "%s" while trying to traverse path "%s" at property "%s".', + gettype($value), + (string) $path, + $path->getElement($pathIndex) + ); + + parent::__construct($message); + } +} diff --git a/vendor/symfony/property-access/LICENSE b/vendor/symfony/property-access/LICENSE new file mode 100644 index 0000000..17d16a1 --- /dev/null +++ b/vendor/symfony/property-access/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2017 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/property-access/PropertyAccess.php b/vendor/symfony/property-access/PropertyAccess.php new file mode 100644 index 0000000..3c8dc1c --- /dev/null +++ b/vendor/symfony/property-access/PropertyAccess.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * Entry point of the PropertyAccess component. + * + * @author Bernhard Schussek + */ +final class PropertyAccess +{ + /** + * Creates a property accessor with the default configuration. + * + * @return PropertyAccessor + */ + public static function createPropertyAccessor() + { + return self::createPropertyAccessorBuilder()->getPropertyAccessor(); + } + + /** + * Creates a property accessor builder. + * + * @return PropertyAccessorBuilder + */ + public static function createPropertyAccessorBuilder() + { + return new PropertyAccessorBuilder(); + } + + /** + * This class cannot be instantiated. + */ + private function __construct() + { + } +} diff --git a/vendor/symfony/property-access/PropertyAccessor.php b/vendor/symfony/property-access/PropertyAccessor.php new file mode 100644 index 0000000..d0cadcf --- /dev/null +++ b/vendor/symfony/property-access/PropertyAccessor.php @@ -0,0 +1,937 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +use Psr\Cache\CacheItemPoolInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Adapter\ApcuAdapter; +use Symfony\Component\Cache\Adapter\NullAdapter; +use Symfony\Component\Inflector\Inflector; +use Symfony\Component\PropertyAccess\Exception\AccessException; +use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException; +use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; +use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; + +/** + * Default implementation of {@link PropertyAccessorInterface}. + * + * @author Bernhard Schussek + * @author Kévin Dunglas + * @author Nicolas Grekas + */ +class PropertyAccessor implements PropertyAccessorInterface +{ + /** + * @internal + */ + const VALUE = 0; + + /** + * @internal + */ + const REF = 1; + + /** + * @internal + */ + const IS_REF_CHAINED = 2; + + /** + * @internal + */ + const ACCESS_HAS_PROPERTY = 0; + + /** + * @internal + */ + const ACCESS_TYPE = 1; + + /** + * @internal + */ + const ACCESS_NAME = 2; + + /** + * @internal + */ + const ACCESS_REF = 3; + + /** + * @internal + */ + const ACCESS_ADDER = 4; + + /** + * @internal + */ + const ACCESS_REMOVER = 5; + + /** + * @internal + */ + const ACCESS_TYPE_METHOD = 0; + + /** + * @internal + */ + const ACCESS_TYPE_PROPERTY = 1; + + /** + * @internal + */ + const ACCESS_TYPE_MAGIC = 2; + + /** + * @internal + */ + const ACCESS_TYPE_ADDER_AND_REMOVER = 3; + + /** + * @internal + */ + const ACCESS_TYPE_NOT_FOUND = 4; + + /** + * @internal + */ + const CACHE_PREFIX_READ = 'r'; + + /** + * @internal + */ + const CACHE_PREFIX_WRITE = 'w'; + + /** + * @internal + */ + const CACHE_PREFIX_PROPERTY_PATH = 'p'; + + /** + * @var bool + */ + private $magicCall; + + /** + * @var bool + */ + private $ignoreInvalidIndices; + + /** + * @var CacheItemPoolInterface + */ + private $cacheItemPool; + + /** + * @var array + */ + private $readPropertyCache = array(); + + /** + * @var array + */ + private $writePropertyCache = array(); + private static $previousErrorHandler = false; + private static $errorHandler = array(__CLASS__, 'handleError'); + private static $resultProto = array(self::VALUE => null); + + /** + * @var array + */ + private $propertyPathCache = array(); + + /** + * Should not be used by application code. Use + * {@link PropertyAccess::createPropertyAccessor()} instead. + * + * @param bool $magicCall + * @param bool $throwExceptionOnInvalidIndex + * @param CacheItemPoolInterface $cacheItemPool + */ + public function __construct($magicCall = false, $throwExceptionOnInvalidIndex = false, CacheItemPoolInterface $cacheItemPool = null) + { + $this->magicCall = $magicCall; + $this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex; + $this->cacheItemPool = $cacheItemPool instanceof NullAdapter ? null : $cacheItemPool; // Replace the NullAdapter by the null value + } + + /** + * {@inheritdoc} + */ + public function getValue($objectOrArray, $propertyPath) + { + $propertyPath = $this->getPropertyPath($propertyPath); + + $zval = array( + self::VALUE => $objectOrArray, + ); + $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices); + + return $propertyValues[count($propertyValues) - 1][self::VALUE]; + } + + /** + * {@inheritdoc} + */ + public function setValue(&$objectOrArray, $propertyPath, $value) + { + $propertyPath = $this->getPropertyPath($propertyPath); + + $zval = array( + self::VALUE => $objectOrArray, + self::REF => &$objectOrArray, + ); + $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1); + $overwrite = true; + + try { + if (\PHP_VERSION_ID < 70000 && false === self::$previousErrorHandler) { + self::$previousErrorHandler = set_error_handler(self::$errorHandler); + } + + for ($i = count($propertyValues) - 1; 0 <= $i; --$i) { + $zval = $propertyValues[$i]; + unset($propertyValues[$i]); + + // You only need set value for current element if: + // 1. it's the parent of the last index element + // OR + // 2. its child is not passed by reference + // + // This may avoid uncessary value setting process for array elements. + // For example: + // '[a][b][c]' => 'old-value' + // If you want to change its value to 'new-value', + // you only need set value for '[a][b][c]' and it's safe to ignore '[a][b]' and '[a]' + // + if ($overwrite) { + $property = $propertyPath->getElement($i); + + if ($propertyPath->isIndex($i)) { + if ($overwrite = !isset($zval[self::REF])) { + $ref = &$zval[self::REF]; + $ref = $zval[self::VALUE]; + } + $this->writeIndex($zval, $property, $value); + if ($overwrite) { + $zval[self::VALUE] = $zval[self::REF]; + } + } else { + $this->writeProperty($zval, $property, $value); + } + + // if current element is an object + // OR + // if current element's reference chain is not broken - current element + // as well as all its ancients in the property path are all passed by reference, + // then there is no need to continue the value setting process + if (is_object($zval[self::VALUE]) || isset($zval[self::IS_REF_CHAINED])) { + break; + } + } + + $value = $zval[self::VALUE]; + } + } catch (\TypeError $e) { + self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0); + + // It wasn't thrown in this class so rethrow it + throw $e; + } finally { + if (\PHP_VERSION_ID < 70000 && false !== self::$previousErrorHandler) { + restore_error_handler(); + self::$previousErrorHandler = false; + } + } + } + + /** + * @internal + */ + public static function handleError($type, $message, $file, $line, $context) + { + if (E_RECOVERABLE_ERROR === $type) { + self::throwInvalidArgumentException($message, debug_backtrace(false), 1); + } + + return null !== self::$previousErrorHandler && false !== call_user_func(self::$previousErrorHandler, $type, $message, $file, $line, $context); + } + + private static function throwInvalidArgumentException($message, $trace, $i) + { + if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file'] && isset($trace[$i]['args'][0])) { + $pos = strpos($message, $delim = 'must be of the type ') ?: (strpos($message, $delim = 'must be an instance of ') ?: strpos($message, $delim = 'must implement interface ')); + $pos += strlen($delim); + $type = $trace[$i]['args'][0]; + $type = is_object($type) ? get_class($type) : gettype($type); + + throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given', substr($message, $pos, strpos($message, ',', $pos) - $pos), $type)); + } + } + + /** + * {@inheritdoc} + */ + public function isReadable($objectOrArray, $propertyPath) + { + if (!$propertyPath instanceof PropertyPathInterface) { + $propertyPath = new PropertyPath($propertyPath); + } + + try { + $zval = array( + self::VALUE => $objectOrArray, + ); + $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices); + + return true; + } catch (AccessException $e) { + return false; + } catch (UnexpectedTypeException $e) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function isWritable($objectOrArray, $propertyPath) + { + $propertyPath = $this->getPropertyPath($propertyPath); + + try { + $zval = array( + self::VALUE => $objectOrArray, + ); + $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1); + + for ($i = count($propertyValues) - 1; 0 <= $i; --$i) { + $zval = $propertyValues[$i]; + unset($propertyValues[$i]); + + if ($propertyPath->isIndex($i)) { + if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) { + return false; + } + } else { + if (!$this->isPropertyWritable($zval[self::VALUE], $propertyPath->getElement($i))) { + return false; + } + } + + if (is_object($zval[self::VALUE])) { + return true; + } + } + + return true; + } catch (AccessException $e) { + return false; + } catch (UnexpectedTypeException $e) { + return false; + } + } + + /** + * Reads the path from an object up to a given path index. + * + * @param array $zval The array containing the object or array to read from + * @param PropertyPathInterface $propertyPath The property path to read + * @param int $lastIndex The index up to which should be read + * @param bool $ignoreInvalidIndices Whether to ignore invalid indices or throw an exception + * + * @return array The values read in the path + * + * @throws UnexpectedTypeException If a value within the path is neither object nor array. + * @throws NoSuchIndexException If a non-existing index is accessed + */ + private function readPropertiesUntil($zval, PropertyPathInterface $propertyPath, $lastIndex, $ignoreInvalidIndices = true) + { + if (!is_object($zval[self::VALUE]) && !is_array($zval[self::VALUE])) { + throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, 0); + } + + // Add the root object to the list + $propertyValues = array($zval); + + for ($i = 0; $i < $lastIndex; ++$i) { + $property = $propertyPath->getElement($i); + $isIndex = $propertyPath->isIndex($i); + + if ($isIndex) { + // Create missing nested arrays on demand + if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) || + (is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !array_key_exists($property, $zval[self::VALUE])) + ) { + if (!$ignoreInvalidIndices) { + if (!is_array($zval[self::VALUE])) { + if (!$zval[self::VALUE] instanceof \Traversable) { + throw new NoSuchIndexException(sprintf( + 'Cannot read index "%s" while trying to traverse path "%s".', + $property, + (string) $propertyPath + )); + } + + $zval[self::VALUE] = iterator_to_array($zval[self::VALUE]); + } + + throw new NoSuchIndexException(sprintf( + 'Cannot read index "%s" while trying to traverse path "%s". Available indices are "%s".', + $property, + (string) $propertyPath, + print_r(array_keys($zval[self::VALUE]), true) + )); + } + + if ($i + 1 < $propertyPath->getLength()) { + if (isset($zval[self::REF])) { + $zval[self::VALUE][$property] = array(); + $zval[self::REF] = $zval[self::VALUE]; + } else { + $zval[self::VALUE] = array($property => array()); + } + } + } + + $zval = $this->readIndex($zval, $property); + } else { + $zval = $this->readProperty($zval, $property); + } + + // the final value of the path must not be validated + if ($i + 1 < $propertyPath->getLength() && !is_object($zval[self::VALUE]) && !is_array($zval[self::VALUE])) { + throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, $i + 1); + } + + if (isset($zval[self::REF]) && (0 === $i || isset($propertyValues[$i - 1][self::IS_REF_CHAINED]))) { + // Set the IS_REF_CHAINED flag to true if: + // current property is passed by reference and + // it is the first element in the property path or + // the IS_REF_CHAINED flag of its parent element is true + // Basically, this flag is true only when the reference chain from the top element to current element is not broken + $zval[self::IS_REF_CHAINED] = true; + } + + $propertyValues[] = $zval; + } + + return $propertyValues; + } + + /** + * Reads a key from an array-like structure. + * + * @param array $zval The array containing the array or \ArrayAccess object to read from + * @param string|int $index The key to read + * + * @return array The array containing the value of the key + * + * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array + */ + private function readIndex($zval, $index) + { + if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) { + throw new NoSuchIndexException(sprintf('Cannot read index "%s" from object of type "%s" because it doesn\'t implement \ArrayAccess.', $index, get_class($zval[self::VALUE]))); + } + + $result = self::$resultProto; + + if (isset($zval[self::VALUE][$index])) { + $result[self::VALUE] = $zval[self::VALUE][$index]; + + if (!isset($zval[self::REF])) { + // Save creating references when doing read-only lookups + } elseif (is_array($zval[self::VALUE])) { + $result[self::REF] = &$zval[self::REF][$index]; + } elseif (is_object($result[self::VALUE])) { + $result[self::REF] = $result[self::VALUE]; + } + } + + return $result; + } + + /** + * Reads the a property from an object. + * + * @param array $zval The array containing the object to read from + * @param string $property The property to read + * + * @return array The array containing the value of the property + * + * @throws NoSuchPropertyException If the property does not exist or is not public. + */ + private function readProperty($zval, $property) + { + if (!is_object($zval[self::VALUE])) { + throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%s]" instead.', $property, $property)); + } + + $result = self::$resultProto; + $object = $zval[self::VALUE]; + $access = $this->getReadAccessInfo(get_class($object), $property); + + if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) { + $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); + } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) { + $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}; + + if ($access[self::ACCESS_REF] && isset($zval[self::REF])) { + $result[self::REF] = &$object->{$access[self::ACCESS_NAME]}; + } + } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) { + // Needed to support \stdClass instances. We need to explicitly + // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if + // a *protected* property was found on the class, property_exists() + // returns true, consequently the following line will result in a + // fatal error. + + $result[self::VALUE] = $object->$property; + if (isset($zval[self::REF])) { + $result[self::REF] = &$object->$property; + } + } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { + // we call the getter and hope the __call do the job + $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); + } else { + throw new NoSuchPropertyException($access[self::ACCESS_NAME]); + } + + // Objects are always passed around by reference + if (isset($zval[self::REF]) && is_object($result[self::VALUE])) { + $result[self::REF] = $result[self::VALUE]; + } + + return $result; + } + + /** + * Guesses how to read the property value. + * + * @param string $class + * @param string $property + * + * @return array + */ + private function getReadAccessInfo($class, $property) + { + $key = (false !== strpos($class, '@') ? rawurlencode($class) : $class).'..'.$property; + + if (isset($this->readPropertyCache[$key])) { + return $this->readPropertyCache[$key]; + } + + if ($this->cacheItemPool) { + $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_READ.str_replace('\\', '.', $key)); + if ($item->isHit()) { + return $this->readPropertyCache[$key] = $item->get(); + } + } + + $access = array(); + + $reflClass = new \ReflectionClass($class); + $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); + $camelProp = $this->camelize($property); + $getter = 'get'.$camelProp; + $getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item) + $isser = 'is'.$camelProp; + $hasser = 'has'.$camelProp; + + if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $getter; + } elseif ($reflClass->hasMethod($getsetter) && $reflClass->getMethod($getsetter)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $getsetter; + } elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $isser; + } elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $hasser; + } elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; + $access[self::ACCESS_NAME] = $property; + $access[self::ACCESS_REF] = false; + } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; + $access[self::ACCESS_NAME] = $property; + $access[self::ACCESS_REF] = true; + } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) { + // we call the getter and hope the __call do the job + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC; + $access[self::ACCESS_NAME] = $getter; + } else { + $methods = array($getter, $getsetter, $isser, $hasser, '__get'); + if ($this->magicCall) { + $methods[] = '__call'; + } + + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; + $access[self::ACCESS_NAME] = sprintf( + 'Neither the property "%s" nor one of the methods "%s()" '. + 'exist and have public access in class "%s".', + $property, + implode('()", "', $methods), + $reflClass->name + ); + } + + if (isset($item)) { + $this->cacheItemPool->save($item->set($access)); + } + + return $this->readPropertyCache[$key] = $access; + } + + /** + * Sets the value of an index in a given array-accessible value. + * + * @param array $zval The array containing the array or \ArrayAccess object to write to + * @param string|int $index The index to write at + * @param mixed $value The value to write + * + * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array + */ + private function writeIndex($zval, $index, $value) + { + if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) { + throw new NoSuchIndexException(sprintf('Cannot modify index "%s" in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($zval[self::VALUE]))); + } + + $zval[self::REF][$index] = $value; + } + + /** + * Sets the value of a property in the given object. + * + * @param array $zval The array containing the object to write to + * @param string $property The property to write + * @param mixed $value The value to write + * + * @throws NoSuchPropertyException If the property does not exist or is not public. + */ + private function writeProperty($zval, $property, $value) + { + if (!is_object($zval[self::VALUE])) { + throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); + } + + $object = $zval[self::VALUE]; + $access = $this->getWriteAccessInfo(get_class($object), $property, $value); + + if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) { + $object->{$access[self::ACCESS_NAME]}($value); + } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) { + $object->{$access[self::ACCESS_NAME]} = $value; + } elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) { + $this->writeCollection($zval, $property, $value, $access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]); + } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) { + // Needed to support \stdClass instances. We need to explicitly + // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if + // a *protected* property was found on the class, property_exists() + // returns true, consequently the following line will result in a + // fatal error. + + $object->$property = $value; + } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { + $object->{$access[self::ACCESS_NAME]}($value); + } elseif (self::ACCESS_TYPE_NOT_FOUND === $access[self::ACCESS_TYPE]) { + throw new NoSuchPropertyException(sprintf('Could not determine access type for property "%s".', $property)); + } else { + throw new NoSuchPropertyException($access[self::ACCESS_NAME]); + } + } + + /** + * Adjusts a collection-valued property by calling add*() and remove*() methods. + * + * @param array $zval The array containing the object to write to + * @param string $property The property to write + * @param array|\Traversable $collection The collection to write + * @param string $addMethod The add*() method + * @param string $removeMethod The remove*() method + */ + private function writeCollection($zval, $property, $collection, $addMethod, $removeMethod) + { + // At this point the add and remove methods have been found + $previousValue = $this->readProperty($zval, $property); + $previousValue = $previousValue[self::VALUE]; + + if ($previousValue instanceof \Traversable) { + $previousValue = iterator_to_array($previousValue); + } + if ($previousValue && is_array($previousValue)) { + if (is_object($collection)) { + $collection = iterator_to_array($collection); + } + foreach ($previousValue as $key => $item) { + if (!in_array($item, $collection, true)) { + unset($previousValue[$key]); + $zval[self::VALUE]->{$removeMethod}($item); + } + } + } else { + $previousValue = false; + } + + foreach ($collection as $item) { + if (!$previousValue || !in_array($item, $previousValue, true)) { + $zval[self::VALUE]->{$addMethod}($item); + } + } + } + + /** + * Guesses how to write the property value. + * + * @param string $class + * @param string $property + * @param mixed $value + * + * @return array + */ + private function getWriteAccessInfo($class, $property, $value) + { + $key = (false !== strpos($class, '@') ? rawurlencode($class) : $class).'..'.$property; + + if (isset($this->writePropertyCache[$key])) { + return $this->writePropertyCache[$key]; + } + + if ($this->cacheItemPool) { + $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_WRITE.str_replace('\\', '.', $key)); + if ($item->isHit()) { + return $this->writePropertyCache[$key] = $item->get(); + } + } + + $access = array(); + + $reflClass = new \ReflectionClass($class); + $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); + $camelized = $this->camelize($property); + $singulars = (array) Inflector::singularize($camelized); + + if (is_array($value) || $value instanceof \Traversable) { + $methods = $this->findAdderAndRemover($reflClass, $singulars); + + if (null !== $methods) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; + $access[self::ACCESS_ADDER] = $methods[0]; + $access[self::ACCESS_REMOVER] = $methods[1]; + } + } + + if (!isset($access[self::ACCESS_TYPE])) { + $setter = 'set'.$camelized; + $getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item) + + if ($this->isMethodAccessible($reflClass, $setter, 1)) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $setter; + } elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $getsetter; + } elseif ($this->isMethodAccessible($reflClass, '__set', 2)) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; + $access[self::ACCESS_NAME] = $property; + } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; + $access[self::ACCESS_NAME] = $property; + } elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) { + // we call the getter and hope the __call do the job + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC; + $access[self::ACCESS_NAME] = $setter; + } elseif (null !== $methods = $this->findAdderAndRemover($reflClass, $singulars)) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; + $access[self::ACCESS_NAME] = sprintf( + 'The property "%s" in class "%s" can be defined with the methods "%s()" but '. + 'the new value must be an array or an instance of \Traversable, '. + '"%s" given.', + $property, + $reflClass->name, + implode('()", "', $methods), + is_object($value) ? get_class($value) : gettype($value) + ); + } else { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; + $access[self::ACCESS_NAME] = sprintf( + 'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '. + '"__set()" or "__call()" exist and have public access in class "%s".', + $property, + implode('', array_map(function ($singular) { + return '"add'.$singular.'()"/"remove'.$singular.'()", '; + }, $singulars)), + $setter, + $getsetter, + $reflClass->name + ); + } + } + + if (isset($item)) { + $this->cacheItemPool->save($item->set($access)); + } + + return $this->writePropertyCache[$key] = $access; + } + + /** + * Returns whether a property is writable in the given object. + * + * @param object $object The object to write to + * @param string $property The property to write + * + * @return bool Whether the property is writable + */ + private function isPropertyWritable($object, $property) + { + if (!is_object($object)) { + return false; + } + + $access = $this->getWriteAccessInfo(get_class($object), $property, array()); + + return self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE] + || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE] + || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE] + || (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) + || self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]; + } + + /** + * Camelizes a given string. + * + * @param string $string Some string + * + * @return string The camelized version of the string + */ + private function camelize($string) + { + return str_replace(' ', '', ucwords(str_replace('_', ' ', $string))); + } + + /** + * Searches for add and remove methods. + * + * @param \ReflectionClass $reflClass The reflection class for the given object + * @param array $singulars The singular form of the property name or null + * + * @return array|null An array containing the adder and remover when found, null otherwise + */ + private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars) + { + foreach ($singulars as $singular) { + $addMethod = 'add'.$singular; + $removeMethod = 'remove'.$singular; + + $addMethodFound = $this->isMethodAccessible($reflClass, $addMethod, 1); + $removeMethodFound = $this->isMethodAccessible($reflClass, $removeMethod, 1); + + if ($addMethodFound && $removeMethodFound) { + return array($addMethod, $removeMethod); + } + } + } + + /** + * Returns whether a method is public and has the number of required parameters. + * + * @param \ReflectionClass $class The class of the method + * @param string $methodName The method name + * @param int $parameters The number of parameters + * + * @return bool Whether the method is public and has $parameters required parameters + */ + private function isMethodAccessible(\ReflectionClass $class, $methodName, $parameters) + { + if ($class->hasMethod($methodName)) { + $method = $class->getMethod($methodName); + + if ($method->isPublic() + && $method->getNumberOfRequiredParameters() <= $parameters + && $method->getNumberOfParameters() >= $parameters) { + return true; + } + } + + return false; + } + + /** + * Gets a PropertyPath instance and caches it. + * + * @param string|PropertyPath $propertyPath + * + * @return PropertyPath + */ + private function getPropertyPath($propertyPath) + { + if ($propertyPath instanceof PropertyPathInterface) { + // Don't call the copy constructor has it is not needed here + return $propertyPath; + } + + if (isset($this->propertyPathCache[$propertyPath])) { + return $this->propertyPathCache[$propertyPath]; + } + + if ($this->cacheItemPool) { + $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_PROPERTY_PATH.$propertyPath); + if ($item->isHit()) { + return $this->propertyPathCache[$propertyPath] = $item->get(); + } + } + + $propertyPathInstance = new PropertyPath($propertyPath); + if (isset($item)) { + $item->set($propertyPathInstance); + $this->cacheItemPool->save($item); + } + + return $this->propertyPathCache[$propertyPath] = $propertyPathInstance; + } + + /** + * Creates the APCu adapter if applicable. + * + * @param string $namespace + * @param int $defaultLifetime + * @param string $version + * @param LoggerInterface|null $logger + * + * @return AdapterInterface + * + * @throws RuntimeException When the Cache Component isn't available + */ + public static function createCache($namespace, $defaultLifetime, $version, LoggerInterface $logger = null) + { + if (!class_exists('Symfony\Component\Cache\Adapter\ApcuAdapter')) { + throw new \RuntimeException(sprintf('The Symfony Cache component must be installed to use %s().', __METHOD__)); + } + + if (!ApcuAdapter::isSupported()) { + return new NullAdapter(); + } + + $apcu = new ApcuAdapter($namespace, $defaultLifetime / 5, $version); + if (null !== $logger) { + $apcu->setLogger($logger); + } + + return $apcu; + } +} diff --git a/vendor/symfony/property-access/PropertyAccessorBuilder.php b/vendor/symfony/property-access/PropertyAccessorBuilder.php new file mode 100644 index 0000000..93fb0d9 --- /dev/null +++ b/vendor/symfony/property-access/PropertyAccessorBuilder.php @@ -0,0 +1,140 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +use Psr\Cache\CacheItemPoolInterface; + +/** + * A configurable builder to create a PropertyAccessor. + * + * @author Jérémie Augustin + */ +class PropertyAccessorBuilder +{ + /** + * @var bool + */ + private $magicCall = false; + + /** + * @var bool + */ + private $throwExceptionOnInvalidIndex = false; + + /** + * @var CacheItemPoolInterface|null + */ + private $cacheItemPool; + + /** + * Enables the use of "__call" by the PropertyAccessor. + * + * @return $this + */ + public function enableMagicCall() + { + $this->magicCall = true; + + return $this; + } + + /** + * Disables the use of "__call" by the PropertyAccessor. + * + * @return $this + */ + public function disableMagicCall() + { + $this->magicCall = false; + + return $this; + } + + /** + * @return bool whether the use of "__call" by the PropertyAccessor is enabled + */ + public function isMagicCallEnabled() + { + return $this->magicCall; + } + + /** + * Enables exceptions when reading a non-existing index. + * + * This has no influence on writing non-existing indices with PropertyAccessorInterface::setValue() + * which are always created on-the-fly. + * + * @return $this + */ + public function enableExceptionOnInvalidIndex() + { + $this->throwExceptionOnInvalidIndex = true; + + return $this; + } + + /** + * Disables exceptions when reading a non-existing index. + * + * Instead, null is returned when calling PropertyAccessorInterface::getValue() on a non-existing index. + * + * @return $this + */ + public function disableExceptionOnInvalidIndex() + { + $this->throwExceptionOnInvalidIndex = false; + + return $this; + } + + /** + * @return bool whether an exception is thrown or null is returned when reading a non-existing index + */ + public function isExceptionOnInvalidIndexEnabled() + { + return $this->throwExceptionOnInvalidIndex; + } + + /** + * Sets a cache system. + * + * @param CacheItemPoolInterface|null $cacheItemPool + * + * @return PropertyAccessorBuilder The builder object + */ + public function setCacheItemPool(CacheItemPoolInterface $cacheItemPool = null) + { + $this->cacheItemPool = $cacheItemPool; + + return $this; + } + + /** + * Gets the used cache system. + * + * @return CacheItemPoolInterface|null + */ + public function getCacheItemPool() + { + return $this->cacheItemPool; + } + + /** + * Builds and returns a new PropertyAccessor object. + * + * @return PropertyAccessorInterface The built PropertyAccessor + */ + public function getPropertyAccessor() + { + return new PropertyAccessor($this->magicCall, $this->throwExceptionOnInvalidIndex, $this->cacheItemPool); + } +} diff --git a/vendor/symfony/property-access/PropertyAccessorInterface.php b/vendor/symfony/property-access/PropertyAccessorInterface.php new file mode 100644 index 0000000..51fa0cc --- /dev/null +++ b/vendor/symfony/property-access/PropertyAccessorInterface.php @@ -0,0 +1,114 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * Writes and reads values to/from an object/array graph. + * + * @author Bernhard Schussek + */ +interface PropertyAccessorInterface +{ + /** + * Sets the value at the end of the property path of the object graph. + * + * Example: + * + * use Symfony\Component\PropertyAccess\PropertyAccess; + * + * $propertyAccessor = PropertyAccess::createPropertyAccessor(); + * + * echo $propertyAccessor->setValue($object, 'child.name', 'Fabien'); + * // equals echo $object->getChild()->setName('Fabien'); + * + * This method first tries to find a public setter for each property in the + * path. The name of the setter must be the camel-cased property name + * prefixed with "set". + * + * If the setter does not exist, this method tries to find a public + * property. The value of the property is then changed. + * + * If neither is found, an exception is thrown. + * + * @param object|array $objectOrArray The object or array to modify + * @param string|PropertyPathInterface $propertyPath The property path to modify + * @param mixed $value The value to set at the end of the property path + * + * @throws Exception\InvalidArgumentException If the property path is invalid + * @throws Exception\AccessException If a property/index does not exist or is not public + * @throws Exception\UnexpectedTypeException If a value within the path is neither object nor array + */ + public function setValue(&$objectOrArray, $propertyPath, $value); + + /** + * Returns the value at the end of the property path of the object graph. + * + * Example: + * + * use Symfony\Component\PropertyAccess\PropertyAccess; + * + * $propertyAccessor = PropertyAccess::createPropertyAccessor(); + * + * echo $propertyAccessor->getValue($object, 'child.name); + * // equals echo $object->getChild()->getName(); + * + * This method first tries to find a public getter for each property in the + * path. The name of the getter must be the camel-cased property name + * prefixed with "get", "is", or "has". + * + * If the getter does not exist, this method tries to find a public + * property. The value of the property is then returned. + * + * If none of them are found, an exception is thrown. + * + * @param object|array $objectOrArray The object or array to traverse + * @param string|PropertyPathInterface $propertyPath The property path to read + * + * @return mixed The value at the end of the property path + * + * @throws Exception\InvalidArgumentException If the property path is invalid + * @throws Exception\AccessException If a property/index does not exist or is not public + * @throws Exception\UnexpectedTypeException If a value within the path is neither object + * nor array + */ + public function getValue($objectOrArray, $propertyPath); + + /** + * Returns whether a value can be written at a given property path. + * + * Whenever this method returns true, {@link setValue()} is guaranteed not + * to throw an exception when called with the same arguments. + * + * @param object|array $objectOrArray The object or array to check + * @param string|PropertyPathInterface $propertyPath The property path to check + * + * @return bool Whether the value can be set + * + * @throws Exception\InvalidArgumentException If the property path is invalid + */ + public function isWritable($objectOrArray, $propertyPath); + + /** + * Returns whether a property path can be read from an object graph. + * + * Whenever this method returns true, {@link getValue()} is guaranteed not + * to throw an exception when called with the same arguments. + * + * @param object|array $objectOrArray The object or array to check + * @param string|PropertyPathInterface $propertyPath The property path to check + * + * @return bool Whether the property path can be read + * + * @throws Exception\InvalidArgumentException If the property path is invalid + */ + public function isReadable($objectOrArray, $propertyPath); +} diff --git a/vendor/symfony/property-access/PropertyPath.php b/vendor/symfony/property-access/PropertyPath.php new file mode 100644 index 0000000..e020182 --- /dev/null +++ b/vendor/symfony/property-access/PropertyPath.php @@ -0,0 +1,217 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException; +use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException; +use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException; + +/** + * Default implementation of {@link PropertyPathInterface}. + * + * @author Bernhard Schussek + */ +class PropertyPath implements \IteratorAggregate, PropertyPathInterface +{ + /** + * Character used for separating between plural and singular of an element. + * + * @var string + */ + const SINGULAR_SEPARATOR = '|'; + + /** + * The elements of the property path. + * + * @var array + */ + private $elements = array(); + + /** + * The number of elements in the property path. + * + * @var int + */ + private $length; + + /** + * Contains a Boolean for each property in $elements denoting whether this + * element is an index. It is a property otherwise. + * + * @var array + */ + private $isIndex = array(); + + /** + * String representation of the path. + * + * @var string + */ + private $pathAsString; + + /** + * Constructs a property path from a string. + * + * @param PropertyPath|string $propertyPath The property path as string or instance + * + * @throws InvalidArgumentException If the given path is not a string + * @throws InvalidPropertyPathException If the syntax of the property path is not valid + */ + public function __construct($propertyPath) + { + // Can be used as copy constructor + if ($propertyPath instanceof self) { + /* @var PropertyPath $propertyPath */ + $this->elements = $propertyPath->elements; + $this->length = $propertyPath->length; + $this->isIndex = $propertyPath->isIndex; + $this->pathAsString = $propertyPath->pathAsString; + + return; + } + if (!is_string($propertyPath)) { + throw new InvalidArgumentException(sprintf( + 'The property path constructor needs a string or an instance of '. + '"Symfony\Component\PropertyAccess\PropertyPath". '. + 'Got: "%s"', + is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath) + )); + } + + if ('' === $propertyPath) { + throw new InvalidPropertyPathException('The property path should not be empty.'); + } + + $this->pathAsString = $propertyPath; + $position = 0; + $remaining = $propertyPath; + + // first element is evaluated differently - no leading dot for properties + $pattern = '/^(([^\.\[]++)|\[([^\]]++)\])(.*)/'; + + while (preg_match($pattern, $remaining, $matches)) { + if ('' !== $matches[2]) { + $element = $matches[2]; + $this->isIndex[] = false; + } else { + $element = $matches[3]; + $this->isIndex[] = true; + } + + $this->elements[] = $element; + + $position += strlen($matches[1]); + $remaining = $matches[4]; + $pattern = '/^(\.([^\.|\[]++)|\[([^\]]++)\])(.*)/'; + } + + if ('' !== $remaining) { + throw new InvalidPropertyPathException(sprintf( + 'Could not parse property path "%s". Unexpected token "%s" at position %d', + $propertyPath, + $remaining[0], + $position + )); + } + + $this->length = count($this->elements); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->pathAsString; + } + + /** + * {@inheritdoc} + */ + public function getLength() + { + return $this->length; + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + if ($this->length <= 1) { + return; + } + + $parent = clone $this; + + --$parent->length; + $parent->pathAsString = substr($parent->pathAsString, 0, max(strrpos($parent->pathAsString, '.'), strrpos($parent->pathAsString, '['))); + array_pop($parent->elements); + array_pop($parent->isIndex); + + return $parent; + } + + /** + * Returns a new iterator for this path. + * + * @return PropertyPathIteratorInterface + */ + public function getIterator() + { + return new PropertyPathIterator($this); + } + + /** + * {@inheritdoc} + */ + public function getElements() + { + return $this->elements; + } + + /** + * {@inheritdoc} + */ + public function getElement($index) + { + if (!isset($this->elements[$index])) { + throw new OutOfBoundsException(sprintf('The index %s is not within the property path', $index)); + } + + return $this->elements[$index]; + } + + /** + * {@inheritdoc} + */ + public function isProperty($index) + { + if (!isset($this->isIndex[$index])) { + throw new OutOfBoundsException(sprintf('The index %s is not within the property path', $index)); + } + + return !$this->isIndex[$index]; + } + + /** + * {@inheritdoc} + */ + public function isIndex($index) + { + if (!isset($this->isIndex[$index])) { + throw new OutOfBoundsException(sprintf('The index %s is not within the property path', $index)); + } + + return $this->isIndex[$index]; + } +} diff --git a/vendor/symfony/property-access/PropertyPathBuilder.php b/vendor/symfony/property-access/PropertyPathBuilder.php new file mode 100644 index 0000000..57751bf --- /dev/null +++ b/vendor/symfony/property-access/PropertyPathBuilder.php @@ -0,0 +1,306 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException; + +/** + * @author Bernhard Schussek + */ +class PropertyPathBuilder +{ + /** + * @var array + */ + private $elements = array(); + + /** + * @var array + */ + private $isIndex = array(); + + /** + * Creates a new property path builder. + * + * @param null|PropertyPathInterface|string $path The path to initially store + * in the builder. Optional. + */ + public function __construct($path = null) + { + if (null !== $path) { + $this->append($path); + } + } + + /** + * Appends a (sub-) path to the current path. + * + * @param PropertyPathInterface|string $path The path to append + * @param int $offset The offset where the appended + * piece starts in $path. + * @param int $length The length of the appended piece + * If 0, the full path is appended. + */ + public function append($path, $offset = 0, $length = 0) + { + if (is_string($path)) { + $path = new PropertyPath($path); + } + + if (0 === $length) { + $end = $path->getLength(); + } else { + $end = $offset + $length; + } + + for (; $offset < $end; ++$offset) { + $this->elements[] = $path->getElement($offset); + $this->isIndex[] = $path->isIndex($offset); + } + } + + /** + * Appends an index element to the current path. + * + * @param string $name The name of the appended index + */ + public function appendIndex($name) + { + $this->elements[] = $name; + $this->isIndex[] = true; + } + + /** + * Appends a property element to the current path. + * + * @param string $name The name of the appended property + */ + public function appendProperty($name) + { + $this->elements[] = $name; + $this->isIndex[] = false; + } + + /** + * Removes elements from the current path. + * + * @param int $offset The offset at which to remove + * @param int $length The length of the removed piece + * + * @throws OutOfBoundsException if offset is invalid + */ + public function remove($offset, $length = 1) + { + if (!isset($this->elements[$offset])) { + throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset)); + } + + $this->resize($offset, $length, 0); + } + + /** + * Replaces a sub-path by a different (sub-) path. + * + * @param int $offset The offset at which to replace + * @param int $length The length of the piece to replace + * @param PropertyPathInterface|string $path The path to insert + * @param int $pathOffset The offset where the inserted piece + * starts in $path. + * @param int $pathLength The length of the inserted piece + * If 0, the full path is inserted. + * + * @throws OutOfBoundsException If the offset is invalid + */ + public function replace($offset, $length, $path, $pathOffset = 0, $pathLength = 0) + { + if (is_string($path)) { + $path = new PropertyPath($path); + } + + if ($offset < 0 && abs($offset) <= $this->getLength()) { + $offset = $this->getLength() + $offset; + } elseif (!isset($this->elements[$offset])) { + throw new OutOfBoundsException('The offset '.$offset.' is not within the property path'); + } + + if (0 === $pathLength) { + $pathLength = $path->getLength() - $pathOffset; + } + + $this->resize($offset, $length, $pathLength); + + for ($i = 0; $i < $pathLength; ++$i) { + $this->elements[$offset + $i] = $path->getElement($pathOffset + $i); + $this->isIndex[$offset + $i] = $path->isIndex($pathOffset + $i); + } + ksort($this->elements); + } + + /** + * Replaces a property element by an index element. + * + * @param int $offset The offset at which to replace + * @param string $name The new name of the element. Optional + * + * @throws OutOfBoundsException If the offset is invalid + */ + public function replaceByIndex($offset, $name = null) + { + if (!isset($this->elements[$offset])) { + throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset)); + } + + if (null !== $name) { + $this->elements[$offset] = $name; + } + + $this->isIndex[$offset] = true; + } + + /** + * Replaces an index element by a property element. + * + * @param int $offset The offset at which to replace + * @param string $name The new name of the element. Optional + * + * @throws OutOfBoundsException If the offset is invalid + */ + public function replaceByProperty($offset, $name = null) + { + if (!isset($this->elements[$offset])) { + throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset)); + } + + if (null !== $name) { + $this->elements[$offset] = $name; + } + + $this->isIndex[$offset] = false; + } + + /** + * Returns the length of the current path. + * + * @return int The path length + */ + public function getLength() + { + return count($this->elements); + } + + /** + * Returns the current property path. + * + * @return PropertyPathInterface The constructed property path + */ + public function getPropertyPath() + { + $pathAsString = $this->__toString(); + + return '' !== $pathAsString ? new PropertyPath($pathAsString) : null; + } + + /** + * Returns the current property path as string. + * + * @return string The property path as string + */ + public function __toString() + { + $string = ''; + + foreach ($this->elements as $offset => $element) { + if ($this->isIndex[$offset]) { + $element = '['.$element.']'; + } elseif ('' !== $string) { + $string .= '.'; + } + + $string .= $element; + } + + return $string; + } + + /** + * Resizes the path so that a chunk of length $cutLength is + * removed at $offset and another chunk of length $insertionLength + * can be inserted. + * + * @param int $offset The offset where the removed chunk starts + * @param int $cutLength The length of the removed chunk + * @param int $insertionLength The length of the inserted chunk + */ + private function resize($offset, $cutLength, $insertionLength) + { + // Nothing else to do in this case + if ($insertionLength === $cutLength) { + return; + } + + $length = count($this->elements); + + if ($cutLength > $insertionLength) { + // More elements should be removed than inserted + $diff = $cutLength - $insertionLength; + $newLength = $length - $diff; + + // Shift elements to the left (left-to-right until the new end) + // Max allowed offset to be shifted is such that + // $offset + $diff < $length (otherwise invalid index access) + // i.e. $offset < $length - $diff = $newLength + for ($i = $offset; $i < $newLength; ++$i) { + $this->elements[$i] = $this->elements[$i + $diff]; + $this->isIndex[$i] = $this->isIndex[$i + $diff]; + } + + // All remaining elements should be removed + for (; $i < $length; ++$i) { + unset($this->elements[$i], $this->isIndex[$i]); + } + } else { + $diff = $insertionLength - $cutLength; + + $newLength = $length + $diff; + $indexAfterInsertion = $offset + $insertionLength; + + // $diff <= $insertionLength + // $indexAfterInsertion >= $insertionLength + // => $diff <= $indexAfterInsertion + + // In each of the following loops, $i >= $diff must hold, + // otherwise ($i - $diff) becomes negative. + + // Shift old elements to the right to make up space for the + // inserted elements. This needs to be done left-to-right in + // order to preserve an ascending array index order + // Since $i = max($length, $indexAfterInsertion) and $indexAfterInsertion >= $diff, + // $i >= $diff is guaranteed. + for ($i = max($length, $indexAfterInsertion); $i < $newLength; ++$i) { + $this->elements[$i] = $this->elements[$i - $diff]; + $this->isIndex[$i] = $this->isIndex[$i - $diff]; + } + + // Shift remaining elements to the right. Do this right-to-left + // so we don't overwrite elements before copying them + // The last written index is the immediate index after the inserted + // string, because the indices before that will be overwritten + // anyway. + // Since $i >= $indexAfterInsertion and $indexAfterInsertion >= $diff, + // $i >= $diff is guaranteed. + for ($i = $length - 1; $i >= $indexAfterInsertion; --$i) { + $this->elements[$i] = $this->elements[$i - $diff]; + $this->isIndex[$i] = $this->isIndex[$i - $diff]; + } + } + } +} diff --git a/vendor/symfony/property-access/PropertyPathInterface.php b/vendor/symfony/property-access/PropertyPathInterface.php new file mode 100644 index 0000000..b627ebc --- /dev/null +++ b/vendor/symfony/property-access/PropertyPathInterface.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * A sequence of property names or array indices. + * + * @author Bernhard Schussek + */ +interface PropertyPathInterface extends \Traversable +{ + /** + * Returns the string representation of the property path. + * + * @return string The path as string + */ + public function __toString(); + + /** + * Returns the length of the property path, i.e. the number of elements. + * + * @return int The path length + */ + public function getLength(); + + /** + * Returns the parent property path. + * + * The parent property path is the one that contains the same items as + * this one except for the last one. + * + * If this property path only contains one item, null is returned. + * + * @return PropertyPath The parent path or null + */ + public function getParent(); + + /** + * Returns the elements of the property path as array. + * + * @return array An array of property/index names + */ + public function getElements(); + + /** + * Returns the element at the given index in the property path. + * + * @param int $index The index key + * + * @return string A property or index name + * + * @throws Exception\OutOfBoundsException If the offset is invalid + */ + public function getElement($index); + + /** + * Returns whether the element at the given index is a property. + * + * @param int $index The index in the property path + * + * @return bool Whether the element at this index is a property + * + * @throws Exception\OutOfBoundsException If the offset is invalid + */ + public function isProperty($index); + + /** + * Returns whether the element at the given index is an array index. + * + * @param int $index The index in the property path + * + * @return bool Whether the element at this index is an array index + * + * @throws Exception\OutOfBoundsException If the offset is invalid + */ + public function isIndex($index); +} diff --git a/vendor/symfony/property-access/PropertyPathIterator.php b/vendor/symfony/property-access/PropertyPathIterator.php new file mode 100644 index 0000000..b49e77e --- /dev/null +++ b/vendor/symfony/property-access/PropertyPathIterator.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * Traverses a property path and provides additional methods to find out + * information about the current element. + * + * @author Bernhard Schussek + */ +class PropertyPathIterator extends \ArrayIterator implements PropertyPathIteratorInterface +{ + /** + * The traversed property path. + * + * @var PropertyPathInterface + */ + protected $path; + + /** + * Constructor. + * + * @param PropertyPathInterface $path The property path to traverse + */ + public function __construct(PropertyPathInterface $path) + { + parent::__construct($path->getElements()); + + $this->path = $path; + } + + /** + * {@inheritdoc} + */ + public function isIndex() + { + return $this->path->isIndex($this->key()); + } + + /** + * {@inheritdoc} + */ + public function isProperty() + { + return $this->path->isProperty($this->key()); + } +} diff --git a/vendor/symfony/property-access/PropertyPathIteratorInterface.php b/vendor/symfony/property-access/PropertyPathIteratorInterface.php new file mode 100644 index 0000000..79b1bbf --- /dev/null +++ b/vendor/symfony/property-access/PropertyPathIteratorInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * @author Bernhard Schussek + */ +interface PropertyPathIteratorInterface extends \Iterator, \SeekableIterator +{ + /** + * Returns whether the current element in the property path is an array + * index. + * + * @return bool + */ + public function isIndex(); + + /** + * Returns whether the current element in the property path is a property + * name. + * + * @return bool + */ + public function isProperty(); +} diff --git a/vendor/symfony/property-access/README.md b/vendor/symfony/property-access/README.md new file mode 100644 index 0000000..1959fd9 --- /dev/null +++ b/vendor/symfony/property-access/README.md @@ -0,0 +1,14 @@ +PropertyAccess Component +======================== + +The PropertyAccess component provides function to read and write from/to an +object or array using a simple string notation. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/property_access/index.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/property-access/StringUtil.php b/vendor/symfony/property-access/StringUtil.php new file mode 100644 index 0000000..6765dd7 --- /dev/null +++ b/vendor/symfony/property-access/StringUtil.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +use Symfony\Component\Inflector\Inflector; + +/** + * Creates singulars from plurals. + * + * @author Bernhard Schussek + * + * @deprecated since version 3.1, to be removed in 4.0. Use {@see Symfony\Component\Inflector\Inflector} instead. + */ +class StringUtil +{ + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Returns the singular form of a word. + * + * If the method can't determine the form with certainty, an array of the + * possible singulars is returned. + * + * @param string $plural A word in plural form + * + * @return string|array The singular form or an array of possible singular + * forms + * + * @deprecated since version 3.1, to be removed in 4.0. Use {@see Symfony\Component\Inflector\Inflector::singularize} instead. + */ + public static function singularify($plural) + { + @trigger_error('StringUtil::singularify() is deprecated since version 3.1 and will be removed in 4.0. Use Symfony\Component\Inflector\Inflector::singularize instead.', E_USER_DEPRECATED); + + return Inflector::singularize($plural); + } +} diff --git a/vendor/symfony/property-access/Tests/Fixtures/NonTraversableArrayObject.php b/vendor/symfony/property-access/Tests/Fixtures/NonTraversableArrayObject.php new file mode 100644 index 0000000..fd00a73 --- /dev/null +++ b/vendor/symfony/property-access/Tests/Fixtures/NonTraversableArrayObject.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +/** + * This class is a hand written simplified version of PHP native `ArrayObject` + * class, to show that it behaves differently than the PHP native implementation. + */ +class NonTraversableArrayObject implements \ArrayAccess, \Countable, \Serializable +{ + private $array; + + public function __construct(array $array = null) + { + $this->array = $array ?: array(); + } + + public function offsetExists($offset) + { + return array_key_exists($offset, $this->array); + } + + public function offsetGet($offset) + { + return $this->array[$offset]; + } + + public function offsetSet($offset, $value) + { + if (null === $offset) { + $this->array[] = $value; + } else { + $this->array[$offset] = $value; + } + } + + public function offsetUnset($offset) + { + unset($this->array[$offset]); + } + + public function count() + { + return count($this->array); + } + + public function serialize() + { + return serialize($this->array); + } + + public function unserialize($serialized) + { + $this->array = (array) unserialize((string) $serialized); + } +} diff --git a/vendor/symfony/property-access/Tests/Fixtures/ReturnTyped.php b/vendor/symfony/property-access/Tests/Fixtures/ReturnTyped.php new file mode 100644 index 0000000..b6a9852 --- /dev/null +++ b/vendor/symfony/property-access/Tests/Fixtures/ReturnTyped.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +/** + * @author Kévin Dunglas + */ +class ReturnTyped +{ + public function getFoos(): array + { + return 'It doesn\'t respect the return type on purpose'; + } + + public function addFoo(\DateTime $dateTime) + { + } + + public function removeFoo(\DateTime $dateTime) + { + } +} diff --git a/vendor/symfony/property-access/Tests/Fixtures/TestClass.php b/vendor/symfony/property-access/Tests/Fixtures/TestClass.php new file mode 100644 index 0000000..e63af3a --- /dev/null +++ b/vendor/symfony/property-access/Tests/Fixtures/TestClass.php @@ -0,0 +1,187 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +class TestClass +{ + public $publicProperty; + protected $protectedProperty; + private $privateProperty; + + private $publicAccessor; + private $publicMethodAccessor; + private $publicGetSetter; + private $publicAccessorWithDefaultValue; + private $publicAccessorWithRequiredAndDefaultValue; + private $publicAccessorWithMoreRequiredParameters; + private $publicIsAccessor; + private $publicHasAccessor; + private $publicGetter; + private $date; + + public function __construct($value) + { + $this->publicProperty = $value; + $this->publicAccessor = $value; + $this->publicMethodAccessor = $value; + $this->publicGetSetter = $value; + $this->publicAccessorWithDefaultValue = $value; + $this->publicAccessorWithRequiredAndDefaultValue = $value; + $this->publicAccessorWithMoreRequiredParameters = $value; + $this->publicIsAccessor = $value; + $this->publicHasAccessor = $value; + $this->publicGetter = $value; + } + + public function setPublicAccessor($value) + { + $this->publicAccessor = $value; + } + + public function setPublicAccessorWithDefaultValue($value = null) + { + $this->publicAccessorWithDefaultValue = $value; + } + + public function setPublicAccessorWithRequiredAndDefaultValue($value, $optional = null) + { + $this->publicAccessorWithRequiredAndDefaultValue = $value; + } + + public function setPublicAccessorWithMoreRequiredParameters($value, $needed) + { + $this->publicAccessorWithMoreRequiredParameters = $value; + } + + public function getPublicAccessor() + { + return $this->publicAccessor; + } + + public function getPublicAccessorWithDefaultValue() + { + return $this->publicAccessorWithDefaultValue; + } + + public function getPublicAccessorWithRequiredAndDefaultValue() + { + return $this->publicAccessorWithRequiredAndDefaultValue; + } + + public function getPublicAccessorWithMoreRequiredParameters() + { + return $this->publicAccessorWithMoreRequiredParameters; + } + + public function setPublicIsAccessor($value) + { + $this->publicIsAccessor = $value; + } + + public function isPublicIsAccessor() + { + return $this->publicIsAccessor; + } + + public function setPublicHasAccessor($value) + { + $this->publicHasAccessor = $value; + } + + public function hasPublicHasAccessor() + { + return $this->publicHasAccessor; + } + + public function publicGetSetter($value = null) + { + if (null !== $value) { + $this->publicGetSetter = $value; + } + + return $this->publicGetSetter; + } + + public function getPublicMethodMutator() + { + return $this->publicGetSetter; + } + + protected function setProtectedAccessor($value) + { + } + + protected function getProtectedAccessor() + { + return 'foobar'; + } + + protected function setProtectedIsAccessor($value) + { + } + + protected function isProtectedIsAccessor() + { + return 'foobar'; + } + + protected function setProtectedHasAccessor($value) + { + } + + protected function hasProtectedHasAccessor() + { + return 'foobar'; + } + + private function setPrivateAccessor($value) + { + } + + private function getPrivateAccessor() + { + return 'foobar'; + } + + private function setPrivateIsAccessor($value) + { + } + + private function isPrivateIsAccessor() + { + return 'foobar'; + } + + private function setPrivateHasAccessor($value) + { + } + + private function hasPrivateHasAccessor() + { + return 'foobar'; + } + + public function getPublicGetter() + { + return $this->publicGetter; + } + + public function setDate(\DateTimeInterface $date) + { + $this->date = $date; + } + + public function getDate() + { + return $this->date; + } +} diff --git a/vendor/symfony/property-access/Tests/Fixtures/TestClassIsWritable.php b/vendor/symfony/property-access/Tests/Fixtures/TestClassIsWritable.php new file mode 100644 index 0000000..4e966cd --- /dev/null +++ b/vendor/symfony/property-access/Tests/Fixtures/TestClassIsWritable.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +class TestClassIsWritable +{ + protected $value; + + public function getValue() + { + return $this->value; + } + + public function __construct($value) + { + $this->value = $value; + } +} diff --git a/vendor/symfony/property-access/Tests/Fixtures/TestClassMagicCall.php b/vendor/symfony/property-access/Tests/Fixtures/TestClassMagicCall.php new file mode 100644 index 0000000..0d6c1f0 --- /dev/null +++ b/vendor/symfony/property-access/Tests/Fixtures/TestClassMagicCall.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +class TestClassMagicCall +{ + private $magicCallProperty; + + public function __construct($value) + { + $this->magicCallProperty = $value; + } + + public function __call($method, array $args) + { + if ('getMagicCallProperty' === $method) { + return $this->magicCallProperty; + } + + if ('getConstantMagicCallProperty' === $method) { + return 'constant value'; + } + + if ('setMagicCallProperty' === $method) { + $this->magicCallProperty = reset($args); + } + } +} diff --git a/vendor/symfony/property-access/Tests/Fixtures/TestClassMagicGet.php b/vendor/symfony/property-access/Tests/Fixtures/TestClassMagicGet.php new file mode 100644 index 0000000..e465325 --- /dev/null +++ b/vendor/symfony/property-access/Tests/Fixtures/TestClassMagicGet.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +class TestClassMagicGet +{ + private $magicProperty; + + public $publicProperty; + + public function __construct($value) + { + $this->magicProperty = $value; + } + + public function __set($property, $value) + { + if ('magicProperty' === $property) { + $this->magicProperty = $value; + } + } + + public function __get($property) + { + if ('magicProperty' === $property) { + return $this->magicProperty; + } + + if ('constantMagicProperty' === $property) { + return 'constant value'; + } + } +} diff --git a/vendor/symfony/property-access/Tests/Fixtures/TestClassSetValue.php b/vendor/symfony/property-access/Tests/Fixtures/TestClassSetValue.php new file mode 100644 index 0000000..f0a7f1f --- /dev/null +++ b/vendor/symfony/property-access/Tests/Fixtures/TestClassSetValue.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +class TestClassSetValue +{ + protected $value; + + public function getValue() + { + return $this->value; + } + + public function setValue($value) + { + $this->value = $value; + } + + public function __construct($value) + { + $this->value = $value; + } +} diff --git a/vendor/symfony/property-access/Tests/Fixtures/TestClassTypeErrorInsideCall.php b/vendor/symfony/property-access/Tests/Fixtures/TestClassTypeErrorInsideCall.php new file mode 100644 index 0000000..44a9390 --- /dev/null +++ b/vendor/symfony/property-access/Tests/Fixtures/TestClassTypeErrorInsideCall.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +class TestClassTypeErrorInsideCall +{ + public function expectsDateTime(\DateTime $date) + { + } + + public function getProperty() + { + } + + public function setProperty($property) + { + $this->expectsDateTime(null); // throws TypeError + } +} diff --git a/vendor/symfony/property-access/Tests/Fixtures/Ticket5775Object.php b/vendor/symfony/property-access/Tests/Fixtures/Ticket5775Object.php new file mode 100644 index 0000000..5954dc3 --- /dev/null +++ b/vendor/symfony/property-access/Tests/Fixtures/Ticket5775Object.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +class Ticket5775Object +{ + private $property; + + public function getProperty() + { + return $this->property; + } + + private function setProperty() + { + } + + public function __set($property, $value) + { + $this->$property = $value; + } +} diff --git a/vendor/symfony/property-access/Tests/Fixtures/TraversableArrayObject.php b/vendor/symfony/property-access/Tests/Fixtures/TraversableArrayObject.php new file mode 100644 index 0000000..3bd9795 --- /dev/null +++ b/vendor/symfony/property-access/Tests/Fixtures/TraversableArrayObject.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +/** + * This class is a hand written simplified version of PHP native `ArrayObject` + * class, to show that it behaves differently than the PHP native implementation. + */ +class TraversableArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable, \Serializable +{ + private $array; + + public function __construct(array $array = null) + { + $this->array = $array ?: array(); + } + + public function offsetExists($offset) + { + return array_key_exists($offset, $this->array); + } + + public function offsetGet($offset) + { + return $this->array[$offset]; + } + + public function offsetSet($offset, $value) + { + if (null === $offset) { + $this->array[] = $value; + } else { + $this->array[$offset] = $value; + } + } + + public function offsetUnset($offset) + { + unset($this->array[$offset]); + } + + public function getIterator() + { + return new \ArrayIterator($this->array); + } + + public function count() + { + return count($this->array); + } + + public function serialize() + { + return serialize($this->array); + } + + public function unserialize($serialized) + { + $this->array = (array) unserialize((string) $serialized); + } +} diff --git a/vendor/symfony/property-access/Tests/Fixtures/TypeHinted.php b/vendor/symfony/property-access/Tests/Fixtures/TypeHinted.php new file mode 100644 index 0000000..ce0f3d8 --- /dev/null +++ b/vendor/symfony/property-access/Tests/Fixtures/TypeHinted.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +/** + * @author Kévin Dunglas + */ +class TypeHinted +{ + private $date; + + /** + * @var \Countable + */ + private $countable; + + public function setDate(\DateTime $date) + { + $this->date = $date; + } + + public function getDate() + { + return $this->date; + } + + /** + * @return \Countable + */ + public function getCountable() + { + return $this->countable; + } + + /** + * @param \Countable $countable + */ + public function setCountable(\Countable $countable) + { + $this->countable = $countable; + } +} diff --git a/vendor/symfony/property-access/Tests/PropertyAccessorArrayAccessTest.php b/vendor/symfony/property-access/Tests/PropertyAccessorArrayAccessTest.php new file mode 100644 index 0000000..b0ed69c --- /dev/null +++ b/vendor/symfony/property-access/Tests/PropertyAccessorArrayAccessTest.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessor; + +abstract class PropertyAccessorArrayAccessTest extends TestCase +{ + /** + * @var PropertyAccessor + */ + protected $propertyAccessor; + + protected function setUp() + { + $this->propertyAccessor = new PropertyAccessor(); + } + + abstract protected function getContainer(array $array); + + public function getValidPropertyPaths() + { + return array( + array($this->getContainer(array('firstName' => 'Bernhard')), '[firstName]', 'Bernhard'), + array($this->getContainer(array('person' => $this->getContainer(array('firstName' => 'Bernhard')))), '[person][firstName]', 'Bernhard'), + ); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testGetValue($collection, $path, $value) + { + $this->assertSame($value, $this->propertyAccessor->getValue($collection, $path)); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException + */ + public function testGetValueFailsIfNoSuchIndex() + { + $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->enableExceptionOnInvalidIndex() + ->getPropertyAccessor(); + + $object = $this->getContainer(array('firstName' => 'Bernhard')); + + $this->propertyAccessor->getValue($object, '[lastName]'); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testSetValue($collection, $path) + { + $this->propertyAccessor->setValue($collection, $path, 'Updated'); + + $this->assertSame('Updated', $this->propertyAccessor->getValue($collection, $path)); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testIsReadable($collection, $path) + { + $this->assertTrue($this->propertyAccessor->isReadable($collection, $path)); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testIsWritable($collection, $path) + { + $this->assertTrue($this->propertyAccessor->isWritable($collection, $path)); + } +} diff --git a/vendor/symfony/property-access/Tests/PropertyAccessorArrayObjectTest.php b/vendor/symfony/property-access/Tests/PropertyAccessorArrayObjectTest.php new file mode 100644 index 0000000..fb0b383 --- /dev/null +++ b/vendor/symfony/property-access/Tests/PropertyAccessorArrayObjectTest.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +class PropertyAccessorArrayObjectTest extends PropertyAccessorCollectionTest +{ + protected function getContainer(array $array) + { + return new \ArrayObject($array); + } +} diff --git a/vendor/symfony/property-access/Tests/PropertyAccessorArrayTest.php b/vendor/symfony/property-access/Tests/PropertyAccessorArrayTest.php new file mode 100644 index 0000000..c982826 --- /dev/null +++ b/vendor/symfony/property-access/Tests/PropertyAccessorArrayTest.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +class PropertyAccessorArrayTest extends PropertyAccessorCollectionTest +{ + protected function getContainer(array $array) + { + return $array; + } +} diff --git a/vendor/symfony/property-access/Tests/PropertyAccessorBuilderTest.php b/vendor/symfony/property-access/Tests/PropertyAccessorBuilderTest.php new file mode 100644 index 0000000..63bd642 --- /dev/null +++ b/vendor/symfony/property-access/Tests/PropertyAccessorBuilderTest.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyAccess\PropertyAccessorBuilder; + +class PropertyAccessorBuilderTest extends TestCase +{ + /** + * @var PropertyAccessorBuilder + */ + protected $builder; + + protected function setUp() + { + $this->builder = new PropertyAccessorBuilder(); + } + + protected function tearDown() + { + $this->builder = null; + } + + public function testEnableMagicCall() + { + $this->assertSame($this->builder, $this->builder->enableMagicCall()); + } + + public function testDisableMagicCall() + { + $this->assertSame($this->builder, $this->builder->disableMagicCall()); + } + + public function testIsMagicCallEnable() + { + $this->assertFalse($this->builder->isMagicCallEnabled()); + $this->assertTrue($this->builder->enableMagicCall()->isMagicCallEnabled()); + $this->assertFalse($this->builder->disableMagicCall()->isMagicCallEnabled()); + } + + public function testGetPropertyAccessor() + { + $this->assertInstanceOf(PropertyAccessor::class, $this->builder->getPropertyAccessor()); + $this->assertInstanceOf(PropertyAccessor::class, $this->builder->enableMagicCall()->getPropertyAccessor()); + } + + public function testUseCache() + { + $cacheItemPool = new ArrayAdapter(); + $this->builder->setCacheItemPool($cacheItemPool); + $this->assertEquals($cacheItemPool, $this->builder->getCacheItemPool()); + $this->assertInstanceOf(PropertyAccessor::class, $this->builder->getPropertyAccessor()); + } +} diff --git a/vendor/symfony/property-access/Tests/PropertyAccessorCollectionTest.php b/vendor/symfony/property-access/Tests/PropertyAccessorCollectionTest.php new file mode 100644 index 0000000..b4dc05d --- /dev/null +++ b/vendor/symfony/property-access/Tests/PropertyAccessorCollectionTest.php @@ -0,0 +1,200 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +class PropertyAccessorCollectionTest_Car +{ + private $axes; + + public function __construct($axes = null) + { + $this->axes = $axes; + } + + // In the test, use a name that StringUtil can't uniquely singularify + public function addAxis($axis) + { + $this->axes[] = $axis; + } + + public function removeAxis($axis) + { + foreach ($this->axes as $key => $value) { + if ($value === $axis) { + unset($this->axes[$key]); + + return; + } + } + } + + public function getAxes() + { + return $this->axes; + } +} + +class PropertyAccessorCollectionTest_CarOnlyAdder +{ + public function addAxis($axis) + { + } + + public function getAxes() + { + } +} + +class PropertyAccessorCollectionTest_CarOnlyRemover +{ + public function removeAxis($axis) + { + } + + public function getAxes() + { + } +} + +class PropertyAccessorCollectionTest_CarNoAdderAndRemover +{ + public function getAxes() + { + } +} + +class PropertyAccessorCollectionTest_CompositeCar +{ + public function getStructure() + { + } + + public function setStructure($structure) + { + } +} + +class PropertyAccessorCollectionTest_CarStructure +{ + public function addAxis($axis) + { + } + + public function removeAxis($axis) + { + } + + public function getAxes() + { + } +} + +abstract class PropertyAccessorCollectionTest extends PropertyAccessorArrayAccessTest +{ + public function testSetValueCallsAdderAndRemoverForCollections() + { + $axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth', 4 => 'fifth')); + $axesMerged = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third')); + $axesAfter = $this->getContainer(array(1 => 'second', 5 => 'first', 6 => 'third')); + $axesMergedCopy = is_object($axesMerged) ? clone $axesMerged : $axesMerged; + + // Don't use a mock in order to test whether the collections are + // modified while iterating them + $car = new PropertyAccessorCollectionTest_Car($axesBefore); + + $this->propertyAccessor->setValue($car, 'axes', $axesMerged); + + $this->assertEquals($axesAfter, $car->getAxes()); + + // The passed collection was not modified + $this->assertEquals($axesMergedCopy, $axesMerged); + } + + public function testSetValueCallsAdderAndRemoverForNestedCollections() + { + $car = $this->getMockBuilder(__CLASS__.'_CompositeCar')->getMock(); + $structure = $this->getMockBuilder(__CLASS__.'_CarStructure')->getMock(); + $axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth')); + $axesAfter = $this->getContainer(array(0 => 'first', 1 => 'second', 2 => 'third')); + + $car->expects($this->any()) + ->method('getStructure') + ->will($this->returnValue($structure)); + + $structure->expects($this->at(0)) + ->method('getAxes') + ->will($this->returnValue($axesBefore)); + $structure->expects($this->at(1)) + ->method('removeAxis') + ->with('fourth'); + $structure->expects($this->at(2)) + ->method('addAxis') + ->with('first'); + $structure->expects($this->at(3)) + ->method('addAxis') + ->with('third'); + + $this->propertyAccessor->setValue($car, 'structure.axes', $axesAfter); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + * @expectedExceptionMessage Could not determine access type for property "axes". + */ + public function testSetValueFailsIfNoAdderNorRemoverFound() + { + $car = $this->getMockBuilder(__CLASS__.'_CarNoAdderAndRemover')->getMock(); + $axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth')); + $axesAfter = $this->getContainer(array(0 => 'first', 1 => 'second', 2 => 'third')); + + $car->expects($this->any()) + ->method('getAxes') + ->will($this->returnValue($axesBefore)); + + $this->propertyAccessor->setValue($car, 'axes', $axesAfter); + } + + public function testIsWritableReturnsTrueIfAdderAndRemoverExists() + { + $car = $this->getMockBuilder(__CLASS__.'_Car')->getMock(); + $this->assertTrue($this->propertyAccessor->isWritable($car, 'axes')); + } + + public function testIsWritableReturnsFalseIfOnlyAdderExists() + { + $car = $this->getMockBuilder(__CLASS__.'_CarOnlyAdder')->getMock(); + $this->assertFalse($this->propertyAccessor->isWritable($car, 'axes')); + } + + public function testIsWritableReturnsFalseIfOnlyRemoverExists() + { + $car = $this->getMockBuilder(__CLASS__.'_CarOnlyRemover')->getMock(); + $this->assertFalse($this->propertyAccessor->isWritable($car, 'axes')); + } + + public function testIsWritableReturnsFalseIfNoAdderNorRemoverExists() + { + $car = $this->getMockBuilder(__CLASS__.'_CarNoAdderAndRemover')->getMock(); + $this->assertFalse($this->propertyAccessor->isWritable($car, 'axes')); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + * expectedExceptionMessageRegExp /The property "axes" in class "Mock_PropertyAccessorCollectionTest_Car[^"]*" can be defined with the methods "addAxis()", "removeAxis()" but the new value must be an array or an instance of \Traversable, "string" given./ + */ + public function testSetValueFailsIfAdderAndRemoverExistButValueIsNotTraversable() + { + $car = $this->getMockBuilder(__CLASS__.'_Car')->getMock(); + + $this->propertyAccessor->setValue($car, 'axes', 'Not an array or Traversable'); + } +} diff --git a/vendor/symfony/property-access/Tests/PropertyAccessorNonTraversableArrayObjectTest.php b/vendor/symfony/property-access/Tests/PropertyAccessorNonTraversableArrayObjectTest.php new file mode 100644 index 0000000..6910d8b --- /dev/null +++ b/vendor/symfony/property-access/Tests/PropertyAccessorNonTraversableArrayObjectTest.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +use Symfony\Component\PropertyAccess\Tests\Fixtures\NonTraversableArrayObject; + +class PropertyAccessorNonTraversableArrayObjectTest extends PropertyAccessorArrayAccessTest +{ + protected function getContainer(array $array) + { + return new NonTraversableArrayObject($array); + } +} diff --git a/vendor/symfony/property-access/Tests/PropertyAccessorTest.php b/vendor/symfony/property-access/Tests/PropertyAccessorTest.php new file mode 100644 index 0000000..b835650 --- /dev/null +++ b/vendor/symfony/property-access/Tests/PropertyAccessorTest.php @@ -0,0 +1,666 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; +use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyAccess\Tests\Fixtures\ReturnTyped; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicGet; +use Symfony\Component\PropertyAccess\Tests\Fixtures\Ticket5775Object; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassSetValue; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassIsWritable; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassTypeErrorInsideCall; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TypeHinted; + +class PropertyAccessorTest extends TestCase +{ + /** + * @var PropertyAccessor + */ + private $propertyAccessor; + + protected function setUp() + { + $this->propertyAccessor = new PropertyAccessor(); + } + + public function getPathsWithUnexpectedType() + { + return array( + array('', 'foobar'), + array('foo', 'foobar'), + array(null, 'foobar'), + array(123, 'foobar'), + array((object) array('prop' => null), 'prop.foobar'), + array((object) array('prop' => (object) array('subProp' => null)), 'prop.subProp.foobar'), + array(array('index' => null), '[index][foobar]'), + array(array('index' => array('subIndex' => null)), '[index][subIndex][foobar]'), + ); + } + + public function getPathsWithMissingProperty() + { + return array( + array((object) array('firstName' => 'Bernhard'), 'lastName'), + array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.lastName'), + array(array('index' => (object) array('firstName' => 'Bernhard')), '[index].lastName'), + array(new TestClass('Bernhard'), 'protectedProperty'), + array(new TestClass('Bernhard'), 'privateProperty'), + array(new TestClass('Bernhard'), 'protectedAccessor'), + array(new TestClass('Bernhard'), 'protectedIsAccessor'), + array(new TestClass('Bernhard'), 'protectedHasAccessor'), + array(new TestClass('Bernhard'), 'privateAccessor'), + array(new TestClass('Bernhard'), 'privateIsAccessor'), + array(new TestClass('Bernhard'), 'privateHasAccessor'), + + // Properties are not camelized + array(new TestClass('Bernhard'), 'public_property'), + ); + } + + public function getPathsWithMissingIndex() + { + return array( + array(array('firstName' => 'Bernhard'), '[lastName]'), + array(array(), '[index][lastName]'), + array(array('index' => array()), '[index][lastName]'), + array(array('index' => array('firstName' => 'Bernhard')), '[index][lastName]'), + array((object) array('property' => array('firstName' => 'Bernhard')), 'property[lastName]'), + ); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testGetValue($objectOrArray, $path, $value) + { + $this->assertSame($value, $this->propertyAccessor->getValue($objectOrArray, $path)); + } + + /** + * @dataProvider getPathsWithMissingProperty + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testGetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path) + { + $this->propertyAccessor->getValue($objectOrArray, $path); + } + + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testGetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path) + { + $this->assertNull($this->propertyAccessor->getValue($objectOrArray, $path)); + } + + /** + * @dataProvider getPathsWithMissingIndex + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException + */ + public function testGetValueThrowsExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) + { + $this->propertyAccessor = new PropertyAccessor(false, true); + $this->propertyAccessor->getValue($objectOrArray, $path); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException + */ + public function testGetValueThrowsExceptionIfNotArrayAccess() + { + $this->propertyAccessor->getValue(new \stdClass(), '[index]'); + } + + public function testGetValueReadsMagicGet() + { + $this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'magicProperty')); + } + + public function testGetValueReadsArrayWithMissingIndexForCustomPropertyPath() + { + $object = new \ArrayObject(); + $array = array('child' => array('index' => $object)); + + $this->assertNull($this->propertyAccessor->getValue($array, '[child][index][foo][bar]')); + $this->assertSame(array(), $object->getArrayCopy()); + } + + // https://github.com/symfony/symfony/pull/4450 + public function testGetValueReadsMagicGetThatReturnsConstant() + { + $this->assertSame('constant value', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'constantMagicProperty')); + } + + public function testGetValueNotModifyObject() + { + $object = new \stdClass(); + $object->firstName = array('Bernhard'); + + $this->assertNull($this->propertyAccessor->getValue($object, 'firstName[1]')); + $this->assertSame(array('Bernhard'), $object->firstName); + } + + public function testGetValueNotModifyObjectException() + { + $propertyAccessor = new PropertyAccessor(false, true); + $object = new \stdClass(); + $object->firstName = array('Bernhard'); + + try { + $propertyAccessor->getValue($object, 'firstName[1]'); + } catch (NoSuchIndexException $e) { + } + + $this->assertSame(array('Bernhard'), $object->firstName); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testGetValueDoesNotReadMagicCallByDefault() + { + $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty'); + } + + public function testGetValueReadsMagicCallIfEnabled() + { + $this->propertyAccessor = new PropertyAccessor(true); + + $this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty')); + } + + // https://github.com/symfony/symfony/pull/4450 + public function testGetValueReadsMagicCallThatReturnsConstant() + { + $this->propertyAccessor = new PropertyAccessor(true); + + $this->assertSame('constant value', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'constantMagicCallProperty')); + } + + /** + * @dataProvider getPathsWithUnexpectedType + * @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + * @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on + */ + public function testGetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $path) + { + $this->propertyAccessor->getValue($objectOrArray, $path); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testSetValue($objectOrArray, $path) + { + $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); + + $this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path)); + } + + /** + * @dataProvider getPathsWithMissingProperty + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testSetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path) + { + $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); + } + + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testSetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path) + { + $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); + + $this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path)); + } + + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testSetValueThrowsNoExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) + { + $this->propertyAccessor = new PropertyAccessor(false, true); + $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); + + $this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path)); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException + */ + public function testSetValueThrowsExceptionIfNotArrayAccess() + { + $object = new \stdClass(); + + $this->propertyAccessor->setValue($object, '[index]', 'Updated'); + } + + public function testSetValueUpdatesMagicSet() + { + $author = new TestClassMagicGet('Bernhard'); + + $this->propertyAccessor->setValue($author, 'magicProperty', 'Updated'); + + $this->assertEquals('Updated', $author->__get('magicProperty')); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testSetValueThrowsExceptionIfThereAreMissingParameters() + { + $object = new TestClass('Bernhard'); + + $this->propertyAccessor->setValue($object, 'publicAccessorWithMoreRequiredParameters', 'Updated'); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testSetValueDoesNotUpdateMagicCallByDefault() + { + $author = new TestClassMagicCall('Bernhard'); + + $this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated'); + } + + public function testSetValueUpdatesMagicCallIfEnabled() + { + $this->propertyAccessor = new PropertyAccessor(true); + + $author = new TestClassMagicCall('Bernhard'); + + $this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated'); + + $this->assertEquals('Updated', $author->__call('getMagicCallProperty', array())); + } + + /** + * @dataProvider getPathsWithUnexpectedType + * @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + * @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on + */ + public function testSetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $path) + { + $this->propertyAccessor->setValue($objectOrArray, $path, 'value'); + } + + public function testGetValueWhenArrayValueIsNull() + { + $this->propertyAccessor = new PropertyAccessor(false, true); + $this->assertNull($this->propertyAccessor->getValue(array('index' => array('nullable' => null)), '[index][nullable]')); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testIsReadable($objectOrArray, $path) + { + $this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path)); + } + + /** + * @dataProvider getPathsWithMissingProperty + */ + public function testIsReadableReturnsFalseIfPropertyNotFound($objectOrArray, $path) + { + $this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path)); + } + + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testIsReadableReturnsTrueIfIndexNotFound($objectOrArray, $path) + { + // Non-existing indices can be read. In this case, null is returned + $this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path)); + } + + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testIsReadableReturnsFalseIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) + { + $this->propertyAccessor = new PropertyAccessor(false, true); + + // When exceptions are enabled, non-existing indices cannot be read + $this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path)); + } + + public function testIsReadableRecognizesMagicGet() + { + $this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicGet('Bernhard'), 'magicProperty')); + } + + public function testIsReadableDoesNotRecognizeMagicCallByDefault() + { + $this->assertFalse($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty')); + } + + public function testIsReadableRecognizesMagicCallIfEnabled() + { + $this->propertyAccessor = new PropertyAccessor(true); + + $this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty')); + } + + /** + * @dataProvider getPathsWithUnexpectedType + */ + public function testIsReadableReturnsFalseIfNotObjectOrArray($objectOrArray, $path) + { + $this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path)); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testIsWritable($objectOrArray, $path) + { + $this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path)); + } + + /** + * @dataProvider getPathsWithMissingProperty + */ + public function testIsWritableReturnsFalseIfPropertyNotFound($objectOrArray, $path) + { + $this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path)); + } + + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testIsWritableReturnsTrueIfIndexNotFound($objectOrArray, $path) + { + // Non-existing indices can be written. Arrays are created on-demand. + $this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path)); + } + + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testIsWritableReturnsTrueIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) + { + $this->propertyAccessor = new PropertyAccessor(false, true); + + // Non-existing indices can be written even if exceptions are enabled + $this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path)); + } + + public function testIsWritableRecognizesMagicSet() + { + $this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicGet('Bernhard'), 'magicProperty')); + } + + public function testIsWritableDoesNotRecognizeMagicCallByDefault() + { + $this->assertFalse($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty')); + } + + public function testIsWritableRecognizesMagicCallIfEnabled() + { + $this->propertyAccessor = new PropertyAccessor(true); + + $this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty')); + } + + /** + * @dataProvider getPathsWithUnexpectedType + */ + public function testIsWritableReturnsFalseIfNotObjectOrArray($objectOrArray, $path) + { + $this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path)); + } + + public function getValidPropertyPaths() + { + return array( + array(array('Bernhard', 'Schussek'), '[0]', 'Bernhard'), + array(array('Bernhard', 'Schussek'), '[1]', 'Schussek'), + array(array('firstName' => 'Bernhard'), '[firstName]', 'Bernhard'), + array(array('index' => array('firstName' => 'Bernhard')), '[index][firstName]', 'Bernhard'), + array((object) array('firstName' => 'Bernhard'), 'firstName', 'Bernhard'), + array((object) array('property' => array('firstName' => 'Bernhard')), 'property[firstName]', 'Bernhard'), + array(array('index' => (object) array('firstName' => 'Bernhard')), '[index].firstName', 'Bernhard'), + array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.firstName', 'Bernhard'), + + // Accessor methods + array(new TestClass('Bernhard'), 'publicProperty', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicAccessor', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicAccessorWithDefaultValue', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicAccessorWithRequiredAndDefaultValue', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicIsAccessor', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicHasAccessor', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicGetSetter', 'Bernhard'), + + // Methods are camelized + array(new TestClass('Bernhard'), 'public_accessor', 'Bernhard'), + array(new TestClass('Bernhard'), '_public_accessor', 'Bernhard'), + + // Missing indices + array(array('index' => array()), '[index][firstName]', null), + array(array('root' => array('index' => array())), '[root][index][firstName]', null), + + // Special chars + array(array('%!@$§.' => 'Bernhard'), '[%!@$§.]', 'Bernhard'), + array(array('index' => array('%!@$§.' => 'Bernhard')), '[index][%!@$§.]', 'Bernhard'), + array((object) array('%!@$§' => 'Bernhard'), '%!@$§', 'Bernhard'), + array((object) array('property' => (object) array('%!@$§' => 'Bernhard')), 'property.%!@$§', 'Bernhard'), + + // nested objects and arrays + array(array('foo' => new TestClass('bar')), '[foo].publicGetSetter', 'bar'), + array(new TestClass(array('foo' => 'bar')), 'publicGetSetter[foo]', 'bar'), + array(new TestClass(new TestClass('bar')), 'publicGetter.publicGetSetter', 'bar'), + array(new TestClass(array('foo' => new TestClass('bar'))), 'publicGetter[foo].publicGetSetter', 'bar'), + array(new TestClass(new TestClass(new TestClass('bar'))), 'publicGetter.publicGetter.publicGetSetter', 'bar'), + array(new TestClass(array('foo' => array('baz' => new TestClass('bar')))), 'publicGetter[foo][baz].publicGetSetter', 'bar'), + ); + } + + public function testTicket5755() + { + $object = new Ticket5775Object(); + + $this->propertyAccessor->setValue($object, 'property', 'foobar'); + + $this->assertEquals('foobar', $object->getProperty()); + } + + public function testSetValueDeepWithMagicGetter() + { + $obj = new TestClassMagicGet('foo'); + $obj->publicProperty = array('foo' => array('bar' => 'some_value')); + $this->propertyAccessor->setValue($obj, 'publicProperty[foo][bar]', 'Updated'); + $this->assertSame('Updated', $obj->publicProperty['foo']['bar']); + } + + public function getReferenceChainObjectsForSetValue() + { + return array( + array(array('a' => array('b' => array('c' => 'old-value'))), '[a][b][c]', 'new-value'), + array(new TestClassSetValue(new TestClassSetValue('old-value')), 'value.value', 'new-value'), + array(new TestClassSetValue(array('a' => array('b' => array('c' => new TestClassSetValue('old-value'))))), 'value[a][b][c].value', 'new-value'), + array(new TestClassSetValue(array('a' => array('b' => 'old-value'))), 'value[a][b]', 'new-value'), + array(new \ArrayIterator(array('a' => array('b' => array('c' => 'old-value')))), '[a][b][c]', 'new-value'), + ); + } + + /** + * @dataProvider getReferenceChainObjectsForSetValue + */ + public function testSetValueForReferenceChainIssue($object, $path, $value) + { + $this->propertyAccessor->setValue($object, $path, $value); + + $this->assertEquals($value, $this->propertyAccessor->getValue($object, $path)); + } + + public function getReferenceChainObjectsForIsWritable() + { + return array( + array(new TestClassIsWritable(array('a' => array('b' => 'old-value'))), 'value[a][b]', false), + array(new TestClassIsWritable(new \ArrayIterator(array('a' => array('b' => 'old-value')))), 'value[a][b]', true), + array(new TestClassIsWritable(array('a' => array('b' => array('c' => new TestClassSetValue('old-value'))))), 'value[a][b][c].value', true), + ); + } + + /** + * @dataProvider getReferenceChainObjectsForIsWritable + */ + public function testIsWritableForReferenceChainIssue($object, $path, $value) + { + $this->assertEquals($value, $this->propertyAccessor->isWritable($object, $path)); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException + * @expectedExceptionMessage Expected argument of type "DateTime", "string" given + */ + public function testThrowTypeError() + { + $object = new TypeHinted(); + + $this->propertyAccessor->setValue($object, 'date', 'This is a string, \DateTime expected.'); + } + + public function testSetTypeHint() + { + $date = new \DateTime(); + $object = new TypeHinted(); + + $this->propertyAccessor->setValue($object, 'date', $date); + $this->assertSame($date, $object->getDate()); + } + + public function testArrayNotBeeingOverwritten() + { + $value = array('value1' => 'foo', 'value2' => 'bar'); + $object = new TestClass($value); + + $this->propertyAccessor->setValue($object, 'publicAccessor[value2]', 'baz'); + $this->assertSame('baz', $this->propertyAccessor->getValue($object, 'publicAccessor[value2]')); + $this->assertSame(array('value1' => 'foo', 'value2' => 'baz'), $object->getPublicAccessor()); + } + + public function testCacheReadAccess() + { + $obj = new TestClass('foo'); + + $propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter()); + $this->assertEquals('foo', $propertyAccessor->getValue($obj, 'publicGetSetter')); + $propertyAccessor->setValue($obj, 'publicGetSetter', 'bar'); + $propertyAccessor->setValue($obj, 'publicGetSetter', 'baz'); + $this->assertEquals('baz', $propertyAccessor->getValue($obj, 'publicGetSetter')); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException + * @expectedExceptionMessage Expected argument of type "Countable", "string" given + */ + public function testThrowTypeErrorWithInterface() + { + $object = new TypeHinted(); + + $this->propertyAccessor->setValue($object, 'countable', 'This is a string, \Countable expected.'); + } + + /** + * @requires PHP 7.0 + */ + public function testAnonymousClassRead() + { + $value = 'bar'; + + $obj = $this->generateAnonymousClass($value); + + $propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter()); + + $this->assertEquals($value, $propertyAccessor->getValue($obj, 'foo')); + } + + /** + * @requires PHP 7.0 + */ + public function testAnonymousClassWrite() + { + $value = 'bar'; + + $obj = $this->generateAnonymousClass(''); + + $propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter()); + $propertyAccessor->setValue($obj, 'foo', $value); + + $this->assertEquals($value, $propertyAccessor->getValue($obj, 'foo')); + } + + private function generateAnonymousClass($value) + { + $obj = eval('return new class($value) + { + private $foo; + + public function __construct($foo) + { + $this->foo = $foo; + } + + /** + * @return mixed + */ + public function getFoo() + { + return $this->foo; + } + + /** + * @param mixed $foo + */ + public function setFoo($foo) + { + $this->foo = $foo; + } + };'); + + return $obj; + } + + /** + * @requires PHP 7.0 + * @expectedException \TypeError + */ + public function testThrowTypeErrorInsideSetterCall() + { + $object = new TestClassTypeErrorInsideCall(); + + $this->propertyAccessor->setValue($object, 'property', 'foo'); + } + + /** + * @requires PHP 7 + * + * @expectedException \TypeError + */ + public function testDoNotDiscardReturnTypeError() + { + $object = new ReturnTyped(); + + $this->propertyAccessor->setValue($object, 'foos', array(new \DateTime())); + } +} diff --git a/vendor/symfony/property-access/Tests/PropertyAccessorTraversableArrayObjectTest.php b/vendor/symfony/property-access/Tests/PropertyAccessorTraversableArrayObjectTest.php new file mode 100644 index 0000000..4e45001 --- /dev/null +++ b/vendor/symfony/property-access/Tests/PropertyAccessorTraversableArrayObjectTest.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +use Symfony\Component\PropertyAccess\Tests\Fixtures\TraversableArrayObject; + +class PropertyAccessorTraversableArrayObjectTest extends PropertyAccessorCollectionTest +{ + protected function getContainer(array $array) + { + return new TraversableArrayObject($array); + } +} diff --git a/vendor/symfony/property-access/Tests/PropertyPathBuilderTest.php b/vendor/symfony/property-access/Tests/PropertyPathBuilderTest.php new file mode 100644 index 0000000..75e9834 --- /dev/null +++ b/vendor/symfony/property-access/Tests/PropertyPathBuilderTest.php @@ -0,0 +1,291 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPathBuilder; + +/** + * @author Bernhard Schussek + */ +class PropertyPathBuilderTest extends TestCase +{ + /** + * @var string + */ + const PREFIX = 'old1[old2].old3[old4][old5].old6'; + + /** + * @var PropertyPathBuilder + */ + private $builder; + + protected function setUp() + { + $this->builder = new PropertyPathBuilder(new PropertyPath(self::PREFIX)); + } + + public function testCreateEmpty() + { + $builder = new PropertyPathBuilder(); + + $this->assertNull($builder->getPropertyPath()); + } + + public function testCreateCopyPath() + { + $this->assertEquals(new PropertyPath(self::PREFIX), $this->builder->getPropertyPath()); + } + + public function testAppendIndex() + { + $this->builder->appendIndex('new1'); + + $path = new PropertyPath(self::PREFIX.'[new1]'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + public function testAppendProperty() + { + $this->builder->appendProperty('new1'); + + $path = new PropertyPath(self::PREFIX.'.new1'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + public function testAppend() + { + $this->builder->append(new PropertyPath('new1[new2]')); + + $path = new PropertyPath(self::PREFIX.'.new1[new2]'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + public function testAppendUsingString() + { + $this->builder->append('new1[new2]'); + + $path = new PropertyPath(self::PREFIX.'.new1[new2]'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + public function testAppendWithOffset() + { + $this->builder->append(new PropertyPath('new1[new2].new3'), 1); + + $path = new PropertyPath(self::PREFIX.'[new2].new3'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + public function testAppendWithOffsetAndLength() + { + $this->builder->append(new PropertyPath('new1[new2].new3'), 1, 1); + + $path = new PropertyPath(self::PREFIX.'[new2]'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + public function testReplaceByIndex() + { + $this->builder->replaceByIndex(1, 'new1'); + + $path = new PropertyPath('old1[new1].old3[old4][old5].old6'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + public function testReplaceByIndexWithoutName() + { + $this->builder->replaceByIndex(0); + + $path = new PropertyPath('[old1][old2].old3[old4][old5].old6'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testReplaceByIndexDoesNotAllowInvalidOffsets() + { + $this->builder->replaceByIndex(6, 'new1'); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testReplaceByIndexDoesNotAllowNegativeOffsets() + { + $this->builder->replaceByIndex(-1, 'new1'); + } + + public function testReplaceByProperty() + { + $this->builder->replaceByProperty(1, 'new1'); + + $path = new PropertyPath('old1.new1.old3[old4][old5].old6'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + public function testReplaceByPropertyWithoutName() + { + $this->builder->replaceByProperty(1); + + $path = new PropertyPath('old1.old2.old3[old4][old5].old6'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testReplaceByPropertyDoesNotAllowInvalidOffsets() + { + $this->builder->replaceByProperty(6, 'new1'); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testReplaceByPropertyDoesNotAllowNegativeOffsets() + { + $this->builder->replaceByProperty(-1, 'new1'); + } + + public function testReplace() + { + $this->builder->replace(1, 1, new PropertyPath('new1[new2].new3')); + + $path = new PropertyPath('old1.new1[new2].new3.old3[old4][old5].old6'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + public function testReplaceUsingString() + { + $this->builder->replace(1, 1, 'new1[new2].new3'); + + $path = new PropertyPath('old1.new1[new2].new3.old3[old4][old5].old6'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + public function testReplaceNegative() + { + $this->builder->replace(-1, 1, new PropertyPath('new1[new2].new3')); + + $path = new PropertyPath('old1[old2].old3[old4][old5].new1[new2].new3'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + /** + * @dataProvider provideInvalidOffsets + * @expectedException \OutOfBoundsException + */ + public function testReplaceDoesNotAllowInvalidOffsets($offset) + { + $this->builder->replace($offset, 1, new PropertyPath('new1[new2].new3')); + } + + public function provideInvalidOffsets() + { + return array( + array(6), + array(-7), + ); + } + + public function testReplaceWithLengthGreaterOne() + { + $this->builder->replace(0, 2, new PropertyPath('new1[new2].new3')); + + $path = new PropertyPath('new1[new2].new3.old3[old4][old5].old6'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + public function testReplaceSubstring() + { + $this->builder->replace(1, 1, new PropertyPath('new1[new2].new3.new4[new5]'), 1, 3); + + $path = new PropertyPath('old1[new2].new3.new4.old3[old4][old5].old6'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + public function testReplaceSubstringWithLengthGreaterOne() + { + $this->builder->replace(1, 2, new PropertyPath('new1[new2].new3.new4[new5]'), 1, 3); + + $path = new PropertyPath('old1[new2].new3.new4[old4][old5].old6'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + // https://github.com/symfony/symfony/issues/5605 + public function testReplaceWithLongerPath() + { + // error occurs when path contains at least two more elements + // than the builder + $path = new PropertyPath('new1.new2.new3'); + + $builder = new PropertyPathBuilder(new PropertyPath('old1')); + $builder->replace(0, 1, $path); + + $this->assertEquals($path, $builder->getPropertyPath()); + } + + public function testReplaceWithLongerPathKeepsOrder() + { + $path = new PropertyPath('new1.new2.new3'); + $expected = new PropertyPath('new1.new2.new3.old2'); + + $builder = new PropertyPathBuilder(new PropertyPath('old1.old2')); + $builder->replace(0, 1, $path); + + $this->assertEquals($expected, $builder->getPropertyPath()); + } + + public function testRemove() + { + $this->builder->remove(3); + + $path = new PropertyPath('old1[old2].old3[old5].old6'); + + $this->assertEquals($path, $this->builder->getPropertyPath()); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testRemoveDoesNotAllowInvalidOffsets() + { + $this->builder->remove(6); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testRemoveDoesNotAllowNegativeOffsets() + { + $this->builder->remove(-1); + } +} diff --git a/vendor/symfony/property-access/Tests/PropertyPathTest.php b/vendor/symfony/property-access/Tests/PropertyPathTest.php new file mode 100644 index 0000000..6c5c6b2 --- /dev/null +++ b/vendor/symfony/property-access/Tests/PropertyPathTest.php @@ -0,0 +1,206 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\PropertyPath; + +class PropertyPathTest extends TestCase +{ + public function testToString() + { + $path = new PropertyPath('reference.traversable[index].property'); + + $this->assertEquals('reference.traversable[index].property', $path->__toString()); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException + */ + public function testDotIsRequiredBeforeProperty() + { + new PropertyPath('[index]property'); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException + */ + public function testDotCannotBePresentAtTheBeginning() + { + new PropertyPath('.property'); + } + + public function providePathsContainingUnexpectedCharacters() + { + return array( + array('property.'), + array('property.['), + array('property..'), + array('property['), + array('property[['), + array('property[.'), + array('property[]'), + ); + } + + /** + * @dataProvider providePathsContainingUnexpectedCharacters + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException + */ + public function testUnexpectedCharacters($path) + { + new PropertyPath($path); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException + */ + public function testPathCannotBeEmpty() + { + new PropertyPath(''); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException + */ + public function testPathCannotBeNull() + { + new PropertyPath(null); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException + */ + public function testPathCannotBeFalse() + { + new PropertyPath(false); + } + + public function testZeroIsValidPropertyPath() + { + $propertyPath = new PropertyPath('0'); + + $this->assertSame('0', (string) $propertyPath); + } + + public function testGetParentWithDot() + { + $propertyPath = new PropertyPath('grandpa.parent.child'); + + $this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent()); + } + + public function testGetParentWithIndex() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent()); + } + + public function testGetParentWhenThereIsNoParent() + { + $propertyPath = new PropertyPath('path'); + + $this->assertNull($propertyPath->getParent()); + } + + public function testCopyConstructor() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + $copy = new PropertyPath($propertyPath); + + $this->assertEquals($propertyPath, $copy); + } + + public function testGetElement() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $this->assertEquals('child', $propertyPath->getElement(2)); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testGetElementDoesNotAcceptInvalidIndices() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $propertyPath->getElement(3); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testGetElementDoesNotAcceptNegativeIndices() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $propertyPath->getElement(-1); + } + + public function testIsProperty() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $this->assertTrue($propertyPath->isProperty(1)); + $this->assertFalse($propertyPath->isProperty(2)); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testIsPropertyDoesNotAcceptInvalidIndices() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $propertyPath->isProperty(3); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testIsPropertyDoesNotAcceptNegativeIndices() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $propertyPath->isProperty(-1); + } + + public function testIsIndex() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $this->assertFalse($propertyPath->isIndex(1)); + $this->assertTrue($propertyPath->isIndex(2)); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testIsIndexDoesNotAcceptInvalidIndices() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $propertyPath->isIndex(3); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testIsIndexDoesNotAcceptNegativeIndices() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $propertyPath->isIndex(-1); + } +} diff --git a/vendor/symfony/property-access/Tests/StringUtilTest.php b/vendor/symfony/property-access/Tests/StringUtilTest.php new file mode 100644 index 0000000..7728e15 --- /dev/null +++ b/vendor/symfony/property-access/Tests/StringUtilTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\StringUtil; + +/** + * @group legacy + */ +class StringUtilTest extends TestCase +{ + public function singularifyProvider() + { + // This is only a stub to make sure the BC layer works + // Actual tests are in the Symfony Inflector component + return array( + array('axes', array('ax', 'axe', 'axis')), + ); + } + + /** + * @dataProvider singularifyProvider + */ + public function testSingularify($plural, $singular) + { + $single = StringUtil::singularify($plural); + if (is_string($singular) && is_array($single)) { + $this->fail("--- Expected\n`string`: ".$singular."\n+++ Actual\n`array`: ".implode(', ', $single)); + } elseif (is_array($singular) && is_string($single)) { + $this->fail("--- Expected\n`array`: ".implode(', ', $singular)."\n+++ Actual\n`string`: ".$single); + } + + $this->assertEquals($singular, $single); + } +} diff --git a/vendor/symfony/property-access/composer.json b/vendor/symfony/property-access/composer.json new file mode 100644 index 0000000..508dd78 --- /dev/null +++ b/vendor/symfony/property-access/composer.json @@ -0,0 +1,41 @@ +{ + "name": "symfony/property-access", + "type": "library", + "description": "Symfony PropertyAccess Component", + "keywords": ["property", "index", "access", "object", "array", "extraction", "injection", "reflection", "property path"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.5.9", + "symfony/polyfill-php70": "~1.0", + "symfony/inflector": "~3.1" + }, + "require-dev": { + "symfony/cache": "~3.1" + }, + "suggest": { + "psr/cache-implementation": "To cache access methods." + }, + "autoload": { + "psr-4": { "Symfony\\Component\\PropertyAccess\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + } +} diff --git a/vendor/symfony/property-access/phpunit.xml.dist b/vendor/symfony/property-access/phpunit.xml.dist new file mode 100644 index 0000000..ebfc564 --- /dev/null +++ b/vendor/symfony/property-access/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/vendor/topthink/think-captcha/.gitignore b/vendor/topthink/think-captcha/.gitignore new file mode 100644 index 0000000..85d49cb --- /dev/null +++ b/vendor/topthink/think-captcha/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +/composer.lock +.idea \ No newline at end of file diff --git a/vendor/topthink/think-captcha/LICENSE b/vendor/topthink/think-captcha/LICENSE new file mode 100644 index 0000000..835ce60 --- /dev/null +++ b/vendor/topthink/think-captcha/LICENSE @@ -0,0 +1,32 @@ + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 +版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn) +All rights reserved。 +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +Apache Licence是著名的非盈利开源组织Apache采用的协议。 +该协议和BSD类似,鼓励代码共享和尊重原作者的著作权, +允许代码修改,再作为开源或商业软件发布。需要满足 +的条件: +1. 需要给代码的用户一份Apache Licence ; +2. 如果你修改了代码,需要在被修改的文件中说明; +3. 在延伸的代码中(修改和有源代码衍生的代码中)需要 +带有原来代码中的协议,商标,专利声明和其他原来作者规 +定需要包含的说明; +4. 如果再发布的产品中包含一个Notice文件,则在Notice文 +件中需要带有本协议内容。你可以在Notice中增加自己的 +许可,但不可以表现为对Apache Licence构成更改。 +具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0 + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/topthink/think-captcha/README.md b/vendor/topthink/think-captcha/README.md new file mode 100644 index 0000000..3d338e8 --- /dev/null +++ b/vendor/topthink/think-captcha/README.md @@ -0,0 +1,33 @@ +# think-captcha +thinkphp5 验证码类库 + +## 安装 +> composer require topthink/think-captcha + + +##使用 + +###模板里输出验证码 + +~~~ +
    {:captcha_img()}
    +~~~ +或者 +~~~ +
    captcha
    +~~~ +> 上面两种的最终效果是一样的 + +### 控制器里验证 +使用TP5的内置验证功能即可 +~~~ +$this->validate($data,[ + 'captcha|验证码'=>'required|captcha' +]); +~~~ +或者手动验证 +~~~ +if(!captcha_check($captcha)){ + //验证失败 +}; +~~~ \ No newline at end of file diff --git a/vendor/topthink/think-captcha/assets/bgs/1.jpg b/vendor/topthink/think-captcha/assets/bgs/1.jpg new file mode 100644 index 0000000..d417136 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/1.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/2.jpg b/vendor/topthink/think-captcha/assets/bgs/2.jpg new file mode 100644 index 0000000..56640bd Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/2.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/3.jpg b/vendor/topthink/think-captcha/assets/bgs/3.jpg new file mode 100644 index 0000000..83e5bd9 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/3.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/4.jpg b/vendor/topthink/think-captcha/assets/bgs/4.jpg new file mode 100644 index 0000000..97a3721 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/4.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/5.jpg b/vendor/topthink/think-captcha/assets/bgs/5.jpg new file mode 100644 index 0000000..220a17a Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/5.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/6.jpg b/vendor/topthink/think-captcha/assets/bgs/6.jpg new file mode 100644 index 0000000..be53ea0 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/6.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/7.jpg b/vendor/topthink/think-captcha/assets/bgs/7.jpg new file mode 100644 index 0000000..fbf537f Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/7.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/8.jpg b/vendor/topthink/think-captcha/assets/bgs/8.jpg new file mode 100644 index 0000000..e10cf28 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/8.jpg differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/1.ttf b/vendor/topthink/think-captcha/assets/ttfs/1.ttf new file mode 100644 index 0000000..d4ee155 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/1.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/2.ttf b/vendor/topthink/think-captcha/assets/ttfs/2.ttf new file mode 100644 index 0000000..3a452b6 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/2.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/3.ttf b/vendor/topthink/think-captcha/assets/ttfs/3.ttf new file mode 100644 index 0000000..d07a4d9 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/3.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/4.ttf b/vendor/topthink/think-captcha/assets/ttfs/4.ttf new file mode 100644 index 0000000..54a14ed Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/4.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/5.ttf b/vendor/topthink/think-captcha/assets/ttfs/5.ttf new file mode 100644 index 0000000..d672876 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/5.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/6.ttf b/vendor/topthink/think-captcha/assets/ttfs/6.ttf new file mode 100644 index 0000000..7f183e2 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/6.ttf differ diff --git a/vendor/topthink/think-captcha/assets/zhttfs/1.ttf b/vendor/topthink/think-captcha/assets/zhttfs/1.ttf new file mode 100644 index 0000000..1c14f7f Binary files /dev/null and b/vendor/topthink/think-captcha/assets/zhttfs/1.ttf differ diff --git a/vendor/topthink/think-captcha/composer.json b/vendor/topthink/think-captcha/composer.json new file mode 100644 index 0000000..272e516 --- /dev/null +++ b/vendor/topthink/think-captcha/composer.json @@ -0,0 +1,20 @@ +{ + "name": "topthink/think-captcha", + "description": "captcha package for thinkphp5", + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "license": "Apache-2.0", + "require": {}, + "autoload": { + "psr-4": { + "think\\captcha\\": "src/" + }, + "files": [ + "src/helper.php" + ] + } +} diff --git a/vendor/topthink/think-captcha/src/Captcha.php b/vendor/topthink/think-captcha/src/Captcha.php new file mode 100644 index 0000000..a09a86c --- /dev/null +++ b/vendor/topthink/think-captcha/src/Captcha.php @@ -0,0 +1,320 @@ + +// +---------------------------------------------------------------------- + +namespace think\captcha; + +use think\Session; + +class Captcha +{ + protected $config = [ + 'seKey' => 'ThinkPHP.CN', + // 验证码加密密钥 + 'codeSet' => '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY', + // 验证码字符集合 + 'expire' => 1800, + // 验证码过期时间(s) + 'useZh' => false, + // 使用中文验证码 + 'zhSet' => '们以我到他会作时要动国产的一是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所二起政三好十战无农使性前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书术状厂须离再目海交权且儿青才证低越际八试规斯近注办布门铁需走议县兵固除般引齿千胜细影济白格效置推空配刀叶率述今选养德话查差半敌始片施响收华觉备名红续均药标记难存测士身紧液派准斤角降维板许破述技消底床田势端感往神便贺村构照容非搞亚磨族火段算适讲按值美态黄易彪服早班麦削信排台声该击素张密害侯草何树肥继右属市严径螺检左页抗苏显苦英快称坏移约巴材省黑武培著河帝仅针怎植京助升王眼她抓含苗副杂普谈围食射源例致酸旧却充足短划剂宣环落首尺波承粉践府鱼随考刻靠够满夫失包住促枝局菌杆周护岩师举曲春元超负砂封换太模贫减阳扬江析亩木言球朝医校古呢稻宋听唯输滑站另卫字鼓刚写刘微略范供阿块某功套友限项余倒卷创律雨让骨远帮初皮播优占死毒圈伟季训控激找叫云互跟裂粮粒母练塞钢顶策双留误础吸阻故寸盾晚丝女散焊功株亲院冷彻弹错散商视艺灭版烈零室轻血倍缺厘泵察绝富城冲喷壤简否柱李望盘磁雄似困巩益洲脱投送奴侧润盖挥距触星松送获兴独官混纪依未突架宽冬章湿偏纹吃执阀矿寨责熟稳夺硬价努翻奇甲预职评读背协损棉侵灰虽矛厚罗泥辟告卵箱掌氧恩爱停曾溶营终纲孟钱待尽俄缩沙退陈讨奋械载胞幼哪剥迫旋征槽倒握担仍呀鲜吧卡粗介钻逐弱脚怕盐末阴丰雾冠丙街莱贝辐肠付吉渗瑞惊顿挤秒悬姆烂森糖圣凹陶词迟蚕亿矩康遵牧遭幅园腔订香肉弟屋敏恢忘编印蜂急拿扩伤飞露核缘游振操央伍域甚迅辉异序免纸夜乡久隶缸夹念兰映沟乙吗儒杀汽磷艰晶插埃燃欢铁补咱芽永瓦倾阵碳演威附牙芽永瓦斜灌欧献顺猪洋腐请透司危括脉宜笑若尾束壮暴企菜穗楚汉愈绿拖牛份染既秋遍锻玉夏疗尖殖井费州访吹荣铜沿替滚客召旱悟刺脑措贯藏敢令隙炉壳硫煤迎铸粘探临薄旬善福纵择礼愿伏残雷延烟句纯渐耕跑泽慢栽鲁赤繁境潮横掉锥希池败船假亮谓托伙哲怀割摆贡呈劲财仪沉炼麻罪祖息车穿货销齐鼠抽画饲龙库守筑房歌寒喜哥洗蚀废纳腹乎录镜妇恶脂庄擦险赞钟摇典柄辩竹谷卖乱虚桥奥伯赶垂途额壁网截野遗静谋弄挂课镇妄盛耐援扎虑键归符庆聚绕摩忙舞遇索顾胶羊湖钉仁音迹碎伸灯避泛亡答勇频皇柳哈揭甘诺概宪浓岛袭谁洪谢炮浇斑讯懂灵蛋闭孩释乳巨徒私银伊景坦累匀霉杜乐勒隔弯绩招绍胡呼痛峰零柴簧午跳居尚丁秦稍追梁折耗碱殊岗挖氏刃剧堆赫荷胸衡勤膜篇登驻案刊秧缓凸役剪川雪链渔啦脸户洛孢勃盟买杨宗焦赛旗滤硅炭股坐蒸凝竟陷枪黎救冒暗洞犯筒您宋弧爆谬涂味津臂障褐陆啊健尊豆拔莫抵桑坡缝警挑污冰柬嘴啥饭塑寄赵喊垫丹渡耳刨虎笔稀昆浪萨茶滴浅拥穴覆伦娘吨浸袖珠雌妈紫戏塔锤震岁貌洁剖牢锋疑霸闪埔猛诉刷狠忽灾闹乔唐漏闻沈熔氯荒茎男凡抢像浆旁玻亦忠唱蒙予纷捕锁尤乘乌智淡允叛畜俘摸锈扫毕璃宝芯爷鉴秘净蒋钙肩腾枯抛轨堂拌爸循诱祝励肯酒绳穷塘燥泡袋朗喂铝软渠颗惯贸粪综墙趋彼届墨碍启逆卸航衣孙龄岭骗休借', + // 中文验证码字符串 + 'useImgBg' => false, + // 使用背景图片 + 'fontSize' => 25, + // 验证码字体大小(px) + 'useCurve' => true, + // 是否画混淆曲线 + 'useNoise' => true, + // 是否添加杂点 + 'imageH' => 0, + // 验证码图片高度 + 'imageW' => 0, + // 验证码图片宽度 + 'length' => 5, + // 验证码位数 + 'fontttf' => '', + // 验证码字体,不设置随机获取 + 'bg' => [243, 251, 254], + // 背景颜色 + 'reset' => true, + // 验证成功后是否重置 + ]; + + private $_image = null; // 验证码图片实例 + private $_color = null; // 验证码字体颜色 + + /** + * 架构方法 设置参数 + * @access public + * @param array $config 配置参数 + */ + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 使用 $this->name 获取配置 + * @access public + * @param string $name 配置名称 + * @return mixed 配置值 + */ + public function __get($name) + { + return $this->config[$name]; + } + + /** + * 设置验证码配置 + * @access public + * @param string $name 配置名称 + * @param string $value 配置值 + * @return void + */ + public function __set($name, $value) + { + if (isset($this->config[$name])) { + $this->config[$name] = $value; + } + } + + /** + * 检查配置 + * @access public + * @param string $name 配置名称 + * @return bool + */ + public function __isset($name) + { + return isset($this->config[$name]); + } + + /** + * 验证验证码是否正确 + * @access public + * @param string $code 用户验证码 + * @param string $id 验证码标识 + * @return bool 用户验证码是否正确 + */ + public function check($code, $id = '') + { + $key = $this->authcode($this->seKey) . $id; + // 验证码不能为空 + $secode = Session::get($key, ''); + if (empty($code) || empty($secode)) { + return false; + } + // session 过期 + if (time() - $secode['verify_time'] > $this->expire) { + Session::delete($key, ''); + return false; + } + + if ($this->authcode(strtoupper($code)) == $secode['verify_code']) { + $this->reset && Session::delete($key, ''); + return true; + } + + return false; + } + + /** + * 输出验证码并把验证码的值保存的session中 + * 验证码保存到session的格式为: array('verify_code' => '验证码值', 'verify_time' => '验证码创建时间'); + * @access public + * @param string $id 要生成验证码的标识 + * @return \think\Response + */ + public function entry($id = '') + { + // 图片宽(px) + $this->imageW || $this->imageW = $this->length * $this->fontSize * 1.5 + $this->length * $this->fontSize / 2; + // 图片高(px) + $this->imageH || $this->imageH = $this->fontSize * 2.5; + // 建立一幅 $this->imageW x $this->imageH 的图像 + $this->_image = imagecreate($this->imageW, $this->imageH); + // 设置背景 + imagecolorallocate($this->_image, $this->bg[0], $this->bg[1], $this->bg[2]); + + // 验证码字体随机颜色 + $this->_color = imagecolorallocate($this->_image, mt_rand(1, 150), mt_rand(1, 150), mt_rand(1, 150)); + // 验证码使用随机字体 + $ttfPath = __DIR__ . '/../assets/' . ($this->useZh ? 'zhttfs' : 'ttfs') . '/'; + + if (empty($this->fontttf)) { + $dir = dir($ttfPath); + $ttfs = []; + while (false !== ($file = $dir->read())) { + if ('.' != $file[0] && substr($file, -4) == '.ttf') { + $ttfs[] = $file; + } + } + $dir->close(); + $this->fontttf = $ttfs[array_rand($ttfs)]; + } + $this->fontttf = $ttfPath . $this->fontttf; + + if ($this->useImgBg) { + $this->_background(); + } + + if ($this->useNoise) { + // 绘杂点 + $this->_writeNoise(); + } + if ($this->useCurve) { + // 绘干扰线 + $this->_writeCurve(); + } + + // 绘验证码 + $code = []; // 验证码 + $codeNX = 0; // 验证码第N个字符的左边距 + if ($this->useZh) { + // 中文验证码 + for ($i = 0; $i < $this->length; $i++) { + $code[$i] = iconv_substr($this->zhSet, floor(mt_rand(0, mb_strlen($this->zhSet, 'utf-8') - 1)), 1, 'utf-8'); + imagettftext($this->_image, $this->fontSize, mt_rand(-40, 40), $this->fontSize * ($i + 1) * 1.5, $this->fontSize + mt_rand(10, 20), $this->_color, $this->fontttf, $code[$i]); + } + } else { + for ($i = 0; $i < $this->length; $i++) { + $code[$i] = $this->codeSet[mt_rand(0, strlen($this->codeSet) - 1)]; + $codeNX += mt_rand($this->fontSize * 1.2, $this->fontSize * 1.6); + imagettftext($this->_image, $this->fontSize, mt_rand(-40, 40), $codeNX, $this->fontSize * 1.6, $this->_color, $this->fontttf, $code[$i]); + } + } + + // 保存验证码 + $key = $this->authcode($this->seKey); + $code = $this->authcode(strtoupper(implode('', $code))); + $secode = []; + $secode['verify_code'] = $code; // 把校验码保存到session + $secode['verify_time'] = time(); // 验证码创建时间 + Session::set($key . $id, $secode, ''); + + ob_start(); + // 输出图像 + imagepng($this->_image); + $content = ob_get_clean(); + imagedestroy($this->_image); + + return response($content, 200, ['Content-Length' => strlen($content)])->contentType('image/png'); + } + + /** + * 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数) + * + * 高中的数学公式咋都忘了涅,写出来 + * 正弦型函数解析式:y=Asin(ωx+φ)+b + * 各常数值对函数图像的影响: + * A:决定峰值(即纵向拉伸压缩的倍数) + * b:表示波形在Y轴的位置关系或纵向移动距离(上加下减) + * φ:决定波形与X轴位置关系或横向移动距离(左加右减) + * ω:决定周期(最小正周期T=2π/∣ω∣) + * + */ + private function _writeCurve() + { + $px = $py = 0; + + // 曲线前部分 + $A = mt_rand(1, $this->imageH / 2); // 振幅 + $b = mt_rand(-$this->imageH / 4, $this->imageH / 4); // Y轴方向偏移量 + $f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量 + $T = mt_rand($this->imageH, $this->imageW * 2); // 周期 + $w = (2 * M_PI) / $T; + + $px1 = 0; // 曲线横坐标起始位置 + $px2 = mt_rand($this->imageW / 2, $this->imageW * 0.8); // 曲线横坐标结束位置 + + for ($px = $px1; $px <= $px2; $px = $px + 1) { + if (0 != $w) { + $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b + $i = (int)($this->fontSize / 5); + while ($i > 0) { + imagesetpixel($this->_image, $px + $i, $py + $i, $this->_color); // 这里(while)循环画像素点比imagettftext和imagestring用字体大小一次画出(不用这while循环)性能要好很多 + $i--; + } + } + } + + // 曲线后部分 + $A = mt_rand(1, $this->imageH / 2); // 振幅 + $f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量 + $T = mt_rand($this->imageH, $this->imageW * 2); // 周期 + $w = (2 * M_PI) / $T; + $b = $py - $A * sin($w * $px + $f) - $this->imageH / 2; + $px1 = $px2; + $px2 = $this->imageW; + + for ($px = $px1; $px <= $px2; $px = $px + 1) { + if (0 != $w) { + $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b + $i = (int)($this->fontSize / 5); + while ($i > 0) { + imagesetpixel($this->_image, $px + $i, $py + $i, $this->_color); + $i--; + } + } + } + } + + /** + * 画杂点 + * 往图片上写不同颜色的字母或数字 + */ + private function _writeNoise() + { + $codeSet = '2345678abcdefhijkmnpqrstuvwxyz'; + for ($i = 0; $i < 10; $i++) { + //杂点颜色 + $noiseColor = imagecolorallocate($this->_image, mt_rand(150, 225), mt_rand(150, 225), mt_rand(150, 225)); + for ($j = 0; $j < 5; $j++) { + // 绘杂点 + imagestring($this->_image, 5, mt_rand(-10, $this->imageW), mt_rand(-10, $this->imageH), $codeSet[mt_rand(0, 29)], $noiseColor); + } + } + } + + /** + * 绘制背景图片 + * 注:如果验证码输出图片比较大,将占用比较多的系统资源 + */ + private function _background() + { + $path = dirname(__FILE__) . '/verify/bgs/'; + $dir = dir($path); + + $bgs = []; + while (false !== ($file = $dir->read())) { + if ('.' != $file[0] && substr($file, -4) == '.jpg') { + $bgs[] = $path . $file; + } + } + $dir->close(); + + $gb = $bgs[array_rand($bgs)]; + + list($width, $height) = @getimagesize($gb); + // Resample + $bgImage = @imagecreatefromjpeg($gb); + @imagecopyresampled($this->_image, $bgImage, 0, 0, 0, 0, $this->imageW, $this->imageH, $width, $height); + @imagedestroy($bgImage); + } + + /* 加密验证码 */ + private function authcode($str) + { + $key = substr(md5($this->seKey), 5, 8); + $str = substr(md5($str), 8, 10); + return md5($key . $str); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-captcha/src/CaptchaController.php b/vendor/topthink/think-captcha/src/CaptchaController.php new file mode 100644 index 0000000..8a0535d --- /dev/null +++ b/vendor/topthink/think-captcha/src/CaptchaController.php @@ -0,0 +1,23 @@ + +// +---------------------------------------------------------------------- + +namespace think\captcha; + +use think\Config; + +class CaptchaController +{ + public function index($id = "") + { + $captcha = new Captcha((array)Config::get('captcha')); + return $captcha->entry($id); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-captcha/src/helper.php b/vendor/topthink/think-captcha/src/helper.php new file mode 100644 index 0000000..ef3970f --- /dev/null +++ b/vendor/topthink/think-captcha/src/helper.php @@ -0,0 +1,64 @@ + +// +---------------------------------------------------------------------- + +\think\Route::get('captcha/[:id]', "\\think\\captcha\\CaptchaController@index"); + +\think\Validate::extend('captcha', function ($value, $id = "") { + return captcha_check($value, $id, (array)\think\Config::get('captcha')); +}); + +\think\Validate::setTypeMsg('captcha', '验证码错误!'); + + +/** + * @param string $id + * @param array $config + * @return \think\Response + */ +function captcha($id = "", $config = []) +{ + $captcha = new \think\captcha\Captcha($config); + return $captcha->entry($id); +} + + +/** + * @param $id + * @return string + */ +function captcha_src($id = "") +{ + return \think\Url::build('/captcha' . ($id ? "/{$id}" : '')); +} + + +/** + * @param $id + * @return mixed + */ +function captcha_img($id = "") +{ + return 'captcha'; +} + + +/** + * @param $value + * @param string $id + * @param array $config + * @return bool + */ +function captcha_check($value, $id = "", $config = []) +{ + $captcha = new \think\captcha\Captcha($config); + return $captcha->check($value, $id); +} + diff --git a/vendor/topthink/think-installer/.gitignore b/vendor/topthink/think-installer/.gitignore new file mode 100644 index 0000000..8f4c02d --- /dev/null +++ b/vendor/topthink/think-installer/.gitignore @@ -0,0 +1,3 @@ +/.idea +composer.lock +/vendor \ No newline at end of file diff --git a/vendor/topthink/think-installer/composer.json b/vendor/topthink/think-installer/composer.json new file mode 100644 index 0000000..4005de2 --- /dev/null +++ b/vendor/topthink/think-installer/composer.json @@ -0,0 +1,25 @@ +{ + "name": "topthink/think-installer", + "type": "composer-plugin", + "require": { + "composer-plugin-api": "^1.0" + }, + "require-dev": { + "composer/composer": "1.0.*@dev" + }, + "license": "Apache-2.0", + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "autoload": { + "psr-4": { + "think\\composer\\": "src" + } + }, + "extra": { + "class": "think\\composer\\Plugin" + } +} diff --git a/vendor/topthink/think-installer/src/Plugin.php b/vendor/topthink/think-installer/src/Plugin.php new file mode 100644 index 0000000..757c30f --- /dev/null +++ b/vendor/topthink/think-installer/src/Plugin.php @@ -0,0 +1,26 @@ +getInstallationManager(); + + //框架核心 + $manager->addInstaller(new ThinkFramework($io, $composer)); + + //单元测试 + $manager->addInstaller(new ThinkTesting($io, $composer)); + + //扩展 + $manager->addInstaller(new ThinkExtend($io, $composer)); + + } +} \ No newline at end of file diff --git a/vendor/topthink/think-installer/src/ThinkExtend.php b/vendor/topthink/think-installer/src/ThinkExtend.php new file mode 100644 index 0000000..d78f118 --- /dev/null +++ b/vendor/topthink/think-installer/src/ThinkExtend.php @@ -0,0 +1,77 @@ + +// +---------------------------------------------------------------------- + +namespace think\composer; + +use Composer\Installer\LibraryInstaller; +use Composer\Package\PackageInterface; +use Composer\Repository\InstalledRepositoryInterface; + +class ThinkExtend extends LibraryInstaller +{ + + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + parent::install($repo, $package); + $this->copyExtraFiles($package); + } + + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + parent::update($repo, $initial, $target); + $this->copyExtraFiles($target); + + } + + protected function copyExtraFiles(PackageInterface $package) + { + if ($this->composer->getPackage()->getType() == 'project') { + + $extra = $package->getExtra(); + + if (!empty($extra['think-config'])) { + + $composerExtra = $this->composer->getPackage()->getExtra(); + + $appDir = !empty($composerExtra['app-path']) ? $composerExtra['app-path'] : 'application'; + + if (is_dir($appDir)) { + + $extraDir = $appDir . DIRECTORY_SEPARATOR . 'extra'; + $this->filesystem->ensureDirectoryExists($extraDir); + + //配置文件 + foreach ((array) $extra['think-config'] as $name => $config) { + $target = $extraDir . DIRECTORY_SEPARATOR . $name . '.php'; + $source = $this->getInstallPath($package) . DIRECTORY_SEPARATOR . $config; + + if (is_file($target)) { + $this->io->write("File {$target} exist!"); + continue; + } + + if (!is_file($source)) { + $this->io->write("File {$target} not exist!"); + continue; + } + + copy($source, $target); + } + } + } + } + } + + public function supports($packageType) + { + return 'think-extend' === $packageType; + } +} \ No newline at end of file diff --git a/vendor/topthink/think-installer/src/ThinkFramework.php b/vendor/topthink/think-installer/src/ThinkFramework.php new file mode 100644 index 0000000..cdb7d84 --- /dev/null +++ b/vendor/topthink/think-installer/src/ThinkFramework.php @@ -0,0 +1,59 @@ +composer->getPackage()->getType() == 'project' && $package->getInstallationSource() != 'source') { + //remove tests dir + $this->filesystem->removeDirectory($this->getInstallPath($package) . DIRECTORY_SEPARATOR . 'tests'); + } + } + + /** + * {@inheritDoc} + */ + public function getInstallPath(PackageInterface $package) + { + if ('topthink/framework' !== $package->getPrettyName()) { + throw new \InvalidArgumentException('Unable to install this library!'); + } + + if ($this->composer->getPackage()->getType() !== 'project') { + return parent::getInstallPath($package); + } + + if ($this->composer->getPackage()) { + $extra = $this->composer->getPackage()->getExtra(); + if (!empty($extra['think-path'])) { + return $extra['think-path']; + } + } + + return 'thinkphp'; + } + + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + parent::update($repo, $initial, $target); + if ($this->composer->getPackage()->getType() == 'project' && $target->getInstallationSource() != 'source') { + //remove tests dir + $this->filesystem->removeDirectory($this->getInstallPath($target) . DIRECTORY_SEPARATOR . 'tests'); + } + } + + /** + * {@inheritDoc} + */ + public function supports($packageType) + { + return 'think-framework' === $packageType; + } +} \ No newline at end of file diff --git a/vendor/topthink/think-installer/src/ThinkTesting.php b/vendor/topthink/think-installer/src/ThinkTesting.php new file mode 100644 index 0000000..bf27f72 --- /dev/null +++ b/vendor/topthink/think-installer/src/ThinkTesting.php @@ -0,0 +1,68 @@ + +// +---------------------------------------------------------------------- + +namespace think\composer; + + +use Composer\Installer\LibraryInstaller; +use Composer\Package\PackageInterface; +use Composer\Repository\InstalledRepositoryInterface; + +class ThinkTesting extends LibraryInstaller +{ + /** + * {@inheritDoc} + */ + public function getInstallPath(PackageInterface $package) + { + if ('topthink/think-testing' !== $package->getPrettyName()) { + throw new \InvalidArgumentException('Unable to install this library!'); + } + + return parent::getInstallPath($package); + } + + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + parent::install($repo, $package); + + $this->copyTestDir($package); + + + } + + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + parent::update($repo, $initial, $target); + + $this->copyTestDir($target); + + } + + private function copyTestDir(PackageInterface $package) + { + $appDir = dirname($this->vendorDir); + $source = $this->getInstallPath($package) . DIRECTORY_SEPARATOR . 'example'; + if (!is_file($appDir . DIRECTORY_SEPARATOR . 'phpunit.xml')) { + $this->filesystem->copyThenRemove($source, $appDir); + } else { + $this->filesystem->removeDirectoryPhp($source); + } + } + + /** + * {@inheritDoc} + */ + public function supports($packageType) + { + return 'think-testing' === $packageType; + } +} \ No newline at end of file diff --git a/超级服务助手 b/超级服务助手 new file mode 100644 index 0000000..e69de29