关于php7:一个PHP7函数类型限定对性能的影响的测试

1. 概述本文次要是对于PHP7函数数据类型限定设定与否对性能的影响,为此,做了个简略的压测另外,分享下本人两个工作中遇到的小问题及其应答,如有谬误,恳请斧正 2. PHP7函数类型限定(1) 介绍函数参数类型限定(包含返回值、成员属性)从PHP5开始反对的,然而反对的类型不多,PHP7做了扩大:int/string/bool/object 等作用 防止谬误调用,表明类型,只能传递同类型的参数,尤其多人协同开发时如果不是能够主动转换数据类型,如下,当然前提是待转化类型能够失常转化而本文正是测试类型限定对性能的影响水平function testInt(int $intNum){ var_dump($intNum);}testInt("123"); // int(123)留神 参数、返回值如果跟设定的类型不统一时会报错,不是百分百确认的须要手动转化下(2) 压测运行环境 PHP 7.2.34Laravel 5.8AB 2.3单机配置 型号名称 MacBook Pro处理器名称 Quad-Core Intel Core i7内存 8 GB核总数 4AB 应用AB (Apache Benchmark) 进行压测,因为不是正式的压测,所以此处只关怀综合指标:Requests per second (均匀每秒申请数)主要参数 -n 压测申请数-c 并发数-p POST申请时指定所需携带参数的文件-r 遇到谬误响应不退出,操作系统有防高并发攻打保护措施 (apr_socket_recv: Connection reset by peer)设置我的项目 设置两个 POST 接口,没有业务逻辑、中间件操作等,如下/***** 1 一般接口 *****/// CommonUserControllerpublic function createUser(Request $request){ $this->validate($request, [ 'name' => 'required|string', 'age' => 'required|integer', 'sex' => ['required', Rule::in([1, 2])], ]); (new CommonUserModel())->createUser($request['age'], $request['name'], $request['sex'], $request['address'] ?? ''); return response()->json(['status' => 200, 'msg' => 'ok']);}// CommonUserModelpublic function createUser($sex, $age, $name, $address){ if(empty($sex) || empty($age) || empty($name)) return false; // 省略DB操作 return true;}/***** 2 类型限定接口 *****/// TypeUserControllerpublic function createUser(Request $request): JsonResponse{ $this->validate($request, [ 'name' => 'required|string', 'age' => 'required|integer', 'sex' => ['required', Rule::in([1, 2])], ]); (new TypeUserModel())->createUser($request['age'], $request['name'], $request['sex'], $request['address'] ?? ''); return response()->json(['status' => 200, 'msg' => 'ok']);}// TypeUserModelpublic function createUser(int $age, string $name, int $sex, string $address): bool{ if(empty($sex) || empty($age) || empty($name)){ return false; } // 省略DB操作 return true;}(3) 施行共进行五次压测,配置及后果展现如下 (对立删除:| grep 'Requests per second')/*****第一次*****/// 类型限定接口 rps=456.16ab -n 100 -c 10 -p '/tmp/ab_post_data.json' -T 'application:json' http://www.laravel_type_test.com/api/type/create_user// 一般接口 rps=450.12ab -n 100 -c 10 -p '/tmp/ab_post_data.json' -T 'application:json' http://www.laravel_type_test.com/api/common/create_user/*****第二次*****/// 类型限定接口 rps=506.74ab -n 1000 -c 100 -p '/tmp/ab_post_data.json' -T 'application:json' http://www.laravel_type_test.com/api/type/create_user// 一般接口 rps=491.24ab -n 1000 -c 100 -p '/tmp/ab_post_data.json' -T 'application:json' http://www.laravel_type_test.com/api/common/create_user/*****第三次*****/// 类型限定接口 rps=238.43 ab -n 5000 -c 150 -p '/tmp/ab_post_data.json' -T 'application:json' -r http://www.laravel_type_test.com/api/type/create_user// 一般接口 rps=237.16ab -n 5000 -c 150 -p '/tmp/ab_post_data.json' -T 'application:json' -r http://www.laravel_type_test.com/api/common/create_user/*****第四次*****/// 类型限定接口 rps=209.21ab -n 10000 -c 150 -p '/tmp/ab_post_data.json' -T 'application:json' -r http://www.laravel_type_test.com/api/type/create_user// 一般接口 rps=198.01ab -n 10000 -c 150 -p '/tmp/ab_post_data.json' -T 'application:json' -r http://www.laravel_type_test.com/api/common/create_user/*****第五次*****/// 类型限定接口 rps=191.17ab -n 100000 -c 150 -p '/tmp/ab_post_data.json' -T 'application:json' -r http://www.laravel_type_test.com/api/type/create_user// 一般接口 rps=190.55ab -n 100000 -c 150 -p '/tmp/ab_post_data.json' -T 'application:json' -r http://www.laravel_type_test.com/api/common/create_user(4) 后果压测不算太谨严,后果仅供参考类型限定对性能的晋升没有预期的大,很渺小,不过还是举荐这种写法

November 15, 2021 · 2 min · jiezi

关于php7:ubuntu安装高版本php72如何卸载安装php70

一、删除php的相干包及配置 sudo apt-get autoremove php7*二、删除关联 sudo find /etc -name "php" |xargs rm -rf三、革除dept列表 sudo apt purge dpkg -l | grep php| awk '{print $2}' |tr "\n" " "四、查看是否卸载洁净(无返回就是卸载实现) dpkg -l | grep php7.0而后是装置php7.0版本,因为php7.0曾经从ubuntu中的包治理移除了,所以咱们须要从新增加受权 add-apt-repository ppa:ondrej/phpapt-get updateapt-get install php7.0通过下面的命令能够装置php7.0版本,如果提醒add-apt-repository 不存在能够执行上面的命令 apt-get install software-properties-common通过以上的操作,咱们就将php版本从php7.2降到php7.0了亲测无效

July 19, 2021 · 1 min · jiezi

搞定PHP面试-常见排序算法及PHP实现

常见排序算法及PHP实现全文代码使用PHP7.2语法编写流程图生成工具:https://visualgo.net 0. 五种基础排序算法对比 1. 冒泡排序(Bubble Sort)冒泡排序 是一种交换排序,它的基本思想是:对待排序记录从后往前(逆序)进行多遍扫描,当发现相邻两条记录的次序与排序要求的规则不符时,就将这两个记录进行交换。这样,值较小的记录将逐渐从后面向前移动,就像气泡在水中向上浮一样。 算法描述假设需要排序的记录有 n 个,其值保存在数组 A 中,使用冒泡排序法,需对数组 A 进行 n-1 次扫描,完成排序操作。具体过程如下: 将 A[n-1] 与 A[n] 进行比较,若 A[n] < A[n-1] ,则交换两元系的位置。修改数组下标,使需要比较的两个元素为 A[n-1] 和 A[n-2] ,重复步骤(1),对这两个元素进行比较。重复这个过程,直到对 A[1] 和 A[0] 进行比较完为止。完成第1遍扫描。经过第1遍扫描后,最小的元素已经像气泡一样“浮”到最上面,即位于元素 A[0] 中了。接下来重复前面的步骤,进行第2遍扫描,只是扫描结束位置到 A[2] 与 A[1] 进行比较完为止(因为A[0]中已经是最小的数据,不用再进行比较)。通过 n-1 遍扫描,前 n-1 个数都已经排序完成,最后一个元素 A[n] 肯定就是最大的数了。至此,完成排序操作。 代码实现/** * 冒泡排序 * @param array $arr */function bubbleSort(array &$arr) : void{ $length = count($arr); // 外层循环,从数组首部开始,每完成一次循环,可确定 $arr[$i] 位置的元素 for ($i = 0; $i < $length; $i++){ // 内层循环,$j 从后往前循环 for ($j = $length - 1; $j > $i; $j--) { // 若前面的值大于后面的值,则互换位置 if ($arr[$j] < $arr[$j - 1]) { // 互换数组两个位置的值 [$arr[$j], $arr[$j - 1]] = [$arr[$j - 1], $arr[$j]]; } } }}2. 选择排序(Selection Sort)选择排序是通过 n-i 次关键字间的比较,从 n-i+1 个记录中选出关键字最小的记录,并和第 i ( 1 <= i <= n ) 个记录交换。 ...

July 15, 2019 · 4 min · jiezi

安装-PHP-737-出现-make-sapicliphp-Error-1-解决办法汇总

问题起因,编译安装 PHP 7.3.7 的过程中 configure 是正常的,但是 make 时一直提示 make: * [sapi/cli/php] Error 1自己花了好久才找到解决方法,便想到记录在这里,可能会对后面遇到同样问题的人有所帮助 以下是解决方法汇总: 如果在错误信息中有看到类似undefined reference to libiconv_open 之类的提示,可以通过在 make 时附加上对应的库来解决,例如 make ZEND_EXTRA_LIBS='-liconv'。这里是网上能够搜索到的解决方法,但是作者自己遇到的并不是这种或者类似的情形。系统中先是装了一个 openssl 版本和自己 yum 安装的 devel 版本不对应,可以通过 yum remove openssl-devel 卸载后再编译,这里自己遇到的就是这种情况。

July 13, 2019 · 1 min · jiezi

php7笔试面试

1.php7对php5新特性参考地址1、地址2 a.null合并运算符(??)//php7以前 三元运算符$param = empty($_GET['param']) ? 1 : $_GET['param'];//PHP7 null合并运算符$param = $_GET['param'] ?? 1;//1 b.define() 定义常量数组c.组合比较符(<=>)//数字、字符串echo "a" <=> "a"; // 0 => 成立顺序对应-1.0.1 d.变量类型声明两种模式: 强制(默认)和严格模式declare(strict_types=1);//值为1代表为严格校验的模式 e.返回值类型声明function getInt(int $value): int { return $value;}

July 10, 2019 · 1 min · jiezi

Centos多版本php共存

需求场景:服务器有几个较老的应用占用了php,且版本不可向上兼容,新的项目需要高版本的php才能支持,实现其实比较简单,php-fpm可以指定端口(默认9000),修改nginx fastcgi_pass指向对应的端口即可。 这里记录2种不同的安装方式:一种是通过yum安装,另外一种是从官网下载源码编译安装。 安装php7# 安装yum源rpm -ivh https://mirror.webtatic.com/yum/el7/webtatic-release.rpm# 查看可安装的版本yum list | grep php# 安装指定版本yum install php70w.x86_64 php70w-cli.x86_64 php70w-common.x86_64 php70w-gd.x86_64 php70w-ldap.x86_64 php70w-mbstring.x86_64 php70w-mcrypt.x86_64 php70w-mysqlnd.x86_64 php70w-pdo.x86_64# 开启php-fpm/usr/sbin/php-fpm# 重启php-fpmps aux|grep php-fpmkill -SIGUSER2 xx(php_master_id)安装php7.2.191.去官网下载压缩包,上传到服务器解压文件,并进入 # 或者直接通过服务器下载源文件安装wget http://cn2.php.net/distributions/php-7.2.19.tar.gz# 解压(下面下载文件默认是在usr/local文件夹下)tar -zxvf php-7.2.19.tar.gz# 进入cd php-7.2.192.配置安装参数 ./configure --prefix=/usr/local/php7 --with-config-file-path=/usr/local/php7/etc --with-curl --with-freetype-dir=/usr/include/freetype2/freetype --with-gd --with-gettext \--with-iconv-dir --with-kerberos --with-libdir=lib64 --with-libxml-dir --with-mysql=mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-openssl --with-pcre-regex --with-pdo-mysql --with-pdo-sqlite --with-pear --with-png-dir --with-xmlrpc --with-xsl --with-zlib --enable-fpm --enable-bcmath --enable-libxml --enable-inline-optimization --enable-gd-native-ttf --enable-mbregex --enable-mbstring --enable-opcache --enable-pcntl --enable-shmop --enable-soap --enable-sockets --enable-sysvsem --enable-xml --enable-zip --disable-fileinfo --disable-inline-optimization3.编译安装 ...

June 25, 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

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

modernPHP专题14内置的http服务器

PHP 5.4起就在CLI SAPI中内置了web服务器,但只是提供开发测试使用,不推荐使用中生产环境中。因为这个服务器接受处理请求时顺序执行的,不能并发处理。 这个内置的web服务器使用起来非常的方便,你只需要执行下面的命令: 启动Web服务器$ php -S localhost:8000然后就可以访问了。这样启动后,默认的web服务目录是执行命令的当前目录,如果不想使用当前目录,你需要使用 -t 参数来指定。 启动web服务器时指定文档的根目录php -S localhost:8000 -t foo/使用路由器脚本在这个例子中,对图片的请求会返回相应的图片,但对HTML文件的请求会显示“Welcome to PHP”: // router.phpif (preg_match('/\.(?:png|jpg|jpeg|gif)$/', $_SERVER["REQUEST_URI"])) { return false; // serve the requested resource as-is.} else { echo "<p>Welcome to PHP</p>";}$ php -S localhost:8000 router.php判断是否是在使用内置web服务器通过程序判断来调整同一个PHP路由器脚本在内置Web服务器中和在生产服务器中的不同行为: // router.phpif (php_sapi_name() == 'cli-server') {/* route static assets and return false */}/* go on with normal index.php operations */$ php -S localhost:8000 router.php这个内置的web服务器能识别一些标准的MIME类型资源,它们的扩展有:.css, .gif, .htm, .html, .jpe, .jpeg, .jpg, .js, .png, .svg, and .txt。对.htm 和 .svg 扩展到支持是在PHP 5.4.4之后才支持的。 ...

May 15, 2019 · 1 min · jiezi

modernPHP专题13ZendOpcache字节码缓存

概述字节码缓存不是PHP的新特性,有很多独立的扩展可以实现,比如APC、eAccelerator和Xache等,但是截至目前这些扩展都没有集成到PHP内核,从PHP 5.5.0开始,PHP内置了字节码缓存功能,名为Zend Opcache。 开始之前,我们先来看看什么是字节码缓存,以及字节码缓存的作用是什么。 众所周知,PHP是解释型语言,构建在Zend 虚拟机之上,PHP解释器在执行PHP脚本时会解析PHP脚本代码,把PHP代码编译成一系列Zend操作码( opcode,由于每个操作码都是一个字节长,所以又叫字节码,字节码可以直接被Zend虚拟机执行),然后执行字节码。每次请求PHP文件都是这样,这会消耗很多资源,如果每次HTTP请求都必须不断解析、编译和执行PHP脚本,消耗的资源更多。如果PHP源码不变,相应的字节码也不会变化,显然没有必要每次都重新生成Opcode,结合在Web应用中无处不在的缓存机制,我们可以把首次生成的Opcode缓存起来,这样下次直接从缓存取,岂不是很快?下面是启用Opcode缓存之前和之后的流程图: 字节码缓存能存储预先编译好的PHP字节码,这样,下次请求PHP脚本时,PHP解释器不用每次读取、解析和编译PHP代码,直接从内存中读取预先编译好的字节码,然后立即执行,这样能省很多时间,极大提升应用的性能。 启用Zend Opcache注:如果使用Windows开发环境,或者使用brew或apt-get等命令安装的PHP可以略过编译步骤。默认情况下,Zend Opcache没有开启,需要我们在编译时使用--enable-opcache指定启用Zend Opcache。 编译好PHP后还需要在php.ini中指定Opcache扩展路径: zend_extension=/path/to/opcache.so一般而言PHP编译成功后会显示Zend Opcache扩展路径,但如果想不起来,可以使用如下命令找到PHP扩展所在目录: php -ini | grep extensions[info] 注:如果你使用Xdebug,需要在php.ini中先加载Zend Opcache,再加载Xdebug。重启后,查看phpinfo php -info | grep Opcache配置Zend Opcache启用Zend Opcache后还需要在php.ini中配置Zend Opcache,下面是一份配置示例作为参考: opcache.validate_timestamps=1 // 生产环境中配置为0opcache.revalidate_freq=0 //检查脚本时间戳是否有更新时间opcache.memory_consumption=64 //Opcache的共享内存大小,以M为单位opcache.interned_strings_buffer=16 //用来存储临时字符串的内存大小,以M为单位opcache.max_accelerated_files=4000 //Opcache哈希表可以存储的脚本文件数量上限opcache.fast_shutdown=1 //使用快速停止续发事件官网配置参考使用Zend OpcacheZend Opcache使用起来很简单,因为启用之后它会自动运行,Zend Opcache会自动在内存中缓存预先编译好的PHP字节码,如果缓存了某个文件的字节码,就执行对应的字节码。 如果php.ini中配置了opcache.validate_timestamps值为0,需要小心,因为Zend Opcache将不能觉察PHP脚本的变化,必须手动清空Zend OPcache缓存的字节码,才能让它发现PHP文件的变动。这个配置适合在生产环境中设置为0,但在开发环境会带来不便,我们可以在开发环境中这样配置启用自动验证缓存功能: opcache.enable=1 # 启用操作码缓存,默认为“1”, 如果禁用此选项,则不会优化和缓存代码。opcache.revalidate_freq=0 # 检查脚本时间戳是否有更新的周期,以秒为单位。设置为 0 会导致针对每个请求, OPcache 都会检查脚本更新。opcache.validate_timestamps=1 # 如果启用,那么 OPcache 会每隔 opcache.revalidate_freq 设定的秒数 检查脚本是否更新。手动清理缓存除了重启php-fpm的进程可以清理opcache缓存外,手动清理缓存涉及到的opcache函数主要为:opcache_reset()和opcache_invalidate() 。 boolean opcache_reset ( void ) # 该函数将重置整个字节码缓存。 在调用 opcache_reset() 之后,所有的脚本将会重新载入并且在下次被点击的时候重新解析。[warning] 需要注意的是,当PHP以PHP-FPM的方式运行的时候,opcache的缓存是无法通过php命令进行清除的,只能通过http或cgi到php-fpm进程的方式来清除缓存。

