关于php框架:composer-包冲突处理

PHP我的项目中须要获取天气信息 网上查了下,决定应用https://github.com/overtrue/weather这个组件 我的项目根目前执行上面命令 composer require overtrue/weather -vvv报抵触问题 解决方案1: 剖析抵触产生起因 执行命令 composer depends guzzlehttp/guzzle 发现我的项目自身依赖这个包,剖析代码发现这里的代码其实曾经作废了,执行上面命令移除旧的包而后重新安装即可 composer remove guzzlehttp/guzzlecomposer require overtrue/weather -vvv顺次执行后胜利装置overtrue/weather 2: fork仓库到composer公有仓库 大略思路:fork后人工更新依赖到7.x版本 而后通过公有composer仓库装置依赖即可 3: Composer的replace 属性 没有应用过,参考 这里 总结遇到composer依赖抵触问题,优先应用计划一,剖析依赖抵触产生的起因,如果是一些旧代码废除的依赖能够间接remove在重新安装新依赖。如果这个办法不行,能够采取计划2或者计划3解决

May 29, 2023 · 1 min · jiezi

关于php框架:基于Github的Start的PHP框架排名

Laravel 72.3kSymfony 28kCodeIgniter 18.2kYii2 14.1kSlim 11.5kcphalcon 10.7kCakePHP 8.6kThinkPHP 7.7kLumen 7.6k

February 10, 2023 · 1 min · jiezi

关于php框架:仿钉钉审批流程后端-PHP-处理一

前端数据结构参照:https://github.com/StavinLi/W... 数据表构造:数据处理: /** * 解决流程配置 * @param $type * @param $data * @return mixed */public function checkProcessConfig($data, $type = 'processConfig'){    if (!isset($data[$type]) || !$data[$type])        return [];    return $this->getSerializeData($data[$type]);} /** * 初始化流程配置数据 * @param $data * @param int $level * @param string $onlyValue * @param int $is_initial * @return array */public function getSerializeData($data, $level = 0, $onlyValue = '', $is_initial = 1, $group = 0){    $info[] = $this->getInfo($data, $onlyValue, $level, $is_initial, $group);    if ($data['childNode']) {        $level++;        if (isset($data['childNode'])) $info = array_merge($info, $this->getSerializeData($data['childNode'], $level, $data['onlyValue'], 0, $group));    }    if (isset($data['conditionNodes']) && $data['conditionNodes']) {        foreach ($data['conditionNodes'] as $v) {            $group++;            $level++;            $info[] = $this->getInfo($v, $data['onlyValue'], $level, 0, $group);            if ($v['childNode']) {                $info = array_merge($info, $this->getSerializeData($v['childNode'], $level, $v['onlyValue'], 0, $group));            }        }    }    return $info;}/** * 组合流程数据 * @param $data * @param string $parent * @param int $level * @param int $is_initial * @param int $group * @return array */public function getInfo($data, $parent = '', $level = 0, $is_initial = 0, $group = 0){    return [        //节点名称(申请人、审核人、抄送人)        'name'           => $data['nodeName'],        //节点类型:0、申请人;1、审核人;2、抄送人;3、条件;4、路由;        'types'          => $data['type'],        'uniqued'        => $data['onlyValue'],//节点惟一值        //审核人类型:1、指定成员;2、指定部门主管;7、间断多部门;5、申请人本人;4、申请人自选;(0、无此条件)        'settype'        => isset($data['settype']) && $data['settype'] ? $data['settype'] : 0,        //指定层级程序:0、从上至下;1、从下至上;(-1、无此条件)        'director_order' => isset($data['directorOrder']) ? $data['directorOrder'] : -1,        //指定主管层级/指定起点层级:1-10;(0、无此条件)        'director_level' => isset($data['directorLevel']) && $data['directorLevel'] ? $data['directorLevel'] : 0,        //以后部门无负责人时:1、上级部门负责人审批;2、为空时跳过;(0、无此条件)        'no_hander'      => isset($data['noHanderAction']) && $data['noHanderAction'] ? $data['noHanderAction'] : 0,        //可选范畴:1、不限范畴;2、指定成员;(0、无此条件)        'select_range'   => isset($data['selectRange']) && $data['selectRange'] ? $data['selectRange'] : 0,        //指定的成员列表        'user_list'      => isset($data['nodeUserList']) && $data['nodeUserList'] ? $data['nodeUserList'] : [],        //选人形式:1、单选;2、多选;(0、无此条件)        'select_mode'    => isset($data['selectMode']) && $data['selectMode'] ? $data['selectMode'] : 0,        //多人审批形式:1、或签;2、会签;3、顺次审批;(0、无此条件)        'examine_mode'   => isset($data['examineMode']) && $data['examineMode'] ? $data['examineMode'] : '',        //条件优先级        'priority'       => isset($data['priorityLevel']) && $data['priorityLevel'] ? $data['priorityLevel'] : 0,        'parent'         => $parent,//父级惟一值        'level'          => $level,        'info'           => $is_initial > 0 ? $data : [],        'is_initial'     => $is_initial,        'is_child'       => $data['childNode'] ? 1 : 0,        'is_condition'   => isset($data['conditionNodes']) && $data['conditionNodes'] ? 1 : 0,        'card_id'        => $this->cardId,        'groups'         => $group,        'entid'          => $this->entId(false),        'condition_list' => isset($data['conditionList']) ? $data['conditionList'] : [],        //指定部门负责人        'dep_head'       => isset($data['departmentHead']) ? $data['departmentHead'] : [],        //是否容许自选抄送人        'self_select'    => isset($data['ccSelfSelectFlag']) ? $data['ccSelfSelectFlag'] : 0,    ];}数据处理实现后保留数据库即可。思路:利用节点惟一值绑定节点的上下级关系,应用层级辨别节点层级深度。 ...

August 4, 2022 · 2 min · jiezi

关于php框架:PHP的框架

简直80%的网站建设都是用的PHP,那么PHP你晓得的框架有哪些?在php中,咱们最罕用的框架有七个,别离是Yii2、Laravel、Yaf、Thinkphp、Code Igniter、Zend Framework以及CakePHP1、Laravel 于2011年推出,现已成为世界上最风行的收费、开源PHP框架。Laravel简化了开发过程,简化了常见的工作,比方路由、会话、缓存和身份验证。 具备许多个性,能够帮忙您定制简单的应用程序。其中包含:无缝数据迁徙、MVC架构反对、安全性、路由、视图模板引擎和身份验证等;对于想要构建B2B或企业网站的开发人员来说,Laravel是一个不错的抉择。 2.Yii 一个用于开发古代web应用程序的高性能、基于组件的PHP框架。采纳严格的OOP编写,并有着欠缺的库援用以及全面的教程。 具备杰出的速度和性能,高度可扩大,并且容许开发人员防止编写反复的SQL语句的复杂性;具备极强的可扩展性,您简直能够定制外围代码的每一部分。 3.Yaf 一个高性能的PHP开发框架,采纳PHP扩大实现(c语言),相比于个别的PHP框架, 它更快,更轻便。 具备高性能的视图引擎;无需编译;高度灵便可扩大的框架, 反对自定义视图引擎, 反对插件, 反对自定义路由等;更快的执行速度, 更少的内存占用。 4.Thinkphp 为了简化企业级利用开发和麻利WEB利用开发而诞生,它的主旨是简化开发、提高效率、易于扩大,并且领有泛滥原创性能和个性。 易于上手,有丰盛的中文文档;学习成本低,社区活跃度高;框架的兼容性较强;从3.0版本开始引入了全新的CBD(外围Core+行为Behavior+驱动Driver)架构模式,框架从底层采纳外围+行为+驱动的架构体系 5.Code Igniter 一个适宜开发动静网站的PHP框架。它提供了许多预构建的模块,帮忙构建强壮的和可重用的组件。 配置简略,全副的配置应用PHP脚本来配置,执行效率高;疾速简洁,代码不多,执行性能高;自带了很多简略好用的library,框架适宜小型利用; 6.Zend Framework 一个残缺的面向对象框架,它应用接口和继承等个性使其具备可扩展性。基于麻利办法构建的,麻利办法帮忙您向企业客户交付高质量的应用程序。是大型It部门和银行的首选框架。 要害个性包含MVC组件、简略的云API、数据加密和会话治理;自带了十分多的library,框架自身应用了很多设计模式来编写,架构上很优雅,执行效率中等;配置文件比拟弱小(可能解决XML和php INI);数据库操作性能很弱小,反对各种驱动。 7.CakePHP 一个简略而优雅的工具包,最容易学习的框架之一,尤其是因为它的CRUD(创立、读取、更新和删除)框架。只须要一个web服务器和框架的正本就能开始应用。 没有自带多余的 library,所有的性能都是纯正的框架,执行效率还不错;具备主动操作命令行脚本性能;平安个性包含SQL注入预防、输出验证、跨站点申请伪造(CSRF)爱护和跨站点脚本编写(XSS)爱护;要害个性包含古代框架、疾速构建、适当的类继承、验证和安全性。 除了这七个罕用的框架,其实PHP还有很多别的好用框架。如Symfony、Phalcon、PHPixie还有Swoft等。 以上数款框架,各有特色,而且都是开源我的项目,不过框架针对的我的项目不一样,在我的项目选型的时候,要充分考虑框架的能够定制性、扩展性,因为每个我的项目都无奈确定你是否会随着需要的变动进行扭转。

June 16, 2022 · 1 min · jiezi

关于php框架:如何解决phpfpm启动不了问题

在本篇文章里小编给大家整顿的是一篇对于如何解决php-fpm启动不了问题相干文章,有须要的敌人们能够跟着学习下。 1、从新下载一个64位的“ZendGuardLoader.so”。 2、将其放到“/usr/local/zend/”下。 3、从新执行“lamp php-fpm start”即可。 修复步骤用file命令查看以后的ZendGuardLoader.so的文件信息,能够看到以后so文件为32位的文件。 test@ubuntu:/usr/local/zend$ file ZendGuardLoader.soZendGuardLoader.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, stripped下载解压64位的ZendGuardLoader,而后替换原来的.so文件之后重新启动php-fpm wget http://downloads.zend.com/guard/5.5.0/ZendGuardLoader-php-5.3-linux-glibc23-x86_64.tar.gztar -xvf ZendGuardLoader-php-5.3-linux-glibc23-x86_64.tar.gz cp ZendGuardLoader-php-5.3-linux-glibc23-x86_64/php-5.3.x/ZendGuardLoader.so /usr/local/zend/从新用file看下替换之后的ZendGuardLoader.so,能够看到以后为64位的文件了。 test@ubuntu:/usr/local/zend$ file ZendGuardLoader.soZendGuardLoader.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, stripped

June 15, 2022 · 1 min · jiezi

关于php框架:PHP使用SWX框架的RedisUML组件对用户信息进行缓存读写

前言官网地址:SW-X框架-专一高性能便捷开发而生的PHP-SwooleX框架 心愿各大佬举起小手,给小弟一个star:https://github.com/swoolex/swoolex 1、Redis-UML介绍Redis-UML组件,相似于游戏我的项目中的人物建模,它是一种面向单个数据表创立缓存模型的概念。 UML有着相似于Mysql-ORM的便捷操作语法反对,同时可自在开关数据更新时的缓存刷新标记,用于读取更新的缓存对象都有哪些,便于将缓存内容回写到数据库中。 2、UML的初衷因为我的项目业务须要用到Reids缓存Mysql的更热新数据,进而缩小数据库在update与select时对单个数据表的频繁操作(这样的高频操作往往会将整个Mysql服务器的性能拖垮)。 在以前,咱们通常会这样实现: $Db = new \x\Db();$Redis = new \x\Redis();// 先查询数据库$user = $Db->name('user')->where('id', 1)->find();// 再更新到redis$res = $Redis->hmset($key.$user['id'], $user);// 须要取得该数据缓存的时候,就须要这样拿$user = $Redis->hgetall($key.$user['id']);该过程看似没有问题,但却只实现了对update场景的优化,实际上并没有解决select的场景,因为往往在业务过程中,select占据了最频繁的地位。 因为一旦将update由缓存代替后,那Mysql查出的数据将不肯定是最新的,这时候只能依赖缓存查问,而传统的实现形式又只能实现单条缓存读取,就变成了上面的读取形式: $Db = new \x\Db();$Redis = new \x\Redis();// 先查数据库$list = $Db->name('user')->field('id')->where('region_code = 430000')>where('status = 1 OR status = 3')->select();// 再循环从Redis中取得foreach ($list as $k=>$v) { // 指定须要返回的字段名 $list[$k] = $Redis->hmget($key.$v['id'], ['id', 'name', 'phone']);}而UML组件的呈现,就是为了解决这样的业务场景。 $User = new \box\uml\User();// 查问条件$where = [];$where[] = ['region_code', '=', 430000];$where[] = ['status', '=', [1, 3]];// 间接从查问中取得$list = $User->where($where)->field('id, name, phone')->select();// 你还能够写成$list = $User->field('id, name, phone') ->where('status', 430000) ->where('region_code', [1, 3]) ->limit(1000) ->order('id DESC, phone ASC') ->select();3、UML的优缺点A、长处1、UML反对相似Mysql-ORM的查问语法,所以它能够很轻易的接替Mysql的大部分日常读写工作。2、UML是由多种Redis数据结构组合而成的查问组件,其中查问条件能够依据模型进行配置,相似于查问索引的概念,能够很好的管制缓存所占用的内存开销。3、UML反对开启缓存回写的数据标记,配合定时器组件很容易就能实现Redis 对 Mysql的缓存回写。B、毛病1、UML不具备事务性,与Redis的hset一样,当不开启Redis事务时,无奈保证数据的并发批改。2、UML的like含糊匹配时把记录集从缓存中检索进去后,再进行遍历匹配的,所以这块会存在肯定的内存开销,应用该查问反对时须要对前置的数据量进行肯定的限度,否则很可能会引起大量的内存开销。4、条件语法的优先级UML的查问逻辑跟Mysql-ORM的不同,语法存在执行优先级的关系。 ...

May 31, 2022 · 3 min · jiezi

关于php框架:PHP-基于-SWX-框架搭建WebSocket服务器一

前言官网地址:SW-X框架-专一高性能便捷开发而生的PHP-SwooleX框架 心愿各大佬举起小手,给小弟一个star:https://github.com/swoolex/swoolex 1、服务阐明在SW-X中,启动websocket服务时,会同时反对HTTP和WebSocket两种申请。 部署好SW-X源码后,批改/config/server.php配置文件中的host和port,0.0.0.0示意不限度内外网IP,port默认为9502端口。 实现后,应用php sw-x start websocket命令启动websocket服务即可。 若你应用云服务器,记得在服务商的控制台->平安组中凋谢对应的端口号;若你还装的是宝塔环境,还须要在宝塔面板的平安组中再凋谢一次端口才行,否则外网是无法访问的。2、事件反对家喻户晓,websocket的日常开发,至多都须要用到onOpen和onClose两种音讯事件。 在SW-X中,会对Swoole的事件进行转发定义,对立寄存在/box/event/server/目录下,文件名称为对应音讯的事件名。 失常状况WebSocket开发,只须要改变到以下3种事件: // 连贯握手时 [这里实现业务鉴权]onOpen.php// 接管到客户端音讯时 [少数状况下这里都不须要重写]onMessage.php// 敞开连贯时 [这里实现下线销毁]onClose.php3、WSS配置SW-X的WebSocket服务要开启WSS很简略,只须要批改/config/server.php配置项中的,ssl_cert_file、ssl_key_file证书门路即可。 4、承受客户端音讯当/config/server.php配置文件中的is_onMessage设置为true时【默认值】,则示意启用框架对onMessage事件进行监听解决,也代表启用框架对WebSocket的控制器解决。 也不倡议改为false,除非你对Swoole很相熟,同时也很相熟WebSocket服务的开发工作。SW-X对WebSocket服务的数据处理,仅反对固定格局的JSON数据包,同时反对配置启用对数据进行AES加解密,只须要批改/config/server.php中的配置项即可开启。 客户端数据包,未加密前的格局应该为: { "action":"申请路由", "data":申请数据}若是将is_onMessage设置为false时,框架则不再监听onMessage事件,改由开发者本人监听/box/event/server/下的onMessage事件,进而实现数据分包。 5、服务端推出音讯WebSocket控制器中输入返回值跟HTTP控制器一样,都是调用fetch()办法,只不过传入的参数格局不一样。 该办法按以下程序,共反对4个参数传递: $action 状态码$msg 阐明,默认值:success,非必填$data 后果集,默认值:[],非必填$fd 客户端的连贯标识符[Swoole的fd],默认值:以后客户端连贯,非必填若须要群发音讯时,须要循环调用该办法,传入客户端对应的$fd值即可。 fetch()最终推送进来的数据结构如下(未开启加密前): { "action":"状态值", "msg":"形容", "data":"返回值",}倡议用HTTP-API接口的交互方式,来解读SW-X对WebSocket服务的设计会比拟好了解一些。6、WebSocket控制器WebSocekt的控制器会依据action字段进行路由匹配,最终找到/app/websocket/目录下的控制器文件进行解决。 该目录下的事件控制器,都须要继承\x\controller\WebSocekt基类。 WebSocekt的路由解决,与HTTP服务的一样,如果你相熟SW-X的路由模式,上手将会非常简单。 例如,定义一个user/login的路由,只须要创立/app/websocket/user/login.php文件,并写入以下代码即可: namespace app\websocket\user;use x\controller\WebSocket;class login extends WebSocket{ // index是默认疏忽的办法 public function index() { return $this->fetch(200, '形容', []); }}7、承受客户端传递的data内容因为WebSocket服务是应用JSON格局进行数据交互,同时反对AES数据加解密,所以日常开发中,只能通过控制器提供的办法来取得客户端申请的表单内容,$this->param()办法用于取得解密后的data内容,返回值是一个多维数组。 例如,将下面的user/login路由文件,改成以下代码: namespace app\websocket\user;use x\controller\WebSocket;class login extends WebSocket{ // index是默认疏忽的办法 public function index() { // 接管申请参数 $param = $this->param(); // 推回给前端做显示 return $this->fetch(200, '别乱发货色', $param); }}8、简略调试SW-X官网提供了WebSocket的在线测试工具,具体地址:http://websocket.sw-x.cn/debu... ...

May 27, 2022 · 1 min · jiezi

关于php框架:MixPHP-V3-发布前的感想-有哪些变化和特点

最近把 MixPHP 逐渐重构到了 V3 版本,之前停更了很长时间,是因为始终在开发 MixGo ,回想起 V2~V2.2 版本中我做了很多尝试,其中特地是 V2.2 我十分激进的间接 all in 单线程协程,过后我是这样想的:MixPHP V2.1 为何从 Reactor+Manager+Worker 多过程改为单线程协程,然而切换后实际上带来了一些问题: 很多用户用了一些奇奇怪怪的第3方库,都是依赖 guzzle 和 curl 的,不论是 swoole hook curl 还是 mix/guzzle hook,总是偶然呈现申请失败,不稳固的状况,最初无奈只能用同步执行器解决。解决简单一些的 cli 后盾计算的时候,通道死锁问题比较严重,问题应该是 db pool 抛出的连贯被用户始终持有,导致死锁,这个是我 db 封装的设计没有思考好这种状况。当然下面也都不是解决不了的问题,前面大家也都解决了,只是带来了一些本不必要的麻烦,总体感触是其实多过程还是有多过程的意义,少了很多不必要的懊恼,很多人示意思念以前的 v1.1 的多进程同步模式,比方:敞开协程用同步模式的话,就兼容 composer 的全副生态,以上懊恼都没有,性能其实也不差。 太过理想化在最后开始设计 V2.2 时,其实我太理想主义了,我心田真的是想复制一个 php 版 golang 的,我本人开发了 mix/runtime 外面蕴含 Select 用来解决多通道,格调齐全与 Go 相似的 Context,Signal、Time 等根底库,然而理论应用时,因为 Swoole 和 Golang 的协程切换机制不同,导致死锁的问题非常容易呈现,最初无奈放弃了,当然我是做非常复杂的那种后盾计算类的需要,如果只是 http 开发 CURD 根本不会遇到。Swoole 还是在 API、WebSocket 等畛域比拟适合。 微服务在 V2.2 前期,我做了很多微服务的尝试,我开发了一个十分好用的 PHP gPRC 服务器开发库,我还把整个框架都接入到了 go-micro v1,v2 的生态中,简直能应用 micro 全副的工具链,难堪的是起初他们示意 v3 版本将全面 all in 云微服务。 ...

July 23, 2021 · 1 min · jiezi

关于php框架:值得推荐的十种PHP测试框架

作为一种能够反对测试和开发流动的工具与过程的汇合,框架往往蕴含了各种实用的程序库、可重用的模块、测试数据的设置、以及其余依赖项。目前,无论是针对Web的开发、还是测试,业界都有多种成熟框架,可供您依据理论需要进行抉择,进而进步团队的效率和生产力。而作为一个优良的框架,它通常须要具备如下长处: 保护具备良好定义的代码构造。提供能无效用于测试的可重用模块和库。可放慢测试的过程与效率。可防止代码的反复。可能剖析需要与测试的范畴。同时,在所有服务器端的编程语言中,有80%的网站用到了PHP。因而,咱们有必要对针对PHP测试框架发展深入研究,以不便采纳规范的格局,编写出各种测试用例。上面,我将和您一起探讨十种优良PHP测试框架是如何实现测试自动化,以及它们各自的优缺点。 1. PHPUnit作为一种面向程序员的最罕用PHP测试框架,PHPUnit非常适合单元测试。它是由Sebastian Bergmann开发的,属于xUnit框架体系结构中的一个实例。 装置步骤您须要当时装置PHP 7.3版或更高版本,能力装置PHPUnit的最新版本--PHPUnit 9.3。以下便是PHPUnit的装置步骤。 步骤1:从链接https://www.php.net/phar处下... Archive)。PHAR已将所有必须的PHPUnit依赖项,都捆绑在了一个文件中。步骤2:从链接https://getcomposer.org/处,装置Composer,以治理我的项目中的所有依赖项。应用PHPUnit框架的劣势作为公认好用的PHP框架之一,PHPUnit在进行自动化测试过程中具备如下劣势: 能够无效地剖析代码的覆盖率。通过深入分析,它能够生成蕴含有各种代码覆盖率信息的报告,或HTML/XML日志文件。在碰到无奈测试的代码块时,咱们能够应用诸如@codeCoverageIgnore、@codeCoverageIgnoreStart和@codeCoverageIgnoreEnd之类的正文,在执行代码覆盖率剖析过程中,疏忽某些代码块。当然,咱们也能够通过应用@covers正文,来指定某个代码块去执行代码覆盖率剖析。在执行测试用例时,某些测试可能无奈被执行到。对此,PHPUnit提供了一个可触发异样的接口。如上面的简略XML配置文件所示,咱们能够将所有测试组合到一个套件中,并在XML配置文件的帮助下触发运行。 2. Codeception因为具备易于应用和保护代码模块的能力,因而Codeception的应用范畴十分广。它提供了多个模块,能够在单个框架中反对验收测试、功能测试、以及单元测试。 装置步骤装置Codeception的先决条件为: 装置了PHP 5.6或更新版本。已启用了CURL。Codeception的具体装置步骤为: 步骤1:从官网—https://getcomposer.org/downl...,下载并装置用于治理PHP中各个依赖项的composer。它将有助于申明要在我的项目中应用到的代码库,并对其进行治理。步骤2:从Codeception的官网—https://codeception.com/insta...,下载并装置PHAR。应用Codeception框架的劣势作为“全栈测试框架”,您能够对指标利用进行单元测试、功能测试、以及验收测试。通过应用简略的命名规定,它可能帮助用户了解代码。其中,actions关键字可用于执行诸如:单击、按键、填充输出字段等用户操作;assertion关键字可用于执行验证,就像应用TestNG和Junit进行验证一样;而grabbers则可用于提取相干信息。提供了一种可在JBehave和Cucumber等行为驱动开发(BDD)中,运行用户故事(user stories)的选项。提供了各种对于数据库测试十分实用的数据库模块。具备WebDriver模块,可用于验收测试。具备对于Web服务测试十分实用的REST模块。为了验证JSON响应,该模块可从特定的JSON标签中提取数据,并验证各种XML响应。有助于与Jenkins和Teamcity等继续集成工具相集成,以帮助生成测试报告。3. Storyplayer作为一个开源的PHP测试框架,Storyplayer可用于执行端到端测试、API自动化测试、以及Web利用测试。因为它提供了对于多种编程语言的反对,因而堪称优良PHP框架之一。 装置步骤步骤1:目前,它仅反对Apple OSX Yosemite和Ubuntu Linux Desktop 14.10,而且须要当时装置PHP。步骤2:依据其官网要求(https://datasift.github.io/st...,须要额定增加诸如CURL、JSON、OpenSSL等扩大。应用Storyplayer框架的长处提供了无需订阅或付费的开源框架服务,这对于PHP的Web开发老手来说,十分实用。反对多种编程语言,让PHP的开发和测试更加容易。4. SeleniumHQ作为已被宽泛应用的Web利用自动化测试框架,Selenium蕴含四个次要组成部分: Selenium IDESelenium RCSelenium WebDriverSelenium Grid因为这些组件是为特定目标设计的,因而您能够依据理论测试需要进行抉择。 装置步骤除了具备最新版本的PHP,您还须要装置Composer,以治理我的项目的所有依赖项。 应用Selenium框架的劣势属于开源的自动化测试框架。具备雄厚的反对社区。易于装置,且易于实现对用例的测试。能够在诸如Google Chrome、Firefox、Safari、Internet Explorer等不同的浏览器中运行测试用例,并可能无缝地测试浏览器的兼容性。可在多个浏览器和操作系统中,并行且疾速地执行测试脚本。可用于执行与浏览器对应的键盘和鼠标之间的交互。5. Behat作为一种PHP行为驱动开发(BDD)框架,Behat专为PHP而构建,并蕴含了许多外围的PHP模块。因为其代码是用简略的英语编写而成,因而它可能被我的项目的各个成员疾速地了解。咱们通常将BDD构造里蕴含的上下文、操作和后果,统称为Gherkin。以下便是应用Gherkin语言编写的简略测试示例: 这些案例能够被写入具备可扩大性能的文件中(如loginTest.feature)。Behat能够从命令行来运行测试,就像通过各种功能性的文件,来测试应用程序一样。 装置步骤步骤1:装置PHP的最新版本。步骤2:装置Composer,以治理我的项目的所有PHP依赖关系。步骤3:如果没有Composer,则能够通过下载最新版本的behat.phar(https://github.com/Behat/Beha...。应用Behat框架的长处属于一种开源的测试框架。易于装置和施行。因为全副是由BDD和简略英语(蕴含Given、When和Then关键字的Gherkin语言)编写,因而我的项目成员易于了解需要和测试用例之间的映射关系。各种场景可被写入功能性文件中,并由上下文、后续操作和最终预期后果所组成。其具体的文档可帮忙用户轻松地实现该框架。6. Atoum作为一款较新的PHP测试框架,Atoum岂但简略,而且易于实现单元测试。因为提供了诸如内联引擎、隔离引擎、以及并发引擎等各种内置的执行引擎,因而Atoum能够在不同的流程中,并行运行各种测试用例。此外,Atoum通过提供模仿构建的高级性能,来最大水平地缩小测试运行中的依赖性。 装置步骤在装置与设置Atoum框架之前,请确保您的零碎中已装置了最新版本的PHP。 步骤1:从其官方网站--https://getcomposer.org/处下...。步骤2:通过链接--https://atoum.readthedocs.io/...。步骤3:从Git存储库http://github.com/atoum/atoum...。应用Atoum框架的劣势易于编写测试用例。提供了多个易于无效实现测试用例的断言(assertions)。通过疾速实现模仿测试,能够缩小解析依赖关系时的等待时间,进而进步测试的执行效率。可生成不同格局的测试报告。可与Jenkins、Travis CI、CircleCI等继续集成工具相整合。可通过增加如下插件,让测试框架更加稳固。a. json-schema-extension — 用于验证JSON标签。 b. bdd-extension — 用于编写具备更好可读性的BDD款式测试用例。 c. reports-extension - 用于编写代码覆盖率的报告。 7. SimpleTest作为一款PHP测试框架,SimpleTest可用于单元测试、Web测试、以及PHP Web开发的模仿对象。该框架通过内置的Web浏览器,以不便用户导航到不同的网页上,并发展测试。 装置步骤步骤1:在零碎中装置最新版本的PHP。步骤2:从其官网http://simpletest.sourceforge...。应用SimpleTest框架的劣势对于单元测试十分实用。为表单、SSL、框架、代理、以及根本身份验证,提供了宽泛的反对。具备内置的Web浏览器,可帮助测试Web利用。可通过浏览器,以及命令行来执行测试。可与PHPUnit一起应用。具备高度的灵活性,并可能自定义输入后果。8. phpspec作为另一种行为驱动开发(BDD)框架,phpspec可用于编写和执行由PHP编写的测试。它可能依据开发人员提供的标准,来驱动设计。 装置步骤步骤1:在零碎中装置PHP 5.6或7。步骤2:依据链接--http://www.phpspec.net/en/sta...,正确地设置Composer,以主动治理所有我的项目的依赖项,否则phpspec将无奈检测到各种类。步骤3:应用命令“composer require –dev Phpspec/Phpspec”,来装置phpspec。之后,您会留神到所有依赖项都已被胜利装置在vendor文件夹中。对应的可执行文件也在vendor/bin/phpspec中。应用phpspec框架的劣势可能主动为类和办法生成框架。具备一个模仿框架。提供了14个内置的匹配器,其中包含常见的比拟匹配器、近似匹配器、类型匹配器、以及标识匹配器等。它们能够验证和形容测试的后果。能够通过配置诸如DataProvider插件、框架集成插件、代码生成插件、以及Matchers插件等,来扩大该框架。9. PeridotPeridot常被开发人员誉为可应用BDD办法的优良PHP框架之一。在Peridot中用describe-it语法编写出的测试代码(见如下代码段),岂但易于浏览和了解,而且便于保护。因为可能疾速地加载,且能并行地运行测试套件,因而它被认为是PHP框架中最快的一款。 装置步骤步骤1:应用Composer来轻松装置该框架,并治理我的项目中的依赖项。步骤2:可通过链接--https://peridot-php.github.io...,来下载并手动装置PHAR。应用Peridot框架的劣势可应用相熟的describe-it语法,来创立清晰易读的测试语言。为了实现扩大,可应用各种类型的插件来自定义测试框架。可通过与WebDriver和数据库相集成,来执行测试。与上述Phpspec和PHPUnit之类的PHP框架相比,Peridot属于轻量级和疾速型。可协助执行高速集成测试。提供针对不同特定目标而专门设计的报告器,如:Peridot List报告器、并发报告器、以及代码覆盖率报告器等。10. Kahlan作为一种单元式和行为驱动的PHP框架,Kahlan应用了相似于Peridot的describe-it语法。无需任何PHP扩大,咱们即可将该开源框架作为补丁植入(stub)代码。 ...

March 22, 2021 · 1 min · jiezi

关于php框架:php中yaf框架的配置

在理解了yaf框架的根本内容后,咱们能够发现这是一种性能较高的框架应用,那么在下载和装置好了后,还要对框架进行肯定的配置。本篇带来了两个配置的方面,一个是惯例的环境配置,另一个是引入常量的定义,置信大家对这两种配置办法都是迫切需要的,上面咱们来看具体的办法吧。1、配置运行环境在配置php反对yaf的时候,能够设置一个参数yaf.environ:把本地开发设置成develop、测试环境配置成test、生产环境配置成product,如: [yaf]extension=yaf.soyaf.use\_namespace=1通过设置运行环境,在我的项目中能够通过ini\_get(‘yaf.environ’)获取环境参数,进而取到相应节的配置。2、配置能够引入php常量定义Yaf 的必须配置,其实就是个目录设定 Ini代码 珍藏代码 yaf.directory=APP\_PATH application.directory=APP\_PATH也就是 appliction 脚本目录,至多要定义一个,application.directory优先。以上就是php中yaf框架配置的办法,大家在装置好框架后,也赶快动起手来把配置方面进行欠缺,之后就能够展现无关yaf框架的应用了。