May 14, 2019 · 1 min · jiezi

modernPHP专题1php7概述

PHP7性能7最大的亮点,应该就是性能提高了两倍,某些测试环境下甚至提高到三到五倍,具体可以了解以下链接: PHP7 VS HHVM (WordPress) HHVM vs PHP 7 – The Competition Gets Closer! PHP 7.0 Is Showing Very Promising Performance Over PHP 5, Closing Gap With HHVM PHP7革新与性能优化 特性简述标量类型声明PHP 7 中的函数的形参类型声明可以是标量了。在 PHP 5 中只能是类名、接口、array 或者 callable (PHP 5.4,即可以是函数,包括匿名函数),现在也可以使用 string、int、float和 bool 了。 <?php// 强制模式,强制把参数int化function sumOfInts(int ...$ints){ return array_sum($ints);}var_dump(sumOfInts(2, '3', 4.1));强制模式(默认,既强制类型转换)下会对不符合预期的参数进行强制类型转换,但严格模式下则触发 TypeError 的致命错误。严格模式:申明 declare(strict_types=1)即可;返回值类型声明PHP 7 增加了对返回类型声明的支持。 类似于参数类型声明,返回类型声明指明了函数返回值的类型。可用的类型与参数声明中可用的类型相同。 <?phpfunction arraysSum(array ...$arrays): array{ # 把返回值强制转换为string return array_map(function(array $array): string { return array_sum($array); }, $arrays);}var_dump(arraysSum([1,2,3], [4,5,6], [7,8,9]));# outputarray(3) { [0]=> string(1) "6" [1]=> string(2) "15" [2]=> string(2) "24"}同样有严格模式和强制模式NULL 合并运算符// 如果 $_GET['user'] 不存在返回 'nobody',否则返回 $_GET['user'] 的值$username = $_GET['user'] ?? 'nobody';// 相当于isset($_GET['user']) ? $_GET['user'] : 'nobody';// 类似于屏蔽notice错误后的:$username = $_GET['user'] ?: 'nobody';太空船操作符(组合比较符)用于比较两个表达式。当$a大于、等于或小于$b时它分别返回-1、0或1。 ...

May 1, 2019 · 2 min · jiezi

PHP FTP 上传与下载远端档案

本教学使用环境介绍伺服器端:Ubuntu 16.04资料库:Mariadb 10.1.34(Mysql)语言版本:php 7.0本机端:MacOS High Sierra可以透过 php 来产生档案,并丢到指定的 FTP 伺服器(需有存取权)本端上传到远端// 基本连线设定$ftpConn = ftp_connect(‘xx.xx.xx.xx’, 21) or die (‘cannot connect to host’);ftp_login($ftpConn, ‘account’, ‘password’) or die(‘cannot login’);// 路径设定$local_file = ‘’; // 本端的路径$ftp_path = ‘’; // 远端的路径// 开始上传$upload = ftp_put($ftpConn, $ftp_path, $local_file, FTP_ASCII);if (!$upload) { exit(‘failed’);}// 关闭连线ftp_close($ftpConn);远端下载到本端// 这块跟上面的做法一样// 开始下载$get = ftp_put($ftpConn, $local_file, $ftp_path, FTP_BINARY);// 注意 $local_file 跟 $ftp_path 摆放的位置跟上面完全相反if (!$get) { exit(‘failed’);}// 关闭连线ftp_close($ftpConn);这个相反的问题我搞了快一小时,才发现顺序错了我还去 php.net 查询老半天想说都设定对了怎么还是没办法下载下来~Line ID:ianmacQQ:1258554508

April 13, 2019 · 1 min · jiezi

PHP GD库无法压缩高解析度的照片解决方式(Allowed memory size bytes exhausted)

本教学使用环境介绍伺服器端:Ubuntu 16.04资料库:Mariadb 10.1.34(Mysql)语言版本:php 7.0本机端:MacOS High Sierra在刚刚遇到了这个问题上传普通解析的图片,例如 2000x2000 都是可以正常上传也能够使用 GD库对其解压缩只是当我上传了 6000x6000 甚至更高的图片时,一样可以普通上传但是却无法上传?查看 error_log 错误表示PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 8192 bytes)类似这样的错误讯息只要在后端表头宣告就能够解决(但很耗能,记忆体要很够…)ini_set(‘memory_limit’, ‘-1’);或是到 php.ini 修改 「memory_limit」值macos 的php.ini路径:/etc/php.iniubuntu 的php.ini路径:/etc/php/7.0/apache2取决于你使用的php版本Line ID:ianmacQQ:1258554508

April 13, 2019 · 1 min · jiezi

解决 php 无法上传档案大小更高的档案(修改 php.ini 上传限制)

本教学使用环境介绍伺服器端:Ubuntu 16.04资料库:Mariadb 10.1.34(Mysql)语言版本:php 7.0本机端:MacOS High Sierra在上传档案的时候发生了一件事就是怎么某些档案无法上传成功?而且这些档案都是特别大的超过 8M 以上?于是去 stackoverflow 找到了答案前往「php.ini」修改上传限制就行了macos 的php.ini路径:/etc/php.iniubuntu 的php.ini路径:/etc/php/7.0/apache2找到后先搜寻 upload_max_filesize单挡上传大小上限,我在这里设定为 1Gupload_max_filesize = 1000M之后再找 post_max_size此为全部的 post 档案合计的大小上限,我这里设定为 50Gpost_max_size = 50000M依照你的需求做变更也别忘了修改记忆体上限 memory_limit我在另一篇设定为 -1memory_limit = 512M记得如果有使用 mysql, mariadb会将 mysql.connect_timeout 设定为 -1 为永不断线mysql.connect_timeout = -1最后别忘了重启 apachesudo apachectl restartLine ID:ianmacQQ:1258554508

April 13, 2019 · 1 min · jiezi

透过 Crontab 排程备份 Mariadb (Mysql)使用 php

本教学使用环境介绍伺服器端:Ubuntu 18.04 LTS资料库:Mariadb 10.1.34(Mysql)语言版本:php 7.3本机端:MacOS High Sierra本教学将使用纯 php 去备份资料库并下载到目录底下$ crontab -e设定每天凌晨00:00 执行0 0 * * * php /var/www/backup.phpbackup.php 脚本记得开头一定要 「<?php」,即便你有启用缩写header(‘Content-Type: text/html; charset=utf-8’);function backup_mysql_database($options){$mtables = array(); $contents = “– Database: ".$options['db_to_backup']." –\n”;$mysqli = new mysqli($options[‘db_host’], $options[‘db_uname’], $options[‘db_password’], $options[‘db_to_backup’]);if ($mysqli->connect_error) { die(‘Error : (’. $mysqli->connect_errno .’) ‘. $mysqli->connect_error);}$mysqli->query(“SET NAMES utf8”);$mysqli->set_charset(“utf8mb4”);$results = $mysqli->query(“SHOW TABLES”);while ($row = $results->fetch_array()){ if (!in_array($row[0], $options[‘db_exclude_tables’])){ $mtables[] = $row[0]; }}foreach($mtables as $table){ $contents .= “– Table ".$table." –\n”; $results = $mysqli->query(“SHOW CREATE TABLE “.$table); while ($row = $results->fetch_array()){ $contents .= $row[1].”;\n\n”; } $results = $mysqli->query(“SELECT * FROM “.$table); $row_count = $results->num_rows; $fields = $results->fetch_fields(); $fields_count = count($fields); $insert_head = “INSERT INTO ".$table." (”; for($i=0; $i < $fields_count; $i++){ $insert_head .= “".$fields[$i]-&gt;name."”; if($i < $fields_count-1){ $insert_head .= ‘, ‘; } } $insert_head .= “)”; $insert_head .= " VALUES\n”; if($row_count>0){ $r = 0; while ($row = $results->fetch_array()){ if(($r % 400) == 0){ $contents .= $insert_head; } $contents .= “(”; for($i=0; $i < $fields_count; $i++){ $row_content = str_replace("\n”,"\n",$mysqli->real_escape_string($row[$i])); switch($fields[$i]->type){ case 8: case 3: $contents .= $row_content; break; default: $contents .= “’”. $row_content ."’"; } if($i < $fields_count-1){ $contents .= ‘, ‘; } } if(($r+1) == $row_count || ($r % 400) == 399){ $contents .= “);\n\n”; } else { $contents .= “),\n”; } $r++; } }}if (!is_dir ( $options[‘db_backup_path’] )) { mkdir ( $options[‘db_backup_path’], 0777, true ); }## 备份后的 sql 名称$backup_file_name = “dev-” . date( “Y-m-d H:i:s”).".sql";$fp = fopen($options[‘db_backup_path’] . ‘/’ . $backup_file_name ,‘w+’);if (($result = fwrite($fp, $contents))) { // echo “Backup file created ‘$backup_file_name’ ($result)”;}fclose($fp);return $backup_file_name;}## 资料库设定$options = array( ‘db_host’=> ’localhost’, ‘db_uname’ => ‘root’, // 资料库使用者帐号 ‘db_password’ => ‘password’, // 资料库密码 ‘db_to_backup’ => ‘db’, // 资料库名称 ‘db_backup_path’ => ‘/var/www/’, // 保存到哪个路径 ‘db_exclude_tables’ => array());$backup_file_name=backup_mysql_database($options);这样就可以透过 php 备份到该主机下,直接产生 sql 档了。Line ID:ianmacQQ:1258554508 ...

April 13, 2019 · 2 min · jiezi

php 将手机号码转为国际码(preg_replace + preg_quote)

本教学使用环境介绍伺服器端:Ubuntu 18.04 LTS资料库:Mariadb 10.1.34(Mysql)语言版本:php 7.3本机端:MacOS High Sierra举例:台湾本地用户互打手机号码时,是 09XX123456,当与第三方串接需要转国际号时,需要变成 +8869XX123456,此时就可以使用此功能自由转换。str_replace_national functionfunction str_replace_national($from, $to, $content) { $from = ‘/’.preg_quote($from, ‘/’).’/’; return preg_replace($from, $to, $content, 1);}使用str_replace_national(‘0’, ‘+886’, $phone);所以他只会取代第一个「0」,将它改为 +886转回来一样原理str_replace_first functionfunction str_replace_first($from, $to, $content) { $from = ‘/’.preg_quote($from, ‘/’).’/’; return preg_replace($from, $to, $content, 4);}使用str_replace_first(’+886’, ‘0’, $phone);将 +886 取代为原本的「0」Line ID:ianmacQQ:1258554508

April 13, 2019 · 1 min · jiezi

php 设定启用 php缩写(php.ini、short_open_tag)

本教学使用环境介绍伺服器端:Ubuntu 18.04 LTS资料库:Mariadb 10.1.34(Mysql)语言版本:php 7.3本机端:MacOS High Sierra启用 php 缩写能节省一些写法,虽然没差多少,只是要是脚本多了,看起来就比较好辨识例如php 脚本一开头需要加上 php<?php // do..?>开了简写后就不用了<? // do..?>以及要 echo 资料时<?php echo $data; ?>开了简写后<?=$data;?>是不是干净多了呢!可能还有其他的简写方式,但是我没有发现~只是注意如果有使用 crontab 跑 php 脚本时,该脚本的开头还是要有 <?php 不然会无法运行喔! (原因不明)知道的人跟我说一下呦!那就开始设定简写功能吧前往你目前正在使用的 php 版本,里面会有 apache2 资料夹直接 nano 进去 php.ini$ nano /etc/php/7.X/apache2/php.ini然后找到 short_open_tag 将它改为 On 就行了short_open_tag = On别忘了 reload$ service apache2 reloadLine ID:ianmacQQ:1258554508

April 13, 2019 · 1 min · jiezi

透过 Crontab 排程备份 Mariadb (Mysql)使用 php + exec + mysqldump + gzip

本教学使用环境介绍伺服器端:Ubuntu 18.04 LTS资料库:Mariadb 10.1.34(Mysql)语言版本:php 7.3本机端:MacOS High Sierra一开始原本是用 php 去备份资料库,但是发现会有一些问题,于是改成这种方式,直接透过 mysql 去备份,出来的格式也不会有什么问题。$ crontab -e设定每天凌晨00:00 执行0 0 * * * php /var/www/backup.phpbackup.php 脚本记得开头一定要 「<?php」,即便你有启用缩写<?php// 设定环境header(‘Content-Type: text/html; charset=utf-8’);date_default_timezone_set(‘Asia/Taipei’);// 设定保存的资料夹位置$dir = “/var/www/db/”;// 设定档名$filename = “dev-” . date(“Y-m-d-H-i-s”) . “.sql.gz”;// 设定资料库$db_host = “localhost”;$db_username = “root”;$db_password = “password”;$db_database = “db”;// 准备 cmd$cmd = “mysqldump -h {$db_host} -u {$db_username} –password={$db_password} {$db_database} | gzip > {$dir}{$filename}”;// 执行 cmdexec($cmd);header(“Content-type: application/octet-stream”);header(“Content-Disposition: attachment; filename="$filename"”);passthru(“cat {$dir}{$filename}”);?>输出时会经过 gzip 解压缩实验过原本 150MB 的 sql 压缩后为 28MBLine ID:ianmacQQ:1258554508 ...

April 13, 2019 · 1 min · jiezi

Carbon —— PHP 中日期 / 时间处理,你只需要这个扩展包就够了