March 4, 2021 · 1 min · jiezi

关于php框架:对PHP框架一些新的见解快速开发那点事

只有一提起到PHP框架,就会呈现Laravel、Yii、CodeIgniter、Zend、ThinkPHP等等一些介绍、一些排名、一些比照。特地在国内,Laravel和ThinkPHP有着很大的争议,Laravel永居榜首,TP性能高出Laravel多少多少倍,并拿出一些解析图等等。框架的抉择没有最好的框架,只有最适宜本人的框架如果拿一部分性能去掂量或抉择框架,那么Java笑了如果要选最容易了解的框架,那非ThinkPHP莫属了,简直不须要思考它就能懂你,而后依照你的套路玩上来Laravel思维散发,不易了解,一旦了解,能力真正的领会其中的优雅,但在领会优雅的过程中会比拟煎熬,从刚开始呈现不是我想要的变成了这就是我想要的,它尽管有点臃肿,但它的灵活性是十分不错的,简直不必本人再造些轮子或修补些破绽,稳定性毋庸置疑,所以这才是它 TOP ONE 的真正起因疾速开发是PHP的实质很多人都为PHP的前景担心,有反对也有拥护,局面一发不可收拾,但反过来想想,争执越强烈就代表有更多的人关注,所以不要低估了PHP的实力PHP在性能方面的确有些短板,但性能真的差的不可承受吗?显然不是的,最重要的起因还是开发者自身技能,并且PHP也在一直降级进化,Swoole曾经证实了PHP的出发点,尽管不是很欠缺,但至多方向是对的,负载平衡、缓存等都是进步PHP的运行性能然而,PHP的真正意义是疾速开发,这是PHP从一开始就为本人定下的标签,目前仍是无可替代,无论你用哪种风行的PHP框架开发,开发速度都是杠杠的疾速开发该如何抉择PHP框架的抉择曾经成为了一个定局,抉择时都在几个风行的框架中打转,尽管有些PHPER本人造轮子造了些框架,并且各种疾速开发手册,甚至有一些开源我的项目模板,看得多了就会发现抉择其中一个作为开发都比拟艰巨,并且有些开发并不疾速,所以基本上还是老老实实的抉择风行的几个框架本人在下面造轮子还快些那么有没有真正意义上的PHP疾速开发框架?TPHP框架(http://www.tphp.com)很有可能就是你所想要的答案TPHP框架特点居于Laravel中的Composer依赖开发, 所以TPHP不属于真正意义上的框架,它属于疾速开发的一种解决办法代码简洁并极易了解,除了开发疾速外,保护起来也非常简单,高深莫测就能找出须要批改的代码门路JS、CSS开发更简略,无需进行繁冗的目录切换,并反对SCSS后盾内置代码编辑器编写代码反对数据库类型:Mysql、Sqlserver、Sqlite、PostgreSQL反对不同类型数据库之间的数据互相关联查问反对数据库字段同步(不同步数据),步骤:先备份数据库字段到本地文件 -> 文件更新到服务器 -> 服务器还原字段到数据库后盾用户菜单权限治理TPHP框架常见问题不能以失常的MVC思维去对待TPHP框架,可能会或多或少的不习惯TPHP框架目录构造有点像原生状态,可能对于习惯于应用命名空间类或函数有点不习惯因为TPHP开发切实是太简略了,无奈展示代码的复杂度,所以对于喜爱高级玩法的PHPER不太习惯总之,只有“放低”本人,能力领会TPHP的魅力

August 30, 2020 · 1 min · jiezi

QueryPHP-V1beta6-新增-400-单元测试全量覆盖

QueryPHP v1.0.0-beta.6,这个版本主要进行单元测试收尾工作,新编写 400 例单元测试用例,除了我们选择主动忽略的、无法测试的代码和一部分 Swoole 的代码,整个产品实现百分之百覆盖。核心库 framework 单元测试用例 3410, 断言 13556,10659 行源代码被覆盖。 QueryPHP 坚持自己的路线,less is more, 不追求炫酷庞大的功能,产品的稳定性、可持续维护性是我们整个项目最为看重的,这也是我们进行大量地编写单元测试进行自动化测试最为直接的动力。 travis-ci coveralls 关于 QueryPHP QueryPHP 是一款现代化的高性能 PHP 渐进式协程框架, 我们还是主要面向传统 PHP-FPM 场景,以工程师用户体验为历史使命,让每一个 PHP 应用都有一个好框架。 百分之百单元测试覆盖直面 Bug,基于 Zephir 实现框架核心常驻,依托 Swoole 协程提升业务性能,此刻未来逐步渐进。 我们的愿景是 USE LEEVEL WITH SWOOLE DO BETTER, 让您的业务撑起更多的用户服务。 github.com/hunzhiwange/queryphpgitee.com/dyhb/queryphpqueryphp.com更新日志【framework】新增 400 单元测试全量覆盖,进一步缩减了 BUG 生存空间。【framework】由于不完整,删除 Swoole RPC 实现,保留了 Http,Websocket,未来 1.0 正式版本重新设计【framework】通过测试 修复 HTTP 组件,请求,响应等 BUG【framework】修复数据库重连错误极端异常情况兼容【framework】数据库查询和执行去掉了重复代码,抽象了一些公共代码,精简了代码【framework】修复了 ORM 关联查询源数据为空的判断,重构了关联模型作用域实现,并精简了代码【framework】修复数据库工作单元 UnitOfWork的 bug 和精简代码【framework】关联模型,改进 ORM 关联查询源值为空的特殊处理 ,不再执行后续查询,减少数据库查询,修复嵌套关联预载入查询未执行到的 BUG。【framework】ORM 实体 Entity 的 toArray() 支持关联属性读取,并支持 SHOW_PROP_NULL 返回自定义默认返回数据【framework】修复更新实体一些错误,抽离公共代码。【framework】其它若干修复项目,主要写测试用例时发现的问题。【application】修复 debugbar 错误,兼容 Swoole HttpServer 和 PHP 内置 WebServerRoadMap【framework】Beta 6 是 QueryPHP 的最后一个 beta 版本,整个框架功能冻结。【framework】RC 版本只修复 BUG、单元测试 和文档完善,不排除可能有一些必要的功能微调。【framework】v1.0.0 正式版本将随 PHP 7.4 版本后发布 php74,计划是在 2019.12 发布。联系方式www.queryphp.com ...

October 17, 2019 · 1 min · jiezi

QueryPHP-V1beta5-改进-ORM-设计体验

QueryPHP v1.0.0-beta.5,这个版本主要改进 ORM 和大量细节的优化。 关于 QueryPHPQueryPHP 是一款现代化的高性能 PHP 渐进式协程框架, 我们还是主要面向传统 PHP-FPM 场景,以工程师用户体验为历史使命,让每一个 PHP 应用都有一个好框架。 百分之百单元测试覆盖直面 Bug,基于 Zephir 实现框架核心常驻,依托 Swoole 协程提升业务性能,此刻未来逐步渐进。 我们的愿景是 USE LEEVEL WITH SWOOLE DO BETTER, 让您的业务撑起更多的用户服务。 https://github.com/hunzhiwange/queryphphttps://gitee.com/dyhb/queryphphttps://www.queryphp.com更新日志[ framework ] 将底层 redis 服务拆分出来注册到 IOC 容器,可以方便使用[ framework ] 删除自己的 dd,dump 调试函数,Symfony 自带不需要再弄了。[ framework ] 移除全局函数 app,hl 助手函数,由静态代理实现 App (别名 Leevel )来,App::path(),App 可以访问 IOC 容器中的方法 App::make('request')。[ framework ] 删除代理中所有接口设计 LeevelKernelProxyIApp,删除组件中的 Proxy 改为用 @method 来实现 IDE helper,并内置一个用于生成这样的命令工具自动生成。[ framework ] 优化 swoole 热重载代码,利于测试。[ framework ] 改进系统异常处理组件,例外将系统内置异常改为 abstract 方便业务层继承更好地处理异常,添加一个业务处理异常 LeevelKernelExceptionBusinessException,异常响应也会经过中间件处理。[ framework ] 核心 kernel 和路由支持对 CORS options 请求的处理,利用自定义中间件轻松处理跨域访问问题,分拆路由绑定解析方法,代码更清。[ framework ] 关联模型,改进 ORM 关联查询源值为空的特殊处理 ,不再执行后续查询,减少数据库查询,修复嵌套关联预载入查询未执行到的 BUG。[ framework ] 为各个组件助手函数添加一个静态访问,分拆各个组手函数到单独的文件方便 f 调用。[ framework ] 实体添加更多 const 如 CONSTRUCT_PROP_WHITE,MIDDLE_SOURCE_KEY 避免写死实体一些约定的名字。[ framework ] 数据库 PDO 查询改进,该是数字就返回数字,不再全部返回字符串。[ framework ] 查询新增 where('foo', '=', null) 的支持,处理非常特殊场景,以前直接报错。[ framework ] 改进实体 LeevelDatabaseDddEntity toArray 设计,现在 null 会被自动忽略。[ framework ] 改进实体 LeevelDatabaseDddEntity 属性相关设计,withProps,withProp,hasProp,prop 来访问。[ framework ] 改进实体 LeevelDatabaseDddEntity 软删相关设计,实体一旦定义了 const DELETE_AT,系统查询和删除自动走软删除,也可以通过 withSoftDeleted 查询包含软删除的数据,forceDelete 强制删除。[ framework ] 改进实体 LeevelDatabaseDddEntity 中查询实体 find($id) 改为 findEntity($id),更容易理解,例外可以避免和 find() 查询使用理解冲突。[ framework ] 修复 redis 连接池连接 PHPRedis 驱动支持重连。[ framework ] 验证器会自动转换验证参数 not_between:1,5 为 int 和 float,以便于框架实现统一强类型的整体方向,例外数据库唯一验证 LeevelValidateUniqueRule 自动识别 int 和 float,避免数据库查询出现转换类型而不再走索引影响性能。[ framework ] 改进实体 LeevelDatabaseDddEntity 查询设计,去掉查询魔术方法,__call 和 ___callStatic 被屏蔽,所有查询均需要通过静态入口 select(别名 find,保留致敬 QeePHP 习惯),meta(保留致敬 QeePHP 习惯) 发起,再加上 ide helper 支持,完美支持 IDE。[ framework ] 改进实体连接 LeevelDatabaseDddEntity 切库设计,由每个实体自己实现 withConnect 和 connect,更加更新可控,提供切库安全沙盒 connectSandbox。[ framework ] 采用 PDOStatement->debugDumpParams() 来获取最后的 SQL,同时在 debug 组件将 SQL 写入日志方便开发调试。[ framework ] 事务工作单元 LeevelDatabaseDddUnitOfWork 增加对软删除」强删除的支持,也包含仓储对此的调整 LeevelDatabaseDddRepository。[ framework ] 修复并删除实体和仓储中的 flushed 已经刷新过数据的特性,无必要。[ tests ] 新增 10 多例 swoole 相关的测试用例和其它,单元测试 3080 多例,断言 10000+。[ application ] QueryPHP 自身是一个基于 IViewUI 的标准后台,自带基于资源的权限系统,由于框架大量调整后台做了相应的调整以便于运行。[ application ] QueryPHP 自身是主要引入 Workflow 来改善 curd 操作,将查询并入到服务中来。RoadMap[ framework ] Beta 4-6 主要是对 Swoole 4 最新版的协程完善支持。[ framework ] RC 版本会冻结计划功能,只修复 BUG、单元测试 和文档完善。[ framework ] v1.0.0 正式版本将随 PHP 7.4 版本后发布 https://wiki.php.net/todo/php74,计划是在 2019.12 发布。联系方式https://www.queryphp.com ...

September 10, 2019 · 2 min · jiezi

Swoft-203-重大更新发布优雅的微服务治理

什么是 Swoft ?Swoft 是一款基于 Swoole 扩展实现的 PHP 微服务协程框架。Swoft 能像 Go 一样,内置协程网络服务器及常用的协程客户端且常驻内存,不依赖传统的 PHP-FPM。有类似 Go 语言的协程操作方式,有类似 Spring Cloud 框架灵活的注解、强大的全局依赖注入容器、完善的服务治理、灵活强大的 AOP、标准的 PSR 规范实现等等。 Swoft 通过长达三年的积累和方向的探索,把 Swoft 打造成 PHP 界的 Spring Cloud, 它是 PHP 高性能框架和微服务治理的最佳选择。 优雅的服务治理Swoft 官方建议开发者使用 Service mesh 模式,比如 Istio/Envoy 框架,把业务和服务治理分开,但是 Swoft 也为中小型企业快速构建微服务提供了一套微服务组件。 服务注册与发现服务熔断服务限流配置中心服务注册与发现服务注册与发现,需要用到 Swoft 官方提供的 swoft-consul 组件,如果其它第三方也类似。 注册与取消服务监听 SwooleEvent::START 事件,注册服务 /** * Class RegisterServiceListener * * @since 2.0 * * @Listener(event=SwooleEvent::START) */class RegisterServiceListener implements EventHandlerInterface{ /** * @Inject() * * @var Agent */ private $agent; /** * @param EventInterface $event */ public function handle(EventInterface $event): void { /* @var HttpServer $httpServer */ $httpServer = $event->getTarget(); $service = [ // .... ]; $scheduler = Swoole\Coroutine\Scheduler(); $scheduler->add(function () use ($service) { // Register $this->agent->registerService($service); CLog::info('Swoft http register service success by consul!'); }); $scheduler->start(); }}监听 SwooleEvent::SHUTDOWN 事件,取消服务 ...

July 9, 2019 · 3 min · jiezi

openadmyii2admin模块化开发骨架

introduceopenadm/yii2-admin 基于yii2-extension包管理和adminlte2主题构建的admin项目,包括了基础的用户管理,RBAC管理,扩展管理等核心功能。 去OpenADM(https://openadm.com)查看更多 SourceSource https://gitee.com/openadm/yii2-admin.git InstallationThe preferred way to install this extension is through composer. Either run php composer.phar require --prefer-dist openadm/yii2-admin "*"or add "openadm/yii2-admin": "*"to the require section of your composer.json file. UsageOnce the extension is installed, simply use it in your code by : Migrate DB./yii migrate --migrationPath=@openadm/admin/migrations./yii migrate all -p=@tecnocen/oauth2server/migrations/tablesscreenshots

July 6, 2019 · 1 min · jiezi

在Repository模式下使用laravel

laravel-repository仓库地址Github Repository文档地址 清晰的目录结构Models只负责定义模型(如:模型关联,scope,get和set attribute等)Repository负责处理这个表相关的所有业务逻辑, 不只是注入model, 相关的redis任何cache都可以注入,代码定位迅速Controllers 只负责处理简单的逻辑,获取转发数据,它应该是 简洁干净 的AppHttpControllerAdmin IndexControllerUserControllerConfigController...Request(所有的request验证类)Admin Index StoreRequestUpdateRequestDestroyRequestUser ...Config ...Request.phpModels (所有的model模型)User(用户相关的所有模型)User.phpUserExt.phpUserMessage.phpConfigConfig.php...BaseModel.phpRepositories (目录结构应与model一致,结构清晰)User(用户相关的所有仓库)UserRepository.phpUserExtRepository.phpUserMessageRepository.php...安装并使用composer require littlebug/laravel-repositorymkdir app/Http/Requests# 创建属于你自己的Request验证基类# 就像下面这个文件关于一键生成代码# 在将命令注入到你的laravel 项目以后# 输入php artisan list# 如果你看到下面这些提示,那么可以开始快速生成代码了!~ core core:controller 生成 Controller {--table=} 指定表名称 [ 指定该参数会通过表生成视图文件 ] {--name=} 指定名称 可以带命名空间 [ --name=Home/IndexController 或者 Home\\IndexController ] {--r=} 指定 Repository 需要从 Repositories 目录开始; 默认使用控制器同名 Repository {--request=} 指定 request 目录; 需要从 Requests 目录开始; 默认使用控制器命名空间 {--pk=} 指定主键名称,默认id core:generate 生成 controller|model|repository|request|views {--table=} 指定表名称 [ 支持指定数据库,例如:log.crontabs ] {--path=} 指定目录 [ 没有传递绝对路径,否则使用相对对路径 从 app/Models 开始 ] {--model=} model名称 默认生成使用表名称生成 core:model # 让我们来试一下# 在commands帮助文档的提示下生成代码# 如果你的项目用到了数据库前缀,不要忘了去database.php中添加,否则会找不到table# 举个栗子,以member_message表为例php artisan core:generate --table=member_message --path=Member --controller=Member/MemberMessageController# 在终端中你可以看到下面的结果文件 [ /Users/wanchao/www/lara-test/app/Models/Member/MemberMessage.php ] 生成成功文件 [ /Users/wanchao/www/lara-test/app/Repositories/Member/MemberMessageRepository.php ] 生成成功文件 [ /Users/wanchao/www/lara-test/app/Http/Requests/Member/MemberMessage/UpdateRequest.php ] 生成成功文件 [ /Users/wanchao/www/lara-test/app/Http/Requests/Member/MemberMessage/DestroyRequest.php ] 生成成功文件 [ /Users/wanchao/www/lara-test/app/Http/Requests/Member/MemberMessage/StoreRequest.php ] 生成成功# 添加路由 routes/web.phpRoute::group(['namespace' => 'Member','prefix' => 'member'], function ($route) { $route->get('index', 'MemberController@indexAction'); $route->get('message', 'MemberMessageController@indexAction');});### 修改MemberMessageController### 在MemberMessageController中dd打印数据public function index(){ $filters = Helper::filter_array(request()->all()); $filters['order'] = 'id desc'; $list = $this->memberMessageRepository->paginate($filters); dd($list);}# 终端php artisan servevist localhost:8001/member/message# 你应该尝试一些你的数据库中存在的表,而不是机械的去复制粘贴我的栗子 ...

June 5, 2019 · 1 min · jiezi

PHP-CI框架中如何实现类库的自动加载及别名逻辑处理

缘由app/controllers/Index.php中有如下代码 public function disable(){ $this->yredis->set('name','tb'); var_dump($this->yredis->get('name')); $this->load->view('welcome_message'); }发现这个yredis没有load,怎么来的?翻翻手册,有自动加载配置 在app/config/autoload.php中配置,部分内容如下| -------------------------------------------------------------------| Auto-load Libraries| -------------------------------------------------------------------| These are the classes located in system/libraries/ or your| application/libraries/ directory, with the addition of the| 'database' library, which is somewhat of a special case.|| Prototype:|| $autoload['libraries'] = array('database', 'email', 'session');|| You can also supply an alternative library name to be assigned| in the controller:|| $autoload['libraries'] = array('user_agent' => 'ua');*/$autoload['libraries'] = array('Yredis','validation'=>'sn');追踪一下那么他这个自动加载是在代码内如何实现的呢?肯定从头$CI大对象来看。在system/core/Controller.php中,有如下两句 $this->load =& load_class('Loader', 'core');$this->load->initialize();通过common.php中定义的load_class方法,加载了loader类。不得不说ci的核心基本都在这里了。(这个load还是个未定义的属性么...?)我们去看system/core/Loader.php中的initialize方法,不过load的时候肯定是先加载__construct方法 ...

May 28, 2019 · 5 min · jiezi

SOLID-PHP-面向对象设计的五个基准原则

S.O.L.I.D 是 首个 5 个面向对象设计(OOD) 准则的首字母缩写 ,这些准则是由 Robert C. Martin 提出的, 他更为人所熟知的名字是 Uncle Bob。 这些准则使得开发出易扩展、可维护的软件变得更容易。也使得代码更精简、易于重构。同样也是敏捷开发和自适应软件开发的一部分。 备注: 这不是一篇简单的介绍 "欢迎来到 _S.O.L.I.D" 的文章,这篇文章想要阐明 S.O.L.I.D 是什么。 S.O.L.I.D 意思是:扩展出来的首字母缩略词看起来可能很复杂,实际上它们很容易理解。 S - 单一功能原则O - 开闭原则L - 里氏替换原则I - 接口隔离原则D - 依赖反转原则接下来让我们看看每个原则,来了解为什么 S.O.L.I.D 可以帮助我们成为更好的开发人员。 单一职责原则缩写是 S.R.P ,该原则内容是: 一个类有且只能有一个因素使其改变,意思是一个类只应该有单一职责.例如,假设我们有一些图形,并且想要计算这些图形的总面积.是的,这很简单对不对? class Circle { public $radius; public function construct($radius) { $this->radius = $radius; }}class Square { public $length; public function construct($length) { $this->length = $length; }}首先,我们创建图形类,该类的构造方法初始化必要的参数.接下来,创建AreaCalculator 类,然后编写计算指定图形总面积的逻辑代码. class AreaCalculator { protected $shapes; public function __construct($shapes = array()) { $this->shapes = $shapes; } public function sum() { // logic to sum the areas } public function output() { return implode('', array( "", "Sum of the areas of provided shapes: ", $this->sum(), "" )); }}AreaCalculator 使用方法,我们只需简单的实例化这个类,并且传递一个图形数组,在页面底部展示输出内容. ...

May 28, 2019 · 3 min · jiezi

10Laravel使用视图组合器View-composer

Laravel的视图组合器很有用,在网站中, 许多页面的侧边栏是相同的,将侧边栏公用部分提取出来肯定是必须的,让Controller专注于业务逻辑。参考:https://laravel.com/docs/5.5/... 创建一个新的 provider类 ViewComposerServiceProvider php artisan make:provider ViewComposerServiceProvider编辑ViewComposerServiceProvider类文件中boot()方法,添加如下代码: // 参数'*':代表所有视图,其实可以指定视图// \App\Http\ViewComposers\PublicComposer:公共视图的业务逻辑,目录位置是自定义的\View::Composer('*', '\App\Http\ViewComposers\PublicComposer');在ConfigApp.php配置文件中配置$provider数组,加入自定义的ViewComposerServiceProvider类 App\Providers\ViewComposerServiceProvider::class编辑AppHttpViewComposersPublicComposer类文件中compose()方法,添加公共数据的业务逻辑 public function compose(View $view){ $categories = $this->cache('categories', function () { // 获取数据逻辑 return Category::all(); }, 60*24); $tags = $this->cache('tags', function () { return Tag::all(); }, 60*24); // $view->with()方法绑定参数到视图 $view->with(compact('categories', 'tags'));}// 封装一个缓存处理方法private function cache($key, $callback, $time = 60){ $key_result = []; if (Cache::has($key)) { $key_result = Cache::get($key); } else { $key_result = $callback(); Cache::put($key, $key_result, $time); } return $key_result;}原文链接:http://www.mi360.cn/articles/4 ...

May 21, 2019 · 1 min · jiezi

php框架

php框架的功能通用的路由,autoload。服务端mysql封装,日志组件。前端的页面渲染(smarty封装个)。在工作时听说别的部门换框架性能提升,所以调研了下常见的框架,包含ci,laravel,yii,yaf,会介绍下功能,另外给出号称最快框架yaf和常用yii和裸写框架的性能差。另外想实现rpc并发,http一般用过multi_curl可以,公司用的thrift没有实现并发,所以研究了下php协程,curl_multi,swoole异步,rpc中并发实现,corotine等。 常用php框架提供功能ci http://codeigniter.org.cn/use...Route.autoload.载入文件正常controller 中load->helper(xx)。直接用xxlog. db. hook.公共函数。代码逻辑分层。ui抽象。模板。laravel https://laravel-china.org/doc...路由中间件(前置后置)配置区分环境,本地和线上密码不放其中数据库 创建表,编辑,删除,迁移,回滚,软删除和恢复(标记删除位)。ORM链式操作依赖注入,依赖自动发现。 IOC 平时的if new 这种工厂模式,IoC模式看作工厂模式的升华,以前在工厂模式里写死了的对象,IoC模式 改为配置XML文件,这就把工厂和要生成的对象两者隔离类(DatabaseQueue,queue,QueueContract),serviceprovider(外部调这个)=>bind(将类绑定到容器)。调用Queue::xx。依赖注入可以直接调用$类->method。通过门面可以类::method【https://www.cnblogs.com/shiwenhu/p/6882340.html】事件 事件映射protected $listen = [ 'App\Events\OrderShipped' => [ 'App\Listeners\SendShipmentNotification', ],];写事件OrderShipped,写监听SendShipmentNotification 可以继承队列分发事件:public function ship($orderId) { order=Order::findOrFail(orderId); // 订单的发货逻辑... event(new OrderShipped($order)); }队列 ,redis,db, 广播js监听任务调度 只是cron yii https://www.yiichina.com/doc/... 功能全面读介于ci和laravel之间,前端支持功能丰富。组件和行为是它的特色行为 要定义行为,通过继承 yii\base\Behavior 。覆盖其中的events方法,public function events() { return [ ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate', ]; } public function beforeValidate($event) { // 处理器方法逻辑 },附加行为:lass User extends ActiveRecord { public function behaviors() { return [ [ 'class' => MyBehavior::className(), 'prop1' => 'value1', 'prop2' => 'value2', ]]}}或者attach组件就可以使用行为了。直观感受下 ...

May 15, 2019 · 3 min · jiezi

为什么我们需要 Laravel IoC 容器?

IOC 容器是一个实现依赖注入的便利机制 - Taylor Otwell文章转自:https://learnku.com/laravel/t…Laravel 是当今最流行、最常使用的开源现代 web 应用框架之一。它提供了一些独特的特性,比如 Eloquent ORM, Query 构造器,Homestead 等时髦的特性,这些特性只有 Laravel 中才有。我喜欢 Laravel 是由于它犹如建筑风格一样的独特设计。Laravel 的底层使用了多设计模式,比如单例、工厂、建造者、门面、策略、提供者、代理等模式。随着本人知识的增长,我越来越发现 Laravel 的美。Laravel 为开发者减少了苦恼,带来了更多的便利。学习 Laravel,不仅仅是学习如何使用不同的类,还要学习 Laravel 的哲学,学习它优雅的语法。Laravel 哲学的一个重要组成部分就是 IoC 容器,也可以称为服务容器。它是一个 Laravel 应用的核心部分,因此理解并使用 IoC 容器是我们必须掌握的一项重要技能。IoC 容器是一个非常强大的类管理工具。它可以自动解析类。接下来我会试着去说清楚它为什么如此重要,以及它的工作原理。首先,我想先谈下依赖反转原则,对它的了解会有助于我们更好地理解 IoC 容器的重要性。该原则规定:高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。一言以蔽之: 依赖于抽象而非具体class MySQLConnection{ /** * 数据库连接 / public function connect() { var_dump(‘MYSQL Connection’); }}class PasswordReminder{ /* * @var MySQLConnection / private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->dbConnection = $dbConnection; }}大家常常会有一个误解,那就是依赖反转就只是依赖注入的另一种说法。但其实二者是不同的。在上面的代码示例中,尽管在 PasswordReminder 类中注入了 MySQLConnection 类,但它还是依赖于 MySQLConnection 类。然而,高层次模块 PasswordReminder 是不应该依赖于低层次模块 MySQLConnection 的。如果我们想要把 MySQLConnection 改成 MongoDBConnection,那我们就还得手动修改 PasswordReminder 类构造函数里的依赖。PasswordReminder 类应该依赖于抽象接口,而非具体类。那我们要怎么做呢?请看下面的例子:interface ConnectionInterface{ public function connect();}class DbConnection implements ConnectionInterface{ /* * 数据库连接 / public function connect() { var_dump(‘MYSQL Connection’); }}class PasswordReminder{ /* * @var DBConnection */ private $dbConnection; public function __construct(ConnectionInterface $dbConnection) { $this->dbConnection = $dbConnection; }}通过上面的代码,如果我们想把 MySQLConnection 改成 MongoDBConnection,根本不需要去修改 PasswordReminder 类构造函数里的依赖。因为现在 PasswordReminder 类依赖的是接口,而非具体类。如果你对接口的概念还不是很了解,可以看下 这篇文章 。它会帮助你理解依赖反转原则和 IoC 容器等。现在我要讲下 IoC 容器里到底发生了什么。我们可以把 IoC 容器简单地理解为就是一个容器,里面装的是类的依赖。OrderRepositoryInterface 接口:namespace App\Repositories;interface OrderRepositoryInterface { public function getAll();}DbOrderRepository 类:namespace App\Repositories;class DbOrderRepository implements OrderRepositoryInterface{ function getAll() { return ‘Getting all from mysql’; }}OrdersController 类:namespace App\Http\Controllers;use Illuminate\Http\Request;use App\Http\Requests;use App\Repositories\OrderRepositoryInterface;class OrdersController extends Controller{ protected $order; function __construct(OrderRepositoryInterface $order) { $this->order = $order; } public function index() { dd($this->order->getAll()); return View::make(orders.index); }}路由:Route::resource(‘orders’, ‘OrdersController’);现在,在浏览器中输入这个地址 <http://localhost:8000/orders>报错了吧,错误的原因是服务容器正在尝试去实例化一个接口,而接口是不能被实例化的。解决这个问题,只需把接口绑定到一个具体的类上:把下面这行代码加在路由文件里就搞定了:App::bind(‘App\Repositories\OrderRepositoryInterface’, ‘App\Repositories\DbOrderRepository’);现在刷新浏览器看看:我们可以这样定义一个容器类:class SimpleContainer { protected static $container = []; public static function bind($name, Callable $resolver) { static::$container[$name] = $resolver; } public static function make($name) { if(isset(static::$container[$name])){ $resolver = static::$container[$name] ; return $resolver(); } throw new Exception(“Binding does not exist in containeer”); }}这里,我想告诉你服务容器解析依赖是多么简单的事。class LogToDatabase { public function execute($message) { var_dump(’log the message to a database :’.$message); }}class UsersController { protected $logger; public function __construct(LogToDatabase $logger) { $this->logger = $logger; } public function show() { $user = ‘JohnDoe’; $this->logger->execute($user); }}绑定依赖:SimpleContainer::bind(‘Foo’, function() { return new UsersController(new LogToDatabase); });$foo = SimpleContainer::make(‘Foo’);print_r($foo->show());输出:string(36) “Log the messages to a file : JohnDoe"Laravel 的服务容器源码: public function bind($abstract, $concrete = null, $shared = false) { $abstract = $this->normalize($abstract); $concrete = $this->normalize($concrete); if (is_array($abstract)) { list($abstract, $alias) = $this->extractAlias($abstract); $this->alias($abstract, $alias); } $this->dropStaleInstances($abstract); if (is_null($concrete)) { $concrete = $abstract; } if (! $concrete instanceof Closure) { $concrete = $this->getClosure($abstract, $concrete); } $this->bindings[$abstract] = compact(‘concrete’, ‘shared’); if ($this->resolved($abstract)) { $this->rebound($abstract); } } public function make($abstract, array $parameters = []) { $abstract = $this->getAlias($this->normalize($abstract)); if (isset($this->instances[$abstract])) { return $this->instances[$abstract]; } $concrete = $this->getConcrete($abstract); if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($concrete, $parameters); } else { $object = $this->make($concrete, $parameters); } foreach ($this->getExtenders($abstract) as $extender) { $object = $extender($object, $this); } if ($this->isShared($abstract)) { $this->instances[$abstract] = $object; } $this->fireResolvingCallbacks($abstract, $object); $this->resolved[$abstract] = true; return $object; } public function build($concrete, array $parameters = []) { if ($concrete instanceof Closure) { return $concrete($this, $parameters); } $reflector = new ReflectionClass($concrete); if (! $reflector->isInstantiable()) { if (! empty($this->buildStack)) { $previous = implode(’, ‘, $this->buildStack); $message = “Target [$concrete] is not instantiable while building [$previous].”; } else { $message = “Target [$concrete] is not instantiable.”; } throw new BindingResolutionException($message); } $this->buildStack[] = $concrete; $constructor = $reflector->getConstructor(); if (is_null($constructor)) { array_pop($this->buildStack); return new $concrete; } $dependencies = $constructor->getParameters(); $parameters = $this->keyParametersByArgument( $dependencies, $parameters ); $instances = $this->getDependencies($dependencies,$parameters); array_pop($this->buildStack); return $reflector->newInstanceArgs($instances); }如果你想了解关于服务容器的更多内容,可以看下 vendor/laravel/framwork/src/Illuminate/Container/Container.php简单的绑定$this->app->bind(‘HelpSpot\API’, function ($app) { return new HelpSpot\API($app->make(‘HttpClient’));});单例模式绑定通过 singleton 方法绑定到服务容器的类或接口,只会被解析一次。$this->app->singleton(‘HelpSpot\API’, function ($app) { return new HelpSpot\API($app->make(‘HttpClient’));});绑定实例也可以通过 instance 方法把具体的实例绑定到服务容器中。之后,就会一直返回这个绑定的实例:$api = new HelpSpot\API(new HttpClient);$this->app->instance(‘HelpSpot\API’, $api);如果没有绑定,PHP 会利用反射机制来解析实例和依赖。如果想了解更多细节,可以查看 官方文档关于 Laravel 服务容器的练习代码, 可以从我的 GitHub (如果喜欢,烦请不吝 star )仓库获取。感谢阅读。文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

April 15, 2019 · 3 min · jiezi

一篇文章帮你了解 PHP 7.3 更新

文章转自:https://learnku.com/php/t/21549 PHP 目前依旧是其它脚本语言强劲的竞争对手,这主要归功于其核心维护团队的快速更新。自从 PHP 7.0 发布以来,社区见证了许多新特性的诞生,极大地改进了开发者在项目中应用 PHP 的方式。提高 PHP 应用的性能和安全性,是这些改进的主要目的。PHP 最近实现了又一个里程碑 —— 发布 PHP 7.3。新版本带来了一些急需的更新。在本文中,我将论述新推出的 PHP 7.3 特性 和更新。好消息是,你可以在你的测试服务器上自行安装新版本、查看新功能。但老生常谈,切勿在生产服务器上使用 RC 版本更新,可能会破坏你已经上线的应用。以下是7.3版中引入的一些更新,与以前的版本相比,它们大大提高了 PHP 7.3 的性能 。灵活的 Heredoc 和 Nowdoc 语法函数调用时允许尾随逗号JSON_THROW_ON_ERRORPCRE2 迁移list() 分配参考is_countable 函数array_key_first(), array_key_last()Argon2 密码哈希增强功能弃用和删除 image2wbmp()弃用和删除不区分大小写的常量相同站点 CookieFPM 更新改进 Windows 下的文件删除让我们逐一讨论上述的每一个更新。灵活的 Heredoc 和 Nowdoc 语法Heredoc 和 Nowdoc 语法能够在使用多行长字符串时起到很大帮助。它要求结束标识符应当为出现在新行的首个字符串。// 除了这样:$query = <<<SQLSELECT *FROM tableWHERE column = true;SQL;// 这样也可以:$query = <<<SQL SELECT * FROM table WHERE column = true; SQL;总的来说,此更新提出了两项改进,如下:闭合标识符前支持缩进闭合标识符后不再强制换行在上面的例子里,可以很容易地看出这些改动。函数调用中允许尾部逗号在参数、元素、变量列表结尾,追加尾部逗号。有时我们在数组内以及函数调用(尤其是可变参函数)时需要传递大量元素,若是漏掉一个逗号,便会报错。鉴于如上情况,尾部逗号便显得十分有用。这个特性已经允许在数组内使用,并且从 PHP 7.2 开始,分组命名空间(Grouped Namespaces)语法也开始支持尾部逗号。use Foo\Bar{ Foo, Bar,};$foo = [ ‘foo’, ‘bar’,];当新值需要被追加在此处时,尾部逗号便显得十分实用。在可变参函数例如 unset() 内,更是如此。unset( $foo, $bar, $baz,);同时,当你使用 compact() 函数给模版引擎传递一批变量时,也是个能用到的例子。echo $twig->render( ‘index.html’, compact( ’title’, ‘body’, ‘comments’, ));在某些需要构造连续或分组数据情况下,经常要使用 array_merge() 函数合并数组。也可以利用尾部逗号:$newArray = array_merge( $arrayOne, $arrayTwo, [‘foo’, ‘bar’],);同样,你也可以在调用任意方法、函数以及闭包时使用此特性。class Foo{ public function __construct(…$args) { // } public function bar(…$args) { // } public function _invoke(…$args) { // }}$foo = new Foo( ‘constructor’, ‘bar’,);$foo->bar( ‘method’, ‘bar’,);$foo( ‘invoke’, ‘bar’,);JSON_THROW_ON_ERROR解析 JSON 响应数据,有 json_encode() 以及 json_decode() 两个函数可供使用。不幸的是,它们都没有恰当的错误抛出表现。json_encode 失败时仅会返回 false;json_decode 失败时则会返回 null,而 null 可作为合法的 JSON 数值。唯一获取错误的方法是,调用 json_last_error() 或 json_last_error_msg(),它们将分别返回机器可读和人类可读的全局错误状态。该 RFC 提出的解决方案是,为 JSON 函数新增 JSON_THROW_ON_ERROR 常量用于忽略全局错误状态。当错误发生时,JSON 函数将会抛出 JsonException 异常,异常消息(message)为 json_last_error() 的返回值,异常代码(code)为 json_last_error_msg() 的返回值。如下是调用例子:json_encode($data, JSON_THROW_ON_ERROR);json_decode(“invalid json”, null, 512, JSON_THROW_ON_ERROR);// 抛出 JsonException 异常升级 PCRE2PHP 使用 PCRE 作为正则表达式引擎。但从 PHP 7.3 开始,PCRE2 将作为新的正则引擎大显身手。所以,你需要将现有的正则表达式迁移到符合 PCRE2 的规则。这些规则比以前更具侵入性。请看以下实例:preg_match(’/[\w-.]+/’, ‘’);这个表达式在新版 PHP 内将会匹配失败且不会触发警告。因为 PCRE2 现严格要求,若需匹配连字符(-)而非用于表示范围,则必须移动到末尾或将其转义。更新到 PCRE2 10.x 后,支持了以下以及更多特性:相对后向引用 \g{+2}(等效于已存在的 \g{-2})PCRE2 版本检查 (?(VERSION>=x)…)(*NOTEMPTY) 和 (*NOTEMPTY_ATSTART) 告知引擎勿返回空匹配(*NO_JIT) 禁用 JIT 优化(*LIMIT_HEAP=d) 限制堆大小为 d KB(*LIMIT_DEPTH=d) 设置回溯深度限制为 d(*LIMIT_MATCH=d) 设置匹配数量限制为 d译者注:国内正则术语参差不一,「后向引用」—— Back References,又称「反向引用」、「回溯引用」等,此处参考 PHP 官方手册的中文译本。list() 赋值引用PHP 中的 list() 现在可以赋值给引用,在当前版本中 list() 中赋值不能使用引用,在 PHP 7.3 中将允许使用引用,新改进的语法如下:$array = [1, 2];list($a, &$b) = $array;相当于$array = [1, 2];$a = $array[0];$b =& $array[1];在 PHP 7.3 的变更中,我们还可以与 foreach() 方法一起嵌套使用$array = [[1, 2], [3, 4]];foreach ($array as list(&$a, $b)) { $a = 7;}var_dump($array);is_countable 函数在 PHP 7.2 中,用 count() 获取对象和数组的数量。如果对象不可数,PHP 会抛出警告⚠️ 。所以需要检查对象或者数组是否可数。 PHP 7.3 提供新的函数 is_countable() 来解决这个问题。该 RFC 提供新的函数 is_countable(),对数组类型或者实现了 Countable 接口的实例的变量返回 true 。 之前:if (is_array($foo) || $foo instanceof Countable) { // $foo 是可数的}之后:if (is_countable($foo)) { // $foo 是可数的}array_key_first(), array_key_last()当前版本的 PHP 允许使用 reset() ,end() 和 key() 等方法,通过改变数组的内部指针来获取数组首尾的键和值。现在,为了避免这种内部干扰,PHP 7.3 推出了新的函数来解决这个问题:$key = array_key_first($array); 获取数组第一个元素的键名$key = array_key_last($array); 获取数组最后一个元素的键名让我们看一个例子:// 关联数组的用法$array = [‘a’ => 1, ‘b’ => 2, ‘c’ => 3];$firstKey = array_key_first($array);$lastKey = array_key_last($array);assert($firstKey === ‘a’);assert($lastKey === ‘c’);// 索引数组的用法$array = [1 => ‘a’, 2 => ‘b’, 3 => ‘c’];$firstKey = array_key_first($array);$lastKey = array_key_last($array);assert($firstKey === 1);assert($lastKey === 3);译者注:array_value_first() 和 array_value_last() 并没有通过 RFC 表决;因此 PHP 7.3 内仅提供了 array_key_first() 以及 array_key_last() 函数。 参考链接:https://wiki.php.net/rfc/arra…Argon2 和 Hash 密码加密性能增强在PHP的早期版本中,我们增加了Argon2和哈希密码加密算法,这是一种使用哈希加密算法来保护密码的现代算法。它有三种不同的类型,Argon2i,Argon2d和Argon 2id。 我们针对Argon2i密码散列和基于密码的密钥生成进行了优化。 Argon2d性能更快,并使用依赖于内存的数据访问。 Argon2i使用与内存无关的数据访问。 Argon2id是Argon2i和Argon2d的混合体,使用依赖于数据和与数据独立的存储器访问的组合。password_hash():Argon2id现在是在paswword *函数中使用的推荐的Argon2变量。具有自定义成员方法的名称的Argon2id与PASSWORD_ARGON2I的使用方法相同password_hash(‘password’,PASSWORD_ARGON2ID,[‘memory_cost’=> 1 << 17,’time_cost’=> 4,’threads’=> 2]);password_verify();除了Argon2i之外,password_verify()函数也适用于Argon2id。password_needs_rehash();此函数也将接受Argon2id哈希值,如果任何变量成员发生变化,则返回true。$hash = password_hash(‘password’, PASSWORD_ARGON2ID);password_needs_rehash($hash, PASSWORD_ARGON2ID); // 返回假password_needs_rehash($hash, PASSWORD_ARGON2ID, [‘memory_cost’ => 1<<17]); // 返回真废弃并移除 image2wbmp()该函数能够将图像输出为 WBMP 格式。另一个名为 imagewbmp() 的函数也同样具备单色转换的作用。因此,出于重复原因,image2wbmp() 现已被废弃,你可使用 imagewbmp() 代替它。此函数被弃用后,再次调用它将会触发已弃用警告。待后续此函数被移除后,再次调用它将会触发致命错误。废弃并移除大小写不敏感的常量使用先前版本的 PHP,你可以同时使用大小写敏感和大小写不敏感的常量。但大小写不敏感的常量会在使用中造成一点麻烦。所以,为了解决这个问题,PHP 7.3 废弃了大小写不敏感的常量。原先的情况是:类常量始终为「大小写敏感」。使用 const 关键字定义的全局常量始终为「大小写敏感」。注意此处仅仅是常量自身的名称,不包含命名空间名的部分,PHP 的命名空间始终为「大小写不敏感」。使用 define() 函数定义的常量默认为「大小写敏感」。使用 define() 函数并将第三个参数设为 true 定义的常量为「大小写不敏感」。如今 PHP 7.3 提议废弃并移除以下用法:In PHP 7.3: 废弃使用 true 作为 define() 的第三个参数。In PHP 7.3: 废弃使用与定义时的大小写不一致的名称,访问大小写不敏感的常量。true、false 以及 null 除外。同站点 CookiePHP 7.3 在建议在使用 cookies 时,增加同站点标志。这个 RFC 会影响4个系统函数。setcookiesetrawcookiesession_set_cookie_paramssession_get_cookie_params这个影响会在两种情况下起作用。其中一种方式会添加函数的新参数,另一种方式允许以数组形式的选项代替其他单独选项。bool setcookie( string $name [, string $value = “" [, int $expire = 0 [, string $path = “" [, string $domain = “" [, bool $secure = false [, bool $httponly = false ]]]]]])bool setcookie ( string $name [, string $value = “" [, int $expire = 0 [, array $options ]]])// 两种方式均可.FPM 更新FastCGI 进程管理器也进行了更新,现在提供了新的方式来记录 FPM 日志。log_limit: 设置允许的日志长度,可以超过 1024 字符。log_buffering: 允许不需要额外缓冲去操作日志。decorate _workers_output: 当启用了 catch_workers_output 时,系统会去禁用渲染输出。改进 Windows 下的文件删除如官方文档所述:默认情况下,文件描述符以共享读、写、删除的方式去操作。 这很有效的去映射 POSIX 并允许去删除正在使用中的文件。但这并不是100%都是一样的,不同的平台可能仍存在一些差异。删除操作之后,文件目录仍存在直到所有的文件操作被关闭。结束语之前我们已经讲解了最新版本的 PHP7.3 的特点,包含了许多新增跟弃用的功能。这些功能都可以在 php.net 网站上找到,并且已经合并到主分支上了。你现在就可以使用这些新功能部署在自己的服务器上,你也可以打开官方RFC页面查阅每一个详细版本。如果你对着新版 PHP7.3 有任何问题,你可以在评论下写下自己的想法。如果你喜欢这篇文章,并且觉得它很有帮助,你可以在 twitter 上关注我,来获得更多的信息!文章转自:https://learnku.com/php/t/21549 更多文章:https://learnku.com/laravel/c… ...

April 4, 2019 · 3 min · jiezi

PHP 安全问题入门:10 个常见安全问题 + 实例讲解

文章转自:https://learnku.com/php/t/24930 更多文章:https://learnku.com/laravel/c…相对于其他几种语言来说, PHP 在 web 建站方面有更大的优势,即使是新手,也能很容易搭建一个网站出来。但这种优势也容易带来一些负面影响,因为很多的 PHP 教程没有涉及到安全方面的知识。此帖子分为几部分,每部分会涵盖不同的安全威胁和应对策略。但是,这并不是说你做到这几点以后,就一定能避免你的网站出现任何问题。如果你想提高你的网站安全性的话,你应该继续通过阅读书籍或者文章,来研究如何提高你的网站安全性出于演示需要,代码可能不是很完美。日常开发过程中,很多代码都包含在了框架跟各种库里面。作为一个后台开发,你不仅要熟练基本的CURD,更要知道如何保护你的数据。1. SQL 注入我赌一包辣条,你肯定会看到这里。 SQL 注入是对您网站最大的威胁之一,如果您的数据库受到别人的 SQL 注入的攻击的话,别人可以转出你的数据库,也许还会产生更严重的后果。网站要从数据库中获取动态数据,就必须执行 SQL 语句,举例如下:<?php$username = $_GET[‘username’];$query = “SELECT * FROM users WHERE username = ‘$username’";攻击者控制通过 GET 和 POST 发送的查询(或者例如 UA 的一些其他查询)。一般情况下,你希望查询户名为「 peter 」的用户产生的 SQL 语句如下:SELECT * FROM users WHERE username = ‘peter’但是,攻击者发送了特定的用户名参数,例如:’ OR ‘1’=‘1这就会导致 SQL 语句变成这样:SELECT * FROM users WHERE username = ‘peter’ OR ‘1’ = ‘1’这样,他就能在不需要密码的情况下导出你的整个用户表的数据了。那么,我们如何防止这类事故的发生呢?主流的解决方法有两种。转义用户输入的数据或者使用封装好的语句。转义的方法是封装好一个函数,用来对用户提交的数据进行过滤,去掉有害的标签。但是,我不太推荐使用这个方法,因为比较容易忘记在每个地方都做此处理。下面,我来介绍如何使用 PDO 执行封装好的语句( mysqi 也一样):$username = $_GET[‘username’];$query = $pdo->prepare(‘SELECT * FROM users WHERE username = :username’);$query->execute([‘username’ => $username]);$data = $query->fetch();动态数据的每个部分都以:做前缀。然后将所有参数作为数组传递给执行函数,看起来就像 PDO 为你转义了有害数据一样。几乎所有的数据库驱动程序都支持封装好的语句,没有理由不使用它们!养成使用他们的习惯,以后就不会忘记了。你也可以参考 phpdelusions 中的一篇关于动态构建 SQL 查询时处理安全问题的文章。链接: https://phpdelusions.net/pdo/… 。2. XSSXSS 又叫 CSS (Cross Site Script) ,跨站脚本攻击。它指的是恶意攻击者往 Web 页面里插入恶意 html 代码,当用户浏览该页之时,嵌入其中 Web 里面的 html 代码会被执行,从而达到恶意攻击用户的特殊目的。下面以一个搜索页面为例子:<body><?php$searchQuery = $_GET[‘q’];/* some search magic here /?><h1>You searched for: <?php echo $searchQuery; ?></h1><p>We found: Absolutely nothing because this is a demo</p></body>因为我们把用户的内容直接打印出来,不经过任何过滤,非法用户可以拼接 URL:search.php?q=%3Cscript%3Ealert(1)%3B%3C%2Fscript%3EPHP 渲染出来的内容如下,可以看到 Javascript 代码会被直接执行:<body><h1>You searched for: <script>alert(1);</script></h1><p>We found: Absolutely nothing because this is a demo</p></body>问:JS 代码被执行有什么大不了的?Javascript 可以:偷走你用户浏览器里的 Cookie;通过浏览器的记住密码功能获取到你的站点登录账号和密码;盗取用户的机密信息;你的用户在站点上能做到的事情,有了 JS 权限执行权限就都能做,也就是说 A 用户可以模拟成为任何用户;在你的网页中嵌入恶意代码;…问:如何防范此问题呢?好消息是比较先进的浏览器现在已经具备了一些基础的 XSS 防范功能,不过请不要依赖与此。正确的做法是坚决不要相信用户的任何输入,并过滤掉输入中的所有特殊字符。这样就能消灭绝大部分的 XSS 攻击:<?php$searchQuery = htmlentities($searchQuery, ENT_QUOTES);或者你可以使用模板引擎 Twig ,一般的模板引擎都会默认为输出加上 htmlentities 防范。如果你保持了用户的输入内容,在输出时也要特别注意,在以下的例子中,我们允许用户填写自己的博客链接:<body> <a href="<?php echo $homepageUrl; ?>">Visit Users homepage</a></body>以上代码可能第一眼看不出来有问题,但是假设用户填入以下内容:#” onclick=“alert(1)会被渲染为:<body> <a href=”#" onclick=“alert(1)">Visit Users homepage</a></body>永远永远不要相信用户输入的数据,或者,永远都假设用户的内容是有攻击性的,态度端正了,然后小心地处理好每一次的用户输入和输出。另一个控制 XSS 攻击的方法是提供一个 CSP Meta 标签,或者标头信息,更多详情请见: https://www.html5rocks.com/en…另外种 Cookie 时,如果无需 JS 读取的话,请必须设置为 “HTTP ONLY”。这个设置可以令 JavaScript 无法读取 PHP 端种的 Cookie。3. XSRF/CSRFCSRF 是跨站请求伪造的缩写,它是攻击者通过一些技术手段欺骗用户去访问曾经认证过的网站并运行一些操作。虽然此处展示的例子是 GET 请求,但只是相较于 POST 更容易理解,并非防护手段,两者都不是私密的 Cookies 或者多步表单。假如你有一个允许用户删除账户的页面,如下所示:<?php//delete-account.php$confirm = $_GET[‘confirm’];if($confirm === ‘yes’) { //goodbye}攻击者可以在他的站点上构建一个触发这个 URL 的表单(同样适用于 POST 的表单),或者将 URL 加载为图片诱惑用户点击:<img src=“https://example.com/delete-account.php?confirm=yes" />用户一旦触发,就会执行删除账户的指令,眨眼你的账户就消失了。防御这样的攻击比防御 XSS 与 SQL 注入更复杂一些。最常用的防御方法是生成一个 CSRF 令牌加密安全字符串,一般称其为 Token,并将 Token 存储于 Cookie 或者 Session 中。每次你在网页构造表单时,将 Token 令牌放在表单中的隐藏字段,表单请求服务器以后会根据用户的 Cookie 或者 Session 里的 Token 令牌比对,校验成功才给予通过。由于攻击者无法知道 Token 令牌的内容(每个表单的 Token 令牌都是随机的),因此无法冒充用户。<?php / 你嵌入表单的页面 */ ?><form action="/delete-account.php” method=“post”> <input type=“hidden” name=“csrf” value="<?php echo $_SESSION[‘csrf’]; ?>"> <input type=“hidden” name=“confirm” value=“yes” /> <input type=“submit” value=“Delete my account” /></form>## <?php//delete-account.php$confirm = $_POST[‘confirm’];$csrf = $_POST[‘csrf’];$knownGoodToken = $_SESSION[‘csrf’];if($csrf !== $knownGoodToken) { die(‘Invalid request’);}if($confirm === ‘yes’) { //goodbye}请注意,这是个非常简单的示例,你可以加入更多的代码。如果你使用的是像 Symfony 这样的 PHP 框架,那么自带了 CSRF 令牌的功能。你还可以查看关于 OWASP 更详细的问题和更多防御机制的文章: https://github.com/OWASP/CheatS….4. LFILFI (本地文件包含) 是一个用户未经验证从磁盘读取文件的漏洞。我经常遇到编程不规范的路由代码示例,它们不验证过滤用户的输入。我们用以下文件为例,将它要渲染的模板文件用 GET 请求加载。<body><?php $page = $GET[‘page’]; if(!$page) { $page = ‘main.php’; } include($page);?></body>由于 Include 可以加载任何文件,不仅仅是PHP,攻击者可以将系统上的任何文件作为包含目标传递。index.php?page=../../etc/passwd这将导致 /etc/passwd 文件被读取并展示在浏览器上。要防御此类攻击,你必须仔细考虑允许用户输入的类型,并删除可能有害的字符,如输入字符中的“.” “/” “”。如果你真的想使用像这样的路由系统(我不建议以任何方式),你可以自动附加 PHP 扩展,删除任何非 [a-zA-Z0-9-] 的字符,并指定从专用的模板文件夹中加载,以免被包含任何非模板文件。我在不同的开发文档中,多次看到造成此类漏洞的 PHP 代码。从一开始就要有清晰的设计思路,允许所需要包含的文件类型,并删除掉多余的内容。你还可以构造要读取文件的绝对路径,并验证文件是否存在来作为保护,而不是任何位置都给予读取。5. 不充分的密码哈希大部分的 Web 应用需要保存用户的认证信息。如果密码哈希做的足够好,在你的网站被攻破时,即可保护用户的密码不被非法读取。首先,最不应该做的事情,就是把用户密码明文储存起来。大部分的用户会在多个网站上使用同一个密码,这是不可改变的事实。当你的网站被攻破,意味着用户的其他网站的账号也被攻破了。其次,你不应该使用简单的哈希算法,事实上所有没有专门为密码哈希优化的算法都不应使用。哈希算法如 MD5 或者 SHA 设计初衷就是执行起来非常快。这不是你需要的,密码哈希的终极目标就是让黑客花费无穷尽的时间和精力都无法破解出来密码。另外一个比较重要的点是你应该为密码哈希加盐(Salt),加盐处理避免了两个同样的密码会产生同样哈希的问题。以下使用 MD5 来做例子,所以请千万不要使用 MD5 来哈希你的密码, MD5 是不安全的。假如我们的用户 user1 和 user315 都有相同的密码 ilovecats123,这个密码虽然看起来是强密码,有字母有数字,但是在数据库里,两个用户的密码哈希数据将会是相同的:5e2b4d823db9d044ecd5e084b6d33ea5 。如果一个如果黑客拿下了你的网站,获取到了这些哈希数据,他将不需要去暴力破解用户 user315 的密码。我们要尽量让他花大精力来破解你的密码,所以我们对数据进行加盐处理:<?php//warning: !!这是一个很不安全的密码哈希例子,请不要使用!!$password = ‘cat123’;$salt = random_bytes(20);$hash = md5($password . $salt);最后在保存你的唯一密码哈希数据时,请不要忘记连 $salt 也已经保存,否则你将无法验证用户。在当下,最好的密码哈希选项是 bcrypt,这是专门为哈希密码而设计的哈希算法,同时这套哈希算法里还允许你配置一些参数来加大破解的难度。新版的 PHP 中也自带了安全的密码哈希函数 password_hash ,此函数已经包含了加盐处理。对应的密码验证函数为 password_verify 用来检测密码是否正确。password_verify 还可有效防止 时序攻击.以下是使用的例子:<?php//user signup$password = $_POST[‘password’];$hashedPassword = password_hash($password, PASSWORD_DEFAULT);//login$password = $_POST[‘password’];$hash = ‘1234’; //load this value from your dbif(password_verify($password, $hash)) { echo ‘Password is valid!’;} else { echo ‘Invalid password.’;}需要澄清的一点是:密码哈希并不是密码加密。哈希(Hash)是将目标文本转换成具有相同长度的、不可逆的杂凑字符串(或叫做消息摘要),而加密(Encrypt)是将目标文本转换成具有不同长度的、可逆的密文。显然他们之间最大的区别是可逆性,在储存密码时,我们要的就是哈希这种不可逆的属性。6. 中间人攻击MITM (中间人) 攻击不是针对服务器直接攻击,而是针对用户进行,攻击者作为中间人欺骗服务器他是用户,欺骗用户他是服务器,从而来拦截用户与网站的流量,并从中注入恶意内容或者读取私密信息,通常发生在公共 WiFi 网络中,也有可能发生在其他流量通过的地方,例如ISP运营商。对此的唯一防御是使用 HTTPS,使用 HTTPS 可以将你的连接加密,并且无法读取或者篡改流量。你可以从 Let’s Encrypt 获取免费的 SSL 证书,或从其他供应商处购买,这里不详细介绍如何正确配置 WEB 服务器,因为这与应用程序安全性无关,且在很大程度上取决于你的设置。你还可以采取一些措施使 HTTPS 更安全,在 WEB 服务器配置加上 Strict-Transport-Security 标示头,此头部信息告诉浏览器,你的网站始终通过 HTTPS 访问,如果未通过 HTTPS 将返回错误报告提示浏览器不应显示该页面。然而,这里有个明显的问题,如果浏览器之前从未访问过你的网站,则无法知道你使用此标示头,这时候就需要用到 Hstspreload。可以在此注册你的网站: https://hstspreload.org/你在此处提交的所有网站都将被标记为仅 HTTPS,并硬编码到 Google Chrome、FireFox、Opera、Safari、IE11 和 Edge 的源代码中。你还可以在 DNS 配置中添加 Certification Authority Authorization (CAA) record ,可以仅允许一个证书颁发机构(例如: Let’s encrypt)发布你的域名证书,这进一步提高了用户的安全性。7. 命令注入这可能是服务器遇到的最严重的攻击,命令注入的目标是欺骗服务器执行任意 Shell 命令你如果使用 shell_exec 或是 exec 函数。让我们做一个小例子,允许用户简单的从服务器 Ping 不同的主机。<?php$targetIp = $_GET[‘ip’];$output = shell_exec(“ping -c 5 $targetIp”);输出将包括对目标主机 Ping 5次。除非采用 sh 命令执行 Shell 脚本,否则攻击者可以执行想要的任何操作。ping.php?ip=8.8.8.8;ls -l /etcShell 将执行 Ping 和由攻击者拼接的第二个命令,这显然是非常危险的。感谢 PHP 提供了一个函数来转义 Shell 参数。escapeshellarg 转义用户的输入并将其封装成单引号。<?php$targetIp = escapeshellarg($_GET[‘ip’]);$output = shell_exec(“ping -c 5 $targetIp”);现在你的命令应该是相当安全的,就个人而言,我仍然避免使用 PHP 调用外部命令,但这完全取决于你自己的喜好。另外,我建议进一步验证用户输入是否符合你期望的形式。8. XXEXXE (XML 外部实体) 是一种应用程序使用配置不正确的 XML 解析器解析外部 XML 时,导致的本地文件包含攻击,甚至可以远程代码执行。XML 有一个鲜为人知的特性,它允许文档作者将远程和本地文件作为实体包含在其 XML 文件中。<?xml version=“1.0” encoding=“ISO-8859-1”?> <!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY passwd SYSTEM “file:///etc/passwd” >]> <foo>&passwd;</foo>就像这样, /etc/passwd 文件内容被转储到 XML 文件中。如果你使用 libxml 可以调用 libxml_disable_entity_loader 来保护自己免受此类攻击。使用前请仔细检查 XML 库的默认配置,以确保配置成功。9. 在生产环境中不正确的错误报告暴露敏感数据[](https://secure.php.net/manual…,可能会在生产环境中因为不正确的错误报告泄露了敏感信息,例如:文件夹结构、数据库结构、连接信息与用户信息。你是不希望用户看到这个的吧?一般根据你使用的框架或者 CMS ,配置方法会有不同的变化。通常框架具有允许你将站点更改为某种生产环境的设置。这样会将所有用户可见的错误消息重定向到日志文件中,并向用户显示非描述性的 500 错误,同时允许你根据错误代码检查。但是你应该根据你的 PHP 环境设置: error_reporting 与 display_errors.10. 登录限制像登录这样的敏感表单应该有一个严格的速率限制,以防止暴力攻击。保存每个用户在过去几分钟内失败的登录尝试次数,如果该速率超过你定义的阈值,则拒绝进一步登录尝试,直到冷却期结束。还可通过电子邮件通知用户登录失败,以便他们知道自己的账户被成为目标。一些其他补充不要信任从用户传递给你的对象 ID ,始终验证用户对请求对象的访问权限服务器与使用的库时刻保持最新订阅关注安全相关的博客,了解最新的解决方案从不在日志中保存用户的密码不要将整个代码库存储在 WEB 根目录中永远不要在 WEB 根目录创建 Git 存储库,除非你希望泄露整个代码库始终假设用户的输入是不安全的设置系统禁止可疑行为的 IP 显示,例如:工具对 URL 随机扫描、爬虫不要过分信任第三方代码是安全的不要用 Composer 直接从 Github 获取代码如果不希望站点被第三方跨域 iframe,请设置反 iframe 标示头含糊是不安全的如果你是缺乏实践经验的运营商或合作开发人员,请确保尽可能时常检查代码当你不了解安全功能应该如何工作,或者为什么会安装,请询问知道的人,不要忽视它永远不要自己写加密方式,这可能是个坏的方法如果你没有足够的熵,请正确播种你的伪随机数生成并舍弃如果在互联网上不安全,并有可能被窃取信息,请为这种情况做好准备并制定事件响应计划禁用 WEB 根目录列表显示,很多 WEB 服务器配置默认都会列出目录内容,这可能导致数据泄露客户端验证是不够的,需要再次验证 PHP 中的所有内容不惜一切代价避免反序列化用户内容,这可能导致远程代码执行,有关此问题的详细信息,请参阅此文章: https://paragonie.com/blog/20…小贴士我不是一个安全专家,恐无法做到事无巨细。尽管编写安全软件是一个非常痛苦的过程,但还是可以通过遵循一些基本规则,编写合理安全的应用程序。其实,很多框架在这方面也帮我们做了很多工作。在问题发生之前,安全性问题并不像语法错误等可以在开发阶段追踪到。因此,在编写代码的过程中,应该时刻有规避安全风险的意识。如果你迫于业务需求的压力而不得不暂时忽略一些安全防范的工作,我想你有必要事先告知大家这样做的潜在风险。如果你从这篇文章有所收益,也请把它分享给你的朋友们把,让我们共建安全网站。文章转自:https://learnku.com/php/t/24930 更多文章:https://learnku.com/laravel/c… ...

April 3, 2019 · 3 min · jiezi

Laravel 中创建 Zip 压缩文件并提供下载

文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c…如果您需要您的用户支持多文件下载的话,最好的办法是创建一个压缩包并提供下载。看下在 Laravel 中的实现。事实上,这不是关于 Laravel 的,而是和 PHP 的关联更多,我们准备使用从 PHP 5.2 以来就存在的 ZipArchive 类 ,如果要使用,需要确保php.ini 中的 ext-zip 扩展开启。任务 1: 存储用户的发票文件到 storage/invoices/aaa001.pdf下面是代码展示:$zip_file = ‘invoices.zip’; // 要下载的压缩包的名称// 初始化 PHP 类$zip = new \ZipArchive();$zip->open($zip_file, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);$invoice_file = ‘invoices/aaa001.pdf’;// 添加文件:第二个参数是待压缩文件在压缩包中的路径// 所以,它将在 ZIP 中创建另一个名为 “storage/” 的路径,并把文件放入目录。$zip->addFile(storage_path($invoice_file), $invoice_file);$zip->close();// 我们将会在文件下载后立刻把文件返回原样return response()->download($zip_file);例子很简单,对吗?*任务 2: 压缩 全部 文件到 storage/invoices 目录中Laravel 方面不需要有任何改变,我们只需要添加一些简单的 PHP 代码来迭代这些文件。$zip_file = ‘invoices.zip’;$zip = new \ZipArchive();$zip->open($zip_file, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);$path = storage_path(‘invoices’);$files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));foreach ($files as $name => $file){ // 我们要跳过所有子目录 if (!$file->isDir()) { $filePath = $file->getRealPath(); // 用 substr/strlen 获取文件扩展名 $relativePath = ‘invoices/’ . substr($filePath, strlen($path) + 1); $zip->addFile($filePath, $relativePath); }}$zip->close();return response()->download($zip_file);到这里基本就算完成了。你看,你不需要任何 Laravel 的扩展包来实现这个压缩方式。文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

April 2, 2019 · 1 min · jiezi

使用 Zephir 轻松构建 PHP 扩展

简介:通过 PHP 扩展, 我们可以在 php 代码中使用一些特定的方法(大部分的 php 扩展都是用 C 写的)。比如,在 PHP 中需要与 SQLite3 交互,我们可以自己写方法与之进行连接,再写 SQL 语句请求数据。然而,这都是些既琐碎又重复度相当高的工作,因此,所有开发者对插件的需求呼之欲出。现在,这款插件已经诞生了。你只需像安装其他扩展一样进行安装,然后在 ‘php.ini’ 文件执行 ’extension=sqllite3.so’,就可以在你的 php 项目里对 sqlite3 进行访问了。你该安装的第一个扩展Zephir:官网文档给出的定义是:一种开源的高级语言,旨在简化 PHP 扩展的创建和可维护性,重点关注类型和内存安全性。特点:类型:动态 / 静态。内存安全性:不允许指针或者直接内存管理。编译模式:提前编译。内存模型:本地任务垃圾回收机制。工作原理?把你写好的 php 代码编译成 c,然后你可以将其以扩展的形式添加到 ‘php.ini’ 文件中。下面是编译方案的例子:编译方案让我们从 Hello world 的扩展开始吧。Zephir 安装配置要求 :gcc >= 4.x/clang >= 3.xre2c 0.13 or latergnu make 3.81 or laterautoconf 2.31 or laterautomake 1.14 or laterlibpcre3php development headers and toolsre2cphp-zephir-parser如果你是用 Ubuntu, 可以采取如下方式安装:sudo apt-get updatesudo apt-get install git gcc make re2c php7.0 php7.0-json php7.0-dev libpcre3-dev参考下图命令,确认安装了较新的 PHP 版本:参考下图命令,确认有 PHP 开发库:然后git clone https://github.com/phalcon/zephircd zephir./install -c验证是否安装正确:zephir help如果一切就绪,你应该能在你的屏幕上看到以下帮助信息:扩展初始化:zephir init helloworld然后,一个名字为 “helloworld” 的目录在当前工作目录中被创建:扩展目录结构:ext: 包含被编译器用来生成扩展的代码。helloworld: 这个目录与我们的扩展同名。我们在这个目录中放置 Zephir 代码。config.json: 这个文件包含我们可用于更改 Zephir 与/或此扩展的行为的配置添加我们的第一个类:在 helloworld 目录中 .Zephir 的设计初衷是用来生成面对对象的扩展,接下来我们先添加一个初始类。我们先来在扩展中创建一个 helloworld 类,使用此类来渲染 Hello World!。helloworld/helloworld/greeting.zepnamespace HelloWorld;class Greeting{ public static function say() { echo “Hello World!”; }}接下来命令行执行以下命令来编译扩展:zephir build第一次运行以上命令时,会初始化一些东西。如果一些顺利的话,会输出以下内容:zephir build output检测下是否安装成功:在 PHP 代码中测试:zephir_helloworld.php<?phpecho HelloWorld\Greeting::say()."\n";接下来运行命令:php zephir_helloworld.php你可以可以看到输出 Hello World!结语如果你是 PHP 软件工程师,对内存管理等底层语言的编程方式不是特别熟悉,可以尝试从 Zephir 开始写一些简单的扩展。Zephir 内部已经做好了内存管理,但是基于其内存安全的设计,你无法使用 C 语言的强大手动内存管理功能,如果你是 C 程序员,你会觉得 Zephir 更加简单易用,但是在有些地方可能不够强大。文章转自:https://learnku.com/php/t/25350 更多文章:https://learnku.com/laravel/c… ...

March 29, 2019 · 1 min · jiezi

100 个最常用的 PHP 函数

下面的列表是最常用的前100个 PHP 函数:它们是最常用的 PHP 中自带的函数。这些函数被命名使用,并从1到100进行排序。 其他的4500个函数现在还没在排名中。 下面频率列表表示在 PHP 代码中使用此函数的频率 : 参考资料来自于 1900 个 PHP 开源项目。 他们使用了 Exakat static analysis engine 静态分析引擎的 1.2.5 版本进行了审核。平均值是在一个项目中调用此函数的次数。有些函数是被封装使用的,而另一些则是主要函数。比如说,5个项目里面有4个用到了 count 函数,函数被调用大概150次。说明它很流行且被大量使用。单击函数名跳转到 PHP 文档。在 Top 100文章的底部有一些总结见解。排行函数频率平均值1count81.41 %147.672is_array77.32 %117.863substr74.62 %142.924in_array74.16 %79.555explode73.19 %71.516str_replace72.32 %101.057implode72.27 %66.598strlen70.07 %98.329array_merge69.46 %64.0110strpos67.98 %78.1811preg_match67.31 %76.6012sprintf67.16 %119.4613trim66.75 %81.2814strtolower65.99 %59.6215file_exists65.12 %45.1316is_string61.39 %45.1017preg_replace60.27 %54.2818file_get_contents59.96 %20.7119array_key_exists59.70 %57.5020array_keys59.35 %39.5921dirname56.44 %54.8422function_exists53.58 %42.6223array_map53.22 %19.4524get_class53.12 %33.0725class_exists52.50 %23.1326is_object51.94 %35.3527time51.79 %41.4228json_encode51.48 %24.8129date50.72 %52.1830is_null49.69 %60.5231is_numeric49.49 %40.6932array_shift49.49 %23.2833defined48.72 %86.8234is_dir48.57 %22.8635json_decode48.42 %17.3936header48.16 %59.7137strtoupper47.80 %30.9538array_values47.24 %17.2739md546.88 %23.7440method_exists46.73 %19.0541file_put_contents46.68 %12.4942rtrim45.91 %18.0843array_pop45.51 %20.6044unlink44.59 %23.5545basename44.59 %27.2346realpath44.08 %15.9047call_user_func43.97 %16.4148call_user_func_array43.92 %18.4049fopen43.77 %25.6150microtime43.46 %14.4151fclose42.85 %28.3652is_int42.75 %15.7853is_file42.08 %20.5254array_slice41.83 %13.2055preg_match_all40.55 %14.6656ucfirst40.25 %17.0257intval40.19 %88.1358str_repeat40.14 %19.5159serialize40.14 %22.0560array_filter39.99 %13.8761mkdir39.79 %11.1762is_callable39.43 %11.9463ltrim39.17 %10.9064ob_start39.12 %13.2665round39.07 %28.5666fwrite38.97 %23.3967array_unique38.87 %15.9668array_search38.82 %14.1969reset38.71 %20.7970array_unshift38.10 %10.3271parse_url37.90 %9.6172func_get_args37.79 %28.3373end37.49 %12.7074base64_encode37.39 %14.1575unserialize37.18 %18.3576max36.98 %22.8877preg_split36.98 %13.2778gettype36.93 %16.1679strrpos36.67 %11.9580version_compare36.67 %14.8781array_push36.67 %26.1882floor36.11 %18.7883strtotime36.01 %27.9484htmlspecialchars35.96 %51.0885ini_get35.85 %19.2586ini_set35.60 %14.4987chr35.34 %186.9788extension_loaded35.29 %14.1789is_bool35.24 %11.4490ksort34.98 %10.8291array_reverse34.93 %8.2792ord34.73 %53.1793uniqid34.68 %9.8394strtr34.47 %12.9095array_diff34.32 %11.1396error_reporting34.17 %8.9997ceil33.35 %11.9998urlencode33.30 %29.6399min32.69 %18.31100print_r32.64 %14.12前 100 分析最常用的 PHP 函数是字符串函数,然后是数组函数,接着是文件函数。 (运算函数不在此列主要是因为他们通常基于运算符)每个函数的链接都指向其对应的文档,事实上许多函数经过发展以及获得了许多新特性,例如:count() 的第二个参数,dirname() 的第二个参数以及 preg_match() 和 str_replace() 接受数组作为参数等。有很多彩蛋。以上 100 个函数没有近期要废弃的计划。在非内置库中,mbstring 排名第一、curl 第二,然后是 gd、filter 和 iconv。md5 是最常用的加密函数,其次是 Sha1 (#147)。print_r 出现在 1/3 的项目代码里面的某处,任何地方 …由于 dirname(dirname(dirname())) 的调用,dirname 的使用频率非常高。array、echo、print、empty、isset 这些没有纳入此排名,它们的使用度肯定是非常高的。如下几个函数应该用运算符替代 : array_push, is_object, func_get_arg, chr, call_user_func。相当多的调用是为了知道值的类型。数据库函数没有在这里排名:他们经常使用类,但功能仍然很频繁。可能是另一个前 100 名?PHP 代码通常读取文件多余写入文件。它还解码 base64。使用键排序比使用值或使用键更频繁。通常使用 file_get_contents 读取文件, 使用 fwrite 写入文件。后记如果你正在学习 PHP,最好回顾一下这里排名的 100 个功能。它们是你加入一个编码团队后最常找到的功能。它们不是唯一的,但遇到它们时你会不那么惊讶。文章转自:https://learnku.com/php/t/25799 更多文章:https://learnku.com/laravel/c… ...

March 27, 2019 · 1 min · jiezi

干货:构建复杂的 Eloquent 搜索过滤

最近,我需要在开发的事件管理系统中实现搜索功能。 一开始只是简单的几个选项 (通过名称,邮箱等搜索),到后面参数变得越来越多。今天,我会介绍整个过程以及如何构建灵活且可扩展的搜索系统。如果你想查看代码,请访问 Git 仓库 。我们将创造什么我们公司需要一种跟踪我们与世界各地客户举办的各种活动和会议的方式。我们目前的唯一方法是让每位员工在 Outlook 日程表上存储会议的详细信息。可拓展性较差!我们需要公司的每个人都可以访问,来查看我们客户的被邀请的详细信息以及他们的RSVP(国际缩用语:请回复)状态。这样,我们可以通过上次与他们互动的数据来确定哪些用户可以邀请来参加未来的活动。使用高级搜索过滤器查找的截图查找用户常用过滤用户的方法:通过姓名,电子邮件,位置通过用户工作的公司被邀请参加特定活动的用户参加过特定活动的用户邀请及已参加活动的用户邀请但尚未回复的用户答应参加但未出席的用户分配给销售经理的用户虽然这个列表不算完整,但可以让我们知道需要多少个过滤器。这将是个挑战!前端的条件过滤的截图。模型及模型关联在这个例子中我们回用到很多模型:User — 代表被邀请参加活动的用户。一个用户可以参加很多活动。Event — 代表我公司举办的活动。活动可以有多个。Rsvp — 代表用户对活动邀请的回复。一个用户对一个活动的回复是一对一的。Manager — 一个用户可以对应多个我公司的销售经理.搜索的需求在开始代码之前,我想先把搜索的需求明确一下。也就是说我要很清楚地知道我要对哪些数据做搜索功能。下面就是一个例子:{ “name”: “Billy”, “company”: “Google”, “city”: “London”, “event”: “key-note-presentation-new-york-05-2016”, “responded”: true, “response”: “I will be attending”, “managers”: [ “Tom Jones”, “Joe Bloggs” ],}总结一下上面数据想表达的搜索的条件:客人的名字是 ‘Billy’,来自 ‘Google’ 公司,目前居住在 ‘London’,已经对 ‘key-note-presentation-new-york-05–2016’ 的活动邀请做出了回复,并且回复的内容是 ‘I will be attending’,负责跟进这位客人的销售经理是 ‘Tom Jones’ 或者 ‘Joe Bloggs’。开始 — 最佳实践我是一个坚定不移的极简主义者,我坚信少即是多。下面就让我们以最简单的方式探索出解决这个需求的最佳实践。首先,在 routes.php 文件中添加如下代码:Route::post(’/search’, ‘SearchController@filter’);接下来,创建 SearchController.php artisan make:controller SearchController添加前面路由中明确的 filter() 方法:<?phpnamespace App\Http\Controllers;use App\User;use App\Http\Requests;use Illuminate\Http\Request;use App\Http\Controllers\Controller;class SearchController extends Controller{ public function filter(Request $request, User $user) { // }}由于我们需要在 filter 方法中处理请求提交的数据,所以我把 Request 类做了依赖注入。Laravel 的服务容器 会解析这个依赖,我们可以在方法中直接使用 Request 的实例,也就是 $request。User 类也是同样道理,我们需要从中检索一些数据。这个搜索需求有一点比较麻烦的是,每个参数都是可选的。所以我们要先写一系列的条件语句来判断每个参数是否存在:这是我初步写出来的代码:public function filter(Request $request, User $user){ // 根据姓名查找用户 if ($request->has(’name’)) { return $user->where(’name’, $request->input(’name’))->get(); } // 根据公司名查找用户 if ($request->has(‘company’)) { return $user->where(‘company’, $request->input(‘company’)) ->get(); } // 根据城市查找用户 if ($request->has(‘city’)) { return $user->where(‘city’, $request->input(‘city’))->get(); } // 继续根据其他条件查找 // 再无其他条件, // 返回所有符合条件的用户。 // 在实际项目中需要做分页处理。 return User::all();}很明显,上面的代码逻辑是错误的。首先,它只会根据一个条件去检索用户表,然后就返回了。所以,通过上面的代码逻辑,我们根本无法获得姓名为 ‘Billy’, 而且住在 ‘London’ 的用户。实现这种目的的一种方式是嵌套条件:// 根据用户名搜索用户if ($request->has(’name’)) { // 是否还提供了 ‘city’ 搜索参数 if ($request->has(‘city’)) { // 基于用户名及城市搜索用户 return $user->where(’name’, $request->input(’name’)) ->where(‘city’, $request->input(‘city’)) ->get(); } return $user->where(’name’, $request->input(’name’))->get();}我确信你可以看到这在两个或者三个参数的时候起作用,但是一旦我们添加更多选项,这将会难以管理。改进我们的搜索 api所以我们如何让这个生效,而同时不会因为嵌套条件而变得疯狂?我们可以使用 User 模型继续重构,来使用 builder 而不是直接返回模型。public function filter(Request $request, User $user){ $user = $user->newQuery(); // 根据用户名搜索用户 if ($request->has(’name’)) { $user->where(’name’, $request->input(’name’)); } // 根据用户公司信息搜索用户 if ($request->has(‘company’)) { $user->where(‘company’, $request->input(‘company’)); } // 根据用户城市信息搜索用户 if ($request->has(‘city’)) { $user->where(‘city’, $request->input(‘city’)); } // 继续执行其他过滤 // 获得并返回结果 return $user->get();}好多了!我们现在可以将每个搜索参数做为修饰符添加到从 $user->newQuery() 返回的查询实例中。我们现在可以根据所有的参数来做搜索了, 再多参数都不怕.一起来实践吧:$user = $user->newQuery();// 根据姓名查找用户if ($request->has(’name’)) { $user->where(’name’, $request->input(’name’));}// 根据公司名查找用户if ($request->has(‘company’)) { $user->where(‘company’, $request->input(‘company’));}// 根据城市查找用户if ($request->has(‘city’)) { $user->where(‘city’, $request->input(‘city’));}// 只查找有对接我公司销售经理的用户if ($request->has(‘managers’)) { $user->whereHas(‘managers’, function ($query) use ($request) { $query->whereIn(‘managers.name’, $request->input(‘managers’)); });}// 如果有 ’event’ 参数if ($request->has(’event’)) { // 只查找被邀请的用户 $user->whereHas(‘rsvp.event’, function ($query) use ($request) { $query->where(’event.slug’, $request->input(’event’)); }); // 只查找回复邀请的用户( 以任何形式回复都可以 ) if ($request->has(‘responded’)) { $user->whereHas(‘rsvp’, function ($query) use ($request) { $query->whereNotNull(‘responded_at’); }); } // 只查找回复邀请的用户( 限制回复的具体内容 ) if ($request->has(‘response’)) { $user->whereHas(‘rsvp’, function ($query) use ($request) { $query->where(‘response’, ‘I will be attending’); }); }}// 最终获取对象并返回return $user->get();搞定,棒极了!是否还需要重构?通过上面的代码我们实现了业务需求,可以根据搜索条件返回正确的用户信息。但是我们能说这是最佳实践吗?显然是不能。现在是通过一系列条件判断的嵌套来实现业务逻辑,而且所有的逻辑都在控制器里,这样做真的合适吗?这可能是一个见仁见智的问题,最好还是结合自己的项目,具体问题具体分析。如果你的项目比较小,逻辑相对简单,而且只是一个短期需求的项目,那么就不必纠结这个问题了,直接照上面的逻辑写就好了。 然而,如果你是在构建一个比较复杂的项目,那么我们还是需要更加优雅且扩展性好的解决方案。编写新的搜索 api当我要写一个功能接口的时候,我不会立刻去写核心代码,我通常会先想想我要怎么用这个接口。这可能就是俗称的「面向结果编程」(或者说是「结果导向思维」)。「在你写一个组件之前,建议你先写一些要用这个组件的测试代码。通过这种方式,你会更加清晰地知道你究竟要写哪些函数,以及传哪些必要的参数,这样你才能写出真正好用的接口。因为写接口的目的是简化使用组件的代码,而不是简化接口自身的代码。」 ( 摘自: c2.com)根据我的经验,这个方法能帮助我写出可读性更强,更加优雅的程序。还有一个很大的额外收获就是,通过这种阶段性的验收测试,我能更好地抓住商业需求。因此,我可以很自信地说我写的程序可以很好地满足市场的需求,具有很高商业价值。以下添加到搜索功能的代码中,我希望我的搜索 api 是这样写的:return UserSearch::apply($filters);这样有着很好的可读性。 根据经验, 如果我查阅代码能想看文章的句子一样,那会非常美妙。像刚刚的情况下:搜索用户时加上一个过滤器再返回搜索结果。这对技术人员和非技术人员都有意义。我想我需要新建一个 UserSearch 类,还需要一个静态的 apply 函数来接收过滤条件。让我开始吧:<?phpnamespace App\Search;use Illuminate\Http\Request;class UserSearch{ public static function apply(Request $filters) { // 返回搜索结果 }}最简单的方式,让我们把控制器中的代码复制到 apply 函数中:<?phpnamespace App\UserSearch;use App\User;use Illuminate\Http\Request;class UserSearch{ public static function apply(Request $filters) { $user = (new User)->newQuery(); // 基于用户名搜索 if ($filters->has(’name’)) { $user->where(’name’, $filters->input(’name’)); } // 基于用户的公司名搜索 if ($filters->has(‘company’)) { $user->where(‘company’, $filters->input(‘company’)); } // 基于用户的城市名搜索 if ($filters->has(‘city’)) { $user->where(‘city’, $filters->input(‘city’)); } // 只返回分配了销售经理的用户 if ($filters->has(‘managers’)) { $user->whereHas(‘managers’, function ($query) use ($filters) { $query->whereIn(‘managers.name’, $filters->input(‘managers’)); }); } // 搜索条件中是否包含 ’event’ ? if ($filters->has(’event’)) { // 只返回被邀请参加了活动的用户 $user->whereHas(‘rsvp.event’, function ($query) use ($filters) { $query->where(’event.slug’, $filters->input(’event’)); }); // 只返回以任何形式答复了邀请的用户 if ($filters->has(‘responded’)) { $user->whereHas(‘rsvp’, function ($query) use ($filters) { $query->whereNotNull(‘responded_at’); }); } // 只返回以某种方式答复了邀请的用户 if ($filters->has(‘response’)) { $user->whereHas(‘rsvp’, function ($query) use ($filters) { $query->where(‘response’, ‘I will be attending’); }); } } // 返回搜索结果 return $user->get(); }}我们做了一系列的改变。 首先, 我们将在控制器中的 $request 变量更名为 filters 来提高可读性。其次,由于 newQuery() 方法不是静态方法,无法通过 User 类静态调用,所以我们需要先创建一个 User 对象,再调用这个方法:$user = (new User)->newQuery();调用上面的 UserSearch 接口,对控制器的代码进行重构:<?phpnamespace App\Http\Controllers;use App\Http\Requests;use App\Search\UserSearch;use Illuminate\Http\Request;use App\Http\Controllers\Controller;class SearchController extends Controller{ public function filter(Request $request) { return UserSearch::apply($request); }}好多了,是不是?把一系列的条件判断交给专门的类处理,使控制器的代码简介清新。下面进入见证奇迹的时刻在这篇文章的例子中,一共有 7 个过滤条件,但是现实的情况是更多更多。所以在这种情况下,只用一个文件来处理所有的过滤逻辑,就显得差强人意了。扩展性不好,而且也不符合 S.O.L.I.D. principles 原则。目前,apply() 方法需要处理这些逻辑:检查参数是否存在把参数转成查询条件执行查询如果我想增加一个新的过滤条件,或者修改一下现有的某个过滤条件的逻辑,我都要不停地修改 UserSearch 类,因为所有过滤条件的处理都在这一个类里,随着业务逻辑的增加,会有点尾大不掉的感觉。所以对每个过滤条件单独建个类文件是非常有必要的。先从 Name 条件开始吧。但是,就像我们前面讲的,还是想一下我们需要怎样使用这种单一条件过滤的接口。我希望可以这样调用这个接口:$user = (new User)->newQuery();$user = static::applyFiltersToQuery($filters, $user);return $user->get();不过这里再使用 $user 这个变量名就不合适了,应该用 $query 更有意义。public static function apply(Request $filters){ $query = (new User)->newQuery(); $query = static::applyFiltersToQuery($filters, $query); return $query->get();}然后把所有条件过滤的逻辑都放到 applyFiltersToQuery() 这个新接口里。下面开始创建第一个条件过滤类:Name.<?phpnamespace App\UserSearch\Filters;class Name{ public static function apply($builder, $value) { return $builder->where(’name’, $value); }}在这个类里定义一个静态方法 apply(),这个方法接收两个参数,一个是 Builder 实例,另一个是过滤条件的值( 在这个例子中,这个值是 ‘Billy’ )。然后带着这个过滤条件返回一个新的 Builder 实例。接下来是 City 类:<?phpnamespace App\UserSearch\Filters;class City{ public static function apply($builder, $value) { return $builder->where(‘city’, $value); }}如你所见,City 类的代码逻辑跟 Name 类相同,只是过滤条件变成了 ‘city’。让每个条件过滤类都只有一个简单的 apply() 方法,而且方法接收的参数和处理的逻辑都相同,我们可以把这看成一个协议,这一点很重要,下面我会具体说明。为了确保每个条件过滤类都能遵循这个协议,我决定写一个接口,让每个类都实现这个接口。<?phpnamespace App\UserSearch\Filters;use Illuminate\Database\Eloquent\Builder;interface Filter{ /** * 把过滤条件附加到 builder 的实例上 * * @param Builder $builder * @param mixed $value * @return Builder $builder / public static function apply(Builder $builder, $value);}我为这个接口的方法写了详细的注释,这样做的好处是,对于每一个实现这个接口的类,我都可以利用我的 IDE ( PHPStorm ) 自动生成同样的注释。下面,分别在 Name 和 City 类中实现这个 Filter 接口:<?phpnamespace App\UserSearch\Filters;use Illuminate\Database\Eloquent\Builder;class Name implements Filter{ /* * 把过滤条件附加到 builder 的实例上 * * @param Builder $builder * @param mixed $value * @return Builder $builder / public static function apply(Builder $builder, $value) { return $builder->where(’name’, $value); }}以及<?phpnamespace App\UserSearch\Filters;use Illuminate\Database\Eloquent\Builder;class City implements Filter{ /* * 把过滤条件附加到 builder 的实例上 * * @param Builder $builder * @param mixed $value * @return Builder $builder */ public static function apply(Builder $builder, $value) { return $builder->where(‘city’, $value); }}完美。现在已经有两个条件过滤类完美地遵循了这个协议。把我的目录结构附在下面给大家参考一下:这是到目前为止关于搜索的文件结构。我把所有的条件过滤类的文件放在一个单独的文件夹里,这让我对已有的过滤条件一目了然。使用新的过滤器现在回过头来看 UserSearch 类的 applyFiltersToQuery() 方法,发现我们可以再做一些优化了。首先,把每个条件判断里构建查询语句的工作,交给对应的过滤类去做。// 根据姓名搜索用户if ($filters->has(’name’)) { $query = Name::apply($query, $filters->input(’name’));}// 根据城市搜索用户if ($filters->has(‘city’)) { $query = City::apply($query, $filters->input(‘city’));}现在根据过滤条件构建查询语句的工作已经转给各个相应的过滤类了,但是判断每个过滤条件是否存在的工作,还是通过一系列的条件判断语句完成的。而且条件判断的参数都是写死的,一个参数对应一个过滤类。这样我每增加一个新的过滤条件,我都要重新修改 UserSearch 类的代码。这显然是一个需要解决的问题。其实,根据我们前面介绍的命名规则, 我们很容易把这段条件判断的代码改成动态的:AppUserSearchFiltersNameAppUserSearchFiltersCity就是结合命名空间和过滤条件的名称,动态地创建过滤类(当然,要对接收到的过滤条件参数做适当的处理)。大概就是这个思路,下面是具体实现:private static function applyFiltersToQuery( Request $filters, Builder $query) { foreach ($filters->all() as $filterName => $value) { $decorator = NAMESPACE . ‘\Filters\’ . str_replace(’ ‘, ‘’, ucwords( str_replace(’’, ’ ‘, $filterName))); if (class_exists($decorator)) { $query = $decorator::apply($query, $value); } } return $query;}下面逐行分析这段代码:foreach ($filters->all() as $filterName => $value) {遍历所有的过滤参数,把参数名(比如 city)赋值给变量 $filterName,参数值(比如 London)复制给变量 $value。$decorator = NAMESPACE . ‘\Filters\’ . str_replace(’ ‘, ‘’, ucwords( str_replace(’’, ’ ‘, $filterName)));这里是对参数名进行处理,将下划线改成空格,让每个单词都首字母大写,然后去掉空格,如下例子:“name” => App\UserSearch\Filters\Name,"company" => App\UserSearch\Filters\Company,"city" => App\UserSearch\Filters\City,"event" => App\UserSearch\Filters\Event,"responded" => App\UserSearch\Filters\Responded,"response" => App\UserSearch\Filters\Response,"managers" => App\UserSearch\Filters\Managers如果有参数名是带下划线的,比如 has_responded,根据上面的规则,它将被处理成 HasResponded,因此,其相应的过滤类的名字也要是这个。if (class_exists($decorator)) {这里就是要先确定这个过滤类是存在的,再执行下面的操作,否则在客户端报错就尴尬了。$query = $decorator::apply($query, $value);这里就是神器的地方了,PHP 允许把变量 $decorator 作为类,并调用其方法(在这里就是 apply() 方法了)。现在再看这个接口的代码,发现我们再次实力证明了磨刀不误砍柴工。现在我们可以确保每个过滤类对外响应一致,内部又可以分别处理各自的逻辑。最后的优化现在 UserSearch 类的代码应该已经比之前好多了,但是,我觉得还可以更好,所以我又做了些改动,这是最终版本:<?phpnamespace App\UserSearch;use App\User;use Illuminate\Http\Request;use Illuminate\Database\Eloquent\Builder;class UserSearch{ public static function apply(Request $filters) { $query = static::applyDecoratorsFromRequest( $filters, (new User)->newQuery() ); return static::getResults($query); } private static function applyDecoratorsFromRequest(Request $request, Builder $query) { foreach ($request->all() as $filterName => $value) { $decorator = static::createFilterDecorator($filterName); if (static::isValidDecorator($decorator)) { $query = $decorator::apply($query, $value); } } return $query; } private static function createFilterDecorator($name) { return return NAMESPACE . ‘\Filters\’ . str_replace(’ ‘, ‘’, ucwords(str_replace(’_’, ’ ‘, $name))); } private static function isValidDecorator($decorator) { return class_exists($decorator); } private static function getResults(Builder $query) { return $query->get(); }}我最后决定去掉 applyFiltersToQuery() 方法,是因为感觉跟接口的主要方法名 apply() 有点冲突了。而且,为了贯彻执行单一职责原则,我把原来 applyFiltersToQuery() 方法里比较复杂的逻辑又做了拆分,为动态创建过滤类名称,和确认过滤类是否存在的判断,都写了单独的方法。这样,即便要扩展搜索接口,我也不需要再去反复修改 UserSearch 类里的代码了。需要增加新的过滤条件吗?简单,只要在 App\UserSearch\Filters 目录下创建一个过滤类,并使之实现 Filter 接口就 OK 了。结论我们已经把一个拥有所有搜索逻辑的巨大控制器方法保存成一个允许打开过滤器的模块化过滤系统,而不需要添加修改核心代码。 像评论里 @rockroxx所建议的,另一个重构的方案是把所有方法提取到 trait 并将 User 设置成 const 然后由 Interface 实现。class UserSearch implements Searchable { const MODEL = App\User; use SearchableTrait;}如果你很好的理解了这个设计模式,你可以 利用多态代替多条件。代码会提交到 GitHub 你可以 fork,测试和实验。如何解决多条件高级搜索,我希望你能留下你的想法、建议和评论。文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

March 26, 2019 · 5 min · jiezi

使用 TDD 测试驱动开发来构建 Laravel REST API

TDD 以及敏捷开发的先驱者之一的 James Grenning有句名言:如果你没有进行测试驱动开发,那么你应该正在做开发后堵漏的事 - James Grenning今天我们将进行一场基于 Laravel 的测试驱动开发之旅。 我们将创建一个完整的 Laravel REST API,其中包含身份验证和 CRUD 功能,而无需打开 Postman 或浏览器。?注意:本旅程假定你已经理解了 Laravel 和 PHPUnit 的基本概念。你是否已经明晰了这个问题?那就开始吧。项目设置首先创建一个新的 Laravel 项目 composer create-project –prefer-dist laravel/laravel tdd-journey。然后,我们需要创建 用户认证 脚手架,执行 php artisan make:auth ,设置好 .env 文件中的数据库配置后,执行数据库迁移 php artisan migrate。本测试项目并不会使用到刚生成的用户认证相关的路由和视图。我们将使用 jwt-auth。所以需要继续 安装 jwt 到项目。注意:如果您在执行 jwt:generate 指令时遇到错误, 您可以参考 这里解决这个问题,直到 jwt 被正确安装到项目中。最后,您需要在 tests/Unit 和 tests/Feature 目录中删除 ExampleTest.php 文件,使我们的测试结果不被影响。编码首先将 JWT 驱动配置为 auth 配置项的默认值:<?php // config/auth.php file’defaults’ => [ ‘guard’ => ‘api’, ‘passwords’ => ‘users’,],‘guards’ => [ … ‘api’ => [ ‘driver’ => ‘jwt’, ‘provider’ => ‘users’, ],],然后将如下内容放到你的 routes/api.php 文件里:<?phpRoute::group([‘middleware’ => ‘api’, ‘prefix’ => ‘auth’], function () { Route::post(‘authenticate’, ‘AuthController@authenticate’)->name(‘api.authenticate’); Route::post(‘register’, ‘AuthController@register’)->name(‘api.register’);});现在我们已经将驱动设置完成了,如法炮制,去设置你的用户模型:<?php…class User extends Authenticatable implements JWTSubject{ … //获取将被存储在 JWT 主体 claim 中的标识 public function getJWTIdentifier() { return $this->getKey(); } // 返回一个键值对数组,包含要添加到 JWT 的任何自定义 claim public function getJWTCustomClaims() { return []; }}我们所需要做的就是实现 JWTSubject 接口然后添加相应的方法即可。接下来,我们需要增加权限认证方法到控制器中.运行 php artisan make:controller AuthController 并且添加以下方法:<?php…class AuthController extends Controller{ public function authenticate(Request $request){ // 验证字段 $this->validate($request,[’email’ => ‘required|email’,‘password’=> ‘required’]); // 测试验证 $credentials = $request->only([’email’,‘password’]); if (! $token = auth()->attempt($credentials)) { return response()->json([’error’ => ‘Incorrect credentials’], 401); } return response()->json(compact(’token’)); } public function register(Request $request){ // 表达验证 $this->validate($request,[ ’email’ => ‘required|email|max:255|unique:users’, ’name’ => ‘required|max:255’, ‘password’ => ‘required|min:8|confirmed’, ]); // 创建用户并生成 Token $user = User::create([ ’name’ => $request->input(’name’), ’email’ => $request->input(’email’), ‘password’ => Hash::make($request->input(‘password’)), ]); $token = JWTAuth::fromUser($user); return response()->json(compact(’token’)); }}这一步非常直接,我们要做的就是添加 authenticate 和 register 方法到我们的控制器中。在 authenticate 方法,我们验证了输入,尝试去登录,如果成功就返回令牌。在 register 方法,我们验证输入,然后基于此创建一个用户并且生成令牌。4. 接下来,我们进入相对简单的部分。 测试我们刚写入的内容。 使用 php artisan make:test AuthTest 生成测试类。 在新的 tests / Feature / AuthTest 中添加以下方法:<?php /** * @test * Test registration /public function testRegister(){ //创建测试用户数据 $data = [ ’email’ => ’test@gmail.com’, ’name’ => ‘Test’, ‘password’ => ‘secret1234’, ‘password_confirmation’ => ‘secret1234’, ]; //发送 post 请求 $response = $this->json(‘POST’,route(‘api.register’),$data); //判断是否发送成功 $response->assertStatus(200); //接收我们得到的 token $this->assertArrayHasKey(’token’,$response->json()); //删除数据 User::where(’email’,’test@gmail.com’)->delete();}/* * @test * Test login /public function testLogin(){ //创建用户 User::create([ ’name’ => ’test’, ’email’=>’test@gmail.com’, ‘password’ => bcrypt(‘secret1234’) ]); //模拟登陆 $response = $this->json(‘POST’,route(‘api.authenticate’),[ ’email’ => ’test@gmail.com’, ‘password’ => ‘secret1234’, ]); //判断是否登录成功并且收到了 token $response->assertStatus(200); $this->assertArrayHasKey(’token’,$response->json()); //删除用户 User::where(’email’,’test@gmail.com’)->delete();}上面代码中的几行注释概括了代码的大概作用。 您应该注意的一件事是我们如何在每个测试中创建和删除用户。 测试的全部要点是它们应该彼此独立并且应该在你的理想情况下存在数据库中的状态。如果你想全局安装它,可以运行 $ vendor / bin / phpunit 或 $ phpunit 命令。 运行后它应该会给你返回是否安装成功的数据。 如果不是这种情况,您可以浏览日志,修复并重新测试。 这就是 TDD 的美丽之处。5. 对于本教程,我们将使用『菜谱 Recipes』作为我们的 CRUD 数据模型。首先创建我们的迁移数据表 php artisan make:migration create_recipes_table 并添加以下内容:<?php …public function up(){ Schema::create(‘recipes’, function (Blueprint $table) { $table->increments(‘id’); $table->string(’title’); $table->text(‘procedure’)->nullable(); $table->tinyInteger(‘publisher_id’)->nullable(); $table->timestamps(); });}public function down(){ Schema::dropIfExists(‘recipes’);}然后运行数据迁移。 现在使用命令 php artisan make:model Recipe 来生成模型并将其添加到我们的模型中。<?php …protected $fillable = [’title’,‘procedure’];/* * 发布者 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo /public function publisher(){ return $this->belongsTo(User::class);}然后将此方法添加到 user 模型。<?php… /* * 获取所有菜谱 * @return \Illuminate\Database\Eloquent\Relations\HasMany */public function recipes(){ return $this->hasMany(Recipe::class);}6. 现在我们需要最后一部分设置来完成我们的食谱管理。 首先,我们将创建控制器 php artisan make:controller RecipeController 。 接下来,编辑 routes / api.php 文件并添加 create 路由端点。<?php … Route::group([‘middleware’ => [‘api’,‘auth’],‘prefix’ => ‘recipe’],function (){ Route::post(‘create’,‘RecipeController@create’)->name(‘recipe.create’);});在控制器中,还要添加 create 方法<?php … public function create(Request $request){ //验证数据 $this->validate($request,[’title’ => ‘required’,‘procedure’ => ‘required|min:8’]); //创建配方并附加到用户 $user = Auth::user(); $recipe = Recipe::create($request->only([’title’,‘procedure’])); $user->recipes()->save($recipe); //返回 json 格式的食谱数据 return $recipe->toJson();}使用 php artisan make:test RecipeTest 生成特征测试并编辑内容,如下所示:<?php …class RecipeTest extends TestCase{ use RefreshDatabase; … //创建用户并验证用户身份 protected function authenticate(){ $user = User::create([ ’name’ => ’test’, ’email’ => ’test@gmail.com’, ‘password’ => Hash::make(‘secret1234’), ]); $token = JWTAuth::fromUser($user); return $token; } public function testCreate() { //获取 token $token = $this->authenticate(); $response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token, ])->json(‘POST’,route(‘recipe.create’),[ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’ ]); $response->assertStatus(200); }}上面的代码你可能还是不太理解。我们所做的就是创建一个用于处理用户注册和 token 生成的方法,然后在 testCreate() 方法中使用该 token 。注意使用 RefreshDatabase trait ,这个 trait 是 Laravel 在每次测试后重置数据库的便捷方式,非常适合我们漂亮的小项目。好的,所以现在,我们只要判断当前请求是否是响应状态,然后继续运行 $ vendor/bin/phpunit 。如果一切运行顺利,您应该收到错误。 ?There was 1 failure:1) TestsFeatureRecipeTest::testCreateExpected status code 200 but received 500.Failed asserting that false is true./home/user/sites/tdd-journey/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:133/home/user/sites/tdd-journey/tests/Feature/RecipeTest.php:49FAILURES!Tests: 3, Assertions: 5, Failures: 1.查看日志文件,我们可以看到罪魁祸首是 Recipe 和 User 类中的 publisher 和 recipes 的关系。 Laravel 尝试在表中找到一个字段为 user_id 的列并将其用作于外键,但在我们的迁移中,我们将publisher_id 设置为外键。 现在,将行调整为://食谱文件public function publisher(){ return $this->belongsTo(User::class,‘publisher_id’);}//用户文件public function recipes(){ return $this->hasMany(Recipe::class,‘publisher_id’);}然后重新运行测试。 如果一切顺利,我们将获得所有绿色测试!?… 3 / 3 (100%)…OK (3 tests, 5 assertions)现在我们仍然需要测试创建配方的方法。为此,我们可以判断用户的『菜谱 Recipes』计数。更新你的 testCreate 方法,如下所示:<?php…//获取 token$token = $this->authenticate();$response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token,])->json(‘POST’,route(‘recipe.create’),[ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’]);$response->assertStatus(200);//得到计数做出判断$count = User::where(’email’,’test@gmail.com’)->first()->recipes()->count();$this->assertEquals(1,$count);我们现在可以继续编写其余的方法。首先,编写我们的 routes/api.php<?php…Route::group([‘middleware’ => [‘api’,‘auth’],‘prefix’ => ‘recipe’],function (){ Route::post(‘create’,‘RecipeController@create’)->name(‘recipe.create’); Route::get(‘all’,‘RecipeController@all’)->name(‘recipe.all’); Route::post(‘update/{recipe}’,‘RecipeController@update’)->name(‘recipe.update’); Route::get(‘show/{recipe}’,‘RecipeController@show’)->name(‘recipe.show’); Route::post(‘delete/{recipe}’,‘RecipeController@delete’)->name(‘recipe.delete’);});接下来,我们将方法添加到控制器。 以下面这种方式更新 RecipeController 类。<?php ….//创建配方public function create(Request $request){ //验证 $this->validate($request,[’title’ => ‘required’,‘procedure’ => ‘required|min:8’]); //创建配方并附加到用户 $user = Auth::user(); $recipe = Recipe::create($request->only([’title’,‘procedure’])); $user->recipes()->save($recipe); //返回配方的 json 格式数据 return $recipe->toJson();}//获取所有的配方public function all(){ return Auth::user()->recipes;}//更新配方public function update(Request $request, Recipe $recipe){ //检查用户是否是配方的所有者 if($recipe->publisher_id != Auth::id()){ abort(404); return; } //更新并返回 $recipe->update($request->only(’title’,‘procedure’)); return $recipe->toJson();}//显示单个食谱的详细信息public function show(Recipe $recipe){ if($recipe->publisher_id != Auth::id()){ abort(404); return; } return $recipe->toJson();}//删除一个配方public function delete(Recipe $recipe){ if($recipe->publisher_id != Auth::id()){ abort(404); return; } $recipe->delete();}代码和注释已经很好地解释了这个逻辑。最后我们的 test/Feature/RecipeTest:<?php… use RefreshDatabase;protected $user;// 创建用户并验证他protected function authenticate(){ $user = User::create([ ’name’ => ’test’, ’email’ => ’test@gmail.com’, ‘password’ => Hash::make(‘secret1234’), ]); $this->user = $user; $token = JWTAuth::fromUser($user); return $token;}// 测试创建路由public function testCreate(){ // 获取令牌 $token = $this->authenticate(); $response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token, ])->json(‘POST’,route(‘recipe.create’),[ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’ ]); $response->assertStatus(200); // 获取计数并断言 $count = $this->user->recipes()->count(); $this->assertEquals(1,$count);}// 测试显示所有路由public function testAll(){ // 验证并将配方附加到用户 $token = $this->authenticate(); $recipe = Recipe::create([ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’ ]); $this->user->recipes()->save($recipe); // 调用路由并断言响应 $response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token, ])->json(‘GET’,route(‘recipe.all’)); $response->assertStatus(200); // 断言计数为1,第一项的标题相关 $this->assertEquals(1,count($response->json())); $this->assertEquals(‘Jollof Rice’,$response->json()[0][’title’]);}// 测试更新路由public function testUpdate(){ $token = $this->authenticate(); $recipe = Recipe::create([ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’ ]); $this->user->recipes()->save($recipe); // 调用路由并断言响应 $response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token, ])->json(‘POST’,route(‘recipe.update’,[‘recipe’ => $recipe->id]),[ ’title’ => ‘Rice’, ]); $response->assertStatus(200); // 断言标题是新标题 $this->assertEquals(‘Rice’,$this->user->recipes()->first()->title);}// 测试单一的展示路由public function testShow(){ $token = $this->authenticate(); $recipe = Recipe::create([ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’ ]); $this->user->recipes()->save($recipe); $response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token, ])->json(‘GET’,route(‘recipe.show’,[‘recipe’ => $recipe->id])); $response->assertStatus(200); // 断言标题是正确的 $this->assertEquals(‘Jollof Rice’,$response->json()[’title’]);}// 测试删除路由public function testDelete(){ $token = $this->authenticate(); $recipe = Recipe::create([ ’title’ => ‘Jollof Rice’, ‘procedure’ => ‘Parboil rice, get pepper and mix, and some spice and serve!’ ]); $this->user->recipes()->save($recipe); $response = $this->withHeaders([ ‘Authorization’ => ‘Bearer ‘. $token, ])->json(‘POST’,route(‘recipe.delete’,[‘recipe’ => $recipe->id])); $response->assertStatus(200); // 断言没有食谱 $this->assertEquals(0,$this->user->recipes()->count());}除了附加测试之外,我们还添加了一个类范围的 $user 属性。 这样,我们不止可以利用 $user 来使用 authenticate 方法不仅生成令牌,而且还为后续其他对 $user 的操作做好了准备。现在运行 $ vendor/bin/phpunit 如果操作正确,你应该进行所有绿色测试。结论希望这能让你深度了解在 TDD 在 Laravel 项目中的运行方式。 他绝对是一个比这更宽泛的概念,一个不受特地方法约束的概念。虽然这种开发方法看起来比常见的调试后期程序要耗时, 但他很适合在代码中尽早捕获错误。虽然有些情况下非 TDD 方式会更有用,但习惯于 TDD 模式开发是一种可靠的技能和习惯。本演练的全部代码可参见 Github here 仓库。请随意使用。干杯!文章转自:https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

March 6, 2019 · 5 min · jiezi

2019 为什么我们还会继续使用 PHP ?

我们来开门见山地说。 PHP 是一门奇葩的语言。它既不快,语法又不漂亮。还没有遵守良好的软件开发实践。 但我还是使用它开发了很多软件。 那么问题就很明显了 为什么今天还在使用 PHP ?除了个人理想主义的偏好以外,还有很多理由。这才是我们要讨论的范围。为什么选 PHP 作为我的 Web 开发语言?PHP 是什么?「PHP 代表什么意思?」 或者这么问,「PHP 是什么?」 PHP 是一门编写 web 页面的语言,底层使用 C 语言实现,使用 HTML 的标签语法包裹代码。PHP 通常跑在服务端,与 web 服务器配合工作,负责把 HTML 加工完返回给访问者。PHP 最初的意思是 「 Personal Home Page 」。因为这完全限制了使用该语言进行一般使用的意义和可取性,所以该语言现在代表「 PHP:Hypertext Preprocessor」。这种写法被称为递归缩写(一种全称中引用自己的缩写)。 极客们都喜欢用这种名字。PHP 能做什么?PHP 能用来做什么?通常来说 PHP 能做任何你想在 web 服务器上实现的东西。 比如说做一个博客。实现一个 SAAS 应用也完全不在话下。写一个临时处理数据的脚本也是得心应手。又或者写了一个复杂的脚本,某一天突然变成了一门成功的软件业务?过去也经常发生。如果你不相信我,可以看下 PHP 官网列出使用场景:服务端脚本命令行脚本编写桌面应用我不太鼓励最后一项,但它确实可行。但是前两项确实是很棒的理由 。这就引出了一个重要且无法回避的事实…PHP 无所不在了解和喜爱 PHP 的理由有非常多,可能最给力和有效的理由是:它可以在网络的任何地方使用和运行。如果你仔细寻找,你买到每月最低3美元的托管账户可能可以运行 Python 或 Ruby 的 Web 应用程序。但它肯定能运行 PHP 。这意味着无论什么时候你都可以依赖 PHP 。因为 PHP 可以运行在任何地方,并且它容易上手,很多非常受欢迎的软件都是用 PHP 写的。 WordPress 是对我影响最大和最熟悉的例子,除此之外,像 Joomla , Drupal , Magento , ExpressionEngine , vBulletin (是的,它仍然存在), MediaWiki 等这些工具,它们都在服务器上运行 PHP 。不仅如此,PHP 应用框架更是多得数不胜数, 比如:Symfony , Zend , Laravel , Aura , CakePHP , Yii 甚至是古老的 CodeIgnitor 框架。当然你可以为其它任何一门语言制作一些长度相当的 Web 框架列表。比如常用的 Web 语言像 Python , Ruby ,或 Node/JavaScript ,你甚至可以积累一个数字竞争列表来跟 PHP 比较。但运行 PHP 的网站数量庞大到无法估计。WordPress 自豪地声称在互联网上有 30% 的用户量。 你甚至不需要相信这个说法就会意识到许多的互联网应用必须使用 PHP 即使这个说法甚至可以想象成是真的。PHP 的优点动态化特点PHP 和 HTML 可以非常融洽地工作在一起。将 file.html 修改为 file.php ,即可在 <?php 和 ?> 标签里编写 PHP 代码,并且大部分的 Web 服务器默认配置就是可以直接运行 PHP 脚本。正因为其门槛非常低,不需要懂太多编程知识的菜鸟程序员也可以直接上手操作,以至于产生了很多低品质的代码。因为其易学性,大量的菜鸟程序员涌入,暴露出来另一个 PHP 一直被人诟病的问题:PHP 并没有对自己是最好的 Web 服务器端语言有清晰的远景,当然也没为此做过太好的设计。导致走向了另一个结果,来自全球各地松散的合作,大杂烩般地贡献代码和想法,难以避免地,有一些坏的想法渗入其中。拥有强大的面向对象包管理是现在 PHP 中的一种标准Composer 是 PHP 的加分项。在 PHP 的开发中,经常会有一些奇葩的东西被发布出来。最著名的栗子当属 PHP 5.3 (广泛的被视为 PHP 的第一个现代化版本 )中引入 goto 声明。类似的问题是在 PHP 成长过程中产生:面向对象最初是作为一个有缺陷和有限制的概念来实现的,标准库中充满了不一致的名称和参数排序,并且(举个近来备受关注的栗子)在 PHP 5.3 中 :: 操作符无法识别时,解析器会抛出可读性很差的报错信息:语法错误 (T_PAAMAYIM_NEKUDOTAYIM) ,如果不看文档,你能用英文来理解这个 T_PAAMAYIM_NEKUDOTAYIM 标示的意思吗?但是现在,PHP 已经完全支持 OOP。很少语言具有像 PHP 这样类似 Java 的 OOP 实现。另外,与 Java不同的是,PHP 有一个单独的并拥有广泛支持的包管理器,叫作 Composer。它非常的好用,并且不容忽视的一点是它可以方便地引用优质且维护良好的库,具有很高的易用性。PHP 的高速发展通过这些事实发现, PHP 正在以有趣的方式发展着。它正逐渐成为像java一样功能齐全的面向对象语言(更好或更糟糕),正在为函数式编程提供简单的抽象,这可以说是当前的热点。并且,正在发展一套非常棒的工具, PHP 喜欢 Composer , 并有充分的理由 — 因为它在一些大型开源项目的协同工作上做出了值得赞扬的努力.当然,我们不应该忘记当前的热点:PHP 在 PHP7 系列版本中的发展取得了速度上的提升。这被广泛认为是由 Facbook 出现的 HHVM 引起的,在短时间内,存在 HHVM 的发展速度会破坏 PHP 社区的风险,但事实并非如此,相反,PHP 的发展速度快得多,以至于人们几乎忘记了 HHVM 的存在。PHP 有一个巨大的社区;它对新手友好。如果你决定使用什么很酷的新技术,我认为经常得到很少关注的是与语言达成协议的方式。学习PHP是什么感觉? PHP教程很好找,质量通常也很好。PHP流行的一个缺点是,你会从那些对工具不够了解的人那里找到一些不那么好的教学。或者从某人那里学到的“最佳实践”是十年前的。但总的来说,这很少见,我不认为它应该让你灰心。或者突然发现自己使用了几个月的最佳实践并不是最好的是很少见的,也不是一个大问题。PHP 与其他语言的比较接下来我们要对比下 PHP 与其他的语言,这些语言需满足以下要求:开源:意味着你可以自由、免费地使用或者查看底层语言或者程序;应用于 Web 开发领域:不一定是专注于 Web 开发,但是在 Web 开发中有比较大的应用;高阶动态语言:具备动态语言的特性,大部分面向 Web 应用的语言兼具此特性;足够大的社区:有很多满足以上条件,但是只是在小范围使用的语言,我们使用这最后一个条件将他们过滤掉。为什么使用 PHP?而不是 JavaScript?也许和 PHP 比较的最重要的语言是 JavaScript。现代开发要求每个项目都至少会一点 JavaScript 来进行客户端开发和交互。借助 Node,这使得在服务器上使用 JS 变得相对简单。在服务器上和客户端“同构”使用相同的语言非常吸引人。如同 PHP,JavaScript 是一个兼容并蓄但有时丑陋的语言,有很多的瑕疵和“陷阱”,但 JavaScript 在过去十年变得非常快,所以他的情况是真实的。为什么选择 PHP 而不是 JavaScript?你已有专业知识或者在 PHP 使用库。否则,我认为 JavaScript 或许是个更好的选择。PHP 对比动态服务器页面 (.Net Core)动态服务器语言起源于一个用于网友编程的 Microsoft 语言。他和 PHP 非常直接相似。但他运行在 Windows 服务器环境。这被 ASP.net 取代了。现在已被 ASP.NET Core 取代。后两者现在是 PHP 一样的开源语言。而我个人从没有在任何变体中写过一点 ASP。如果我使用且更喜欢 Microsoft 服务器的话,我会更喜欢 ASP。其他情况,请给我 PHP。两者社区的规模和大小没有可比性。我应该使用 Ruby 还是 PHP?Ruby,特别是 Ruby on Rails,在过去十年前 非常 受欢迎。Ruby 依然是一个备受喜爱的语言,这在我看来他比 PHP 优雅。也就是说,他的社区较小。同时我认识到 Ruby 已经不再是 “热门语言”(被 JavaScript 取代了此角色)。Ruby 的优雅,并且有足够多的人擅长使用,因此我不会避免他。但招聘已熟悉 Ruby 的人依旧比招聘 PHP 要难。(虽然我认为一般的 Ruby 开发者水平比相同的 PHP 开发者要高。)Python vs PHP:谁更好一些?最后一种与 PHP 一对一比较起来有意义的是 Python 。 Python 的使用场景要更多,相对于专注于 web 开发的 PHP 来说(尤其是在数据统计与分析上面)。而且人们普遍都觉得它是一种更为更稳定和优雅的语言。就像 Ruby 和 JavaScript 一样, Python 在服务器上运行要比 PHP 稍微麻烦一点。但是它是一种非常完美的语言,而且比起 PHP 的使用更具多样化,我认为它是比 PHP 更流行的语言之一,而且在其他方面(例如:各种第三方的库、专业技术知识的获取、招聘求职)都是一样的。PHP vs Go 语言?Scala语言?Java语言?等等一开始曾提到过,许多语言在做比较时或多或少会有一些相似的地方。这里有很多的语言可以拿来做对比,因此简单的聊几个:java 非常受欢迎,并且效率很高。通常被用来搭建安卓应用程序,桌面应用程序,和 web 端应用。不过,它不是动态类型的,它有更好的性能保证,但很适合Web 编程。Go 是 Google 支持的正在发展的一门新语言。它专注 web 服务。但是这块相比 PHP 还是有些不足(更像 C 语言),它的执行速度很快,但是社区相对有限。 Scala 是一种流行的运行在内存区(Java 兼容)的语言,也似乎越来越受欢迎。它比 PHP 设计的更优雅,但是除此之外,似乎没有更多了。此外,我觉得这些分析足够可以做出选择了,但是你有更多的选项可以去考量,不过最后我的替代方案将会考虑 PHP。需要按需选择编程语言有了上面的比较,为什么还要使用 PHP ?上文我已经提过了一些 PHP 里坏的设计,有一些我经常性会经历,例如在『查找类型的函数』里,这些函数needle 和 haystack 顺序不一致,在没有代码补全的环境下,我还是会掉坑里。如果你在构建一个全新的 Web 项目,并且此项目不需要与其他系统交互,并且只有你一个人在工作,你想试试看其他语言,类似 Python 什么的。但是,你需要知道的是,即使是这样的一个项目,还是有很多理由让你选择 PHP。任何现代化的语言都可以让你构建一个 Web App。每一门语言都有他们不足之处,例如 PHP 就是这样的,PHP 要求你清晰地了解其不足之处,然后才能决策是否要在项目里使用它。但是对于我来说,我仍然会选择 PHP 来构建我的 Web 项目,就如 Keith Adams 在其 演讲里 提到的:PHP 的开发效率真不是一般的高。如果你有一个 Python 的团队,请使用 Python。如果你有个合伙人熟悉 Java,请使用 Java 来编写你的 SaaS 程序。语言,永远都不是最重要的。文章转自: https://learnku.com/php/t/24576 更多文章:https://learnku.com/laravel/c… ...

March 5, 2019 · 2 min · jiezi

Laravel Excel 的五个隐藏功能

Laravel Excel package 最近发布了 3.0 版本,它所具有的新功能,可以帮助简化高级需求,并且可用性极高。大家一起来探讨一下可能不知道的一些隐藏功能,这些功能使 Laravel Excel 成为 Excel 拓展的最佳首选。1. 从 HTML 或者是 Blade 导入数据假设已经有一个 HTML 表格模版代码 – resources/views/customers/table.blade.php:<table class=“table”> <thead> <tr> <th></th> <th>First name</th> <th>Last name</th> <th>Email</th> <th>Created at</th> <th>Updated at</th> </tr> </thead> <tbody> @foreach ($customers as $customer) <tr> <td>{{ $customer->id }}</td> <td>{{ $customer->first_name }}</td> <td>{{ $customer->last_name }}</td> <td>{{ $customer->email }}</td> <td>{{ $customer->created_at }}</td> <td>{{ $customer->updated_at }}</td> </tr> @endforeach </tbody></table>你可以使用它去重复导入这个表格到 Excel步骤1. 生成一个 Export 类php artisan make:export CustomersFromView –model=Customer步骤2. 使用 FromView 进行操作namespace App\Exports;use App\Customer;use Illuminate\Contracts\View\View;use Maatwebsite\Excel\Concerns\FromView;class CustomersExportView implements FromView{ public function view(): View { return view(‘customers.table’, [ ‘customers’ => Customer::orderBy(‘id’, ‘desc’)->take(100)->get() ]); }}这里是导入的 Excel 文件:注意:这里只能导出 HTML 表格,不能具有任何标签,比如 html,body,div 等。2. 导出到 PDF,HTML,或是其他格式的文件虽然包的名称是 Laravel Excel,但是提供了多种导出格式,并且使用起来十分简单,只要在类里再添加一个参数即可:return Excel::download(new CustomersExport(), ‘customers.xlsx’, ‘Html’);比如这么做,就导出到了HTML,如下图所示:没有太多的样式,下面是源代码:不仅如此,它还可以导出到 PDF,甚至你可以从中选择三种库,使用方法是一样的,你只要在最后一个参数指定格式就好了,下面是一些例子。 文档示例:注意:你必须通过 composer 安装指定的 PDF 包,比如:composer require dompdf/dompdf导出的 PDF 如下所示:3. 按需格式化单元格Laravel Excel 有一个强有力的「爸爸」 – PhpSpreadsheet。因此它就拥有其各种底层功能,包括各种方式的单元格格式化。此处是一个如何在 Laravel Export 类中使用它的例子,例如 app/Exports/CustomersExportStyling.php:步骤 1. 在头部引入适当的类。use Maatwebsite\Excel\Concerns\WithEvents;use Maatwebsite\Excel\Events\AfterSheet;步骤 2. 在 implements 部分使用 WithEvents 接口。class CustomersExportStyling implements FromCollection, WithEvents{ // …步骤 3. 用 AfterSheet 事件来创建 registerEvents() 方法。/ * @return array /public function registerEvents(): array{ return [ AfterSheet::class => function(AfterSheet $event) { // … 此处你可以任意格式化 }, ];}这里有个例子:/* * @return array /public function registerEvents(): array{ return [ AfterSheet::class => function(AfterSheet $event) { // 所有表头-设置字体为14 $cellRange = ‘A1:W1’; $event->sheet->getDelegate()->getStyle($cellRange)->getFont()->setSize(14); // 将样式数组应用于B2:G8范围单元格 $styleArray = [ ‘borders’ => [ ‘outline’ => [ ‘borderStyle’ => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THICK, ‘color’ => [‘argb’ => ‘FFFF0000’], ] ] ]; $event->sheet->getDelegate()->getStyle(‘B2:G8’)->applyFromArray($styleArray); // 将第一行行高设置为20 $event->sheet->getDelegate()->getRowDimension(1)->setRowHeight(20); // 设置 A1:D4 范围内文本自动换行 $event->sheet->getDelegate()->getStyle(‘A1:D4’) ->getAlignment()->setWrapText(true); }, ];}这些「随机」样例展示的结果如下所示:你可以在 Recipes page of PhpSpreadsheet docs中找到所有的以上以及更多示例。4. 隐藏模型属性假设我们已经创建了Laravel 5.7默认的users表:现在我们尝试用简单的FromCollection来导出用户表数据:class UsersExport implements FromCollection{ public function collection() { return User::all(); }}在导出的Excel 里,你只能看到如下字段,但是没有password和remember_token:这是因为在User模型里定义了隐藏字段的属性:class User extends Authenticatable{ // … / * 这个数组用来定义需要隐藏的字段。 * * @var array / protected $hidden = [ ‘password’, ‘remember_token’, ];}所以,默认情况下这些字段是隐藏的,如果你想在导出数据的时候某些字段不被导出的话,可以直接在模型中定义隐藏属性$hidden。5. 公式出于某种原因,Laravel Excel 包的官方文档中并没有提及公式,但是这是Excel 重要的功能!幸运的是,我们可以直接将公式写在导出数据的类中,我们需要设置cell 的值,就像这样:=A2+1 or SUM(A1:A10)。其中一种方式就是实现WithMapping 接口:use App\Customer;use Maatwebsite\Excel\Concerns\FromCollection;use Maatwebsite\Excel\Concerns\WithMapping;class CustomersExportFormulas implements FromCollection, WithMapping{ public function collection() { return Customer::all(); } / * @var Customer $customer * @return array */ public function map($customer): array { return [ $customer->id, ‘=A2+1’, $customer->first_name, $customer->last_name, $customer->email, ]; }}以上就是Laravel Excel的五个鲜为人知的功能。文章转自: https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

March 4, 2019 · 2 min · jiezi

上线清单 —— 20 个 Laravel 应用性能优化项

让我们开始吧!假若你的 laravel 应用已经投入生产环境中。从第一个用户,到第十,第一百,直到成千上万的用户!慢慢地,随着用户越多,你的网站会越来越慢那我们应该如何做?细节决定成败经过一番搜索,我决定写下这20个使你网站提升速度的小提示我将从基础开始,大部分都是可以瞬间完成的操作。然后,我将逐步提高难度。最后,就是更高级的内容了。如果你跟着我的步骤一步一步来,我相信你的网站会得到质的提升。享受你的学习之旅!如果你有什么建议,可以在下方留言!我很高兴跟大家共同探讨。基础的优化项让我们看看我们能够在短短几秒钟内做些什么。1. 路由缓存每次服务器执行请求时,都会注册所有的路由,这会花费一些时间。但是,你可以选择缓存路由列表来跳过这个步骤。缓存路由列表是非常简单的。你需要做的是在部署应用程序后,执行下面的这个命令:php artisan route:cache但是,如果你添加或修改了任意一个路由信息,请不要忘记清除之前的缓存以及重新执行缓存命令。php artisan route:clear# 然后,再次执行php artisan route:cache注意,这只对控制器类路由有效。2. 缓存配置就如路由一样,你同样可以在应用中缓存配置文件。设想一下这种场景:每次你发送一个请求到 App 中,Laravel 都需要去加载不同的配置文件,并且要去打开.env 文件读取其中的内容。这种方式性能低下,是不?不过不用担心,这里有个 Artisan 命令专治这个。php artisan config:cache你在部署之后可以使用它。和路由差不多,别忘了编辑东西的时候清理一下缓存。php artisan config:clear# 然后,再来一次…php artisan config:cache3. 优化 Composer 自动加载通常,Composer 生成自动加载文件非常快。但是,在生产环境中,如果设置了PSR-4 和 PSR-0 自动加载规则,这可能会变慢。您可以通过将下面命令添加到部署脚本来优化自动加载器文件创建过程。$ composer dump-autoload -o不要忘记它。4. 谣言:「不要大量使用 Blade 视图」这个谣言我都听到头大了。“千万不要大量使用 Blade 视图,因为它会使得应用性能降低!“彻头彻尾的大谎言!认真脸!铭记这个:Laravel 编译 Blade 视图。编译就是说,在流程结束时,你将拥有一个已编译的完整文件,而非使用多个文件。所以,丝毫不需要担心。*中级干货5. 换个其他/更好的 Cache/Session 驱动默认的,当你新建一个 Laravel 项目的时候Cache 和 Sessions 的驱动默认为 「文件」。在本地开发环境和小项目中它没啥问题,但是项目增长时事情就大条了。所以,考虑下换个更好的驱动例如 Redis。 Laravel 有内置支持它的方式,而你要做的就是 安装 Predis。更多细节在 这里和 此处。6. 尽快升级 Laravel 版本当新版本发布时,请记得尽快升级 Laravel。这不仅关乎新功能:在可能的情况下,所有贡献者都花时间修复代码库周边的性能问题。所以,要持续关注并保持代码更新。7. 删除未使用的服务这是很多人经常忘记的小技巧,要向自己提问:“我需要它吗?*我们知道 Laravel 自带了很多服务,毕竟,这是一个全栈框架,每一个服务都有其用武之地。所以,请花一些时间检查 config/app.php 文件,看看你是否能找到一个你不需要的服务。如果一切正常,请尝试将其删除并测试您的应用程序。它应该有所帮助(一点点)!8. 使用预加载进行查询如果你知道 Laravel 是什么,你可能也知道预加载是什么。 如果您信息不够及时,预加载是一种通过使用特定语法来减少发送到数据库的查询数量来提高 Eloquent 性能的方法。此问题称为N + 1查询问题。 让我们举个例子。 你有两个模型:Book 和 Author。 每本 book 都有它的 author。$books = App\Book::all();foreach ($books as $book) { echo $book->author->name;}想象一下,您的数据库中有1000本书。 现在,此代码将执行 1001 次查询以检索这1000本书的作者姓名。1(查询以获取1000本书的数据)+ 1000(查询以获取每本书的作者数据)= 1001。但是,如果你编写这样的代码$books = App\Book::with(‘author’)->get();foreach ($books as $book) { echo $book->author->name;}更改基础查询以避免此性能问题。 您将只执行两个查询而不是1001! 这是巨大的性能提升。9. 缓存查询结果有时候, 缓存一个具体的查询结果可能是一个好主意。 想象这样一个场景:你准备在你的应用主页上展示 “十大专辑” 排行榜。 这项工作是通过从数据库中执行查询完成的(查询可能涉及到artists表以及其他的一些表)。 你的主页访问量是 1000 次/小时 。如果这个排行榜数据的查询次数是 1000次每小时,那么一天下来执行的查询次数就是24000次。现在,让我们假设这个排行榜是每小时更新一次 。那么,将每次的查询结果缓存一小时如何 ?$value = Cache::remember(’top_10_albums’, 60, function () { return Album::with(‘artist’, ‘producer’)->getTopTen();});这个缓存组件的 remember 方法在未找到缓存的情况下将会先从数据库中获取数据,并缓存60分钟。到期后,将会再次从数据库中获取最新的数据,更新缓存。查询次数 从 24000 到 24 次/天 。10. 为你的数据表建立索引记住,必要的时候请为您的数据表建立索引。 这看起来像是个没什么卵用的提示,但实际上这很有必要。 因为我见过非常多的应用,它们的数据表没有索引。实现起来很简单,您可以创建一个新的数据库迁移并使用里面的方法来添加索引.Schema::table(‘my_table_name’, function(Blueprint $table){ $table->index(‘field_to_be_indexed’);});当然,索引不是您喜欢在哪建就直接创建一个就是了。您必须研究您的业务、代码和查询,去分析哪里才是最需要索引的地方,然后再建立索引。11. 中间件太多?Laravel 会对你注册的中间件进行大量的(前/后)调用。所以,请你仔细检查它们,并且去掉那些你不需要的中间件。通常中间件列表在 Kernel.php 。12. 是时候使用队列了!有些时候,Laravel 比预期慢,这时你可以考虑异步执行任务。最常见的情况就是发送一封欢迎邮件,让我们一起看看任务流程。用户填写我们的表单;将他/她的详细信息写入数据库;发送一封写有欢迎语和确认链接的邮件给他/她;并展示感谢页面;很多时候,这些任务完全是在控制器中并且按照顺序执行。我的建议是学会如何使用事件和队列,可以将发送邮件任务交给专门的流程,以致于改善用户使用体验。*高级干货13. 使用 Pusher 改进异步队列上面我写了一些跟队列有关的内容。有时,你也可以使用队列来改善面向用户的任务。想象一下,你正在创建一个繁重的(在计算方面)进程,并且希望给用户显示这个任务的进度条。你可以轻松地使用队列的异步任务并集成 Pusher 来向前端发送消息以达到目的,即使这个任务没有完成。另一个经常使用的示例是向用户发送消息不需要刷新页面。考虑一下吧!14. 使用 Logs / Debugbars / Laravel Telescope 测量调试工具在提升自己方面,有一句我自己非常喜欢的引用。是从我的 CEO (感谢 Massimo !)引用 Peter Drucker 的话那听来的。如果你无法衡量它,你就无法改进它。这个概念非常适合 Web 应用程序的上下文。要想改善 Web 应用的请求管理时间,需要测量很多东西。幸运地,我们有许多非常优秀的工具来完成这件事。慢日志: MYSQL , MariaDB 和其他数据库可以启用慢日志来追踪那些语句花了大量的时间。你可以使用这些数据来判定是否必须更改或优化特定的代码(或查询);Debugbar : Laravel Debugbar 是一个很棒的扩展包。在很多应用程序方面,你可以使用它来收集数据。比如查询,视图,时间等等;Laravel Telescope : 另一个非常酷的工具是 Laravel Telescope ,对 Laravel 应用,有“优雅的调试助手”的美称。如果你感兴趣, 我已经在这里写了一篇关于它的文章 ;15. 更新你的PHP版本虽然这看起来很简单,但是如果你的项目够大的话,这执行起来会很麻烦,所以我觉得把这条加入高级技巧里面。如果你的 PHP 版本在7.0以下,更新你的 PHP 和 laravel 版本。16. 在服务器上使用 Lumen如果你的应用程序数据量增长很大,你可以考虑为你的系统做服务拆分了。不过,这并没有一个明确的方法指南来引导你完成它:拆分标准的高与低取决于来自应用程序的领域到拆分所有必需的内容所需的工作中的许多因素。但是,一旦你拆分成功,你的项目将获得新生。如果你对这个主题感兴趣的话,可以从 https://medium.com/@munza/lar… 开始。17. 为静态资源提供 CDN 服务我非常肯定你有很多前端的资源,比如 CSS 文件和 JS 脚本。你可以通过多种方式来减少发送给用户的数据量:压缩静态资源;捆绑静态资源(将多个 CSS 文件或者 JS 脚本合并为一个,以减少请求次数);开启 gzip 压缩;然而,如果你遇到大量的流量,则你可以将你的静态资源托管到专用的 CDN 服务器上,比如:Akamai(阿卡迈);Max CDN;Cloudflare;亚马逊 AWS 服务 (S3 + CloudFront);译者注:国内可以使用又拍云和七牛云18. 使用高级测量工具安装 Laravel Debugbar 或 Telescope 将是一个良好的开端,但对于更重大的项目,这还不够。你需要选择更高级的工具,如下:New Relic;AppOptics;Datadog;Sentry;以上列表的应用程序不做同样的事情:他们被设计用于不同目的。花些时间去学习他们以理解他们如何提供帮助。19. 垂直扩展你已经对代码的细枝末节都进行了彻底优化,但是你的应用体量在不断增长。迟早你都要进行垂直扩展。有个简单的说法就是:更多的 RAM,更多的空间,更多的带宽就,以及更多的 CPU注意这个只是对许多没有足够时间来安排重构/优化的初创公司的通常做法。法子是不错,所以你可以认为这是能让你喘口气的临时解决方案。20. 水平扩展水平扩展是另一种扩展的方式,它不同于传统的垂直扩展,主要有两点:取代在现有配置上增加硬件资源的方式,你可能将会添加更多的功能模块来处理日益增加的流量。 在垂直扩展的环境中,你只需要增加服务器配置就行,但是水平扩展应用就意味着你的应用将会部署运行在不同的机器上,有可能是在一个负载均衡机器或者其他服务之后。这就意味着需要更多的设置和配置;此时事情就没那么简单了;并非所有的应用都可以在短时间内扩展完毕,有时候你需要重构隔离一些代码;有时候你需要把应用拆分为不同规模的小型服务。水平扩展会有有很多事情要做,但是一旦你能对应用进行水平扩展时,好处也是超乎想象的。结论今天有足够的内容了!我通过合并我的个人经验和以前做过的一些研究创建了在这个列表。如果你愿意,请尽管提出一些新东西,我很乐意相应更新一下此文章。转自 https://learnku.com/laravel/t… ...

February 28, 2019 · 2 min · jiezi

Laravel 5.8 正式发布(文档翻译已启动)

Laravel 5.8 现在面向所有人正式发布了。这个版本包括了几个新特性以及最新的错误修复和对框架核心的改进。一些新特性如下:PHP dotenvLaravel 5.8 集成了 PHP 的 dotenv 3.0 ,下面是 PHP dotenv 3.0 的新特性:在阅读和更改环境变量部分具有更大的灵活性对多行变量的一流支持不再格式化值,你获取到的值就是它们现在的样子支持按顺序多行查找 dotenv 文件,以前只支持一行更强的变量名称验证,避免静态变量或模糊变量造成的错误支持 Carbon 2.0Laravel 5.8 上可以使用 Carbon 1.0 或 Carbon 2.0, 包括可以使用 CarbonImmutable, 甚至可以默认使用 CarbonImmutable 。本地化 Carbon 2.0 做了很大改变,2.0 版本相比较 1.0 版本提供了更友好的国际化支持。了解更多资讯。 Carbon 类在 Laravel 5.8 上的升级.Cache TTL 的改变可能产生中到高影响的重大改变是 来自 Laravel 5.8 的 Cache TTL 的改变 。现在将整型传到缓存的方法由分改为秒。如果你想要在迁移过程中将整型改为 Carbon 或 \DateInterval 实例,请查看我的文章。已弃用的字符串和数组辅助函数不用太担心这个修改,在使用上虽然变更为类的方式,但是具体的使用方法与之前一致。并且 Laravel 有计划将 Helper 作为可选扩展包发布,你仍然可以在项目中使用它们。参考: Laravel 5.8 已弃用的字符串和数组辅助函数自动解析策略从 Laravel 5.8 开始,只要解析策略和模型位于传统位置,您就不需要在 AuthServiceProvider 类中注册它们。如果您更喜欢将非常规路径用于模型和解析策略,则可以注册回调以注册策略或继续手动配置它们:Gate::guessPolicyNamesUsing(function ($class) { // Do stuff return $policyClass;});更多相关信息: Laravel 5.8 将支持授权 Policy 类的自动解析更多新功能Nexmo 和 Slack 信息通知通道Blade 模板文件路径Markdown 文件目录的改变随着今天的发布, Laravel 5.7 将不再接收功能错误修复和更新。 但是,5.7 将在2019年8月之前收到安全更新。Laravel 5.8 是最新的稳定版本,将在2019年8月左右处理收到的错误修复和更新,并在2020年2月左右之前进行安全修复。了解更多可以访问 laravel.com 查看「官方文档」。需要从 Laravel 5.7 升级到 Laravel 5.8,请查看 「升级指南」。升级指南提供了预估的升级影响级别,以帮助你了解升级中最有影响的内容。请确保通读整篇升级指南,以使升级顺利进行。中文翻译中文翻译已启动,请关注:https://learnku.com/laravel/t…更多翻译文章请见 Laravel 开发者社区 https://learnku.com/laravel/c… ...

February 27, 2019 · 1 min · jiezi

专为 Laravel 定制的 Visual Studio Code 编辑器

嗨 工匠,我从 Laravel4.1 到 5.4 一直再用它,我相信它仍然是最流行的PHP框架。它提供许多功能为快速开发 web 和 Api ,以及5.3支持 VueJs 前端开发。你也有很多神奇的功能在这吧?我已经尝试使用了很多编辑器如 sublime,phpstorm(在用vs code之前都用它),atom 和现在用的 visual studio code 。每个编辑器都有它各自的优点,但是我第一次试用 visual studio code 的时候,我印象它又酷有强大,特别在 Git 管理,Debug(下面有尝试)及各种扩展插件??设置 Laravel 的 Vscode 环境安装下面的插件:Auto Close Tag自动添加 HTML/XML 的闭合标签,像 Visual Studio IDE 或 Sublime Text 一样。 Beautify在 Visual Studio Code 中格式化 javascript 、JSON 、 CSS 、Sass,以及 HTML。 Better MergeVisual Studio Code 中非常好用的可视化合并冲突工具,灵感来自于 Atom 中的 merge-conflicts 插件。 Debugger For Chrome用于在谷歌浏览器中调试 JavaScript 代码的 VS Code 扩展,或支持 Chrome Debugging Protocol 其他功能。 Eslint此扩展使用安装在已打开的工作区文件夹内的 ESLint 库。如果文件夹没有提供这个库,将会匹配全局安装的版本。如果既没有局部安装、也没有全局安装 ESLint,可以通过运行npm install eslint 进行局部安装或者npm install -g eslint进行全局安装。 Npm此扩展支持定义在package.json文件里的 npm 脚本,并根据定义在package.json里的依赖项验证已安装的模块。 Laravel Blade SnippetsLaravel blade 代码片段和语法高亮支持 Visual Studio Code。 PHP Debug此扩展由 Derick Rethan 开发,是一个 VS Code 与 XDebug 之间的调试适配器。XDebug 是一个 PHP 扩展(Linux 下的.so文件或 Windows 下的.dll),需要安装在你的服务器上。 PHP Intellisense CraneCrane 是 Visual Studio Code 的生产力增强扩展,提供了 PHP 代码的自动完成。它具有零依赖性,并可以极大程度地工作于任何规模的项目里。它仍在开发中,可能存在 Bug 或缺失某些功能。 Git History使用图表查看 Git 历史,查看 commit 的详情信息,例如作者名、邮件、日期、提交者的作者名、邮件、日期和提交注释。查看先前文件的拷贝或者将其与工作区版本或先前版本进行比较,查看编辑器(Git Blame)里对活动行的更改。我使用的 Dracula 主题和 Material Icon Theme 图标主题,现在尝试使用 Vscode 在 laravel 里进行调试吧,运行得好吗?更多翻译文章请见 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 24, 2019 · 1 min · jiezi

我设计一个phpms框架前的准备

phpms框架源码https://github.com/wuxiumu/ms一、PHP常用的四种数据结构简介:spl是php的一个标准库。官方文档:http://php.net/manual/zh/book…<?php //spl(php标准库)数据结构 /** * 栈(先进后出) /function zan(){ $stack = new SplStack(); $stack->push(‘data1’);//入栈(先进后出) $stack->push(‘data2’);//入栈 $stack->push(‘data3’);//入栈 echo $stack->pop().PHP_EOL;//出栈 echo $stack->pop().PHP_EOL;//出栈 echo $stack->pop().PHP_EOL;//出栈} /* 队列(先进先出) /function duilie(){ $queue = new SplQueue(); $queue->enqueue(‘data4’);//入队列 $queue->enqueue(‘data5’);//入队列 $queue->enqueue(‘data6’);//入队列 echo $queue->dequeue().PHP_EOL;//出队列 echo $queue->dequeue().PHP_EOL;//出队列 echo $queue->dequeue().PHP_EOL;//出队列} / * 堆 /function dui(){ $heap = new SplMinHeap(); $heap->insert(‘data8’);//入堆 $heap->insert(‘data9’);//入堆 $heap->insert(‘data10’);//入堆 echo $heap->extract().PHP_EOL;//从堆中提取数据 echo $heap->extract().PHP_EOL;//从堆中提取数据 echo $heap->extract().PHP_EOL;//从堆中提取数据} /* * 固定数组(不论使不使用,都会分配相应的内存空间) /$array = new SplFixedArray(15);$array[‘0’] = 54;$array[‘6’] = 69;$array[‘10’] = 32;var_dump($array);二、PHP链式操作的实现(原理)1、入口文件index.phpheader(“content-type:text/html;charset=utf-8”); define(‘PHPMSFRAME’,DIR); define(‘CORE’,PHPMSFRAME.’/core’);define(‘APP’,PHPMSFRAME.’/app’);define(‘MODULE’,‘app’);define(‘DEBUG’,true); include “vendor/autoload.php”;if(DEBUG){ $whoops = new \Whoops\Run; $whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler); $whoops->register(); ini_set(‘display_error’, ‘On’);}else{ ini_set(‘display_error’, ‘Off’);}include CORE.’/common/function.php’;include CORE.’/phpmsframe.php’;spl_autoload_register(’\core\phpmsframe::load’);\core\phpmsframe::run();2、自动加载类corephpmsframe.php<?phpnamespace core;class phpmsframe { public static $classMap = array(); public $assign; static public function run() { $route = new \core\lib\route(); $ctrlClass = $route->ctrl; $action = $route->action; $ctrlfile = APP.’/ctrl/’.$ctrlClass.‘Ctrl.php’; $ctrlClass = ‘\’.MODULE.’\ctrl\’.$ctrlClass.‘Ctrl’; if(is_file($ctrlfile)){ include $ctrlfile; $ctrl = new $ctrlClass(); $ctrl->$action(); }else{ $msg = “控制器 $ctrlClass 不存在\n”; self::reportingDog($msg); } } static public function load($class) { if(isset($classMap[$class])){ return true; }else{ $class = str_replace(’\’, ‘/’, $class); $file = PHPMSFRAME.’/’.$class.’.php’; if(is_file($file)){ include $file; self::$classMap[$class] = $class; }else{ return false; } } } public function assign($name,$value){ $this->assign[$name]=$value; } public function display($file){ $file_path = APP.’/views/’.$file; if(is_file($file_path)){ /twig模板/ $loader = new \Twig_Loader_Filesystem(APP.’/views’); $twig = new \Twig_Environment($loader, array( ‘cache’ => PHPMSFRAME.’/cache’, ‘debug’=>DEBUG, )); $template = $twig->load($file); $template->display($this->assign?$this->assign:’’); /twig模板end/ /原生模板/ //extract($this->assign); //include $file_path; /原生模板end/ } } static private function reportingDog($msg){ echo $msg."\n"; include ‘smile/havefun.php’; $num = str_pad(rand(00,32),2,“0”,STR_PAD_LEFT); $num = “str_”.$num; $Parsedown = new \Parsedown(); echo $Parsedown->text($$num); $num = “str_".rand(50,84); echo $Parsedown->text($$num); exit; }}3、数据库类注:只是原理,并没有对方法进行具体的封装,具体的封装还是看个人喜好去定链式查询的风格。corelibmodel.php<?phpnamespace core\lib;use core\lib\drive\database\medooModel;class model extends medooModel{ }corelibdrivedatabasemedooModel.php<?php/* * 继承medoo第3方类库,模型基类 /namespace core\lib\drive\database;use core\lib\conf;use Medoo\Medoo;class medooModel extends Medoo{ //初始化,继承pdo应该是就可以直接用手册中的pdo中的方法了 public function __construct() { $option = conf::all(‘database’); parent::__construct($option[‘mysql_medoo_conf’]); }}三、PHP魔术方法的使用在php设计模式中,会涉及到很多魔术方法的使用,这里也对经常会用到的魔术方法进行简单总结。1、框架入口文件corephpmsframe.php /* * 魔术方法的使用 / / 这是一个魔术方法,当一个对象或者类获取其不存在的属性的值时, * 如:$obj = new BaseController ; * $a = $obj -> a ; * 该方法会被自动调用,这样做很友好,可以避免系统报错 / public function __get($property_name){ $msg = “属性 $property_name 不存在\n”; self::reportingDog($msg); } / 这是一个魔术方法,当一个对象或者类给其不存在的属性赋值时, * 如:$obj = new BaseController ; * $obj -> a = 12 ; * 该方法(_set(属性名,属性值))会被自动调用,这样做很友好,可以避免系统报错 / public function __set($property_name,$value){ $msg = “属性 $property_name 不存在\n”; self::reportingDog($msg); } / 这是一个魔术方法,当一个对象或者类的不存在属性进行isset()时, * 注意:isset 用于检查一个量是否被赋值 如果为NULL会返回false * 如:$obj = new BaseController ; * isset($obj -> a) ; * 该方法会被自动调用,这样做很友好,可以避免系统报错 / public function __isset($property_name){ $msg = “属性 $property_name 不存在\n”; self::reportingDog($msg); } / 这是一个魔术方法,当一个对象或者类的不存在属性进行unset()时, * 注意:unset 用于释放一个变量所分配的内存空间 * 如:$obj = new BaseController ; * unset($obj -> a) ; * 该方法会被自动调用,这样做很友好,可以避免系统报错 / public function __unset($property_name){ $msg = “属性 $property_name 不存在\n”; self::reportingDog($msg); } / 当对这个类的对象的不存在的实例方法进行“调用”时,会自动调用该方法, * 这个方法有2个参数(必须带有的): * $methodName 表示要调用的不存在的方法名; * $argument 是一个数组,表示要调用该不存在的方法时,所使用的实参数据, / public function __call($methodName,$argument){ $msg = “实例方法 $methodName 不存在\n”; self::reportingDog($msg); }四、三种基础设计模式1、工厂模式通过传入参数的不同,来实例化不同的类。$cache =\Core\extend\CacheFactory::getCacheObj(‘redis’,array( ‘host’ => ‘127.0.0.1’, ‘pass’ => ‘myRedis&&&’ ));var_dump($cache);coreextendCacheFactory.php<?phpnamespace core\extend;class CacheFactory{ const FILE = 1; const MEMCACHE = 2; const REDIS = 3; static $instance;//定义静态属性,用于存储对象 /* * 工厂类创建缓存对象 * @param $type 指定缓存类型 * @param array $options 传入缓存参数 * @return FileCache|Memcache|RedisCache / static function getCacheObj($type, array $options) { switch ($type) { case ‘file’: case self::FILE: self::$instance = new FileCache($options); break; case ‘memcache’: case self::MEMCACHE: self::$instance = new Memcache($options); break; case ‘redis’: case self::REDIS: self::$instance = new RedisCache($options); break; default: self::$instance = new FileCache($options); break; } return self::$instance; }}2、单例模式保证一个类只实例化一个类对象,进而减少系统开销和资源的浪费//单例模式创建对象$obj = Extend\SingleObject::getInstance();$obj2 = Extend\SingleObject::getInstance();var_dump($obj,$obj2);//从结果可以看出,两个实例化的对象其实是一个对象coreextendSingleObject.php<?phpnamespace core\extend;class SingleObject{ //私有的静态属性,用于存储类对象 private static $instance = null; //私有的构造方法,保证不允许在类外 new private function __construct(){ } //私有的克隆方法, 确保不允许通过在类外 clone 来创建新对象 private function __clone(){ } //公有的静态方法,用来实例化唯一当前类对象 public static function getInstance() { if(is_null(self::$instance)){ self::$instance = new self; } return self::$instance; }}3、注册树模式将我们用到的对象注册到注册树上,然后在之后要用到这个对象的时候,直接从注册树上取下来就好。(就和我们用全局变量一样方便)coreextendRegisterTree,php<?phpnamespace core\extend;class RegisterTree{ static protected $objects;//静态类属性,用于储存注册到注册树上的对象 /* * 将对象注册到注册树上 * @param $alias 对象的别名 * @param $object 对象 / static function setObject($alias,$object) { self::$objects[$alias] = $object; } /* * 从注册树上取出给定别名相应的对象 * @param $alias 将对象插入到注册树上时写的别名 * @return mixed 对象 / static protected function getObject($alias) { return self::$objects[$alias]; } /* * 将对象从注册树上删除 * @param $alias 将对象插入到注册树上时写的别名 / public function unsetObject($alias) { unset(self::$objects[$alias]); }}关于注册树模式,这里推荐一篇文章 ,可以方便理解。 https://segmentfault.com/a/11…五、其他常见的8种PHP设计模式1、适配器模式将一个类的接口转换成客户希望的另一个接口,适配器模式使得原本的由于接口不兼容而不能一起工作的那些类可以一起工作。应用场景:老代码接口不适应新的接口需求,或者代码很多很乱不便于继续修改,或者使用第三方类库。常见的有两种适配器,分别是类适配器和对象适配器,这里拿更看好的对象适配器举例:<?phpnamespace Extend; /* * 对象适配器模式具体流程 * 1、根据需求定义接口,进而满足新需求功能 * 2、定义新类,继承并实现定义的接口 * 3、在实现接口时,原有的功能,只通过原有类对象调用原有类功能(委托) * 4、再根据需求,在新类中实现新需求功能 * 【适用性】 * (1)你想使用一个已经存在的类,而它的接口不符合你的需求 * (2)你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类协同工作 * (3)你想使用一个已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口(仅限于对 / /* * 目标角色(根据需求定义含有旧功能加上新功能的接口) * Interface Target 我们期望得到的功能类 * @package Extend /interface Target{ public function simpleMethod1(); public function simpleMethod2();} /* * 源角色(在新功能提出之前的旧功能类和方法) * Class Adaptee * @package Extend /class Adaptee{ public function simpleMethod1() { echo ‘Adapter simpleMethod1’."<br>”; } } /* * 类适配器角色(新定义接口的具体实现) * Class Adapter * @package Extend /class Adapter implements Target{ private $adaptee; function __construct() { //适配器初始化直接new 原功能类,以方便之后委派 $adaptee = new Adaptee(); $this->adaptee = $adaptee; } //委派调用Adaptee的sampleMethod1方法 public function simpleMethod1() { echo $this->adaptee->simpleMethod1(); } public function simpleMethod2() { echo ‘Adapter simpleMethod2’."<br>"; } } /* * 客户端调用 /$adapter = new Adapter();$adapter->simpleMethod1();$adapter->simpleMethod2();这篇文章介绍了类适配器的使用,感兴趣的可以了解一下 https://segmentfault.com/a/11...2、策略模式将一组特定的行为和算法封装成类,以适应某些特定的上下文环境,这种模式就是策略模式,策略模式可以实现依赖倒置以及控制反转。实例举例:假如一个电商网站系统,针对男性女性用户要各自跳转到不同的商品类目,并且所有的广告位展示展示不同的广告。<?php /* * 首页数据控制器 * Class Index /class Home{ /* * 最好写上这个注释,告诉phpstorm是对应的哪个接口类,否则虽然程序执行正确,但phpstorm识别不了 * @var \Extend\UserType / protected $userType; /* * 首页展示数据 * 使用策略模式 * Index constructor. / function index() { echo “AD:”; $this->userType->showAd(); echo “Category:”; $this->userType->showCategory(); } /* * 策略模式 * 根据传递的用户性别展示不同类别数据 * @param \Extend\UserType $userType / function setUserType(\Extend\UserType $userType) { $this->userType = $userType; } } $obj = new Home();if ($_GET[‘userType’] == ‘female’){ $userType = new \Extend\FemaleUserType();} else { $userType = new \Extend\MaleUserType();}$obj->setUserType($userType);$obj->index();Extend/userType.php(定义的接口)<?php namespace Extend; /* * 策略模式 * 定义根据性别不同展示不同商品类目和广告接口 * Interface UserType * @package Extend /interface UserType{ //显示广告 function showAd(); //展示类目 function showCategory(); }MaleUserType.php、FemaleUserType.php(具体实现的类 )<?php namespace Extend; /* * 定义男性商品类目和广告位数据接口 * Class MaleUserType * @package Extend /class MaleUserType implements UserType{ /* * 广告栏数据展示 / function showAd() { echo “this is 男性’s 广告条目数据”; } /* * 商品类目数据展示 / function showCategory() { echo “this is 男性’s 商品类目数据”; } }<?php namespace Extend; /* * 定义女性商品类目和广告位数据接口 * Class FemaleUserType * @package Extend /class FemaleUserType implements UserType{ /* * 广告栏数据展示 / function showAd() { echo “this is 女性’s 广告条目数据”; } /* * 商品类目数据展示 / function showCategory() { echo “this is 女性’s 商品类目数据”; } }3、数据对象映射模式将对象和数据存储映射起来,对一个对象的操作会映射为对数据存储的操作。下面在代码中实现数据对象映射模式,我们将实现一个ORM类,将复杂的sql语句映射成对象属性的操作。并结合使用数据对象映射模式、工厂模式、注册模式。 (1)数据库映射模式简单实例实现<?php //使用数据对象映射模式代替写sql$user = new Extend\User(25);$user->name = ‘小卜丢饭团子’;$user->salary = ‘20000’;$user->city = ‘浙江省’; Extend/User.php<?php namespace Extend; class User{ //对应数据库中的4个字段 public $id; public $name; public $salary; public $city; //存储数据库连接对象属性 protected $pdo; public $data; function __construct($id) { $this->id = $id; $this->pdo = new \PDO(‘mysql:host=127.0.0.1;dbname=test’,‘root’,‘123456’); } function __destruct() { $this->pdo->query(“update user set name = ‘{$this->name}’,salary = ‘{$this->salary}’,city = ‘{$this->city}’ where id=’{$this->id}’”); }}这样,执行index.php文件,数据库就会发生相应的操作,也就实现了基本的数据对象映射。(2)数据库映射模式复杂案例实现<?php class EX{ function index() { //使用数据对象映射模式代替写sql $user = Extend\Factory::getUserObj(25); $user->name = ‘小卜丢饭团子’; $user->salary = ‘20000’; $user->city = ‘浙江省’; } function test() { $user = Extend\Factory::getUserObj(25); $user->city = ‘广东省’; } } $ex = new EX();$ex->index();Extend/Factory.php<?php namespace Extend; class Factory{ /* * 工厂模式创建数据库对象,单例模式保证创建唯一db对象 * @return mixed / static function CreateDatabaseObj() { $db = Database::getInstance(); return $db; } /* * 工厂模式创建user对象,注册树模式保证创建唯一对象,避免资源浪费 * @param $id * @return User|mixed / static function getUserObj($id) { $key = ‘user’.$id; $user = RegisterTree::getObject($key); if (!$user) { $user = new User($id); RegisterTree::setObject($key,$user); } return $user; }}Extend/Register.php<?php namespace Extend; /* * 注册树模式 * Class RegisterTree * @package Extend /class RegisterTree{ static protected $objects;//静态类属性,用于储存注册到注册树上的对象 /* * 将对象注册到注册树上 * @param $alias 对象的别名 * @param $object 对象 / static function setObject($alias,$object) { self::$objects[$alias] = $object; } /* * 从注册树上取出给定别名相应的对象 * @param $alias 将对象插入到注册树上时写的别名 * @return mixed 对象 / static function getObject($alias) { return self::$objects[$alias]; } /* * 将对象从注册树上删除 * @param $alias 将对象插入到注册树上时写的别名 / public function unsetObject($alias) { unset(self::$objects[$alias]); } }Extend/User.php<?php namespace Extend; class User{ //对应数据库中的4个字段 public $id; public $name; public $salary; public $city; //存储数据库连接对象属性 protected $pdo; public $data; function __construct($id) { $this->id = $id; $this->pdo = new \PDO(‘mysql:host=127.0.0.1;dbname=test’,‘root’,‘123456’); } function __destruct() { $this->pdo->query(“update user set name = ‘{$this->name}’,salary = ‘{$this->salary}’,city = ‘{$this->city}’ where id=’{$this->id}’”); }}这样,就实现了稍复杂的数据对象映射模式和工厂模式、注册树模式相结合的案例。 4、观察者模式当一个对象状态发生改变时,依赖它的对象会全部收到通知,并自动更新。场景:一个事件发生后,要执行一连串更新操作。传统的编程方式就是在事件的代码之后直接加入处理逻辑,当更新的逻辑增多之后,代码会变的难以维护。这种方式是耦合的,侵入式的,增加新的逻辑需要修改事件主体的代码。观察者模式实现了低耦合,非侵入式的通知与更新机制。 4.1、传统模式举例:<?php/* * 一个事件的逻辑控制器 * Class Event /class Event{ /* * 用户确认订单 / function firmOrder() { //这里假设一个事件发生了,比如用户已经完成下单 echo “用户已下单<br>”; //传统方式是在发生一个事件之后直接进行一系列的相关处理,耦合度比较高,比如写入日志,给用户发邮件等等 echo “在用户下单之后进行的一系列操作<br>”; } } $event = new Event();$event->firmOrder();4.2、观察者模式典型实现方式:(1)定义2个接口:观察者(通知)接口、被观察者(主题)接口(2)定义2个类,观察者类实现观察者接口、被观察者类实现被观察者接口(3)被观察者注册自己需要通知的观察者(4)被观察者类某个业务逻辑发生时,通知观察者对象,进而每个观察者执行自己的业务逻辑。代码示例:<?php/* * 观察者模式场景描述: * 1、购票后记录文本日志 * 2、购票后记录数据库日志 * 3、购票后发送短信 * 4、购票送抵扣卷、兑换卷、积分 * 5、其他各类活动等 / /* * 观察者接口 /interface TicketObserver{ function buyTicketOver($sender, $args); //得到通知后调用的方法} /* * 被观察者接口(购票主题接口) /interface TicketObserved{ function addObserver($observer); //提供注册观察者方法} /* * 主体逻辑,继承被观察者接口 * Class BuyTicket /class BuyTicket implements TicketObserved{ /* * 定义观察者数组属性,用于储存观察者 * @var array / private $observers = array(); /* * 实现被观察者接口定义的方法(添加观察者) * @param $observer 实例化后的观察者对象 / public function addObserver($observer) { $this->observers[] = $observer; } /* * 购票主体方法 * BuyTicket constructor. * @param $ticket 购票排号 / public function buyTicket($ticket) { //1、根据需求写购票逻辑 //………….. //2、购票成功之后,循环通知观察者,并调用其buyTicketOver实现不同业务逻辑 foreach ($this->observers as $observe) { $observe->buyTicketOver($this, $ticket); //$this 可用来获取主题类句柄,在通知中使用 } } } /* * 购票成功后,发送短信通知 * Class buyTicketMSN /class buyTicketMSN implements TicketObserver{ public function buyTicketOver($sender, $ticket) { echo (date ( ‘Y-m-d H:i:s’ ) . " 短信日志记录:购票成功:$ticket<br>"); }} /* * 购票成功后,记录日志 * Class buyTicketLog /class buyTicketLog implements TicketObserver{ public function buyTicketOver($sender, $ticket) { echo (date ( ‘Y-m-d H:i:s’ ) . " 文本日志记录:购票成功:$ticket<br>"); }} /* * 购票成功后,赠送优惠券 * Class buyTicketCoupon /class buyTicketCoupon implements TicketObserver{ public function buyTicketOver($sender, $ticket) { echo (date ( ‘Y-m-d H:i:s’ ) . " 赠送优惠券:购票成功:$ticket 赠送10元优惠券1张。<br>"); }} //实例化购票类$buy = new BuyTicket();//添加多个观察者$buy->addObserver(new buyTicketMSN());$buy->addObserver(new buyTicketLog());$buy->addObserver(new buyTicketCoupon());//开始购票$buy->buyTicket (“7排8号”);5、原型模式原型模式与工厂模式的作用类似,都是用来创建对象的。但是实现方式是不同的。原型模式是先创建好一个原型对象,然后通过clone原型对象来创建新的对象。这样,就免去了类创建时重复的初始化操作。原型模式适用于大对象的创建,创建一个大对象需要很大的开销,如果每次new就会消耗很大,原型模式仅需内存拷贝即可。代码实例:<?php/* * 抽象原型角色 /interface Prototype{ public function copy();} /* * 具体原型角色 /class ConcretePrototype implements Prototype{ private $_name; public function __construct($name) { $this->_name = $name; } public function setName($name) { $this->_name = $name; } public function getName() { return $this->_name; } public function copy() { //深拷贝实现 //$serialize_obj = serialize($this); // 序列化 //$clone_obj = unserialize($serialize_obj); // 反序列化 //return $clone_obj; // 浅拷贝实现 return clone $this; } } /* * 测试深拷贝用的引用类 /class Demo{ public $array;} //测试$demo = new Demo();$demo->array = array(1, 2);$object1 = new ConcretePrototype($demo);$object2 = $object1->copy(); var_dump($object1->getName());echo ‘<br />’;var_dump($object2->getName());echo ‘<br />’; $demo->array = array(3, 4);var_dump($object1->getName());echo ‘<br />’;var_dump($object2->getName());echo ‘<br />’;关于原型模式文章:https://www.imooc.com/article…6、装饰器模式可以动态的添加或修改类的功能一个类实现一个功能,如果要再修改或添加额外的功能,传统的编程模式需要写一个子类继承它,并重新实现类的方法。使用装饰器模式,仅需在运行时添加一个装饰器对象即可实现,可以实现最大的灵活性。<?php/* * 装饰器流程 * 1、声明装饰器接口(装饰器接口) * 2、具体类继承并实现装饰器接口(颜色装饰器实现,字体大小装饰器实现) * 3、在被装饰者类中定义"添加装饰器"方法(EchoText类中的addDecorator方法) * 4、在被装饰者类中定义调用装饰器的方法(EchoText类中的beforeEcho和afterEcho方法) * 5、使用时,实例化被装饰者类,并传入装饰器对象(比如new ColorDecorator(‘yellow’)) / /* * 装饰器接口 * Class Decorator /interface Decorator{ public function beforeEcho(); public function afterEcho();} /* * 颜色装饰器实现 * Class ColorDecorator /class ColorDecorator implements Decorator{ protected $color; public function __construct($color) { $this->color = $color; } public function beforeEcho() { echo “<dis style=‘color: {$this->color}’>”; } public function afterEcho() { echo “</div>”; }} /* * 字体大小装饰器实现 * Class SizeDecorator /class SizeDecorator implements Decorator{ protected $size; public function __construct($size) { $this->size = $size; } public function beforeEcho() { echo “<dis style=‘font-size: {$this->size}px’>”; } public function afterEcho() { echo “</div>”; }} /* * 被装饰者 * 输出一个字符串 * 装饰器动态添加功能 * Class EchoText /class EchoText{ protected $decorators = array();//存放装饰器 //装饰方法 public function Index() { //调用装饰器前置操作 $this->beforeEcho(); echo “你好,我是装饰器。”; //调用装饰器后置操作 $this->afterEcho(); } //添加装饰器 public function addDecorator(Decorator $decorator) { $this->decorators[] = $decorator; } //执行装饰器前置操作 先进先出原则 protected function beforeEcho() { foreach ($this->decorators as $decorator) $decorator->beforeEcho(); } //执行装饰器后置操作 先进后出原则 protected function afterEcho() { $tmp = array_reverse($this->decorators); foreach ($tmp as $decorator) $decorator->afterEcho(); }} //实例化输出类$echo = new EchoText();//增加装饰器$echo->addDecorator(new ColorDecorator(‘yellow’));//增加装饰器$echo->addDecorator(new SizeDecorator(‘22’));//输出$echo->Index();7、迭代器模式在不需要了解内部实现的前提下,遍历一个聚合对象的内部元素而又不暴露该对象的内部表示,这就是PHP迭代器模式的定义。相对于传统编程模式,迭代器模式可以隐藏遍历元素的所需的操作。<?php$users = new Extend\AllUser();//循环遍历出所有用户数据foreach ($users as $user) { var_dump($user);}Extend/AllUser.php<?phpnamespace Extend; /* * 迭代器模式,继承php内部自带的迭代器接口(\Iterator) * Class AllUser * @package Extend /class AllUser implements \Iterator{ protected $index = 0;//表示索引 protected $ids = array();//用于储存所有user的id(实际应用中,可以采用注册树模式进行存储) protected $pdo;//用于存储数据库对象 function __construct() { //获取pdo数据库对象 $this->pdo = new \PDO(‘mysql:host=127.0.0.1;dbname=test’,‘root’,‘123456’); //获取所有用户的id $this->ids = $this->pdo->query(“select id from user”)->fetchAll(2); } /* * 实现接口方法,重置迭代器,回到集合开头 / public function rewind() { $this->index = 0; } /* * 实现接口方法,获取当前元素 * @return mixed|void / public function current() { $id = $this->ids[$this->index][‘id’]; //获取当前用户的数据 $user_data = $this->pdo->query(“select * from user where id=’{$id}’”)->fetch(2); return $user_data; } /* * 实现接口方法,获取当前元素键值 * @return mixed|void / public function key() { return $this->index; } /* * 实现接口方法,获取下一个元素 / public function next() { $this->index++; } /* * 实现接口方法,验证是否还有下一个元素 * @return bool|void */ public function valid() { return $this->index < count($this->ids); } }关于php迭代器文章 http://www.php.cn/php-weiziji… 8、代理模式在客户端与实体之间建立一个代理对象(proxy),客户端对实体进行操作全部委派给代理对象,隐藏实体的具体实现细节。典型的应用就是mysql的主从结构,读写分离。在mysql中,对所有读的操作请求从库,所有写的操作请求主库。声明一个代理类,前台使用时只需创建一个代理类,调用对应方法即可。代码实例:<?php // 1、传统编程模式是手动选择#查询操作使用从库//$db_slave = Extend\Factory::getDatabase(‘slave’);//$info = $db_slave->query(“select * from user where id = 1 limit 1”);#增删改操作使用主库//$db_master = Extend\Factory::getDatabase(‘master’);//$db_master->query(“update user name = ‘xiaobudiu’ where id = 29 limit 1”); // 2、使用代理模式$db_proxy = new Extend\Proxy();$db_proxy->getUserName(1);$db_proxy->setUserName(29,‘xiaobudiu’);Extend/Proxy.php<?phpnamespace Extend; class Proxy implements IUserProxy{ function getUserName($id) { $db = Factory::getDatabase(‘slave’); $db->query(“select name from user where id =$id limit 1”); } function setUserName($id, $name) { $db = Factory::getDatabase(‘master’); $db->query(“update user set name = $name where id =$id limit 1”); }}Extend/Factory.php<?phpnamespace Extend; class Factory{ static function getDatabase($id) { $key = ‘database’.$id; if ($id == ‘slave’) { $slaves = Application::getInstance()->config[‘database’][‘slave’]; $db_conf = $slaves[array_rand($slaves)]; } else { $db_conf = Application::getInstance()->config[‘database’][$id]; } //注册树模式存储及获取对象 $db = Register::get($key); if (!$db) { $db = new Database\MySQLi(); $db->connect($db_conf[‘host’], $db_conf[‘user’], $db_conf[‘password’], $db_conf[‘dbname’]); Register::set($key, $db); } return $db; } }Extend/Application.php<?phpnamespace Extend; class Application{ public $base_dir; protected static $instance; public $config; protected function __construct($base_dir) { $this->base_dir = $base_dir; $this->config = new Config($base_dir.’/configs’); } static function getInstance($base_dir = ‘’) { if (empty(self::$instance)) { self::$instance = new self($base_dir); } return self::$instance; } }Extend/Config.php<?phpnamespace Extend; /** * 配置类,继承于php自带的ArrayAccess接口 * 允许一个对象以数组的方式访问 * Class Config * @package Extend /class Config implements \ArrayAccess{ protected $path; protected $configs = array(); function __construct($path) { $this->path = $path; } function offsetGet($key) { if (empty($this->configs[$key])) { $file_path = $this->path.’/’.$key.’.php’; $config = require $file_path; $this->configs[$key] = $config; } return $this->configs[$key]; } function offsetSet($key, $value) { throw new \Exception(“cannot write config file.”); } function offsetExists($key) { return isset($this->configs[$key]); } function offsetUnset($key) { unset($this->configs[$key]); }}configs/database.php<?php$config = array( ‘master’ => array( ’type’ => ‘MySQL’, ‘host’ => ‘127.0.0.1’, ‘user’ => ‘root’, ‘password’ => ‘123456’, ‘dbname’ => ’test’, ), ‘slave’ => array( ‘slave1’ => array( ’type’ => ‘MySQL’, ‘host’ => ‘127.0.0.1’, ‘user’ => ‘root’, ‘password’ => ‘123456’, ‘dbname’ => ’test’, ), ‘slave2’ => array( ’type’ => ‘MySQL’, ‘host’ => ‘127.0.0.1’, ‘user’ => ‘root’, ‘password’ => ‘123456’, ‘dbname’ => ’test’, ), ),);return $config;关于php代理模式文章 https://segmentfault.com/a/11…五、其余设计模式以及总结文章接:https://segmentfault.com/a/11…https://segmentfault.com/a/11… 六、面向对象编程的基本原则1、单一职责原则:一个类只需要做好一件事情。不要使用一个类完成很多功能,而应该拆分成更多更小的类。2、开放封闭原则:一个类写好之后,应该是可扩展而不可修改的。3、依赖倒置原则:一个类不应该强依赖另外一个类,每个类对于另外一个类都是可替换的。4、配置化原则:尽量使用配置,而不是硬编码。5、面向接口编程原则:只需要关心某个类提供了哪些接口,而不需要关心他的实现。 七、自动加载配置类文件1、php中使用ArrayAccess实现配置文件的加载(使得程序可以以数组的方式进行读取配置)(1)定义Config.php,继承php自带的ArrayAccess接口,并实现相应的方法,用于读取和设置配置Extend/Config.php<?phpnamespace Extend; /* * 配置类,继承于php自带的ArrayAccess接口 * 允许一个对象以数组的方式访问 * Class Config * @package Extend /class Config implements \ArrayAccess{ protected $path; protected $configs = array(); function _construct($path) { $this->path = $path; } function offsetGet($key) { if (empty($this->configs[$key])) { $file_path = $this->path.’/’.$key.’.php’; $config = require $file_path; $this->configs[$key] = $config; } return $this->configs[$key]; } function offsetSet($key, $value) { throw new \Exception(“cannot write config file.”); } function offsetExists($key) { return isset($this->configs[$key]); } function offsetUnset($key) { unset($this->configs[$key]); }}(2)configs/database.php<?php$config = array( ‘master’ => array( ’type’ => ‘MySQL’, ‘host’ => ‘127.0.0.1’, ‘user’ => ‘root’, ‘password’ => ‘123456’, ‘dbname’ => ’test’, ), ‘slave’ => array( ‘slave1’ => array( ’type’ => ‘MySQL’, ‘host’ => ‘127.0.0.1’, ‘user’ => ‘root’, ‘password’ => ‘123456’, ‘dbname’ => ’test’, ), ‘slave2’ => array( ’type’ => ‘MySQL’, ‘host’ => ‘127.0.0.1’, ‘user’ => ‘root’, ‘password’ => ‘123456’, ‘dbname’ => ’test’, ), ),);return $config;(3)读取配置<?php $config = new Extend\Config(DIR.’/configs’);var_dump($config[‘database’]);到此,就可以在程序中随心所欲的加载配置文件了。2、在工厂方法中读取配置,生成可配置化的对象Extend/Factory.php<?phpnamespace Extend; class Factory{ static function getDatabase($id) { $key = ‘database’.$id; if ($id == ‘slave’) { $slaves = Application::getInstance()->config[‘database’][‘slave’]; $db_conf = $slaves[array_rand($slaves)]; } else { $db_conf = Application::getInstance()->config[‘database’][$id]; } //注册树模式存储及获取对象 $db = Register::get($key); if (!$db) { $db = new Database\MySQLi(); $db->connect($db_conf[‘host’], $db_conf[‘user’], $db_conf[‘password’], $db_conf[‘dbname’]); Register::set($key, $db); } return $db; } }Extend/Application.php<?phpnamespace Extend; class Application{ public $base_dir; protected static $instance; public $config; protected function __construct($base_dir) { $this->base_dir = $base_dir; $this->config = new Config($base_dir.’/configs’); } static function getInstance($base_dir = ‘’) { if (empty(self::$instance)) { self::$instance = new self($base_dir); } return self::$instance; } }Extend/Config.php<?phpnamespace Extend; /* * 配置类,继承于php自带的ArrayAccess接口 * 允许一个对象以数组的方式访问 * Class Config * @package Extend */class Config implements \ArrayAccess{ protected $path; protected $configs = array(); function __construct($path) { $this->path = $path; } function offsetGet($key) { if (empty($this->configs[$key])) { $file_path = $this->path.’/’.$key.’.php’; $config = require $file_path; $this->configs[$key] = $config; } return $this->configs[$key]; } function offsetSet($key, $value) { throw new \Exception(“cannot write config file.”); } function offsetExists($key) { return isset($this->configs[$key]); } function offsetUnset($key) { unset($this->configs[$key]); }} ...

January 19, 2019 · 11 min · jiezi

ReactPHP 爬虫实战:下载整个网站的图片

什么是网页抓取?你是否曾经需要从一个没有提供 API 的站点获取信息? 我们可以通过网页抓取,然后从目标网站的 HTML 中获得我们想要的信息,进而解决这个问题。 当然,我们也可以手动提取这些信息, 但手动操作很乏味。 所以, 通过爬虫来自动化来完成这个过程会更有效率。在这个教程中我们会从 Pexels 抓取一些猫的图片。这个网站提供高质量且免费的素材图片。他们提供了API, 但这些 API 有 200次/小时 的请求频率限制。[](https://user-gold-cdn.xitu.io…发起并发请求在网页抓取中使用异步 PHP (相比使用同步方式)的最大好处是可以在更短的时间内完成更多的工作。使用异步 PHP 使得我们可以立刻请求尽可能多的网页而不是每次只能请求单个网页并等待结果返回。 因此,一旦请求结果返回我们就可以开始处理。首先,我们从 GitHub 上拉取一个叫做 buzz-react 的异步 HTTP 客户端的代码 – 它是一个基于 ReactPHP 的简单、致力于并发处理大量 HTTP 请求的异步 HTTP 客户端:composer require clue/buzz-react现在, 我们就可以请求 pexels 上的图片页面 了:<?phprequire DIR . ‘/vendor/autoload.php’;use Clue\React\Buzz\Browser;$loop = \React\EventLoop\Factory::create();$client = new Browser($loop);$client->get(‘https://www.pexels.com/photo/kitten-cat-rush-lucky-cat-45170/') ->then(function(\Psr\Http\Message\ResponseInterface $response) { echo $response->getBody(); });$loop->run();我们创建了 Clue\React\Buzz\Browser 的实例, 把它作为 HTTP client 使用。上面的代码发起了一个异步的 GET 请求来获取网页内容(包含一张小猫们的图片)。 $client->get($url) 方法返回了一个包含 PSR-7 response 的 promise 对象。客户端是异步工作的,这意味着我们可以很容易地请求几个页面,然后这些请求会被同步执行:<?phprequire DIR . ‘/vendor/autoload.php’;use Clue\React\Buzz\Browser;$loop = \React\EventLoop\Factory::create();$client = new Browser($loop);$client->get(‘https://www.pexels.com/photo/kitten-cat-rush-lucky-cat-45170/') ->then(function(\Psr\Http\Message\ResponseInterface $response) { echo $response->getBody(); });$client->get(‘https://www.pexels.com/photo/adorable-animal-baby-blur-177809/') ->then(function(\Psr\Http\Message\ResponseInterface $response) { echo $response->getBody(); });$loop->run();这里的代码含义如下:发起一个请求获取响应添加响应的处理程序当响应解析完毕就处理响应所以,这个逻辑可以提取到一个类里,这样我们可以很容易地请求多个 URL 并添加相同的响应处理程序。让我们基于Browser创建一个包装器。用下面的代码创建一个名为Scraper的类:<?phpuse Clue\React\Buzz\Browser;use Psr\Http\Message\ResponseInterface;final class Scraper{ private $client; public function __construct(Browser $client) { $this->client = $client; } public function scrape(array $urls) { foreach ($urls as $url) { $this->client->get($url)->then( function (ResponseInterface $response) { $this->processResponse((string) $response->getBody()); }); } } private function processResponse(string $html) { // … }}我们把Browser作为依赖项注入到构造函数并提供一个公共方法scrape(array $urls)。接着对每个指定的 URL 发起一个GET请求。当响应完成时,我们调用一个私有方法processResponse(string $html)。这个方法负责遍历 HTML 代码并下载图片。下一步是审查收到的 HTML 代码,然后从里面提取图片。发起并发请求在网页抓取中使用异步 PHP (相比使用同步方式)的最大好处是可以在更短的时间内完成更多的工作。使用异步 PHP 使得我们可以立刻请求尽可能多的网页而不是每次只能请求单个网页并等待结果返回。 因此,一旦请求结果返回我们就可以开始处理。首先,我们从 GitHub 上拉取一个叫做 buzz-react 的异步 HTTP 客户端的代码 – 它是一个基于 ReactPHP 的简单、致力于并发处理大量 HTTP 请求的异步 HTTP 客户端:composer require clue/buzz-react现在, 我们就可以请求 pexels 上的图片页面 了:<?phprequire DIR . ‘/vendor/autoload.php’;use Clue\React\Buzz\Browser;$loop = \React\EventLoop\Factory::create();$client = new Browser($loop);$client->get(‘https://www.pexels.com/photo/kitten-cat-rush-lucky-cat-45170/') ->then(function(\Psr\Http\Message\ResponseInterface $response) { echo $response->getBody(); });$loop->run();我们创建了 Clue\React\Buzz\Browser 的实例, 把它作为 HTTP client 使用。上面的代码发起了一个异步的 GET 请求来获取网页内容(包含一张小猫们的图片)。 $client->get($url) 方法返回了一个包含 PSR-7 response 的 promise 对象。客户端是异步工作的,这意味着我们可以很容易地请求几个页面,然后这些请求会被同步执行:<?phprequire DIR . ‘/vendor/autoload.php’;use Clue\React\Buzz\Browser;$loop = \React\EventLoop\Factory::create();$client = new Browser($loop);$client->get(‘https://www.pexels.com/photo/kitten-cat-rush-lucky-cat-45170/') ->then(function(\Psr\Http\Message\ResponseInterface $response) { echo $response->getBody(); });$client->get(‘https://www.pexels.com/photo/adorable-animal-baby-blur-177809/') ->then(function(\Psr\Http\Message\ResponseInterface $response) { echo $response->getBody(); });$loop->run();这里的代码含义如下:发起一个请求获取响应添加响应的处理程序当响应解析完毕就处理响应所以,这个逻辑可以提取到一个类里,这样我们可以很容易地请求多个 URL 并添加相同的响应处理程序。让我们基于Browser创建一个包装器。用下面的代码创建一个名为Scraper的类:<?phpuse Clue\React\Buzz\Browser;use Psr\Http\Message\ResponseInterface;final class Scraper{ private $client; public function __construct(Browser $client) { $this->client = $client; } public function scrape(array $urls) { foreach ($urls as $url) { $this->client->get($url)->then( function (ResponseInterface $response) { $this->processResponse((string) $response->getBody()); }); } } private function processResponse(string $html) { // … }}我们把Browser作为依赖项注入到构造函数并提供一个公共方法scrape(array $urls)。接着对每个指定的 URL 发起一个GET请求。当响应完成时,我们调用一个私有方法processResponse(string $html)。这个方法负责遍历 HTML 代码并下载图片。下一步是审查收到的 HTML 代码,然后从里面提取图片。爬取网站此刻我们只是获取到了响应页面的 HTML 代码。现在需要提取图片 URL。为此,我们需要审查收到的 HTML 代码结构。前往 Pexels 的图片页,右击图片并选择审查元素,你会看到一些东西,就像这样:[](https://user-gold-cdn.xitu.io…我们可以看到img标签有个image-section__image类名。我们要使用这个信息从收到的 HTML 中提取这个标签。图片的 URL 存储在src属性里:[](https://user-gold-cdn.xitu.io…为提取 HTML 标签,我们需要使用 Symfony 的 DomCrawler 组件。拉取需要的包:composer require symfony/dom-crawlercomposer require symfony/css-selectorDomCrawler 的适配组件 CSS-selector 允许我们使用类 - jQuery 的选择器遍历 DOM。当安装好一切之后,打开我们的Scraper类,在processResponse(string $html) 方法里书写一些代码。首先,我们需要创建一个Symfony\Component\DomCrawler\Crawler 类的实例,它的构造函数接受一个用于遍历的 HTML 代码字符串:<?phpuse Clue\React\Buzz\Browser;use Psr\Http\Message\ResponseInterface;use Symfony\Component\DomCrawler\Crawler;final class Scraper{ // … private function processResponse(string $html) { $crawler = new Crawler($html); }}通过类 - jQuery 选择器查找任意元素时,请使用filter()方法。然后,attr($attribute)方法允许提取已过滤元素的某个属性:<?phpuse Clue\React\Buzz\Browser;use Psr\Http\Message\ResponseInterface;use Symfony\Component\DomCrawler\Crawler;final class Scraper{ // … private function processResponse(string $html) { $crawler = new Crawler($html); $imageUrl = $crawler->filter(’.image-section__image’)->attr(‘src’); echo $imageUrl . PHP_EOL; }}让我们只打印提取出的图片 URL,检查下我们的 scraper 是否如期工作:<?php// index.phprequire DIR . ‘/vendor/autoload.php’;require DIR . ‘/Scraper.php’;use Clue\React\Buzz\Browser;$loop = \React\EventLoop\Factory::create();$scraper = new Scraper(new Browser($loop));$scraper->scrape([ ‘https://www.pexels.com/photo/adorable-animal-blur-cat-617278/’]);$loop->run();当运行这个脚本时,将会输出所需图片的完整 URL。然后我们要使用这个 URL 下载该图片。 我们再次创建一个Browser实例,然后发起一个GET请求:<?phpuse Clue\React\Buzz\Browser;use Psr\Http\Message\ResponseInterface;use Symfony\Component\DomCrawler\Crawler;final class Scraper{ // … private function processResponse(string $html) { $crawler = new Crawler($html); imageUrl = $crawler->filter(’.image-section__image’)->attr(‘src’); $this->client->get($imageUrl)->then( function(ResponseInterface $response) { // 存储图片到磁盘上 }); }}到达的响应携带了请求的图片内容。现在我们需要把它保存到磁盘上。但是请花费一点时间,不要使用file_put_contents()。所有的原生 PHP 函数都在文件系统下阻塞式运行。这意味着一旦你调用了file_put_contents(),我们的应用就会停止异步行为。然后流程控制会被阻塞直到文件保存完毕。ReactPHP 有个专门的包可以解决这个问题。异步保存文件要以非阻塞方式异步处理文件的话,我们需要一个叫做 reactphp/filesystem 的包。拉取下来:composer require react/filesystem要异步使用文件系统,请创建一个Filesystem对象并把它作为依赖项提供给Scraper。此外,我们还需要提供一个目录存放下载的图片:<?php// index.phprequire DIR . ‘/vendor/autoload.php’;require DIR . ‘/Scraper.php’;use Clue\React\Buzz\Browser;use React\Filesystem\Filesystem;$loop = \React\EventLoop\Factory::create();$scraper = new ScraperForImages( new Browser($loop), Filesystem::create($loop), DIR . ‘/images’);$scraper->scrape([ ‘https://www.pexels.com/photo/adorable-animal-blur-cat-617278/’]);$loop->run();这是更新后Scraper的构造函数:<?phpuse Clue\React\Buzz\Browser;use Psr\Http\Message\ResponseInterface;use React\Filesystem\FilesystemInterface;use Symfony\Component\DomCrawler\Crawler;final class Scraper{ private $client; private $filesystem; private $directory; public function __construct(Browser $client, FilesystemInterface $filesystem, string $directory) { $this->client = $client; $this->filesystem = $filesystem; $this->$directory = $directory; } // …}好的,现在我们准备保存文件到磁盘上。首先,我们需要从 URL 提取文件名。图片的 URL 看起来就像这样:https://images.pexels.com/pho…这些 URL 的文件名是这样的:jumping-cute-playing-animals.jpg\pexels-photo-617278.jpeg让我们使用正则表达式从 URL 里提取出文件名。为了给磁盘上的未来文件获取完整路径,我们用目录把名字串联起来:<?phppreg_match(’/photos/\d+/([\w-.]+)?/’, $imageUrl, $matches); // $matches[1] 包含一个文件名$filePath = $this->directory . DIRECTORY_SEPARATOR . $matches[1];当我们有了一个文件路径,就可以用它创建一个 文件 对象:<?php$file = $this->filesystem->file($filePath);此对象表示我们要使用的文件。接着调用putContents($contents) 方法并提供一个响应体(response body)字符串:<?php$file = $this->filesystem->file($filePath);$file->putContents((string)$response->getBody());就是这样。所有异步的底层魔法隐藏在一个单独的方法内。此 hook 会创建一个写模式的流,写入数据后关闭这个流。这是Scraper::processResponse(string $html)方法的更新版本:<?phpuse Clue\React\Buzz\Browser;use Psr\Http\Message\ResponseInterface;use React\Filesystem\FilesystemInterface;use Symfony\Component\DomCrawler\Crawler;final class Scraper{ // … private function processResponse(string $html) { $crawler = new Crawler($html); $imageUrl = $crawler->filter(’.image-section__image’)->attr(‘src’); preg_match(’/photos/\d+/([\w-.]+)?/’, $imageUrl, $matches); $filePath = $matches[1]; $this->client->get($imageUrl)->then( function(ResponseInterface $response) use ($filePath) { $this->filesystem->file($filePath)->putContents((string)$response->getBody()); }); }}我们传递了一个完整路径到响应的处理程序里。然后,我们创建了一个文件并填充了响应体。实际上,完整的Scraper只有不到 50 行的代码!注意:在你想存储文件的位置先创建目录。putContents() 方法只创建文件,不会为指定的文件创建文件夹。scraper 完成了。现在,打开你的主脚本,给scrape方法传递一个 URL 列表:<?php// index.php<?phprequire DIR . ‘/../vendor/autoload.php’;require DIR . ‘/ScraperForImages.php’;use Clue\React\Buzz\Browser;use React\Filesystem\Filesystem;$loop = \React\EventLoop\Factory::create();$scraper = new ScraperForImages( new Browser($loop), Filesystem::create($loop), DIR . ‘/images’);$scraper->scrape([ ‘https://www.pexels.com/photo/adorable-animal-blur-cat-617278/’, ‘https://www.pexels.com/photo/kitten-cat-rush-lucky-cat-45170/’, ‘https://www.pexels.com/photo/adorable-animal-baby-blur-177809/’, ‘https://www.pexels.com/photo/adorable-animals-cats-cute-236230/’, ‘https://www.pexels.com/photo/relaxation-relax-cats-cat-96428/’,]);$loop->run();上面的代码爬取 5 个 URL 并下载相应图片。所有这些工作会快速地异步完成。[](https://user-gold-cdn.xitu.io…结尾在 上一个教程里,我们使用 ReactPHP 加速网站抓取过程并同时查询页面。但是,如果我们也需要同时保存文件呢?在异步的应用程序中,我们不能使用诸如file_put_contents()的原生 PHP 函数,因为它们会阻塞程序流程,所以在磁盘上存储图片不会有任何加速。想要在 ReactPHP 里以异步 - 非阻塞的方式处理文件时,我们需要使用 reactphp/filesystem 包。所以,在上面 50 行的代码里,我们就能加速网站抓取并运行起来。这只是一个你也可以做的简洁例子。现在你有了怎样构建爬虫的基础知识,请尝试做一个自己的吧!我还有一些用 ReactPHP 抓取网站的文章:如果你想 使用代理 或者 限制并发请求的数量,可以阅读一下。*你也可以从 GitHub 找到这篇文章的例子。转自 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 17, 2019 · 3 min · jiezi

基于 Module 的 Laravel API 架构

转自 PHP / Laravel 开发者社区 https://laravel-china.org/top…我非常喜欢编写基于模块化设计的软件和编程方式,但我不太喜欢依赖第三方软件包和类库来处理一些琐碎的事情,因为它们不会让你的编程水平得到很好的提升。所以这两年来,我一直在用Laravel编写基于模块的软件,现在我对这个结果非常满意。推动我走向基于模块化设计的软件和编程方式的决定性因素是我想持续提升我的编程水平。想象一下,你构建了一个项目结构,6个月后你发现这个项目存在很多bug。在不影响6个月现有代码的情况下,通常不会轻易改变项目架构。在分析这个项目时,我注意到了两个要点:你要么在整个项目中都有一个标准,要么坚持下去,要么模块化并逐个模块地改进。有些人倾向于不惜一切代价、固守标准地开发,即使这可能意味着要坚持一个你不再喜欢的标准。就我个人来言,我更喜欢持续地改进,若是第 20 个模块和第 1 个模块写得完全不一样也没关系。如果某天我需要回到模块 1 修复 BUG 或重构,我可以将其改进为第 20 个模块使用的最新标准。假设,你也像我一样喜欢基于模块化开发 Laravel 应用、尽可能避免在项目中添加不必要的第三方依赖——本文是我的一点经验。1- 路由服务提供者Laravel 路由系统可以说是整个应用的入口。首先需要修改的是默认的 RouteServiceProvider.php 文件,它应当将现有路由模块化。<?phpnamespace App\Providers;use Illuminate\Support\Facades\Route;use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;class RouteServiceProvider extends ServiceProvider{ /** * 定义应用路由。 * * @return void */ public function map() { $this->mapModulesRoutes(); } protected function mapModulesRoutes() { // 如果你在编写传统 Web 应用而非 HTTP API,请使用 web 中间件。 Route::middleware(‘api’) ->group(base_path(‘routes/modules.php’)); }}如上,我们可以直接摆脱该文件的整个样板,只需设置一个模块化的路由文件即可。2- 模块文件Laravel 在 routes 文件夹中自带了一些文件。由于我们已经不在 RouteServiceProvider 中映射这些路由,所以可以直接删除它们。接下来,我们创建一个 modules.php 路由文件。<?phpuse Illuminate\Support\Facades\Route;Route::group([], base_path(‘app/Modules/Books/routes.php’));Route::group([], base_path(‘app/Modules/Authors/routes.php’));3- Books 模块在 app 文件夹中,创建 Modules/Books/routes.php 文件。在此文件中,我们可以定义该应用 Books 模块的路由规则。<?phpuse App\Modules\Books\ListBooks;use Illuminate\Support\Facades\Route;Route::get(’/books’, ListBooks::class);你可以使用基于控制器——也就是 Laravel 中默认标准的路由方式,但我个人更喜欢 Good bye controllers, hello Request Handlers(放弃控制器,采用请求处理器) 的方式。 如下是 ListBooks 的实现。<?phpnamespace App\Modules\Books;use App\Eloquent\Book;use App\Modules\Books\Resources\BookResource;class ListBooks{ public function __invoke(Book $book) { return BookResource::collection($book->paginate()); }}以上代码中 BookResource 是 Laravel 的资源转换层。按照官方对于命名空间的建议,我们可以在 app/Modules/Books/Resources 文件夹中创建它。<?phpnamespace App\Modules\Books\Resources;use Illuminate\Http\Resources\Json\Resource;class BookResource extends Resource{ public function toArray($request) { return [ ‘id’ => $this->resource->id, ’title’ => $this->resource->title, ]; }}4- Authors 模块我们还可以通过 Routes 文件来启动 Authors 模块。<?phpuse App\Modules\Authors\ListAuthors;use Illuminate\Support\Facades\Route;Route::get(’/authors’, ListAuthors::class);注意: app/Modules/Authors 这个命名空间正表示我们所编写的文件,对于请求处理程序来说也是非常简单的。<?phpnamespace App\Modules\Authors;use App\Eloquent\Author;use App\Modules\Authors\Resources\AuthorResource;class ListAuthors{ public function __invoke(Author $author) { return AuthorResource::collection($author->paginate()); }}最后,我们将编写的 Resource 类转变为响应式的 JSON 格式。<?phpnamespace App\Modules\Authors\Resources;use App\Modules\Books\Resources\BookResource;use Illuminate\Http\Resources\Json\Resource;class AuthorResource extends Resource{ public function toArray($request) { return [ ‘id’ => $this->resource->id, ’name’ => $this->resource->name, ‘books’ => $this->whenLoaded(‘books’, function () { return BookResource::collection($this->resource->books); }) ]; }}注意资源是如何进入另一个模块以重用 BookResource 。 这通常不是一个比较好的选择,因为模块应该是完全自给自足的,并且只能重用标准类,例如 Eloquent Models 或设计用于在任何模块上通用的通用的组件。 这个问题的解决方案通常是将 BookResource 复制到 Authors 模块中,从而可以在不使用另一个模块的情况下进行更改,反之亦然。 我决定保留这个跨模块的用法,这个例子表现出一个很好的经验方法,就是让模块之间彼此隔离,但是如果你认为上面的例子很简单并且不太可能带来任何问题。 始终确保编写测试以涵盖您编写的功能,以避免其他人在不知不觉中修改您的应用程序。5- 结语虽然这是一个非常简单的例子,但我希望它能够让人们根据自己的需要来轻松操作使用 Laravel 框架的结构标准。您可以非常轻松地更改文件的位置,以便构建基于模块化的应用程序。我的大多数项目都附带了 App / Components 模块,可以适用于任何模块可重用的泛类型的基础类; App / Eloquent ,Modules 文件夹可以用于保存 Eloquent 模型和数据库关系模型,我们可以在其中构建任何基于模块化的功能。 这是我最近开始研究的应用程序的文件夹目录结构:我希望每个人都能从中得到这个概念,每个模块都有自己的需求,并且可以拥有自己的文件夹/实体/类/方法/属性。没有必要将所有模块标准化完全相同,因为某些模块比其他模块简单得多,并且不需要大量的结构设计。此示例显示AccountChurn模块通过 HTTP 文件夹提供 API,同时仍通过控制台提供 Artisan 命令。另一方面,AccountOverview则仅提供 HTTP API,并且依赖仓库、值对象(bags)以及服务类(paginators)来提供更大的数据价值。 ...

January 11, 2019 · 2 min · jiezi

跟控制器说再见吧,从今天开始使用请求处理器(Request Handlers) 范式

在过去几年中, PHP 开发环境发生了很大的变化。我们开始使用更多更好的设计模式,比如 DRY 和 SOLID) 设计模式原则。但为什么我们仍然在使用控制器?如果您以前曾经参与过大型项目的架构编写,那么您可能已经注意到迟早会出现控制器过多的这种现象。即使您将控制器逻辑分离到各种类库或服务类中,大量的依赖项和方法以及代码的行数还是会随着时间的推移不断增长。我来介绍一下请求处理器。这个概念很简单,但很多 PHP 开发人员都不知道。请求处理器可以理解为仅包含单个动作(Action)的控制器,能够使请求到响应的流程更加清晰明确。这个概念与 Paul M. Jones 提出的 Action-Domain-Responder 设计模式有相似之处,后者是MVC模式的替代品。一个好的方法去建立请求处理器就是使用调用类。可调用类是使用PHP中的魔术方法 __invoke ,把他们变成一个 Callable ,这将允许他们作为函数调用。这里有一个关于调用类的简单例子:class Greeting{ public function __invoke($name) { echo ‘Hello ’ . $name; }}$welcome = new Greeting();$welcome(‘John Doe’); //输出 Hello John Doe看到这里你大概会想;“我为什么要这样做?”。我知道这是一个有点荒谬的例子。但是它与某些代码一起使用时例如可调用对象和依赖注入,它将变得很有意义。一个好的使用例子是路由的请求处理在Laravel和Slim框架中。Route::get(’/{name}’, Greeting::class);是否让你大吃一惊?没有?让我们把它和你通常写的比较一下:Route::get(’/{name}’, ‘SomeController@greeting’);还没有?除了代码好看之外,还有其他优点。让我们先去看看使用请求处理程序比控制器有那些优点。单一模式SOLID 的第一个原则是“单一模式”。在我看来,控制器中存在许多的方法,就打破了这个原则。请求处理程序提供了一个很好的解决方案,可以将这些操作分成它们自己的类,使它们更易于维护,重构和测试。这是从 UsersController 中提取的2个请求处理程序的示例,它处理用户配置文件的编辑和保存:class EditUserHandler{ public function __construct( UserRepository $repository, Twig $twig ) { … } public function __invoke(Request $request, Response $response) { … }}class UpdateUserHandler{ public function __construct( UserRepository $repository, UpdateUserValidator $validator, ImageManager $resizer, Filesystem $storage ) { … } public function __invoke(Request $request, Response $response) { … }}接下来让我们看下一个优势;测试性能你最近有没有为你的项目编写过单元测试?在编写单元测试的时候你可能编写了一些与测试无关的模拟依赖项。由于请求处理器将不同的控制器操作拆分为单独的类,因此您只需注入或绑定该动作所需要的依赖项即可。这是 Jeffrey Way 的一些建议 Twitter 。提示:让你的功能测试尽可能更加详细具体,使用测试用例来描述重要的规则和能力。这基本不会让你的请求处理器都有一个测试文件。对于那些繁琐的控制器测试文件来说是一个非常好的改进。重构PhpStorm 和其他的编辑器都有强大的代码重构功能,但是如果你使用的是 Laravel 或者 Slim 框架默认的路由方法将控制器绑定到路由,那么你可能会遇到这种问题。例如重命名:Route::get(’/{name}’, Greeting::class);比这简单得很多:Route::get(’/{name}’, ‘SomeController@greeting’);结论请求处理器是控制器很好的替代品。控制器的动作(Actions)被分为多个独立的请求处理器类,分别负责响应单一的动作。这使整个项目的代码更易于维护、重构和测试。您是否应当使用请求处理器替换所有控制器?可能不是。对于小型应用程序而言,为了简单,将动作组合成控制器或许更加合理。当我开始在 Teamleader 工作后,我才开始发掘请求处理器,我觉得近期没什么换回控制器的必要了。如果有什么不清楚或有疑问,请在下面留下评论告诉我,我会更新这篇文章。转自 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 7, 2019 · 1 min · jiezi

一个极简的基于swoole常驻内存框架

背景在用过laravel框架,发现它的路由和数据库ORM确实非常好用,但是整体确实有点慢,执行到控制器大于需要耗时60ms左右。于是打算做一个拥有非常好用的路由和orm又非常简单的框架。所以你会发现one框的路由和ORM有laravel的影子。但也有一些自己的特色,例如ORM支持自动化缓存(自动化读、写、刷新)保持与数据库同步,对外使用无感知。one框架也支持在fpm下运行,在fpm下框架自身整体耗时在1ms左右。hello world安装composer create-project lizhichao/one-app appcd appphp App/swoole.php 测试curl http://127.0.0.1:8081/主要功能RESTful路由中间件websocket/tcp/http……任意协议路由ORM模型统一的session处理mysql连接池redis连接池tcp连接池HTTP/TCP/WEBOSCKET/UDP服务器缓存进程间内存共享RPC(http,tcp,udp)日志RequestId跟踪路由Router::get(’/’, \App\Controllers\IndexController::class . ‘@index’);// 带参数路由Router::get(’/user/{id}’, \App\Controllers\IndexController::class . ‘@user’);// 路由分组 Router::group([’namespace’=>‘App\Test\WebSocket’],function (){ // websocket 路由 Router::set(‘ws’,’/a’,‘TestController@abc’); Router::set(‘ws’,’/b’,‘TestController@bbb’); });// 中间件Router::group([ ‘middle’ => [ \App\Test\MixPro\TestMiddle::class . ‘@checkSession’ ]], function () { Router::get(’/mix/ws’, HttpController::class . ‘@ws’); Router::get(’/mix/http’, HttpController::class . ‘@http’); Router::post(’/mix/http/loop’, HttpController::class . ‘@httpLoop’); Router::post(’/mix/http/send’, HttpController::class . ‘@httpSend’);});orm 模型定义模型namespace App\Model;use One\Database\Mysql\Model;// 模型里面不需要指定主键,框架会缓存数据库结构// 自动匹配主键,自动过滤非表结构里的字段class User extends Model{ // 定义模型对应的表名 CONST TABLE = ‘users’; // 定义关系 public function articles() { return $this->hasMany(‘id’,Article::class,‘user_id’); } // 定义事件 // 是否开启自动化缓存 // ……}使用模型在fpm下数据库连接为单列,在swoole模式下数据库连接自动切换为连接池// 查询一条记录$user = User::find(1);// 关联查询$user_list = User::whereIn(‘id’,[1,2,3])->with(‘articles’)->findAll()->toArray();// 更新$r = $user->update([’name’ => ‘aaa’]);// 或者$r = user::where(‘id’,1)->update([’name’ => ‘aaa’]);// $r 为影响记录数量缓存// 设置缓存Cache::set(‘ccc’,1);// 获取Cache::get(‘ccc’);// 或者 缓存ccc 过期10s 在tag1下面Cache::get(‘ccc’,function (){ return ‘缓存的信息’;},10,[’tag1’]);// 刷新tag1下的所有缓存Cache::flush(’tag1’);HTTP/TCP/WEBOSCKET/UDP服务器启动一个websocket服务器,添加http服务监听,添加tcp服务监听[ // 主服务器 ‘server’ => [ ‘server_type’ => \One\Swoole\OneServer::SWOOLE_WEBSOCKET_SERVER, ‘port’ => 8082, // 事件回调 ‘action’ => \One\Swoole\Server\WsServer::class, ‘mode’ => SWOOLE_PROCESS, ‘sock_type’ => SWOOLE_SOCK_TCP, ‘ip’ => ‘0.0.0.0’, // swoole 服务器设置参数 ‘set’ => [ ‘worker_num’ => 5 ] ], // 添加监听 ‘add_listener’ => [ [ ‘port’ => 8081, // 事件回调 ‘action’ => \App\Server\AppHttpPort::class, ’type’ => SWOOLE_SOCK_TCP, ‘ip’ => ‘0.0.0.0’, // 给监听设置参数 ‘set’ => [ ‘open_http_protocol’ => true, ‘open_websocket_protocol’ => false ] ], [ ‘port’ => 8083, // 打包 解包协议 ‘pack_protocol’ => \One\Protocol\Text::class, // 事件回调 ‘action’ => \App\Test\MixPro\TcpPort::class, ’type’ => SWOOLE_SOCK_TCP, ‘ip’ => ‘0.0.0.0’, // 给监听设置参数 ‘set’ => [ ‘open_http_protocol’ => false, ‘open_websocket_protocol’ => false ] ] ]];RPC像调用本项目的方法一样调用远程服务器的方法。跨语言,跨机器。服务端启动rpc服务,框架已经内置了各个协议的rpc服务,添加到到上面配置文件的action即可。列如: 支持http调用,又支持tpc调用。// http 协议 rpc服务[ ‘port’ => 8082, ‘action’ => \App\Server\RpcHttpPort::class, ’type’ => SWOOLE_SOCK_TCP, ‘ip’ => ‘0.0.0.0’, ‘set’ => [ ‘open_http_protocol’ => true, ‘open_websocket_protocol’ => false ]],// tpc 协议 rpc服务[ ‘port’ => 8083, ‘action’ => \App\Server\RpcTcpPort::class, ’type’ => SWOOLE_SOCK_TCP, ‘pack_protocol’ => \One\Protocol\Frame::class, // tcp 打包 解包协议 ‘ip’ => ‘0.0.0.0’, ‘set’ => [ ‘open_http_protocol’ => false, ‘open_websocket_protocol’ => false, ‘open_length_check’ => 1, ‘package_length_func’ => ‘\One\Protocol\Frame::length’, ‘package_body_offset’ => \One\Protocol\Frame::HEAD_LEN, ]]添加具体服务到rpc,例如有个类Abcclass Abc{ private $a; // 初始值 public function __construct($a = 0) { $this->a = $a; } // 加法 public function add($a, $b) { return $this->a + $a + $b; } public function time() { return date(‘Y-m-d H:i:s’); } // 重新设初始值 public function setA($a) { $this->a = $a; return $this; }}把Abc添加到rpc服务// 添加Abc到rpc服务RpcServer::add(Abc::class);// 如果你不希望把Abc下的所有方法都添加到rpc服务,也可以指定添加。// 未指定的方法客户端无法调用.//RpcServer::add(Abc::class,‘add’);// 分组添加//RpcServer::group([// // 中间件 在这里可以做 权限验证 数据加解密 等等// ‘middle’ => [// TestMiddle::class . ‘@aa’// ],// // 缓存 如果设置了 当以同样的参数调用时 会返回缓存信息 不会真正调用 单位:秒// ‘cache’ => 10//], function () {// RpcServer::add(Abc::class);// RpcServer::add(User::class);//});客户端调用为了方便调用我们建立一个映射类(one框架可自动生成)class ClientAbc extends RpcClientHttp { // rpc服务器地址 protected $_rpc_server = ‘http://127.0.0.1:8082/’; // 远程的类 不设置 默认为当前类名 protected $_remote_class_name = ‘Abc’;}调用rpc服务的远程方法, 和调用本项目的方法一样的。你可以想象这个方法就在你的项目里面。$abc = new ClientAbc(5);// $res === 10$res = $abc->add(2,3);// 链式调用 $res === 105$res = $abc->setA(100)->add(2,3);// 如果把上面的模型的User添加到rpc// RpcServer::add(User::class);// 下面运行结果和上面一样// $user_list = User::whereIn(‘id’,[1,2,3])->with(‘articles’)->findAll()->toArray();上面是通过http协议调用的。你也可以通过其他协议调用。例如Tpc协议class ClientAbc extends RpcClientTcp { // rpc服务器地址 protected $_rpc_server = ’tcp://127.0.0.1:8083/’; // 远程的类 不设置 默认为当前类名 protected $_remote_class_name = ‘Abc’;}其中类 RpcClientHttp,RpcClientTcp在框架里。 你也可以复制到任何其他地方使用。githubQQ交流群: 731475644 ...

December 17, 2018 · 2 min · jiezi

php微框架 flight源码阅读——框架初始化、Loader、Dispatcher

在自动加载实现完成后,接着new flightEngine()实例化了下框架的核心类Engine,这个类翻译过来名字就是引擎发动机的意思,是flight的引擎发动机,很有想象力吧。public static function app() { static $initialized = false; if (!$initialized) { require_once DIR.’/autoload.php’; self::$engine = new \flight\Engine(); $initialized = true; } return self::$engine;}在实例化Engine这个类的时候,当前类的构造方法进行了对框架的初始化工作。public function __construct() { $this->vars = array(); $this->loader = new Loader(); $this->dispatcher = new Dispatcher(); $this->init();}接着来看init方法都做了什么,将初始化状态标记为静态变量static $initialized,判断如果为true,将$this->vars以及$this->loader、$this->dispatcher中保存的属性重置为默认状态。static $initialized = false;$self = $this;if ($initialized) { $this->vars = array(); $this->loader->reset(); $this->dispatcher->reset();} 接下来将框架的Request、Response、Router、View类的命定空间地址register(设置)到Loader类的classes属性中。// Register default components$this->loader->register(‘request’, ‘\flight\net\Request’);$this->loader->register(‘response’, ‘\flight\net\Response’);$this->loader->register(‘router’, ‘\flight\net\Router’);$this->loader->register(‘view’, ‘\flight\template\View’, array(), function($view) use ($self) { $view->path = $self->get(‘flight.views.path’); $view->extension = $self->get(‘flight.views.extension’);}); flight/core/Loader.phppublic function register($name, $class, array $params = array(), $callback = null) { unset($this->instances[$name]); $this->classes[$name] = array($class, $params, $callback);}再接下来就是将框架给用户提供的调用方法,设置到调度器Dispatcher类的events属性中。// Register framework methods$methods = array( ‘start’,‘stop’,‘route’,‘halt’,’error’,’notFound’, ‘render’,‘redirect’,’etag’,’lastModified’,‘json’,‘jsonp’);foreach ($methods as $name) { $this->dispatcher->set($name, array($this, ‘_’.$name));}flight/core/Dispatcher.php/** * Assigns a callback to an event. * * @param string $name Event name * @param callback $callback Callback function /public function set($name, $callback) { $this->events[$name] = $callback;}接下来呢,就是设置框架的一些配置,将这些配置保存在Engine类的vars属性中。// Default configuration settings$this->set(‘flight.base_url’, null);$this->set(‘flight.case_sensitive’, false);$this->set(‘flight.handle_errors’, true);$this->set(‘flight.log_errors’, false);$this->set(‘flight.views.path’, ‘./views’);$this->set(‘flight.views.extension’, ‘.php’);flight/Engine.php/* * Sets a variable. * * @param mixed $key Key * @param string $value Value /public function set($key, $value = null) { if (is_array($key) || is_object($key)) { foreach ($key as $k => $v) { $this->vars[$k] = $v; } } else { $this->vars[$key] = $value; }}最后一步的操作,当调用框架的start方法时,给其设置一些前置操作,通过set_error_handler()和set_exception_handler()设置用户自定义的错误和异常处理函数,如何使用自定义的错误和异常函数,可以看这两个范例:https://segmentfault.com/n/13…https://segmentfault.com/n/13…。// Startup configuration$this->before(‘start’, function() use ($self) { // Enable error handling if ($self->get(‘flight.handle_errors’)) { set_error_handler(array($self, ‘handleError’)); set_exception_handler(array($self, ‘handleException’)); } // Set case-sensitivity $self->router()->case_sensitive = $self->get(‘flight.case_sensitive’);});$initialized = true;/* * Custom error handler. Converts errors into exceptions. * * @param int $errno Error number * @param int $errstr Error string * @param int $errfile Error file name * @param int $errline Error file line number * @throws \ErrorException /public function handleError($errno, $errstr, $errfile, $errline) { if ($errno & error_reporting()) { throw new \ErrorException($errstr, $errno, 0, $errfile, $errline); }}/* * Custom exception handler. Logs exceptions. * * @param \Exception $e Thrown exception /public function handleException($e) { if ($this->get(‘flight.log_errors’)) { error_log($e->getMessage()); } $this->error($e);}在$this->before()操作中,会将前置操作设置到Dispatcher类的filters属性中。这些操作完成后,将$initialized = true。/* * Adds a pre-filter to a method. * * @param string $name Method name * @param callback $callback Callback function /public function before($name, $callback) { $this->dispatcher->hook($name, ‘before’, $callback);}flight/core/Dispatcher.php/* * Hooks a callback to an event. * * @param string $name Event name * @param string $type Filter type * @param callback $callback Callback function */public function hook($name, $type, $callback) { $this->filters[$name][$type][] = $callback;} ...

December 6, 2018 · 2 min · jiezi

Laravel核心解读--HTTP内核

Http KernelHttp Kernel是Laravel中用来串联框架的各个核心组件来网络请求的,简单的说只要是通过public/index.php来启动框架的都会用到Http Kernel,而另外的类似通过artisan命令、计划任务、队列启动框架进行处理的都会用到Console Kernel, 今天我们先梳理一下Http Kernel做的事情。内核绑定既然Http Kernel是Laravel中用来串联框架的各个部分处理网络请求的,我们来看一下内核是怎么加载到Laravel中应用实例中来的,在public/index.php中我们就会看见首先就会通过bootstrap/app.php这个脚手架文件来初始化应用程序:下面是 bootstrap/app.php 的代码,包含两个主要部分创建应用实例和绑定内核至 APP 服务容器<?php// 第一部分: 创建应用实例$app = new Illuminate\Foundation\Application( realpath(DIR.’/../’));// 第二部分: 完成内核绑定$app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);$app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class);$app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class);return $app;HTTP 内核继承自 IlluminateFoundationHttpKernel类,在 HTTP 内核中 内它定义了中间件相关数组, 中间件提供了一种方便的机制来过滤进入应用的 HTTP 请求和加工流出应用的HTTP响应。<?phpnamespace App\Http;use Illuminate\Foundation\Http\Kernel as HttpKernel;class Kernel extends HttpKernel{ /** * The application’s global HTTP middleware stack. * * These middleware are run during every request to your application. * * @var array / protected $middleware = [ \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \App\Http\Middleware\TrustProxies::class, ]; /* * The application’s route middleware groups. * * @var array / protected $middlewareGroups = [ ‘web’ => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ‘api’ => [ ’throttle:60,1’, ‘bindings’, ], ]; /* * The application’s route middleware. * * These middleware may be assigned to groups or used individually. * * @var array / protected $routeMiddleware = [ ‘auth’ => \Illuminate\Auth\Middleware\Authenticate::class, ‘auth.basic’ => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, ‘bindings’ => \Illuminate\Routing\Middleware\SubstituteBindings::class, ‘can’ => \Illuminate\Auth\Middleware\Authorize::class, ‘guest’ => \App\Http\Middleware\RedirectIfAuthenticated::class, ’throttle’ => \Illuminate\Routing\Middleware\ThrottleRequests::class, ];}在其父类 「IlluminateFoundationHttpKernel」 内部定义了属性名为 「bootstrappers」 的 引导程序 数组:protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \Illuminate\Foundation\Bootstrap\RegisterFacades::class, \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class,];引导程序组中 包括完成环境检测、配置加载、异常处理、Facades 注册、服务提供者注册、启动服务这六个引导程序。有关中间件和引导程序相关内容的讲解可以浏览我们之前相关章节的内容。应用解析内核在将应用初始化阶段将Http内核绑定至应用的服务容器后,紧接着在public/index.php中我们可以看到使用了服务容器的make方法将Http内核实例解析了出来:$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);在实例化内核时,将在 HTTP 内核中定义的中间件注册到了 路由器,注册完后就可以在实际处理 HTTP 请求前调用路由上应用的中间件实现过滤请求的目的:namespace Illuminate\Foundation\Http;…class Kernel implements KernelContract{ /* * Create a new HTTP kernel instance. * * @param \Illuminate\Contracts\Foundation\Application $app * @param \Illuminate\Routing\Router $router * @return void / public function __construct(Application $app, Router $router) { $this->app = $app; $this->router = $router; $router->middlewarePriority = $this->middlewarePriority; foreach ($this->middlewareGroups as $key => $middleware) { $router->middlewareGroup($key, $middleware); } foreach ($this->routeMiddleware as $key => $middleware) { $router->aliasMiddleware($key, $middleware); } }}namespace Illuminate/Routing;class Router implements RegistrarContract, BindingRegistrar{ /* * Register a group of middleware. * * @param string $name * @param array $middleware * @return $this / public function middlewareGroup($name, array $middleware) { $this->middlewareGroups[$name] = $middleware; return $this; } /* * Register a short-hand name for a middleware. * * @param string $name * @param string $class * @return $this / public function aliasMiddleware($name, $class) { $this->middleware[$name] = $class; return $this; }}处理HTTP请求通过服务解析完成Http内核实例的创建后就可以用HTTP内核实例来处理HTTP请求了//public/index.php$response = $kernel->handle( $request = Illuminate\Http\Request::capture());在处理请求之前会先通过Illuminate\Http\Request的 capture() 方法以进入应用的HTTP请求的信息为基础创建出一个 Laravel Request请求实例,在后续应用剩余的生命周期中Request请求实例就是对本次HTTP请求的抽象,关于Laravel Request请求实例的讲解可以参考以前的章节。将HTTP请求抽象成Laravel Request请求实例后,请求实例会被传导进入到HTTP内核的handle方法内部,请求的处理就是由handle方法来完成的。namespace Illuminate\Foundation\Http;class Kernel implements KernelContract{ /* * Handle an incoming HTTP request. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response / public function handle($request) { try { $request->enableHttpMethodParameterOverride(); $response = $this->sendRequestThroughRouter($request); } catch (Exception $e) { $this->reportException($e); $response = $this->renderException($request, $e); } catch (Throwable $e) { $this->reportException($e = new FatalThrowableError($e)); $response = $this->renderException($request, $e); } $this->app[’events’]->dispatch( new Events\RequestHandled($request, $response) ); return $response; }}handle 方法接收一个请求对象,并最终生成一个响应对象。其实handle方法我们已经很熟悉了在讲解很多模块的时候都是以它为出发点逐步深入到模块的内部去讲解模块内的逻辑的,其中sendRequestThroughRouter方法在服务提供者和中间件都提到过,它会加载在内核中定义的引导程序来引导启动应用然后会将使用Pipeline对象传输HTTP请求对象流经框架中定义的HTTP中间件们和路由中间件们来完成过滤请求最终将请求传递给处理程序(控制器方法或者路由中的闭包)由处理程序返回相应的响应。关于handle方法的注解我直接引用以前章节的讲解放在这里,具体更详细的分析具体是如何引导启动应用以及如何将传输流经各个中间件并到达处理程序的内容请查看服务提供器、中间件还有路由这三个章节。protected function sendRequestThroughRouter($request){ $this->app->instance(‘request’, $request); Facade::clearResolvedInstance(‘request’); $this->bootstrap(); return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter());} /引导启动Laravel应用程序1. DetectEnvironment 检查环境2. LoadConfiguration 加载应用配置3. ConfigureLogging 配置日至4. HandleException 注册异常处理的Handler5. RegisterFacades 注册Facades 6. RegisterProviders 注册Providers 7. BootProviders 启动Providers/public function bootstrap(){ if (! $this->app->hasBeenBootstrapped()) { /**依次执行$bootstrappers中每一个bootstrapper的bootstrap()函数 $bootstrappers = [ ‘Illuminate\Foundation\Bootstrap\DetectEnvironment’, ‘Illuminate\Foundation\Bootstrap\LoadConfiguration’, ‘Illuminate\Foundation\Bootstrap\ConfigureLogging’, ‘Illuminate\Foundation\Bootstrap\HandleExceptions’, ‘Illuminate\Foundation\Bootstrap\RegisterFacades’, ‘Illuminate\Foundation\Bootstrap\RegisterProviders’, ‘Illuminate\Foundation\Bootstrap\BootProviders’, ];/ $this->app->bootstrapWith($this->bootstrappers()); }}发送响应经过上面的几个阶段后我们最终拿到了要返回的响应,接下来就是发送响应了。//public/index.php$response = $kernel->handle( $request = Illuminate\Http\Request::capture());// 发送响应$response->send();发送响应由 Illuminate\Http\Response的send()方法完成父类其定义在父类Symfony\Component\HttpFoundation\Response中。public function send(){ $this->sendHeaders();// 发送响应头部信息 $this->sendContent();// 发送报文主题 if (function_exists(‘fastcgi_finish_request’)) { fastcgi_finish_request(); } elseif (!\in_array(PHP_SAPI, array(‘cli’, ‘phpdbg’), true)) { static::closeOutputBuffers(0, true); } return $this;}关于Response对象的详细分析可以参看我们之前讲解Laravel Response对象的章节。终止应用程序响应发送后,HTTP内核会调用terminable中间件做一些后续的处理工作。比如,Laravel 内置的「session」中间件会在响应发送到浏览器之后将会话数据写入存储器中。// public/index.php// 终止程序$kernel->terminate($request, $response);//Illuminate\Foundation\Http\Kernelpublic function terminate($request, $response){ $this->terminateMiddleware($request, $response); $this->app->terminate();}// 终止中间件protected function terminateMiddleware($request, $response){ $middlewares = $this->app->shouldSkipMiddleware() ? [] : array_merge( $this->gatherRouteMiddleware($request), $this->middleware ); foreach ($middlewares as $middleware) { if (! is_string($middleware)) { continue; } list($name, $parameters) = $this->parseMiddleware($middleware); $instance = $this->app->make($name); if (method_exists($instance, ’terminate’)) { $instance->terminate($request, $response); } }}Http内核的terminate方法会调用teminable中间件的terminate方法,调用完成后从HTTP请求进来到返回响应整个应用程序的生命周期就结束了。总结本节介绍的HTTP内核起到的主要是串联作用,其中设计到的初始化应用、引导应用、将HTTP请求抽象成Request对象、传递Request对象通过中间件到达处理程序生成响应以及响应发送给客户端。这些东西在之前的章节里都有讲过,并没有什么新的东西,希望通过这篇文章能让大家把之前文章里讲到的每个点串成一条线,这样对Laravel整体是怎么工作的会有更清晰的概念。本文已经收录在系列文章Laravel源码学习里,欢迎访问阅读。 ...

November 11, 2018 · 3 min · jiezi