文章转自:https://learnku.com/php/t/26998在 PHP 中使用日期和时间并不是容易或清晰的任务。我们必须处理 strtotime ,格式化问题,大量计算等等。这个漂亮的包叫做 Carbon 可以帮助在 PHP 开发中处理日期/时间变得更加简单、更语义化,从而使得我们的代码更容易阅读和维护。CarbonCarbon 是由 Brian Nesbit 开发的一个包,它扩展了 PHP 自己的 DateTime 类。它提供了一些很好的功能来处理 PHP 中的日期,特别是诸如:处理时区轻松获取当前时间将 datetime 转换成可读的内容将英语短语解析成 datetime (first day of January 2016)日期的加减 (+ 2 weeks, -6 months)处理日期的语义方法所有的这些都带来了一个非常有用的包,使得这些在 PHP 中处理时间非常容易。设置为了使用 Carbon ,你需要从 Carbon 命名空间中导入 Carbon 。幸运的是,在 Laravel 中已经包括了 Carbon ,所以不需要和 Composer 一起添加。当我们需要使用 Carbon 的时候,我们可以这样导入它:<?phpuse Carbon\Carbon;在导入之后,让我们看看我们可以用这个很棒的包做一些很酷的事情。获取特定的日期/时间// 获取当前时间 - 2015-12-19 10:10:54$current = Carbon::now();$current = new Carbon();// 获取今天 - 2015-12-19 00:00:00$today = Carbon::today();// 获取昨天 - 2015-12-18 00:00:00$yesterday = Carbon::yesterday();// 获取明天 - 2015-12-20 00:00:00$tomorrow = Carbon::tomorrow();// 解析特定字符串 - 2016-01-01 00:00:00$newYear = new Carbon(‘first day of January 2016’);// 设定一个特定的时区 - 2016-01-01 00:00:00$newYearPST = new Carbon(‘first day of January 2016’, ‘America\Pacific’);创造具有更细粒度控制的日期除了快速定义日期/时间方法之外,Carbon 也可以让我们从特定数量的参数中创建时间。Carbon::createFromDate($year, $month, $day, $tz);Carbon::createFromTime($hour, $minute, $second, $tz);Carbon::create($year, $month, $day, $hour, $minute, $second, $tz);当你以一种通常不被 Carbon 识别的格式获得某种日期或时间时,这些是非常有用的。如果你为任何一个参数传递 null 值,则它默认会使用当前日期/时间传递 。操作日期/时间抓取日期/时间并不是你在处理日期时唯一要做的事情。你经常需要操作日期或时间。例如,当为一个用户创建一个试用期时,你将希望试用期在一定时间后过期。假设我们有 30 天的试用期。我们可以用 add 和 subtract 很容易的计算出时间。在这段试用期内,我们会:// 获取当前时间$current = Carbon::now();// 添加 30 天到当前时间$trialExpires = $current->addDays(30);从 Carbon 文档 中,我们可以找到一些其他的 add() 和 sub() 方法:$dt = Carbon::create(2012, 1, 31, 0);echo $dt->toDateTimeString(); // 2012-01-31 00:00:00echo $dt->addYears(5); // 2017-01-31 00:00:00echo $dt->addYear(); // 2018-01-31 00:00:00echo $dt->subYear(); // 2017-01-31 00:00:00echo $dt->subYears(5); // 2012-01-31 00:00:00echo $dt->addMonths(60); // 2017-01-31 00:00:00echo $dt->addMonth(); // 2017-03-03 00:00:00 equivalent of $dt->month($dt->month + 1); so it wrapsecho $dt->subMonth(); // 2017-02-03 00:00:00echo $dt->subMonths(60); // 2012-02-03 00:00:00echo $dt->addDays(29); // 2012-03-03 00:00:00echo $dt->addDay(); // 2012-03-04 00:00:00echo $dt->subDay(); // 2012-03-03 00:00:00echo $dt->subDays(29); // 2012-02-03 00:00:00echo $dt->addWeekdays(4); // 2012-02-09 00:00:00echo $dt->addWeekday(); // 2012-02-10 00:00:00echo $dt->subWeekday(); // 2012-02-09 00:00:00echo $dt->subWeekdays(4); // 2012-02-03 00:00:00echo $dt->addWeeks(3); // 2012-02-24 00:00:00echo $dt->addWeek(); // 2012-03-02 00:00:00echo $dt->subWeek(); // 2012-02-24 00:00:00echo $dt->subWeeks(3); // 2012-02-03 00:00:00echo $dt->addHours(24); // 2012-02-04 00:00:00echo $dt->addHour(); // 2012-02-04 01:00:00echo $dt->subHour(); // 2012-02-04 00:00:00echo $dt->subHours(24); // 2012-02-03 00:00:00echo $dt->addMinutes(61); // 2012-02-03 01:01:00echo $dt->addMinute(); // 2012-02-03 01:02:00echo $dt->subMinute(); // 2012-02-03 01:01:00echo $dt->subMinutes(61); // 2012-02-03 00:00:00echo $dt->addSeconds(61); // 2012-02-03 00:01:01echo $dt->addSecond(); // 2012-02-03 00:01:02echo $dt->subSecond(); // 2012-02-03 00:01:01echo $dt->subSeconds(61); // 2012-02-03 00:00:00 Getters and Setters另外一种快速操作或读取时间的方法是使用可用的 getters 和 serrers 。$dt = Carbon::now();// 设置一些参数$dt->year = 2015;$dt->month = 04;$dt->day = 21;$dt->hour = 22;$dt->minute = 32;$dt->second = 5;// 获取一些参数var_dump($dt->year);var_dump($dt->month);var_dump($dt->day);var_dump($dt->hour);var_dump($dt->second);var_dump($dt->dayOfWeek);var_dump($dt->dayOfYear);var_dump($dt->weekOfMonth);var_dump($dt->daysInMonth);我们甚至还可以把一些 setter 串在一起。$dt = Carbon::now();$dt->year(1975)->month(5)->day(21)->hour(22)->minute(32)->second(5)->toDateTimeString();$dt->setDate(1975, 5, 21)->setTime(22, 32, 5)->toDateTimeString();$dt->setDateTime(1975, 5, 21, 22, 32, 5)->toDateTimeString();格式化在上面的示例中,你可能注意到了 ->toDateTimeString() 方法。我们可以方便的为达到我们的目的去进行格式化。在这种情况下,我们得到了一个日期时间字符串。$dt = Carbon::now();echo $dt->toDateString(); // 2015-12-19echo $dt->toFormattedDateString(); // Dec 19, 2015echo $dt->toTimeString(); // 10:10:16echo $dt->toDateTimeString(); // 2015-12-19 10:10:16echo $dt->toDayDateTimeString(); // Sat, Dec 19, 2015 10:10 AM// ……当然 format() 也可以这样用echo $dt->format(’l jS \of F Y h:i:s A’); // Saturday 19th of December 2015 10:10:16 AM相对时间通过 diff() 方法可以很容易的显示相对时间。例如,我们有一篇博客,并且我们想显示它是在 三小时 前发布的。可以利用这些方法。求时间差这些方法用于求两个时间的时间差。$current = Carbon::now();$dt = Carbon::now();$dt = $dt->subHours(6);echo $dt->diffInHours($current); // -6echo $current->diffInHours($dt); // 6$future = $current->addMonth();$past = $current->subMonths(2);echo $current->diffInDays($future); // 31echo $current->diffInDays($past); // -62显示人类容易阅读的时间差在过去的几年,显示相对时间变得越来越流行。在 Twitter 和 Facebook 等社交网络中经常可以看到。例如,将时间显示为 3 小时前 比显示 上午 8:12,更适合人类阅读。这些方法被用于计算时间差,并转换为人类可阅读的格式。这里有四种表达时间差的方式:将一个过去的时间和现在做比较:1 小时前5 个月前将一个未来的时间和现在做比较:1 小时后5 个月后将一个过去的时间和另一个时间做比较:1 小时前5 小时前将一个未来的时间和另一个做比较:1 小时后5 小时后$dt = Carbon::now();$past = $dt->subMonth();$future = $dt->addMonth();echo $dt->subDays(10)->diffForHumans(); // 10 天前echo $dt->diffForHumans($past); // 1 个月前echo $dt->diffForHumans($future); // 1 个月前总结Carbon 能做的远远不止这些。请务必查看 Carbon 官方文档。希望这能帮助你在 PHP 中更容易的使用日期 / 时间并加快开发效率!文章转自:https://learnku.com/php/t/26998 更多文章:https://learnku.com/laravel/c… ...

April 12, 2019 · 3 min · jiezi

怎样用 PHP 来实现枚举?

枚举在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。枚举是一个被命名的整型常数的集合,枚举在日常生活中很常见,例如表示星期的SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY就是一个枚举。—— 维基百科在上一篇文章中,我谈到了 PHP 中的类型约束,这次我们来谈实际应用。业务场景在实际开发过程中我们非常容易接触到枚举类型,但是又因为 PHP 原生对枚举的支持不是太好,所以很多时候 开发人员并没有重视枚举的使用,而是使用全局常量或者类常量代替,而这两个数据原则上还是 字符串 并不能用来做类型判断。业务????订单状态 待支付/待发货/待收货/待评价会员状态 激活/未激活….等等 ,很多时候我们都会用简单的 1/2/3/4 或者0/1 这样的方式去代表,然后在文档或者注释中规定这些东西。更高级一点儿的就是定义成常量,然后方便统一存取,但是常量的值还是是字符串,无法进行类型判断。这里就要看一下 PHP 对枚举的支持,虽然 PHP 对枚举没有完美的支持,但是在 SPL 中还是有一个基础的枚举类SPL 枚举SplEnum extends SplType {/ Constants /const NULL __default = NULL ;/ 方法 /public getConstList ([ bool $include_default = FALSE ] ) : array/ 继承的方法 /SplType::__construct ( [mixed $initial_value [, bool $strict ]] )}但是!这个需要额外的安装 PECL 用 PECL 安装 Spl_Types,无意间增加了使用成本,那有没有其他解决方案?答案是肯定的。直接手写一个。开始准备首先定一个枚举class Enum{ // 默认值 const __default = self::WAIT_PAYMENT; // 待付款 const WAIT_PAYMENT = 0; // 待发货 const WAIT_SHIP = 1; // 待收货 const WAIT_RECEIPT = 2; // 待评价 const WAIT_COMMENT = 3;}这样似乎就完成了,我们直接使用 Enum::WAIT_PAYMENT 就可以拿到里面的值了,但是传参的地方我们并没法校验他。function setStatus(Enum $status){ // TODO}setStatus(Enum::WAIT_PAYMENT);// Error 显然这是不行的 因为上面常量的值时一个 int 并不是 Enum 类型。这里我们就需要用到 PHP 面向对象中的一个魔术方法 __toString()public __toString ( void ) : string__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。现在我们来完善一下这个方法。class OrderStatus extends Enum{ // 默认值 const __default = self::WAIT_PAYMENT; // 待付款 const WAIT_PAYMENT = 0; // 待发货 const WAIT_SHIP = 1; // 待收货 const WAIT_RECEIPT = 2; // 待评价 const WAIT_COMMENT = 3; public function __toString() { return ‘233’; }}// objectecho gettype($orderStatus) . PHP_EOL;// boolean truevar_dump($orderStatus instanceof Enum);// 233echo $orderStatus;初具模型这里似乎实现了一部分,那我们应该怎么样让他做的更好?再来改造一下。class OrderStatus extends Enum{ // 默认值 const __default = self::WAIT_PAYMENT; // 待付款 const WAIT_PAYMENT = 0; // 待发货 const WAIT_SHIP = 1; // 待收货 const WAIT_RECEIPT = 2; // 待评价 const WAIT_COMMENT = 3; /** * @var string / protected $value; public function __construct($value = null) { $this->value = is_null($value) ? self::__default : $value; } public function __toString() { return (string)$this->value; }}// 1️$orderStatus = new OrderStatus(OrderStatus::WAIT_SHIP);// objectecho gettype($orderStatus) . PHP_EOL;// boolean truevar_dump($orderStatus instanceof Enum);// 1echo $orderStatus . PHP_EOL;// 2️$orderStatus = new OrderStatus();// objectecho gettype($orderStatus) . PHP_EOL;// boolean truevar_dump($orderStatus instanceof Enum);// 0echo $orderStatus;// 3️$orderStatus = new OrderStatus(‘意外的参数’);// objectecho gettype($orderStatus) . PHP_EOL;// boolean truevar_dump($orderStatus instanceof Enum);// 意外的参数echo $orderStatus;在这一次,我们加入了 构造函数 并且允许他传入一个可选的值,然后来作为 __toString 方法的输出值,这次看起来不错,功能都已经实现了,如果传入的参数否和我们的预期的话。但是 万一不符合呢?看看,第 3️ 个那里,就已经成了意外了,哪还有没有办法补救?答案当然是 有的 ,在这里我们会用到 PHP 另一个好东西 反射类 ,当然这个不是 PHP 特有的,其他语言也有。当然,除了反射,我们还会用到另外一个东西 方法重载 里面的 __callStatic 方法。更进一步public static __callStatic ( string $name , array $arguments ) : mixed在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。$name 参数是要调用的方法名称。$arguments 参数是一个枚举数组,包含着要传递给方法 $name 的参数。继续改造。class Enum{ const __default = null; /* * @var string / protected static $value; // 注意这里 将构造函数的 修饰符改成了 受保护的 即 外部无法直接 new protected function __construct($value = null) { // 很常规 self::$value = is_null($value) ? static::__default : $value; } /* * @param $name * @param $arguments * @return mixed * @throws ReflectionException / public static function __callStatic($name, $arguments) { // 实例化一个反射类 static::class 表示调用者 $reflectionClass = new ReflectionClass(static::class); // 这里我们要有一个约定, 就是类常量成员的名字必须的大写。 // 这里就是取出来调用的静态方法名对应的常量值 虽然这里有个 getValue 方法 // 但是因为其返回值不可靠 我们就依赖于他原本的隐式的 __toString 方法来帮我们输出字符串即可。 $constant = $reflectionClass->getConstant(strtoupper($name)); // 获取调用者的 构造方法 $construct = $reflectionClass->getConstructor(); // 设置成可访问 因为我们把修饰符设置成了受保护的 这里需要访问到,所以就需要设置成可访问的。 $construct->setAccessible(true); // 因为现在类已经是可以访问的了所以我们直接实例化即可,实例化之后 PHP 会自动调用 __toString 方法 使得返回预期的值。 $static = new static($constant); return $static; } public function __toString() { return (string)self::$value; }}class OrderStatus extends Enum{ // 默认值 const __default = self::WAIT_PAYMENT; // 待付款 const WAIT_PAYMENT = 0; // 待发货 const WAIT_SHIP = 1; // 待收货 const WAIT_RECEIPT = 2; // 待评价 const WAIT_COMMENT = 3;}$WAIT_SHIP = OrderStatus::WAIT_SHIP();var_dump($WAIT_SHIP . ‘’);var_dump($WAIT_SHIP instanceof Enum);到这里 一个简单的枚举类就完成了。完结那如果我们还有其他需求、比如 判断一个值是不是在枚举范围内?获取所有的枚举值?获取所有的枚举键,判断枚举键是否有效?自动格式化「因为 __toString 方法只允许返回字符串 ,但是有的时候我们强制需要整形、bool 等类型」class Enum{ const __default = null; /* * @var string / protected static $value; /* * @var ReflectionClass / protected static $reflectionClass; // 注意这里 将构造函数的 修饰符改成了 受保护的 即 外部无法直接 new protected function __construct($value = null) { // 很常规 self::$value = is_null($value) ? static::__default : $value; } /* * @param $name * @param $arguments * @return mixed / public static function __callStatic($name, $arguments) { // 实例化一个反射类 static::class 表示调用者 $reflectionClass = self::getReflectionClass(); // 这里我们要有一个约定, 就是类常量成员的名字必须的大写。 // 这里就是取出来调用的静态方法名对应的常量值 虽然这里有个 getValue 方法 // 但是因为其返回值不可靠 我们就依赖于他原本的隐式的 __toString 方法来帮我们输出字符串即可。 $constant = $reflectionClass->getConstant(strtoupper($name)); // 获取调用者的 构造方法 $construct = $reflectionClass->getConstructor(); // 设置成可访问 因为我们把修饰符设置成了受保护的 这里需要访问到,所以就需要设置成可访问的。 $construct->setAccessible(true); // 因为现在类已经是可以访问的了所以我们直接实例化即可,实例化之后 PHP 会自动调用 __toString 方法 使得返回预期的值。 $static = new static($constant); return $static; } /* * 实例化一个反射类 * @return ReflectionClass * @throws ReflectionException / protected static function getReflectionClass() { if (!self::$reflectionClass instanceof ReflectionClass) { self::$reflectionClass = new ReflectionClass(static::class); } return self::$reflectionClass; } /* * @return string / public function __toString() { return (string)self::$value; } /* * 判断一个值是否有效 即是否为枚举成员的值 * @param $val * @return bool * @throws ReflectionException / public static function isValid($val) { return in_array($val, self::toArray()); } /* * 转换枚举成员为键值对输出 * @return array * @throws ReflectionException / public static function toArray() { return self::getEnumMembers(); } /* * 获取枚举的常量成员数组 * @return array * @throws ReflectionException / public static function getEnumMembers() { return self::getReflectionClass() ->getConstants(); } /* * 获取枚举成员值数组 * @return array * @throws ReflectionException / public static function values() { return array_values(self::toArray()); } /* * 获取枚举成员键数组 * @return array * @throws ReflectionException / public static function keys() { return array_keys(self::getEnumMembers()); } /* * 判断 Key 是否有效 即存在 * @param $key * @return bool * @throws ReflectionException / public static function isKey($key) { return in_array($key, array_keys(self::getEnumMembers())); } /* * 根据 Key 去获取枚举成员值 * @param $key * @return static / public static function getKey($key) { return self::$key(); } /* * 格式枚举结果类型 * @param null|bool|int $type 当此处的值时什么类时 格式化输出的即为此类型 * @return bool|int|string|null */ public function format($type = null) { switch (true) { // 当为纯数字 或者类型处传入的为 int 值时 转为 int case ctype_digit(self::$value) || is_int($type): return (int)self::$value; break; // 当 type 传入 true 时 返回 bool 类型 case $type === true: return (bool)filter_var(self::$value, FILTER_VALIDATE_BOOLEAN); break; default: return self::$value; break; } }}class OrderStatus extends Enum{ // 默认值 const __default = self::WAIT_PAYMENT; // 待付款 const WAIT_PAYMENT = 0; // 待发货 const WAIT_SHIP = 1; // 待收货 const WAIT_RECEIPT = 2; // 待评价 const WAIT_COMMENT = 3;}$WAIT_SHIP = OrderStatus::WAIT_SHIP();// 直接输出是字符串echo $WAIT_SHIP;// 判断类型是否存在var_dump($WAIT_SHIP instanceof OrderStatus);// 格式化输出一下 是要 字符串 、还是 bool 还是整形// 自动var_dump($WAIT_SHIP->format());// 整形var_dump($WAIT_SHIP->format(1));// boolvar_dump($WAIT_SHIP->format(true));// 判断这个值是否有效的枚举值var_dump(OrderStatus::isValid(2));// 判断这个值是否有效的枚举值var_dump(OrderStatus::isValid(8));// 获取所有枚举成员的 Keyvar_dump(OrderStatus::keys());// 获取所有枚举成员的值var_dump(OrderStatus::values());// 获取枚举成员的键值对var_dump(OrderStatus::toArray());// 判断枚举 Key 是否有效var_dump(OrderStatus::isKey(‘WAIT_PAYMENT’));// 判断枚举 Key 是否有效var_dump(OrderStatus::isKey(‘WAIT_PAYMENT_TMP’));// 根据 Key 取去 值 注意 这里取出来的已经不带有类型了// 更加建议直接使用 取类常量的方式去取 或者在高版本的 直接使用类常量修饰符 // 将类常量不可见最佳,但是需要额外处理了var_dump(OrderStatus::getKey(‘WAIT_PAYMENT’) ->format(1));截至目前 一个完整的枚举就完成了~ ...

April 11, 2019 · 5 min · jiezi

[PHP 安全] OWASP 维护的 PHP 安全配置速查表

文章转自:https://learnku.com/php/t/26973介绍这个页面的目的是为了帮助那些配置 PHP 和运行它的 web 服务器的人确保它的安全性。下面你将找到有关 php.ini 文件的正确配置信息。php.ini下面的一些设置需要适应你的系统,特别是 session.save_path, session.cookie_path (例如: /var/www/mysite),和 session.cookie_domain (例如:ExampleSite.com)。你还应该运行 PHP 7.2 或者更高版本。如果你运行的版本是 PHP 7.0 和 PHP 7.1 ,你将在下面的几个地方使用略有不同的值(看内联的注释)。最后,查看 PHP 文档 以获得关于 php.ini 配置文件中每个值的参考。你可以在一个现成的 php.ini 文件中找到以下配置的副本 此处 。PHP 错误处理expose_php = Offerror_reporting = E_ALLdisplay_errors = Offdisplay_startup_errors = Offlog_errors = Onerror_log = /valid_path/PHP-logs/php_error.logignore_repeated_errors = Off请注意:你需要在生产环境中 display_errors 设置成 Off, 同时最好养成经常查看这些日志的好习惯。PHP 通用设置doc_root = /path/DocumentRoot/PHP-scripts/open_basedir = /path/DocumentRoot/PHP-scripts/include_path = /path/PHP-pear/extension_dir = /path/PHP-extensions/mime_magic.magicfile = /path/PHP-magic.mimeallow_url_fopen = Offallow_url_include = Offvariables_order = “GPCS"allow_webdav_methods = Offsession.gc_maxlifetime = 600allow_url_* 很容易发生 LFI 还有 RFI 完全漏洞。PHP上传文件处理file_uploads = Onupload_tmp_dir = /path/PHP-uploads/upload_max_filesize = 2Mmax_file_uploads = 2如果你的应用没有使用文件上传功能,或者说用户唯一的输入上传的方式是通过没有包含文档附件的表单提交, file_uploads 应当被设置成 Off。PHP 可执行处理enable_dl = Offdisable_functions = system, exec, shell_exec, passthru, phpinfo, show_source, highlight_file, popen, proc_open, fopen_with_path, dbmopen, dbase_open, putenv, move_uploaded_file, chdir, mkdir, rmdir, chmod, rename, filepro, filepro_rowcount, filepro_retrieve, posix_mkfifo# 请查看:http://ir.php.net/features.safe-modedisable_classes = 以上是PHP中存在危险的方法和类.。你应当禁用其中不会使用到的方法和类。PHP session 处理Session 设置中有一些需要重点关注的值, 将 session.name 改成新的是个很好的练习. session.save_path = /path/PHP-session/ session.name = myPHPSESSID session.auto_start = Off session.use_trans_sid = 0 session.cookie_domain = full.qualified.domain.name #session.cookie_path = /application/path/ session.use_strict_mode = 1 session.use_cookies = 1 session.use_only_cookies = 1 session.cookie_lifetime = 14400 # 4小时 session.cookie_secure = 1 session.cookie_httponly = 1 session.cookie_samesite = Strict session.cache_expire = 30 session.sid_length = 256 session.sid_bits_per_character = 6 # PHP 7.2+ session.hash_function = 1 # PHP 7.0-7.1 session.hash_bits_per_character = 6 # PHP 7.0-7.1更多的安全隐患的检查session.referer_check = /application/pathmemory_limit = 50Mpost_max_size = 20Mmax_execution_time = 60report_memleaks = Ontrack_errors = Offhtml_errors = Off文章转自:https://learnku.com/php/t/26973 更多文章:https://learnku.com/laravel/c… ...

April 10, 2019 · 1 min · jiezi

谈谈 PHP 中的类型约束

起点众所周知,PHP 是所类型语言,与其他强类型语言项目,在这方面会有很多的坑,但是已经发展到 PHP 7 之后,PHP 也对类型约束有了所指,并且在许多流行框架中被大量使用比如Laravel,因为这确确实实在软件开发过程中无论是运行,还是 IDE 的代码提示都能为我们带来极大的便利,下面就一步步来看看 PHP 中的类型约束。早期的约束虽然 PHP 是隐式转换,但是在实际开发中也会存在一些无法转换的窘境,当然这些问题我们在开发阶段很容易发现,但是如果是一些动态的内容导致不可控就会呈现在用户面前,也就是 BUG ,在 PHP 中有一批以 is_* 开头的方法用来做一些简单类型判断(这其中一些方法也是新方法没有翻译的基本都是)。is_array — 检测变量是否是数组is_bool — 检测变量是否是布尔型is_callable — 检测参数是否为合法的可调用结构is_countable — Verify that the contents of a variable is a countable valueis_double — is_float 的别名is_float — 检测变量是否是浮点型is_int — 检测变量是否是整数is_integer — is_int 的别名is_iterable — Verify that the contents of a variable is an iterable valueis_long — is_int 的别名is_null — 检测变量是否为 NULLis_numeric — 检测变量是否为数字或数字字符串is_object — 检测变量是否是一个对象is_real — is_float 的别名is_resource — 检测变量是否为资源类型is_scalar — 检测变量是否是一个标量is_string — 检测变量是否是字符串is_a — 如果对象属于该类或该类是此对象的父类则返回 TRUEis_subclass_of — 如果此对象是该类的子类,则返回 TRUE在 PHP 5 之前,如果我们要做类型约束,那么就必须用到这些,这些方法对参数进行复杂的判断,并处理错误返回给调用者。但是在 PHP 5 以来,在面向对象中,为方法带来了类型约束,然而这些都非常的鸡肋,从文档上可以看到。PHP 5 支持 对象、接口、PHP 5.1 支持 数组PHP 5.4 支持匿名函数类型约束不能用于标量类型如 int 或 string。Traits 也不允许。在 PHP 5 中其实光是第一条,就够大部分场景使用,但是也有一些知名问题,比如最后一条的 不支持标量类型 ,也就是说支持不是很全面,而且还有一种情况没有考虑 那就 null 虽然 null 是一个特殊类型,但是有时候当数据不可控时也会出现,而且,在 PHP 5 阶段,类型约束并没有被很好的使用,或许是那个时候并不是那么的重视,毕竟弱类型是 PHP 的一大特点,但也是致命伤,甚至很多时候被强类型语言牵着鼻子走。PHP 7PHP 7 相对于先前的PHP版本可谓是焕然一新。错误与异常的处理标量类型声明返回值类型声明可为空(Nullable)类型 (PHP 7.1)Void 函数比较扎眼的就是完善了对类型限制的支持,补上了之前的短缺,包括标量类型、返回值类型,而且,在 PHP 7.1 中还加入了严格类型验证。强制类型验证strict_types/declare()指令默认情况下,所有的PHP文件都处于弱类型校验模式。新的declare指令,通过指定strict_types的值(1或者0),1表示严格类型校验模式,作用于函数调用和返回语句;0表示弱类型校验模式。declare(strict_types=1)必须是文件的第一个语句。如果这个语句出现在文件的其他地方,将会产生一个编译错误,块模式是被明确禁止的。类似于encoding指令,但不同于ticks指令,strict_types指令只影响指定使用的文件,不会影响被它包含(通过include等方式)进来的其他文件。该指令在运行时编译,不能修改。它的运作方式,是在opcode中设置一个标志位,让函数调用和返回类型检查符合类型约束。举个????// 非严格模式// 1️function testInt():int{ return 0.01;}// 2️function testStr():string{ return true;}// 3️function testBool():bool{ return “1”;}// 4️function testInt2():int{ return “1string”;}如你所见,上面的代码 通通都没有问题,都不会出现异常,甚至在部分 PHP 7.2 以下的版本中,4️都是可以通过的。这是因为 PHP 7 虽然有了严格类型验证,但是默认情况下并没有启用,而是需要手动去启用,如果手动设置启用了之后,返回或者传递的参数不符合声明的类型,那么 PHP 会直接抛出一个 TypeError 错误,要求你去处理。启用强制类型验证 只需要在 PHP 文件的顶部加入以下代码即可。declare(strict_types=1);后话类型验证不但有利于我们的程序在运行过程中所得到和返回的参数都是完全符合预期的并且还有另一个好处,那就是开发工具中的类型提示。有时候可能会到一个情况,某个方法传递了一个参数为对象,里面有一些方法,但是 IDE 就是不提示。interface UserInfo{ getSex();}interface User{ getUserInfo(); getUserId(); getUserName();}function getUserSex($user){ // 你会发现 在这里 IDE 并不能很好的给你提示代码,和一些可以用的方法 return $user->getUserInfo()->getSex();}class VipUser importants User(){ // TODO …..}getUserSex(new VipUser());这种情况下就 2 个解决方案了,如果你是项目,因为自 PHP 5 开始就支持对象的类型声明了,所以这里就不是那么担心,直接声明类型就好了。function getUserSex(User $user){ // 这里就可以提示了 return $user->getUserInfo()->getSex();}当然 还有方法就是使用 PHPDoc,即注解方案,这个方案已经在 PSR-5 中,虽然还没有完全通过,但是在 早期也有 PHPDoc 的一些 unofficial 的,而且主流 IDE 已经完全实现了,来协助我们提高开发体验。最后总结一下,PHP 中接近完善的 类型约束,让我们之前的一些不可能变成了可能,让一些不可靠变的更加的可靠,降低了代码中一些因为类型约束而导致的问题,从源头提升了在开发工具中的开发体验 。参考资料PHP7类型提示:作为PHP开发者应该永远铭记 ...

April 9, 2019 · 1 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

php 使用Curl传递json资料给对方及显示对方回传的json(Json格式/ API串接/ HttpRequest)

本教学使用环境介绍伺服器端:Ubuntu 18.04 LTS资料库:Mariadb 10.1.34(Mysql)语言版本:php 7.3本机端:MacOS High Sierrafunction httpRequest($api, $data_string) { $ch = curl_init($api); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, “POST”); curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, array( ‘Content-Type: application/json’, ‘Content-Length: ’ . strlen($data_string)) ); $result = curl_exec($ch); curl_close($ch); return json_decode($result);}将以下资料变成json格式传输传给对方接应的 <https-api-url>$data = array( “id” => $id, “field” => $field);$data = httpRequest(’<https-api-url>’, json_encode($data));要印出对方回的 json key and value 内容时echo $data->{‘message’};如果对方回的是json array,使用foreach接应即可就能够印出回圈,对方回传多少笔就印多少笔foreach ($data as $value) { echo $value[‘message’];}可以使用sizeof查看object的长度,轻松做判断echo sizeof($data); // int如果对方回的不是json只是直接传 body 过来将上面的function中的return json_decode($result);改为return $result;然后直接印出即可echo $data;Line ID:ianmacQQ:1258554508 ...

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

RabbitMQ二三事

RabbitMQ概览RabbitMQ是一个高性能的分布式消息中间件。它由Erlang编写,这种语言天生支持分布式,而且性能极高(但是比较难上手)。通信概念RabbitMQ简单理解就是一个队列服务,我们的生产者不断地往它投递消息,而消费者不断地从它那里获取消息。但相较于利用redis的List这类简单队列,RabbitMQ的消息投递更灵活一点。首先需要知道一些RabbitMQ中的通信概念:Exchange(交换器)Queue(队列)Producer(生产者)Consumer(消费者)RabbitMQ中Exchange类似于一个路由器,我们的Consumer并不会把消息直接投递给队列,而是投递给Exchange,Exchange根据我们投递时的路由键(routing key)再发送到特定的队列。这样的设计让消息可以灵活选路,发送到某一类的队列中,形成一对多的关系,而不仅仅是一对一。Exchange所以说RabbitMQ中的Exchange很方便,很强大,它有这样几种类型:directfanouttopicheaders(几乎用不到)direct交换器很简单,有时候我们仅仅需要一个很简单的队列(消息投递到其中,然后不断消费它),这时候我们就可以用direct交换器,它的规则是:如果路由键匹配,消息就会被投递到对应的队列。fanout交换器忽略路由键,把消息同时发到一批队列。topic则就是根据不同路由键,把消息发送到某一类队列中。

March 28, 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

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

Laravel 测试: PHPUnit 入门教程

介绍 PHPUnit 测试的基础知识,使用基本的 PHPUnit 断言和 Laravel 测试助手。介绍PHPUnit 是最古老和最著名的 PHP 单元测试包之一。它主要用于单元测试,这意味着可以用尽可能小的组件测试代码,但是它也非常灵活,可以用于很多不仅仅是单元测试。PHPUnit 包含许多简单和灵活的断言允许您轻松地测试代码,当您测试特定的组件时,这些断言非常有效。但是,它确实意味着测试更高级的代码(如控制器和表单提交验证)可能会复杂得多。为了帮助开发人员更容易地进行开发, Laravel 框架 包含了一系列 应用程序测试帮助程序 ,允许您编写非常简单的 PHPUnit 测试来测试应用程序的复杂部分。本教程的目的是向您介绍 PHPUnit 测试的基础知识,使用默认 PHPUnit 断言和 Laravel 测试助手。这样做的目的是在本教程结束时,您可以自信地为应用程序编写基本测试。前提本教程假设您已经熟悉 Laravel 并知道如何在应用程序目录中运行命令(例如 php artisan 命令)。我们将创建几个基本的示例类来学习不同的测试工具如何工作,因此建议您为本教程创建一个新的应用程序。如果已经安装了 Laravel ,则可以通过运行以下命令创建新的测试应用程序:laravel new phpunit-tests或者,您可以直接使用 Composer 创建新应用程序:composer create-project laravel/laravel –prefer-dist其他安装方法也可以在 Laravel 文档中找到。创建一个新的测试使用 PHPUnit 的第一步是创建一个新的测试类。测试类的约定是它们存储在应用程序目录的 ./tests/ 下。在这个文件夹中,每个测试类都被命名为 <name>Test.php 。这种格式允许 PHPUnit 查找每个测试类—它将忽略任何不以 Test.php 结尾的文件。在新的 Laravel 应用程序中,你会注意到 ./tests/ 目录中有两个文件: ExampleTest.php 和 TestCase.php. TestCase.php 文件是一个引导文件用于在我们的测试中设置 Laravel 环境。这允许我们在测试中使用 Laravel Facades 并为测试助手提供框架,我们将在稍后介绍。 ExampleTest.php 是一个示例测试类,其中包含使用应用程序测试助手的基本测试用例-暂时忽略它。要创建一个新的测试类,我们可以手动创建一个新文件,或者运行由 Laravel 提供的 Artisan 命令 make:test 为了创建一个名为 BasicTest 的测试类,我们只需要运行这个 artisan 命令:php artisan make:test BasicTestLaravel 将创建一个如下所示的基本测试类:<?phpclass BasicTest extends TestCase{ /** * 一个基本的测试示例。 * * @return void / public function testExample() { $this->assertTrue(true); }}这里要注意的最重要的事情是 test 方法名称上的前缀,与 Test 类名后缀一样,这样 test 前缀告诉 PHPUnit 在测试时运行哪些方法。如果您忘记了 test 前缀,那么 PHPUnit 将忽略该方法。在我们第一次运行测试套件之前,有必要指出 Laravel 提供的默认 phpunit.xml 文件。 PHPUnit 在运行时会自动在当前目录中查找名为 phpunit.xml 或者 phpunit.xml.dist 的文件。您可以在此处配置测试的特定选项。这个文件中有很多信息,但是现在最重要的部分是在 testsuite 目录定义:<?xml version=“1.0” encoding=“UTF-8”?><phpunit … > <testsuites> <testsuite name=“Application Test Suite”> <directory>./tests/</directory> </testsuite> </testsuites> …</phpunit>这将告诉 PHPUnit 运行时在 ./tests/ 目录中找到的测试,正如我们之前所知,这是存储测试的约定。现在我们已经创建了一个基本测试,并且知道了 PHPUnit 配置,现在是第一次运行测试的时候了。您可以通过运行以下 phpunit 命令来运行测试:./vendor/bin/phpunit您应该看到与此类似的输出:PHPUnit 4.8.19 by Sebastian Bergmann and contributors…Time: 103 ms, Memory: 12.75MbOK (2 tests, 3 assertions)现在我们已经有了一个有效的 PHPUnit 设置,现在是时候开始编写一个基本测试了。注意,它会统计2个测试和3个断言,因为 ExampleTest.php 文件包含了一个带有两个断言的测试。我们的新基本测试包括一个单独的断言,该断言已通过。写一个基础测试为了帮助 PHPUnit 提供的基本断言,我们将首先创建一个提供一些简单功能的基本类在 ./app/ 目录中创建一个名为 Box.php 的新文件,并复制此示例类:<?phpnamespace App;class Box{ /* * @var array / protected $items = []; /* * 使用给定项构造框 * * @param array $items / public function __construct($items = []) { $this->items = $items; } /* * 检查指定的项目是否在框中。 * * @param string $item * @return bool / public function has($item) { return in_array($item, $this->items); } /* * 从框中移除项,如果框为空,则为 null 。 * * @return string / public function takeOne() { return array_shift($this->items); } /* * 从包含指定字母开头的框中检索所有项目。 * * @param string $letter * @return array / public function startsWith($letter) { return array_filter($this->items, function ($item) use ($letter) { return stripos($item, $letter) === 0; }); }}接下来, 打开你的 ./tests/BasicTest.php 类(我们之前创建的类),并删除默认创建的 testExample 方法, 你应该留一个空类。我们现在将使用七个基本的 PHPUnit 断言来为我们的 Box 类编写测试。这些断言是:assertTrue()assertFalse()assertEquals()assertNull()assertContains()assertCount()assertEmpty()assertTrue() 和 assertFalse()assertTrue() 和 assertFalse() 允许你声明一个值等于 true 或 false 。这意味着它们非常适合测试返回布尔值的方法。在我们的 Box 类中,我们有一个名为 has($item) 的方法,当指定的项在 box 中或不在 box 中时,该方法返回对应返回 true 或 false .要在 PHPUnit 中为此编写测试,我们可以执行以下操作:<?phpuse App\Box;class BasicTest extends TestCase{ public function testHasItemInBox() { $box = new Box([‘cat’, ’toy’, ’torch’]); $this->assertTrue($box->has(’toy’)); $this->assertFalse($box->has(‘ball’)); }}注意我们如何只将一个参数传递给 assertTrue() 和 assertFalse() 方法,并且它是 has($item) 方法的输入.如果您现在运行 ./vendor/bin/phpunit 命令,您会注意到输出包括:OK (2 tests, 4 assertions)这意味着我们的测试已经通过。如果您将 assertFalse() 替换成 assertTrue() 并运行 phpunit 命令,输出将如下所示:PHPUnit 4.8.19 by Sebastian Bergmann and contributors.F.Time: 93 ms, Memory: 13.00MbThere was 1 failure:1) BasicTest::testHasItemInBoxFailed asserting that false is true../tests/BasicTest.php:12FAILURES!Tests: 2, Assertions: 4, Failures: 1.这告诉我们第12行的断言未能断言 false 值是 true - 因为我们将 assertFalse() 替换为 assertTrue() 。将其交换回来,然后重新运行 PHPUnit 。测试应该再次通过,因为我们已经修复了破损的测试。assertEquals() 与 assertNull()接下来,让我们看看 assertEquals(), 以及 assertNull()。assertEquals() 用于比较变量实际值与预期值是否相等。我们用它来检查 takeOne() 方法的返回值是否为 Box 内的当前值。当 Box 为空时,takeOne() 将返回 null,我们亦可使用 assertNull() 来进行检查。与 assertTrue()、assertFalse() 以及 assertNull() 不同,assertEquals() 需要两个参数。第一个参数为 预期 值,第二个参数则为 实际 值。可参照如下代码实现以上断言(assertions):<?phpuse App\Box;class BasicTest extends TestCase{ public function testHasItemInBox() { $box = new Box([‘cat’, ’toy’, ’torch’]); $this->assertTrue($box->has(’toy’)); $this->assertFalse($box->has(‘ball’)); } public function testTakeOneFromTheBox() { $box = new Box([’torch’]); $this->assertEquals(’torch’, $box->takeOne()); // 当前 Box 为空,应当为 Null $this->assertNull($box->takeOne()); }}运行 phpunit 命令,你应当看到如下输出:OK (3 tests, 6 assertions)assertContains() 和 assertCount() 以及 assertEmpty()终于,我们有三个作用于数组有关的断言,我们能够使用它们去检查 Box 类中的 startsWith($item) 方法。 assertContains() 断言传递进来的数组中包含指定值, assertCount() 断言数组的项数为指定数量,assertEmpty() 断言传递进来的数组为空。让我们来执行以下测试:<?phpuse App\Box;class BasicTest extends TestCase{ public function testHasItemInBox() { $box = new Box([‘cat’, ’toy’, ’torch’]); $this->assertTrue($box->has(’toy’)); $this->assertFalse($box->has(‘ball’)); } public function testTakeOneFromTheBox() { $box = new Box([’torch’]); $this->assertEquals(’torch’, $box->takeOne()); // Null,现在这个 box 是空的。 $this->assertNull($box->takeOne()); } public function testStartsWithALetter() { $box = new Box([’toy’, ’torch’, ‘ball’, ‘cat’, ’tissue’]); $results = $box->startsWith(’t’); $this->assertCount(3, $results); $this->assertContains(’toy’, $results); $this->assertContains(’torch’, $results); $this->assertContains(’tissue’, $results); // 如果传递复数断言数组为空 $this->assertEmpty($box->startsWith(’s’)); }}保存并再一次运行你的测试:OK (4 tests, 9 assertions)恭喜你,你刚刚使用七个基础的 PHPUnit 断言完成了对 Box 类的全部测试。通过这些简单的断言你能够做许多事,对于其他断言,大多数要更复杂,不过它们仍遵循以上使用规则。测试你的程序在你的程序里,对每个组件进行单元测试在很多情况下都是有必要的,而且也应该成为你开发过程中必不可少的一部分,但这并不是你需要做的全部的测试。当你构建一个包含复杂视图、导航和表单的程序时,你同样想测试这些组件。这时,Laravel的测试助手可以使这些测试像单元测试简单组件一样容易。我们之前查看在 ./tests/ 目录下的默认文件时跳过了 ./tests/ExampleTest.php 文件。 现在打开它,内容如下所示:<?phpclass ExampleTest extends TestCase{ /** 一个基本功能测试示例。** @return void*/ public function testBasicExample() { $this->visit(’/’) ->see(‘Laravel 5’); }}我们可以看到这个测试示例非常简单。在不知道测试助手如何运作的情况下,我们可以猜测它的意思如下:当我访问/ (根目录)我应该看到 ‘Laravel 5’如果你打开你的web浏览器,访问我们的程序(如果你没有启动你的web服务器,你可以运行 php artisan serve ),你应该可以在web根目录上看到屏幕上有“Laravel 5”的文本。 鉴于这个测试已经通过了PHPUnit,我们可以很确定地说我们对这个测试示例改造是正确的。这个测试确保了访问/路径,网页可以返回“‘Laravel 5”的文本。一个如此简单的检查也许不代表什么,但如果你的网站上要显示关键信息,它就可以在一个别处的改动导致这个页面无法正常显示正确的信息时,防止你部署一个被损坏的程序。visit()、see() 以及 dontSee()现在尝试编写自己的测试,更进一步理解它吧。首先,编辑 ./app/Http/routes.php ,增加一个新的路由。为了教程目的,我们创建希腊字母定义的路由:<?phpRoute::get(’/’,function () { return view(‘welcome’);});Route::get(’/alpha’,function () { return view(‘alpha’);});然后,创建视图文件 ./resources/views/alpha.blade.php,使用 Alpha 作为关键字,保存基本的HTML文件:<!DOCTYPE html><html> <head> <title>Alpha</title> </head> <body> <p>This is the Alpha page.</p> </body></html>打开浏览器,输入网址: http://localhost:8000/beta,页面会显示出 “This is the Alpha page.” 的内容。现在我们有了测试用到的模版文件,下一步,我们通过运行命令 make:test 来创建一个新的测试文件:php artisan make:test AlphaTest然后变成刚创建好的测试文件,按照框架提供的例子,测试 “alpha” 页面上没有包含 “beta” 。 我们可以使用方法 dontSee() ,它是 see() 的对应的反向方法。下面代码是上面实现的简单例子:<?phpclass AlphaTest extends TestCase{ public function testDisplaysAlpha() { $this->visit(’/alpha’) ->see(‘Alpha’) ->dontSee(‘Beta’); }}保存并运行 PHPUnit (./vendor/bin/phpunit),测试代码应该会全部通过,你会看到像这样的测试状态内容显示:OK (5 tests,12 assertions)开发前先写测试对于测试来说,测试驱动开发 (TDD) 是非常酷的方法,首先我们先写测试。写完测试并执行它们,你会发现测试没通过,接下来 我们编写满足测试的代码,再次执行测试,使测试通过。 接下来让我们开始。首先,建立一个 BetaTest 类使用 make:test artisan 命令:php artisan make:test BetaTest接下来,更新测试用例以便检查 /beta 的路由 route 为「Beta」:<?phpclass BetaTest extends TestCase{ public function testDisplaysBeta() { $this->visit(’/beta’) ->see(‘Beta’) ->dontSee(‘Alpha’); }}现在使用 ./vendor/bin/phpunit 命令来执行测试。结果是一个看起来简洁但不好的错误信息,如下:> ./vendor/bin/phpunitPHPUnit 4.8.19 by Sebastian Bergmann and contributors…..F.Time: 144 ms, Memory: 14.25MbThere was 1 failure:1) BetaTest::testDisplaysBeta一个对 [http://localhost/beta] 的请求失败了。收到状态码 [404]。…FAILURES!Tests: 6, Assertions: 13, Failures: 1.我们现在需要创建这个不存在的路由。让我们开始。首先,编辑 ./app/Http/routes.php 文件来创建新的 /beta 路由:<?phpRoute::get(’/’, function () { return view(‘welcome’);});Route::get(’/alpha’, function () { return view(‘alpha’);});Route::get(’/beta’, function () { return view(‘beta’);});接下来,在 ./resources/views/beta.blade.php 下创建如下视图模版:<!DOCTYPE html><html> <head> <title>Beta</title> </head> <body> <p>This is the Beta page.</p> </body></html>现在再一次执行 PHPUnit,结果应该再一次回到绿色。> ./vendor/bin/phpunitPHPUnit 4.8.19 by Sebastian Bergmann and contributors…….Time: 142 ms, Memory: 14.00MbOK (6 tests, 15 assertions)这样我们就通过在完成新的页面之前写测试的方式,对 测试驱动开发 进行了实践。click() 和 seePageIs()Laravel 也提供一个辅助函数 (click()) 允许测试点击页面中存在的连接 ,以及一个方法 (seePageIs()) 检查点击展示的结果页面。让我们使用这两个辅助函数去执行在 Alpha 和 Beta 页面的链接。首先,我们更新我们的测试。打开 AlphaTest 类,我们将添加一个新的测试方法,这将点击 「alpha」页面上的「Next」链接跳转到 「beta」页面。新的测试代码如下:<?phpclass AlphaTest extends TestCase{ public function testDisplaysAlpha() { $this->visit(’/alpha’) ->see(‘Alpha’) ->dontSee(‘Beta’); } public function testClickNextForBeta() { $this->visit(’/alpha’) ->click(‘Next’) ->seePageIs(’/beta’); }}注意到,在我们新建的 testClickNextForBeta() 方法中,我们并没有检查每一个页面的内容。 其他测试都成功的检查了两个页面的内容,所以这里我们只关心点击 「Next」链接将发送到 /beta。你现在可以运行测试组件了,但就像预料的一样测试将不通过,因为我们还没有更新我们的 HTML。接下来,我们将更新 BetaTest 来做类似的事情:<?phpclass BetaTest extends TestCase{ public function testDisplaysBeta() { $this->visit(’/beta’) ->see(‘Beta’) ->dontSee(‘Alpha’); } public function testClickNextForAlpha() { $this->visit(’/beta’) ->click(‘Previous’) ->seePageIs(’/alpha’); }}接下来,我们更新我们的 HTML 模版。./resources/views/alpha.blade.php:<!DOCTYPE html><html> <head> <title>Alpha</title> </head> <body> <p>This is the Alpha page.</p> <p><a href="/beta">Next</a></p> </body></html>./resources/views/beta.blade.php:<!DOCTYPE html><html> <head> <title>Beta</title> </head> <body> <p>This is the Beta page.</p> <p><a href="/alpha">Previous</a></p> </body></html>保存文件,再一次执行 PHPUnit:> ./vendor/bin/phpunitPHPUnit 4.8.19 by Sebastian Bergmann and contributors.F….F..Time: 175 ms, Memory: 14.00MbThere were 2 failures:1) AlphaTest::testDisplaysAlphaFailed asserting that ‘<!DOCTYPE html><html> <head> <title>Alpha</title> </head> <body> <p>This is the Alpha page.</p> <p><a href="/beta">Next</a></p> </body></html>’ does not match PCRE pattern “/Beta/i”.2) BetaTest::testDisplaysBetaFailed asserting that ‘<!DOCTYPE html><html> <head> <title>Beta</title> </head> <body> <p>This is the Beta page.</p> <p><a href="/alpha">Previous</a></p> </body></html>’ does not match PCRE pattern “/Alpha/i”.FAILURES!Tests: 8, Assertions: 23, Failures: 2.然而测试失败了。如果你仔细观察我们的新 HTML,你将注意到我们分别有术语 beta 和 alpha 在 /alpha 和 /beta 页面。这意味着我们需要稍微更改我们的测试让它们与误报不匹配。在每一个 AlphaTest 和 BetaTest 类,更新 testDisplays* 方法去使用 dontSee(’<page> page’)。通过这种方式,这将仅仅匹配字符串而不是那个术语。两个测试文件如下所示:./tests/AlphaTest.php:<?phpclass AlphaTest extends TestCase{ public function testDisplaysAlpha() { $this->visit(’/alpha’) ->see(‘Alpha’) ->dontSee(‘Beta page’); } public function testClickNextForBeta() { $this->visit(’/alpha’) ->click(‘Next’) ->seePageIs(’/beta’); }}./tests/BetaTest.php:<?phpclass BetaTest extends TestCase{ public function testDisplaysBeta() { $this->visit(’/beta’) ->see(‘Beta’) ->dontSee(‘Alpha page’); } public function testClickNextForAlpha() { $this->visit(’/beta’) ->click(‘Previous’) ->seePageIs(’/alpha’); }}再一次运行你的测试,所有的测试都应该通过了。我们现在已经测试我们所有的新文件,包括页面中的 Next/Previous 链接。通过 Semaphore 对 PHPUnit 持续集成通过 Semaphore设置 持续集成你可以自动执行你的测试。这样每一次你进行 git push 提交代码的时候都会执行你的测试,并且 Semaphore 预装了所有最新的 PHP 版本。如果你还没有一个 Semaphore 账户, 先去 注册一个免费的 Semaphore 账户 。接下来需要做的是将它 添加到你的项目,并按照提示逐步去做来执行你的测试:composer install –prefer-sourcephpunit关于 PHP 持续集成 的更多信息,请参照 Semaphore 文档。结语你应该注意到本教程中的所有测试都有一个共同的主题:它们都非常简单。 这是学习如何使用基本的测试断言和辅助函数,并且尽可能的使用它们的好处之一。编写测试越简单,测试就越容易理解和维护。掌握了本教程中介绍的 PHPUnit 断言之后,你还可以去 PHPUnit 文档 找到更多内容。 所有的断言都遵循基本的模式,但你会发现,在大多数测试中都会返回基本的断言。对于 PHPUnit 断言来说,Laravel 的测试辅助函数是极好的补充,这让应用程序的测试变的非常容易。也就是说,重要的是要认识到,对于我们写测试,我们只检查关键信息,而不是整个页面。这使得测试变得简单,并允许页面内容随着应用程序的变化而变化。如果关键信息仍然存在,测试仍然通过,每个人都会满意。文章转自: https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

March 1, 2019 · 5 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

lar-trace为服务之间调用提供链路追踪

https://github.com/laravelclo...Laravel Version CompatibilityLaravel 5.x.x is supported in the most recent version (composer require laravelcloud/lar-trace)Installation安装 Laravel 5.x安装laravelcloud/lar-trace包:$ composer require laravelcloud/lar-trace在 config/app.php 中做如下配置’providers’ => array( /* * Package Service Providers… */ LaravelCloud\Trace\TraceLaravel\TracingServiceProvider::class,)创建Trace的配置文件(config/trace.php)$ php artisan vendor:publish –provider=“LaravelCloud\Trace\TraceLaravel\TracingServiceProvider"添加变量至.envTRACE_ENABLED=1TRACE_ENDPOINT_URL=http://127.0.0.1:9411/api/v2/spansTRACE_RATE=1.0TRACE_SERVICE_NAME=lar-examplesTRACE_SQL_BINDINGS=falseLumen 5.x…链路追踪系统阿里云-链路追踪zipkinContributingDependencies are managed through composer:$ composer installTests can then be run via phpunit:$ vendor/bin/phpunitCommunityBug TrackerCode

January 16, 2019 · 1 min · jiezi

PHP 安全:如何防范用户上传 PHP 可执行文件

每个专业的 PHP 开发者都知道用户上传的文件都是极其危险的。不论是后端和前端的黑客都可以利用它们搞事情。大约在一个月前,我在 reddit 上看了一篇 PHP 上传漏洞检测 ,因此, 我决定写一篇文章。用户 darpernter 问了一个棘手的问题:尽管我将其重命名为 ‘helloworld.txt’, 攻击者是否仍然能够运行他的php 脚本?置顶的答复是:如果文件后缀修改为 .txt ,那么它不会被当做php文件执行,这样你安心了吧,不过再三确保不是 .php.txt 的后缀上传。不好意思,问题的正确答案并非如此 . 虽然上面的答复并非全部错误,但显然不全面。让人惊讶的是,大多数的答案都非常相似。我想解释清楚这个问题。所以我要讨论的东西变得有点大,我决定让它变得更大。问题人们允许用户上传文件,但是担心用户上传的文件在服务器上被执行。从 php 文件如何被执行开始看。假设一个有 php 环境的服务器,那么它通常有两种方法在外部执行 php 文件。一是直接用 URL 请求文件,像 http://example.com/somefile.php 。第二种是 php 现在常用的,将所有请求转发到 index.php ,并在这个文件中以某种方式引入其他文件。所以,从 php 文件中运行代码有两种方式:执行文件或用 include/include_once/require/require_once 的方法引入其他需要运行的文件。其实还有第三种方法:eval() 函数。它能将传入的字符串当做 php 代码执行。这个函数在大多数 CMS 系统中被用来执行存储在数据库里的代码。eval() 函数非常危险,但如果你用了它,通常就意味着你确认自己在做危险的操作,并确认你已经没有其他选择。实际上, eval() 有它的用途,并且在某些情况下非常有用。但如果你是新手的话,我不推荐你使用它。请看 这篇在 OWASP 的文章。我在上面写了很多。所以,有两种方法执行文件里的代码:直接执行或者在被执行的文件中引入它。那么如何避免这种事情发生呢?解决方法?我们怎样才能知道一个文件包含 php 代码呢?看拓展名,如果以 .php 结尾的,像 somefile.php 我们就认为它里面有 php 代码。如果在网站根目录下有一个 somefile.php 文件,那么在浏览器访问 http://example.com/somefile.php ,这个文件就会被执行并且输出内容到浏览器上。但是如果我重命名这个文件会怎样?如果我把它重命名为 somefile.txt 或者是 somefile.jpg 呢?我会得到什么?我会得到它的内容。它不会被执行。它会从硬盘(或者缓存)直接被发送过来。在这点上 reddit 社区上的答案是对的。重命名能防止一个文件被非预期的执行,那么为什么我认为这种解决方法是错的呢?我相信你注意到我在 “解决方法” 后面加的问号。这个问号是有意义的。现在大多数网站的 URL 上几乎看不到单独的 php 文件。并且就算有,也是人为故意伪造的,因为 URL 上需要有 .php 来实现对老版本 URL 的向后兼容。现在绝大部分 php 代码是在运行中被引入的,因为所有请求都被发送到了网站根目录的 index.php。这个文件会根据特定的规则引入其他 php 文件。这种规则可能(或者在将来会)被恶意使用。如果你应用的规则允许引入用户的文件,那么应用会容易遭到攻击,你应该立即采取措施防止用户的文件被执行。如何防止引入用户上传的文件?重命名文件名可以吗? — 不,办不到!PHP解析器不关心文件的后缀名。事实上,所有程序都不关心。双击文件,文件会被对应的程序打开。文件后缀名只是帮助操作系统识别用什么程序打开文件。只要程序有读取文件的能力,程序就可以打开任何文件。有时程序拒绝打开和操作文件。但那并不是因为后缀名,是文件内容所致。服务器通常被设置成执行 .php 文件并将执行结果回复输出。如果你请求图片 .jpg — 将从磁盘上原样的返回。如果你要求服务器以某种方式运行一张 jpeg 图片,会发生?服务器会执行还是不呢? 图片来源: Echo / Cultura / Getty Images程序不关心文件名。甚至不关心文件是否有名字,也不关心它究竟是不是文件。从文件执行PHP代码需要什么?有至少两个情况可以让PHP执行代码:代码介于 <?php 和 ?> 标记之间代码介于 <?= 和 ?> 标记之间即使文件中填充了一些奇怪的二进制数据或一些奇怪的保护名称,该标记中的代码仍然会被执行。这里有一个图片给您:该图片没有问题它现在很纯净。但是您可能知道 JPEG 格式允许在文件中添加一些注释。比如,拍摄照片的相机型号或坐标地址。如果我们试图在里面放一些PHP代码并尝试 include 或 require 呢?让我们来看看吧!问题! 1下载这个图片到你的硬盘上。或者你自己去弄一张 JPEG 图片也行。你随便用什么格式的文件都无所谓。我建议用一个 JPEG 文件来演示,主要是因为它是一张图片且易于在其中进行文本编辑。我用的是一个 Windows的笔记本,目前我手头上没有 Apple 或 Linux(或其他UNIX系的系统)的笔记本。所以一会我会发一个这个 OS 下的屏幕快照。但是我确信你肯定也能做这个事。用以下这段 PHP 代码建个文件:<h1>Problem?</h1><img src=“troll-face.jpg”><?phpinclude “./troll-face.jpg”;保存一个图片命名为troll-face.jpg把图片和 php 脚本文件都放在同一个文件夹下打开浏览器请求这个 php 文件如果你把你的 php 文件命名为 index.php,然后把它放在文件根目录或者放在你网站目录下的任何一个文件目录中。如果你准确完成了上述步骤,你就可以看到这个画面:到此这都没毛病。没 PHP 代码展示,也没有 PHP 代码被执行。现在,我们来添加一个问题:打开文件属性对话框或运行一些允许编辑 EXIF 信息的应用程序切换到 Details 选项卡或以其他方式编辑该信息向下滚动到 camera 参数将下面代码复制到 “camera maker” 字段后面:<?php echo “<h2>Yep, a problem!</h2>”; phpinfo(); ?>刷新页面!很明显出现了一点问题!您在页面上看到了该图片。相同的图片还存在页面的 PHP 代码中。图片的代码也被执行了。我们该怎么做?!!1长话短说: 如果我们不在程序种引入这些不安全的文件,文件中的脚本就不会执行。仔细看下面的例子。最终答案?如果有人在某处看到我错了 - 请纠正我,这是一个严重的问题。PHP是一种脚本语言。您总是需要引用一些动态组合路径的文件。因此,为了保护服务器,您必须检查路径并防止混淆您的站点文件和用户上传或创建的文件。如果用户的文件与应用程序文件分开,则可以在使用上传或创建文件之前检查文件的路径。如果它位于您的应用程序脚本允许的文件夹中 - 那么它可以使用 include_once 或 require 或 require_once 引入这个文件。如果不是–那么就不引入它。如何进行检查?这很简单。你只需要将 $folder (文件)路径与一个允许程序引入文件 ( $file ) 的路径文件夹进行比较。// 不好的例子,不要用!if (substr($file, 0, strlen($folder)) === $folder) { include $file;}如果 $folder 的存放路径是 /path/to/folder 而且 $file 的存放路径是 /path/to/folder/and/file , 然后我们在代码中使用 substr() 函数把他们的路径都变成字负串进行判断,如果文件位于不同的文件夹中—这个字符串将不相等。反之则反。上面的代码有两个重要的问题。如果 file 路径是 /path/to/folderABC/and/file,很明显,该文件也不在允许引入的文件夹中。通过向两个路径添加斜杠可以防止这种情况。我们在这里向文件路径添加斜杠并不重要,因为我们只需要比较两个字符串。举个例子: 如果 folder 路径是 /path/to/folder 并且 file 路径是 /path/to/folder/and/file ,那么从 file 提取和 folder 具有相同数量的字符,那么 $ folder 将是 /path/to/folder 。再比如 folder 路径是 /path/to/folder 并且 file 路径是 /path/to/folderABC/and/file, 那么从 file 中提取 folder 具有相同数量的字符,和 $folder一样,并且将再次成为/path/to/folder,这种都是错误的,这不是我们期望的结果。因此,在 /path/to/folder/ 添加斜杠后,与 /path/to/folder/and/file 的提取部分 /path/to/folder/ 相同就是安全的。如果将 /path/to/folder/ 与 /path/to/folderABC/and/file 的提取部分 / path/to/folderA ,很明显二个字符串不一样。这就是我们期望得到的。但还有另一个问题。这并不明显。我敢肯定,如果我问你,你看到这里有一个灾难性的漏洞 - 你不会猜到它在哪里。你也许已经在经验中使用过这个东西,甚至可能就在今天。现在,您将看到漏洞是如何隐晦和显而易见。往下看。/../假想一个很常见的场景。有这么一个网站。用户可以上传文件到该站点。所有的文件都位于一个特定的目录下。有一个包含用户文件的脚本。脚本自上而下进行查找是否包含用户的输入(直接或间接)路径—那这个脚本可以通过如下方式进行路径伪造:/path/to/folder/../../../../../../../another/path/from/root/举例。用户发起请求,你的脚本中包含了一个基于类似如下用户输入路径的文件:include $folder . “/” . $_GET[‘some’]; // or $_POST, or whatever你麻烦大了。有天用户发送一个 ../../../../../../etc/.passwd 这种或其他请求,你就哭吧。再不然。假如有人让你的脚本加载一个他想要的文件,你就废了。它不一定就只是出现在用户文件中。它可能是你的CMS或你自己文件的一些插件(别相信任何人),甚至是应用程序逻辑中的错误等。或者用户可能会上传一个名为 file.php 的文件,你会把它和其他的用户文件一样放在一个特定的文件夹里面:move_uploaded_file($filename, $folder . ‘/’ . $filename);用户的文件就存放在那里,你必须常常检查从来没有包含该文件夹中的文件,目前来看,所有的东西都挺正常的。通常,用户发给你的文件不会包含斜杠或者其他特殊字符,因为这是被系统文件系统禁止的。之所以这样,是因为通常情况下浏览器发给你的文件是在真实文件系统中创建的,同时它的名字是一些真实存在的文件的名字。但是 http 请求允许用户发送任何字符。所以如果某人伪造请求创建名为 ../../../../../../var/www/yoursite.com/index.php 的文件—这行代码会覆盖你的 index.php 文件,如果 index.php 处于在上述路径的话。所有的初学者都希望通过过滤 「..」或者斜杠来解决这个问题,但是这种做法是错误的,由于你在安全方面还缺乏经验。同时你必须(是的,必须)明白一个简单的事情:你永远无法在安全和密码学方面的获得足够的知识。这句话的意思是,如果你懂得了「两个点和斜杠」的漏洞,但这不代表你知道所有其他的缺陷、攻击和其他特殊字符,你也不知道在文件写入文件系统或数据库时可能发生的代码转换。解决方案和答案为了解决这个问题,PHP中内置了一些特殊函数方法,只是为了在这种情况下使用。basename()第一个解决方案 — basename() 它从路径结束时提取路径的一部分,直到它遇到第一个斜杠,但忽略字符串末尾的斜杠,参见示例。无论如何,你会收到一个安全的文件名。如果你觉得安全 - 那么是的这很安全。如果它被不法上传利用 - 你可以使用它来校验文件名是否安全。realpath()另一个解决方案 — realpath()它将上传文件路径转换规范化的绝对路径名,从根开始,并且根本不包含任何不安全因素。它甚至会将符号链接转换为此符号链接指向的路径。因此,您可以使用这两个函数来检查上传文件的路径。要检查这个文件路径到底是否真正属于此文件夹路径。我的代码我编写了一个函数来提供如上的检查。我并不是专家,所以风险请自行承担。代码如下。<?php/** * Example for the article at medium.com * Created by Igor Data. * User: igordata * Date: 2017-01-23 * @link https://medium.com/@igordata/php-running-jpg-as-php-or-how-to-prevent-execution-of-user-uploaded-files-6ff021897389 Read the article //* * 检查某个路径是否在指定文件夹内。若为真,返回此路径,否则返回 false。 * @param String $path 被检查的路径 * @param String $folder 文件夹的路径,$path 必须在此文件夹内 * @return bool|string 失败返回 false,成功返回 $path * /function checkPathIsInFolder($path, $folder) { if ($path === ’’ OR $path === null OR $path === false OR $folder === ’’ OR $folder === null OR $folder === false) { / 不能使用 empty() 因为有可能像 “0” 这样的字符串也是有效的路径 */ return false; } $folderRealpath = realpath($folder); $pathRealpath = realpath($path); if ($pathRealpath === false OR $folderRealpath === false) { // Some of paths is empty return false; } $folderRealpath = rtrim($folderRealpath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; $pathRealpath = rtrim($pathRealpath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; if (strlen($pathRealpath) < strlen($folderRealpath)) { // 文件路径比文件夹路径短,那么这个文件不可能在此文件夹内。 return false; } if (substr($pathRealpath, 0, strlen($folderRealpath)) !== $folderRealpath) { // 文件夹的路径不等于它必须位于的文件夹的路径。 return false; } // OK return $path;}结语。必须过滤用户输入,文件名也属于用户输入,所以一定要检查文件名。记得使用 basename() 。必须检查你想存放用户文件的路径,永远不要将这个路径和应用目录混合在一起。文件路径必须由某个文件夹的字符串路径,以及 basename($filename) 组成。文件被写入之前,一定要检查最终组成的文件路径。在你引用某个文件前,必须检查路径,并且是严格检查。记得使用一些特殊的函数,因为你可能并不了解某些弱点或漏洞。并且,很明显,这与文件后缀或 mime-type 无关。JPEG 允许字符串存在于文件内,所以一张合法的 JPEG 图片能够同时包含合法的 PHP 脚本。不要信任用户。不要信任浏览器。构建似乎所有人都在提交病毒的后端。当然,也不必害怕,这其实比看起来的简单。只要记住 “不要信任用户” 以及 “有功能解决此问题” 便可。转自 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 15, 2019 · 3 min · jiezi

Laravel 5.7 最佳实践和开发技巧分享

Laravel 因可编写出干净,可用可调试的代码而为广大的 PHP 开发者所熟知。它同样也支持许许多多的功能,有时却未能在文档中体现,或者由于某种原因它们出现过又被移除了。我已经在生产环境中使用 Laravel 2 年了,从中我学到如何把代码变得更好,从我首次使用它以来我都充分发掘它的优势。接下来我将向你展示一些可能对你在用 Laravel 写代码时很有帮助的奥义之招。*查询数据时使用本地范围Laravel 有一种非常棒的方式来使用 查询构造器 编写查询。就像这样:$orders = Order::where(‘status’, ‘delivered’)->where(‘paid’, true)->get();很不错。这让我专注于编写更友好的代码而不是 SQL 语句。但如果用 本地范围 ,我们可以让这行代码变得更好些。当查询数据时, 本地范围 允许我们创建自己的 查询构造器 链式方法。举个例子,取代 ->where() ,我们可以用更简洁的 ->delivered() 和 ->paid() 。首先在 Order 模型,我们加入一些方法:class Order extends Model{ … public function scopeDelivered($query) { return $query->where(‘status’, ‘delivered’); } public function scopePaid($query) { return $query->where(‘paid’, true); }}当声明本地范围时,你应该使用 scope[Something] 来命名。这样 Laravel 便会知道这是一个本地范围并且可以在查询构造器中使用。请确保你在方法中传入了第一个参数 $query,也就是由 Laravel 自动注入的查询构造器实例。$orders = Order::delivered()->paid()->get();对于可接受额外参数的查询,你可以使用动态范围。每个范围都允许你传入额外的参数。class Order extends Model{ … public function scopeStatus($query, string $status) { return $query->where(‘status’, $status); }}$orders = Order::status(‘delivered’)->paid()->get();在本文的后面,你会知道为什么数据库字段应该使用 蛇形命名,但这里有第一个原因:Laravel 默认用 where[Something] 来替换 scope[Something] 。所以作为 scopeStatus 范围的代替,你可以这样做:Order::whereStatus(‘delivered’)->paid()->get();对于 where[Something] ,Laravel 会搜索 蛇形命名 版本的数据库字段。如果你的数据库中有个 status 字段,你可以用上面那个例子。如果有个 shipping_status 字段,你可以用:Order::whereShippingStatus(‘delivered’)->paid()->get();由你决定!必要的时候使用请求类Laravel 提供了一种优秀的方式来验证表单提交的数据。如果你需要它,不管是 POST 还是 GET 请求,它都可以验证。在控制器中,你可以这样做:public function store(Request $request){ $validatedData = $request->validate([ ’title’ => ‘required|unique:posts|max:255’, ‘body’ => ‘required’, ]); // 如果这篇博客的内容无效……}但是当控制器中已经有很多代码时,再把验证表单数据的代码加进去就会显得很凌乱。你想尽可能地减少控制器的代码 —— 至少这是我在控制器中写很多逻辑时想到的第一件事。Laravel 提供了一种很萌的方式来验证表单请求,那就是创建并使用专门的 请求类 而不是用原始的 Request 。你只需要创建你的请求类:php artisan make:request StoreBlogPost在 app/Http/Requests/ 目录中可以找到你刚创建的请求类:class StoreBlogPostRequest extends FormRequest{ public function authorize() { return $this->user()->can(‘create.posts’); } public function rules() { return [ ’title’ => ‘required|unique:posts|max:255’, ‘body’ => ‘required’, ]; }}现在,你应该用新创建的 App\Http\Requests\StoreBlogPostRequest 来代替原先的 Illuminate\Http\Request 类:use App\Http\Requests\StoreBlogPostRequest;public function store(StoreBlogPostRequest $request){ // 如果这篇博客的内容无效……}请求类中的 authorize() 方法应返回一个布尔值。如果返回了 false,它会抛出一个 403 异常,请确保你在 app/Exceptions/Handler.php 的 render() 方法中捕获了这个异常:public function render($request, Exception $exception){ if ($exception instanceof \Illuminate\Auth\Access\AuthorizationException) { // } return parent::render($request, $exception);}请求类中还有一个 messages() 方法,当验证失败时,它会返回一个包含了错误信息的数组:class StoreBlogPostRequest extends FormRequest{ public function authorize() { return $this->user()->can(‘create.posts’); } public function rules() { return [ ’title’ => ‘required|unique:posts|max:255’, ‘body’ => ‘required’, ]; } public function messages() { return [ ’title.required’ => ‘The title is required.’, ’title.unique’ => ‘The post title already exists.’, … ]; }}@if ($errors->any()) @foreach ($errors->all() as $error) {{ $error }} @endforeach@endif如果你想得到某个字段的验证信息,你可以这样做(当这个字段验证通过时 $errors->has() 会返回一个 false):<input type=“text” name=“title” />@if ($errors->has(’title’)) <label class=“error”>{{ $errors->first(’title’) }}</label>@endif魔术范围构建查询时,可以使用已有的魔术范围:根据 created_at 倒序查询:User::latest()->get();根据任意字段倒序查询:User::latest(’last_login_at’)->get();随机查询(即 SQL 语句中的 ORDER BY RAND())User::inRandomOrder()->get();使用关联关系代替冗长的查询(或者写得不好的查询)你是否曾经为了获取更多的信息而在查询语句中使用大量的 join 操作?即使在使用查询构造器的情况下,编写这样的 SQL 语句也是困难的,但是数据模型已经使用 关联关系 来实现同样的功能。由于文档提供了太多的信息,因此刚开始时你可能对关联关系并不熟悉,但是这些内容可以帮助你更好的理解事物的运行原理,同时让你的程序运行得更加顺畅。通过 这里 查询关联关系的文档。为耗时的任务使用任务系统Laravel 的任务 是后台运行程序必用的功能强大的工具。你要发送电子邮件? 任务系统。你要广播一个消息? 任务系统。你要处理一张图片? 任务系统。任务系统能够帮助你实现,在执行上述这些任务时,减少你的用户的应用加载时间。这些任务可以被放进命名的队列,它们能够被安排优先级,Laravel 几乎在所有可能的地方都实现了队列:无论在后台执行一些 PHP 任务,或者发送消息,或者广播事件,队列都在这些场景中出现。你可以在 这里 查询队列的文档。在使用队列时,我喜欢使用 Laravel Horizon ,因为它很容易安装,它能够通过 Supervisor 工具或者配置文件实现后台运行,同时我能够告诉 Horizon 我希望每个队列产生多少个进程。遵守数据库标准 & 访问器Laravel 从一开始就教给你变量和方法应使用像 $camelCase camelCase() 这样的小驼峰命名而数据库字段应使用像 snake_case 这样的蛇形命名。为什么呢?因为这有助于我们构造更好的 访问器。访问器是可以直接在模型中构造的自定义字段。如果我们的数据库包含了 first_name、last_name、age 这几个字段,我们可以增加一个叫做 name 的自定义字段来把 first_name 和 last_name 拼接起来。别担心,这个 name 不会被写入到数据库。它只是某个模型的自定义属性。所有的访问器,和 范围 一样,都有自定义命名语法:getSomethingAttribute:class User extends Model{ … public function getNameAttribute(): string { return $this->first_name.’ ‘.$this->last_name; }}当使用 $user->name,访问器会返回拼接好的字符串。*默认情况下,用 dd($user) 是看不到 name 属性的,但是通过 $appends 变量我们可以使它一直可用:class User extends Model{ protected $appends = [ ’name’, ]; … public function getNameAttribute(): string { return $this->first_name.’ ‘.$this->last_name; }}现在每次 dd($user),我们都可以看到 name 了。(不过仍然,这个属性不是从数据库取得的,而是每次使用时将 first_name 和 last_name 拼接得到的)。要注意下,如果你数据库里已经有 name 这个字段了,那情况就会有点不一样:$appends 数组里的 name 元素就不需要了,然后访问器需要传入一个参数,这个参数就是数据库中的 name (也就是说我们用不着再使用 $this 了)。 举个例子,我们也许想用 ucfirst() 来使名字的首字母转为大写:class User extends Model{ protected $appends = [ // ]; … public function getFirstNameAttribute($firstName): string { return ucfirst($firstName); } public function getLastNameAttribute($lastName): string { return ucfirst($lastName); }}现在当我们用 $user->first_name,它会返回一个首字母大写的字符串。由于这个特性,数据库字段最好是用 snake_case 这种蛇形命名。不要在配置文件中存储模型相关的静态数据我喜欢把与模型相关的静态数据存放在模型文件中。让我们一起来看一下。不要像下面这样:BettingOdds.phpclass BettingOdds extends Model{ …}config/bettingOdds.phpreturn [ ‘sports’ => [ ‘soccer’ => ‘sport:1’, ’tennis’ => ‘sport:2’, ‘basketball’ => ‘sport:3’, … ],];使用下面的方式访问:config(‘bettingOdds.sports.soccer’);我更喜欢这样做:BettingOdds.phpclass BettingOdds extends Model{ protected static $sports = [ ‘soccer’ => ‘sport:1’, ’tennis’ => ‘sport:2’, ‘basketball’ => ‘sport:3’, … ];}然后访问它们:BettingOdds::$sports[‘soccer’];为什么这样?因为这样有益于后续操作:class BettingOdds extends Model{ protected static $sports = [ ‘soccer’ => ‘sport:1’, ’tennis’ => ‘sport:2’, ‘basketball’ => ‘sport:3’, … ]; public function scopeSport($query, string $sport) { if (! isset(self::$sports[$sport])) { return $query; } return $query->where(‘sport_id’, self::$sports[$sport]); }}现在我们可以使用范围查询:BettingOdds::sport(‘soccer’)->get();使用集合替代原始的数组处理在过去,我们通常以一种原始的方式使用数组:$fruits = [‘apple’, ‘pear’, ‘banana’, ‘strawberry’];foreach ($fruits as $fruit) { echo ‘I have ‘. $fruit;}现在,我们可以使用一种高级的方法(译者注:集合的方式)处理数组中的数据。我们可以过滤、转换、遍历和修改数组中数据:$fruits = collect($fruits);$fruits = $fruits->reject(function ($fruit) { return $fruit === ‘apple’;})->toArray();[‘pear’, ‘banana’, ‘strawberry’]想要了解细节, 请查看 集合的文档.当使用 查询构造器时,->get() 方法返回一个 Collection 实例。但要注意别搞混了 Collection 和 Query builder:从 Query Builder 中,我们无法获取任何数据.。但我们有大量的查询相关的方法可以使用:orderBy(), where(),等等。最终调用 ->get() 方法之后,数据被获取到,内存空间被消耗。它返回一个 Collection 实例。某些查询构造器不可用或者说可用但是方法名不同,关于这些请查阅 所有集合的方法。如果你能在 Query Builder 层次过滤数据,就去做吧!不要依赖于等到结果 Collection 实例返回时再过滤—你将会消耗更多的内存空间。 使用 Limit 限制结果条数,在 DB 层使用索引来加快查询。善用扩展包、不要重复造轮子如下是一些我在用的扩展包:Laravel Blade DirectivesLaravel CORS(跨域请求时,将你的路由限制为指定域名访问)Laravel Tag Helper(在 Blade 内更方便地使用 HTML 标签)Laravel Sluggable(在 Eloquent 模型内生成 Slug 时十分实用)Laravel Responder(更容易地构建 JSON API)Image Intervention(处理图片)Horizon(使用少量配置即可管理队列)Socialite(使用少量配置即可集成第三方社交媒体登录)Passport(集成 OAuth 路由)Spatie’s ActivityLog(追踪模型的修改活动)Spatie’s Backup(备份文件和数据库)Spatie’s Blade-X(定义你自己的 HTML 标签;可与 Laravel Tag Helper 结合)Spatie’s Media Library(快速将模型与文件关联)Spatie’s Response Cache(缓存控制器的完整响应内容)Spatie’s Collection Macros(给集合添加更多宏)以下是我(原文作者)编写的一些扩展包:Befriended(类似社交媒体的点赞、收听、屏蔽操作)Schedule(创建日程表并检查某个时间点是否可用)Rating(为模型增加评分功能)Guardian(易于使用的权限系统)太难理解?联系我吧!如果你有更多关于 Laravel 的问题,如果你需要运维方面的帮助,或者只是想说声 谢谢,你可以在 Twitter @rennokki 上找到我!转自 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 14, 2019 · 3 min · jiezi

使用 Kubernetes 来部署你的 Laravel 程序

Laravel 是开发 PHP 应用程序的优秀框架。 无论您是需要构建新想法的原型,开发 MVP(最小可行产品)还是发布成熟的企业系统,Laravel 都可以促进所有开发任务和工作流程。如何处理部署应用程序是一个很有选择性的问题。 Vagrant 非常适合搭建类似于远程服务器的本地环境。 但是,在生产环境中,您很可能需要的不仅仅是一个 Web 主机和一个数据库。 您可能会针对多个要求提供单独的服务。 您还需要有适当的机制来确保应用程序始终在线运行,并且服务器可以有效地均衡负载。在本文中,我将解释如何在 Kubernetes 上搭建一个简单的 Laravel 应用程序的环境。Kubernetes 是什么?为什么使用它?Kubernetes 是一款由 Google 发起的开源系统,目的在于提高集群环境下管理容器化应用的效率。有些人将其称为容器编排平台,而 Kubernetes 并非唯一的此类平台。不过,相比其它对手,其享誉已盛,且知名度仍在不断提高;更别说你一旦习惯上它,就会发现它真的十分易用。如果你依然好奇为何有人能够愉快地和 Kubernetes 玩耍,答案就是——简单。Kubernetes 能够让部署、管理多个项目所需的大量集群变得更加容易。将 Laravel 应用部署到 Minikube正如我之前提到的,我将会在本文展示如何部署一个简单、无状态的 Laravel 应用到 Kubernetes。我将详细说明此过程中涉及到的步骤,同时向大家解释为何需要执行某项操作。此外,我还将展示如何快速横向扩展应用,并使用 Ingress Controller 使其能够通过特定域名或 IP 访问。你可以在多个云平台上面运行 Kubernetes ,例如 Google Cloud Engine 和 Amazon Web Services。在这个例子中,你会使用 Minikube 运行你的程序,Minikube 是一个让你在本地更容易运行 Kubernetes 的工具。与 Vagrant 类似,Minikube 仅仅是一个包含了 Kubernetes 运行平台和 Docker 的虚拟机。如果使用真正的 Kubernetes 的话,你需要使用 Docker 部署你的应用,同时你需要将运行平台扩展到三个节点。应用我已经准备了一个简单的 Laravel 程序,你可以从 GitHub 克隆下来。它只是一个全新的 Laravel 安装程序。因此,你可以使用本例中的演示程序,也可以自己创建一个新的 Laravel 程序。如果使用本例中的演示程序,请按照下面的命令将其克隆到项目目录里面。cd /to/your/working/directorygit clone git@github.com:learnk8s/laravel-kubernetes-demo.git .预备条件要实现本示例,你需要在你的本地系统中安装如下软件:1) Docker2) Kubectl3) Minikube如果你在Windows系统中安装上述软件遇到问题,请查阅 Windows 10 中 Docker 和 Kubernetes 入门教程,这是一个手把手教学的入门教程。Docker 镜像Kubernetes 部署容器化的应用,因此首先你需要为示例应用创建一个 Dcoker 镜像。由于本例中你在本地运行 Minikube,因此你只能用示例代码中的 Dockerfile 文件创建一个本地 Docker 镜像。FROM composer:1.6.5 as build WORKDIR /app COPY . /app RUN composer installFROM php:7.1.8-apache EXPOSE 80 COPY –from=build /app /app COPY vhost.conf /etc/apache2/sites-available/000-default.conf RUN chown -R www-data:www-data /app \ && a2enmod rewrite该 Dockerfile 文件由两部分组成:第一部分扩展了一个 PHP 的 composer 镜像,因此你能够安装应用依赖。第二部分创建了一个包含 Apache 服务的镜像, Apache 服务将会为示例应用服务。在测试 Docker 镜像前,你需要使用如下的命令创建镜像:cd /to/your/project/directory docker build -t yourname/laravel-kubernetes-demo .然后使用下面的命令运行示例程序:docker run -ti \ -p 8080:80 \ -e APP_KEY=base64:cUPmwHx4LXa4Z25HhzFiWCf7TlQmSqnt98pnuiHmzgY= \ laravel-kubernetes-demo示例程序可以通过 http://localhost:8080 访问。在这个安装中,容器是通用的,同时 APP_KEY 并不是写死或共享的。在 Minikube 中创建镜像cd /to/your/project/directoryeval $(minikube docker-env)docker build -t yourname/laravel-kubernetes-demo .别忘记执行上面的 eval 命令。 要在虚拟机中创建镜像,执行上面的 eval 命令是必须的。你只需要在当前的终端中执行一次这个命令。部署镜像现在示例应用的镜像已经创建完成,并且在 Minikube 中是可用的,因此你可以接下来继续部署这个镜像。我总是一开始就要确保 kubectl 在正确的上下文环境中。在这个例子中,上下文环境是 Minikube。你可以使用下面的命令快速的切换上下文环境:kubectl config use-context minikube然后你可以部署容器镜像:kubectl run laravel-kubernetes-demo \ –image=yourname/laravel-kubernetes-demo \ –port=80 \ –image-pull-policy=IfNotPresent \ –env=APP_KEY=base64:cUPmwHx4LXa4Z25HhzFiWCf7TlQmSqnt98pnuiHmzgY=上述的命令告诉 kubectl 从 Docker 镜像中运行我们的示例程序。上述命令的第一个参数告诉 kubectl 如果在本地存在镜像,就不要去登记处(例如 Docker Hub)拉取镜像。请注意,你仍然需要登录到 Docker 中,因为这样 kubectl 才能检查镜像是否是最新的。通过下面的命令,你会看到有一个 Pod 是为示例程序而创建的:kubectl get pods该命令会返回类似如下的输出:NAME READY STATUS RESTARTS AGElaravel-kubernetes-demo-7dbb9d6b48-q54wp 1/1 Running 0 18m你也可以使用 Minikube 的 GUI 控制面板来监控集群。GUI 还有助于可视化大多数经常讨论的指标。要查看该控制面板,请执行下属命令:minikube dashboard或者获取控制面板的 URL 地址:minikube dashboard –url=true暴露一个服务到目前为止,你已经创建了一个运行示例程序容器的部署。在集群中运行的 Pod 有一个动态的 IP。如果你使用该 IP 并直接把流量路由到那里,在每次重启 Pod 的时候,你可能每次都要更新路由表。事实上,在每次部署或者容器重启的时候,一个新的 IP 会关联到这个 Pod 中。为了避免需要手动的管理 IP 地址,你需要使用服务。服务在 Pods 集合中充当负载均衡器的角色。所以,尽管一个 Pod 的 IP 地址改变了,但是服务总是指向该 Pod。同时,由于服务总是拥有一个固定的 IP,因此你不需要手动更新任何东西。你可以使用下面的命令创建一个服务:kubectl expose deployment laravel-kubernetes-demo –type=NodePort –port=80倘若一切顺利,你会看到一个与下面信息相似的确认信息:service “laravel-kubernetes-demo” exposed执行下面的命令:kubectl get services上述命令显示了正在运行中的服务列表。你也可以通过控制面板中的 「服务」 导航菜单查看正在运行中的服务。很显然,一个更加令人兴奋的验证部署和服务暴露的方法就是在浏览器中运行示例程序。 ?要获取应用(服务)的URL地址,你可以使用下面的命令:minikube service –url=true laravel-kubernetes-demo上述命令会输出 IP 地址和端口号,例如:http://192.168.99.101:31399或者直接在浏览器中启动程序:minikube service laravel-kubernetes-demo不想错过接下来的故事,实验或者小提示。 如果你欣赏这篇文章,敬请期待接下来更多的文章内容。 希望新的内容直接发到你的邮箱并提升在 Kubernetes 方面的专业技能。 现在请订阅扩展你已经成功在 Kubernetes 中部署了应用。这是令人兴奋的。但是做这一切的重点是什么?你只是在一个 Pod 中做了一个部署,在一个节点上面暴露了网页服务。让我们把目前的应用多部署两个实例。现在你应该明白你正在处于什么位置,执行下面的命令获取希望得到的和现在已有的 Pod 列表:kubectl get deploymentNAME DESIRED CURRENT UP-TO-DATE AVAILABLEAGE laravel-kubernetes-demo 1 1 1 1 57m上面的输出中,每一项都是「1」。你希望获得三个 Pod。因此,我们通过下面的命令进行扩展:kubectl scale –replicas=3 deployment/laravel-kubernetes-demo deployment “laravel-kubernetes-demo” scaled命令执行完成。你已经将第一个 Pod 复制另外两个,系统为你提供了三个 Pod 来运行这个服务。执行 get deployment 可以检验这一切:kubectl get deploymentNAME DESIRED CURRENT UP-TO-DATE AVAILABLEAGE laravel-kubernetes-demo 3 3 3 3 59m你也可以在控制面板中的 Pods 页面或服务页面查看这些内容。现在,你正在使用三个 Pod 运行三个应用实例。想象一下这种场景,你的应用越来越受欢迎。成千上万的访客使用你的网页或软件。过去,你可能都焦头烂额在编写脚本创建更多实例的事情上。但是在 Kubernetes 中,您可以快速扩展出多个实例:kubectl scale –replicas=10 deployment/laravel-kubernetes-demo deployment “laravel-kubernetes-demo” scaled你看看使用 Kubernetes 扩展你的网站是何其便捷。Ingress你已经实现了不错的功能,部署了应用并扩展之。当你指向群集的(Minikube)IP地址和节点的端口号时,你就已经可见浏览器中正在运行的程序了。 现在,你将看到如果通过指定的 URL 访问应用程序,就如同之前部署到云端那样。为了在 Kubernetes 中使用 URL,你需要一个 Ingress。 Ingress 是一组允许入站连接到达 Kubernetes 集群的规则。Ingress 是非常必要的,因为在 Kubernetes 中,诸如 Pod 之类的资源仅具有可在集群内和集群内路由的IP地址。也就是说它们是无法进出外部环境的。我在演示应用源码中包含了一个有如下内容的 ingress.yaml 文件:apiVersion: extensions/v1beta1 kind: Ingress metadata: name: laravel-kubernetes-demo-ingress annotations: ingress.kubernetes.io/rewrite-target: / spec: backend: serviceName: default-http-server servicePort: 80 rules: - host: laravel-kubernetes.demo - http: paths: - path: / backend: serviceName: laravel-kubernetes-demo servicePort: 80在你所期望的 Kubernetes 资源文件基本内容里,该文件定义了一组路由流量入站的规则。 laravel-kubernetes.demo URL 会指向应用运行的 Service ,就像之前在 8181 端口上标记 laravel-kubernetes-demo 那样。没有集成 Ingress 资源, Ingress 控制器是无法使用的,因此您需要创建一个新的控制器或使用现有控制器。 本教程使用的是 Nginx Ingress 控制器来管理路由资源。 Minikube(v0.14及以上版本) 附带 Nginx 设置作为插件,您需要手动启用这个插件:minikube addons enable ingress注意,Minikube 可能需要几分钟才能下载并安装 Nginx 作为 Ingress 路由控制器。启用 Ingress 插件后,您可以通过这种方式来创建 Ingress 实例:kubectl create -f path-to-your-ingress-file.yaml您可以通过运行以下命令来验证并获取 Ingress 的实例信息:kubectl describe ing laravel-kubernetes-demo-ingress输出一些配置相关的信息:Name: laravel-kubernetes-demo-ingress Namespace: default Address: 192.168.99.101 Default backend: default-http-server:80 (<none>) Rules: Host Path Backends —- —- ——– * / laravel-kubernetes-demo:8181 (172.17.0.6:8181) Annotations: rewrite-target: / Events: Type Reason Age From Message —- —— —- —- ——- Normal CREATE 39s nginx-ingress-controller Ingress default/laravel-kubernetes-demo-ingressNormal UPDATE 20s nginx-ingress-controller Ingress default/laravel-kubernetes-demo-ingress您现在可以通过 minikube IP地址访问应用程序,如上所示。 要通过 URL https://laravel-kubernetes.demo 访问网站应用,您需要在 hosts 文件中添加一条解析记录。结论希望这篇文章能帮助您熟悉 Kubernetes 的部署和搭建。 根据我自己的经验,如果你经常进行类似的环境搭建,这会让你的搭建过程更加得心应手且有趣。转自 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 10, 2019 · 3 min · jiezi

laravel cache get 是如何调用的?

本文使用版本为laravel5.5cache getpublic function cache() { $c=\Cache::get(‘app’); if(!$c) { \Cache::put(‘app’, ‘cache’, 1); } dump($c);//cache } config/app.php ‘aliases’ => [ ‘App’ => Illuminate\Support\Facades\App::class, ‘Artisan’ => Illuminate\Support\Facades\Artisan::class, ‘Auth’ => Illuminate\Support\Facades\Auth::class, ‘Blade’ => Illuminate\Support\Facades\Blade::class, ‘Broadcast’ => Illuminate\Support\Facades\Broadcast::class, ‘Bus’ => Illuminate\Support\Facades\Bus::class, ‘Cache’ => Illuminate\Support\Facades\Cache::class, ]使用cache实际调用的是Illuminate\Support\Facades\Cache,这个映射是如何做的?public/index.php$response = $kernel->handle($request = Illuminate\Http\Request::capture());bootstarp/app.php$app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);app/http/kernel.phpuse Illuminate\Foundation\Http\Kernel as HttpKernel;class Kernel extends HttpKernel{}Illuminate/Foundation/Http/Kernel.phppublic 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;}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()); } public function bootstrap() { if (! $this->app->hasBeenBootstrapped()) { $this->app->bootstrapWith($this->bootstrappers()); } }Illuminate/Foundation/Application.phppublic function bootstrapWith(array $bootstrappers) { $this->hasBeenBootstrapped = true; foreach ($bootstrappers as $bootstrapper) { $this[’events’]->fire(‘bootstrapping: ‘.$bootstrapper, [$this]); $this->make($bootstrapper)->bootstrap($this); $this[’events’]->fire(‘bootstrapped: ‘.$bootstrapper, [$this]); } }Illuminate/Foundation/Bootstrap/RegisterFacades.phppublic function bootstrap(Application $app) { Facade::clearResolvedInstances(); Facade::setFacadeApplication($app);//将config/app.php 里的aliases数组里面的Facades类设置别名 AliasLoader::getInstance(array_merge( $app->make(‘config’)->get(‘app.aliases’, []), $app->make(PackageManifest::class)->aliases() ))->register(); }Illuminate/Foundation/AliasLoader.phppublic function load($alias) { if (static::$facadeNamespace && strpos($alias, static::$facadeNamespace) === 0) { $this->loadFacade($alias); return true; } // $alias来自于config/app.php中aliases数组 if (isset($this->aliases[$alias])) { //‘Route’ => Illuminate\Support\Facades\Route::class, // class_alias 为一个类创建别名 return class_alias($this->aliases[$alias], $alias); } }Illuminate/Support/Facades/Cache.phpclass Cache extends Facade{ /** * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return ‘cache’; }}Illuminate/Support/Facades/Facade.php这个文件没有get set ,只有__callStaticpublic static function __callStatic($method, $args) { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException(‘A facade root has not been set.’); } return $instance->$method(…$args); } public static function getFacadeRoot() { return static::resolveFacadeInstance(static::getFacadeAccessor()); } protected static function resolveFacadeInstance($name) { //这里$name为cache if (is_object($name)) { return $name; } if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } //$app是容器对象,实现了ArrayAccess接口,最终调用的还是容器的make方法 return static::$resolvedInstance[$name] = static::$app[$name]; }IlluminateContainerContainer.phppublic function make($abstract, array $parameters = []) { return $this->resolve($abstract, $parameters); } protected function resolve($abstract, $parameters = []) { $abstract = $this->getAlias($abstract); $needsContextualBuild = ! empty($parameters) || ! is_null( $this->getContextualConcrete($abstract) ); // If an instance of the type is currently being managed as a singleton we’ll // just return an existing instance instead of instantiating new instances // so the developer can keep using the same objects instance every time. if (isset($this->instances[$abstract]) && ! $needsContextualBuild) { return $this->instances[$abstract]; } $this->with[] = $parameters; $concrete = $this->getConcrete($abstract); // We’re ready to instantiate an instance of the concrete type registered for // the binding. This will instantiate the types, as well as resolve any of // its “nested” dependencies recursively until all have gotten resolved. if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($concrete); } else { $object = $this->make($concrete); } // If we defined any extenders for this type, we’ll need to spin through them // and apply them to the object being built. This allows for the extension // of services, such as changing configuration or decorating the object. foreach ($this->getExtenders($abstract) as $extender) { $object = $extender($object, $this); } // If the requested type is registered as a singleton we’ll want to cache off // the instances in “memory” so we can return it later without creating an // entirely new instance of an object on each subsequent request for it. if ($this->isShared($abstract) && ! $needsContextualBuild) { $this->instances[$abstract] = $object; } $this->fireResolvingCallbacks($abstract, $object); // Before returning, we will also set the resolved flag to “true” and pop off // the parameter overrides for this build. After those two things are done // we will be ready to return back the fully constructed class instance. $this->resolved[$abstract] = true; array_pop($this->with); return $object; }Illuminate/Cache/CacheServiceProvider.php public function register() { $this->app->singleton(‘cache’, function ($app) { return new CacheManager($app); }); $this->app->singleton(‘cache.store’, function ($app) { return $app[‘cache’]->driver(); }); $this->app->singleton(‘memcached.connector’, function () { return new MemcachedConnector; }); }get set$instance->$method(…$args) ...

January 8, 2019 · 3 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

一文看懂 PHP 7.3 更新

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 上关注我,来获得更多的信息!转自 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

December 28, 2018 · 3 min · jiezi

Linux安装二进制PHP7.2

通过性能评测,可以看出PHP7对性能进行了较大的优化,相比与PHP5.x有50%-150%的性能提升,因此,为了提升我们服务的响应速度,降低机器负载,需要进行版本升级。因为对二进制比较熟悉,所以没有用yum的方式进行安装,采用的二进制安装方式比较灵活,但是因为第一次安装PHP的高版本,也引入了很多的问题,总而言之,就是在错误中不断摸索错误,最终找到一个还能用的道路。下载PHP7.2官方下载地址:wget http://cn2.php.net/get/php-7.2.13.tar.bz2/from/this/mirror -O php-7.2.13.tar.bz2tar -xjvf php-7.2.13.tar.bz2// 用于后面编译的生成代码目录mkdir php7cd php-7.2.13配置PHPPHP编译前提供了大量的参数进行配置,包括支持的扩展、执行用户等,可以查看参数列表。我们进行最简单的配置,只支持php-fpm管理,因为我们的PHP是配合Ngnix来进行服务,因此还要指定执行的用户:./configure –prefix=/home/work/lnmp/php7 –enable-fpm –with-fpm-user=nginx –with-fpm-group=nginx我的第一次编译报错:configure: error: OpenSSL version 1.0.1 or greater required.解决这个问题,需要首先看自己的openssl的版本信息:$ openssl versionOpenSSL 1.0.0-fips 29 Mar 2010因此更新openssl版本:wget https://www.openssl.org/source/openssl-1.1.0j.tar.gztar -xzvf openssl-1.1.0j.tar.gzcd openssl-1.1.0j./config –prefix=/usr/local/ssl shared zlib-dynamicmakemake installmv /usr/bin/openssl /usr/bin/openssl1.0.0ln -s /usr/local/ssl/bin/openssl /usr/bin/openssl安装完毕再次配置依然报相同错误,因此我们需要手动指定openssl的位置:// 查看指定openssl的参数$./configure –help | grep openssl –with-openssl=DIR Include OpenSSL support (requires OpenSSL >= 1.0.1) –with-openssl-dir=DIR FTP: openssl install prefix –with-openssl-dir=DIR SNMP: openssl install prefix$ ./configure –prefix=/home/work/lnmp/php7 –enable-fpm –with-fpm-user=nginx –with-fpm-group=nginx –with-openssl=/usr/bin/openssl安装 make && make install 启动因为我是升级,所以原有Nginx和代码以及配置文件都是OK的状态,可能在这个阶段你会遇到不同的问题,这个得结合你的情况进行解决。cd php7// 复制php.ini和php-fpm.conf到etc/目录下,这个过程你也可以自己配置啊// 生成两个目录用于日志和sock文件保存mkdir logmkdir runsbin/php-fpm -c etc/php.ini -y etc/php-fpm.conf -p .启动成功,访问URL,报错:502 Bad Gateway502 Bad Gateway根据nginx的访问日志可以看出:$ cat error.log2018/12/14 10:54:18 [crit] 6260#0: *206 open() “./run/factcgi_temp/0000000015” failed (13: Permission denied) while reading upstream, client: 172.24.162.178, server: , request: “GET /oss/index.php HTTP/1.1”, upstream: “fastcgi://unix:run/phpfpm.sock:”, host: “xx.xx.com"查阅【资料1】【资料2】可以知道,在PHP老版本里,有一个bug,任何能够连接socket文件的用户可以通过它执行任何命令,特别是在Ubuntu系统里允许www-data用户执行任何代码。因此最新版本里修复了这个错误,但也导致我们出现了502的问题,因此我们需要配套升级我们的配置文件:// 在nginx.conf头部添加执行用户user www www;// 在php-fpm.conf里放弃注释这3行; Set permissions for unix socket, if one is used. In Linux, read/write; permissions must be set in order to allow connections from a web server. Many; BSD-derived systems allow connections regardless of permissions.; Default Values: user and group are set as the running user; mode is set to 0666listen.owner = wwwlisten.group = wwwlisten.mode = 0660重启nginx和php-fpm进程,依然报错:nginx: [emerg] getpwnam(“www”) failed因为我们没有加上这个用户:useradd -r www搞定,重启nginx和php-fpm进程,服务正常。总结使用二进制来安装PHP7.2,在编译的时候按需加载扩展,如果有问题,我们可以重新编译,也可以动态扩展。过程比较简单,但我的服务并没有正常服务,因为使用的Yii2.0不能够完美兼容PHP7,我还得对Yii2.0进行升级,以及对自身的代码进行升级。参考资料PHP7.2下载地址:http://php.net/downloads.phpPHP的性能演进:http://www.laruence.com/2016/…OpenSSl downloads:https://www.openssl.org/source/OpenSSL 安装、介绍:https://www.jianshu.com/p/291…Centos7 安装 PHP7最新版:https://www.jianshu.com/p/246…CentOS 7 Linux 安装PHP7.2 - 编译安装:https://blog.csdn.net/ai_zxc/…nginx error connect to php5-fpm.sock failed (13: Permission denied):https://stackoverflow.com/que…nginx安装 nginx: [emerg] getpwnam(“www”) failed 错误:https://blog.csdn.net/justdoi… ...

December 18, 2018 · 1 min · jiezi

PHP工具箱:PHPStan —— PHP 静态代码分析工具

PHPStan:无需写测试就能找到代码中的 Bug每当我看到开发人员从 Java 或 C# 等编译语言切换到 PHP 这样的解释语言时解放了生产力后感到很高兴。除了这些常规的执行模型(发起、处理请求和结束请求)和更短的反馈环(无需等待编译器)外,还有一个能解决开发人员日常问题的开源框架生态系统,因此,PHP 是目前来说 web 开发中最流行的语言。但它有一个缺点。你会在什么时候发现错误?编译型语言需要在程序运行之前了解每个变量的类型,每个方法的返回类型。这就是为什么编译器需要确保程序是没有错误的,并且会在源码中向你指出这些类型的错误,比如调用了未定义的方法或者是向某个函数传递了错误数量的参数。在把应用程序部署到生产环境前,编译器算是第一道防线。然而 PHP 就不会这样了。如果程序出错,会执行到错误的代码的时候崩溃。在测试 PHP 应用时,不管是自动化测试还是手动测试,开发人员都会花费大量时间去查一些其它编译型语言不会犯的错从而减少测试实际业务逻辑的时间。我想改变这一点。欢迎来到 PHPStan 的世界现阶段 PHP 实践所产生的代码库中,我们可以确定大部分数据的类型,并且转换为静态类型的语言,尽管还保留着一些动态语言的特性。人们把现在的 PHP 代码库变得跟其他语言一样更加有趣。面向对象,依赖注入以及设计模式的使用已经变得非常普遍。这让我想到了 PHP 的 静态分析工具,它将替代其他语言的编译器角色。我花了很多时间研究它,并且已经使用它的各种开发版本来检查我们的代码库超过一年。它就是 PHPStan, 开源且免费它目前校验什么?有关类中涉及的,对象实例, 错误/异常捕获,类型约束以及其他语言结构的存在性。 PHP 照旧不会检查这些, 但是会展现其中未被使用的代码。被调用的方法和函数的存在性和可访问性。同样也会检查他们的参数个数。方法是否返回了它声明的返回值类型。被访问成员变量的存在性和可见性。它也可指出是否将一个其他的类型的值赋给了既定类型的成员变量。sprintf/printf 函数基于格式化字符串所应接收的参数个数。分支和循环范围中的变量的存在性。无用的形式指定。例如 (string) ‘foo’ ,以及不同类型变量间的严格比较 (=== 和 !==),因为他们的结果总为 false。这个清单的内容随着每次发布都在递增。但成就 PHPStan 也不会只仰赖此一技之微。PHPStan 迅疾如飞…它设法一次性检查整个代码库。 它无需多次遍历代码。 只需浏览您想要分析的代码,例如 你写的代码。它无需解析和分析第三方依赖项。 相反,它使用反射来找出有关你代码库中引用的他人代码的有用信息。PHPStan 能在一分钟里检查我们的代码库 (6000 个文件, 600k LOCs) 。它可在一秒内完成自查。…可扩展性即便当前正在使用静态类型,开发者也可以合法的使用 PHP 的动态语法特性,例如 get, set 和 __call 这些魔术方法。它们可以在运行时去定义新属性和方法。通常,静态分析都会爆出属性和方法未定义,但是有一种机制可以告诉引擎如何创建新的属性和方法。它得益于对允许用户扩展的原生 PHP 反射的自定义抽象。更多细节可查看 README 中类反射扩展章节。某些方法返回的类型取决于它的参数。它可以取决于你传递给它的类名,也可能返回与传递的对象相同的类的对象。这就是 动态返回类型扩展 的用途。压轴语: 如果你想自己出一个 PHPStan 的新的检查项, 你可以自力更生。可以提出基于特定框架的规则,例如检查 DQL查询中引用的实体和字段是否存在,或者你选择的 MVC 框架中生成的链接是否和现存的控制器有关。选择规范级别我使用过其他工具,并将之集成进现有的代码库中,这种体验真是往事不堪回首。他们爆出成千上万的错误让你没法使用。取而代之,我回顾如何集成 PHPStan 到刚进入开发阶段的代码库中。 首个版本的功能不是很强大,这时并未发现多少错误。但从集成的角度来看,它还是非常不错的 — 有空时,我就为它增加新规则,我修复了它在版本库中找到的错误,并将新代码合并到主分支。我们会使用新版本几周用来发现其找到的错误,并不断重复这件事。这种逐级增加的规范性的做法在实践中看来大有裨益,所以我使用 PHPStan 的现有功能来模拟它。默认情况下,PHPStan 只检查它确定的代码—常量,实例化,调用$ this的方法,静态调用的方法,函数和各种语言结构中的现有类。 通过增加级别(从默认值0到当前值4),您还可以增加它对代码所做的假设数量以及它检查的规则数量。如果内建级别无法满足你的要求,你同样也可以自定义规则。少写单元测试! (披沙拣金)可能这个建议你闻所未闻。即便是非常细碎的代码,开发者也不得不编写单元测试,因为这方面犯错的几率都是均等的,例如简单的拼写错误或者忘记将结果赋值给变量。为那些经常出现在控制器或者门脸中的转发代码编写单元测试是很不划算的事。单元测试也有其成本。它们同样也是代码,难逃编写和维护的窠臼。最理想的做法就是在持续集成服务器上,每次更改时都运行 PHPStan,从而在无需单元测试的情况下防止此类错误的产生。实现100%的代码覆盖率真的很难,并且非常昂贵,但你可以静态分析100%的代码。至于单元测试的重点应当集中在静态分析代码难以察觉的,容易出错的地方。包括:复杂的数据过滤,循环,条件判断,乘除法包含舍入的计算等。站在巨人的肩膀上如果不是 Nikita Popov 创建了 PHP Parser。就不会有 PHPStan 的出现。PHP 在 2016 年开始广泛使用 包管理, 单元测试 和 编码标准 的工具。然而到现在也没有一个广泛使用的工具,可以在不运行代码的情况下检查代码中的错误。所以我创建了一个易于使用,快速,可扩展的版本,既不会对您的代码有严格的要求,你还会从这些检查中受益。查看 GitHub 仓库 ,了解如何将其集成到您的项目中!更多文章:https://laravel-china.org/c/t… ...

November 14, 2018 · 1 min · jiezi