你真的了解现在的PHP吗?
前段时间,公司的项目从PHP5.3升级到PHP7,现在项目里开始使用PHP7的一些新语法和特性。反观PHP的5.4、5.5、5.6版本,有点认知缺失的感觉。所以,决定看《Modern PHP》补一补里面的一些概念。
一、特性
1. 命名空间
命名空间用的比较多,不详细写了,记录几个值得注意的实践和细节。
多重导入
别这么做,这样写容易让人困惑。
1 2 3 4 | <?php use Symfony\Component\HttpFoundation\Request, Symfony\Component\HttpFoundation\Response, Symfony\Component\HttpFoundation\Cookie; |
建议一行写一个use语句:
1 2 3 4 | <?php use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Cookie; |
一个文件中使用多个命名空间
你可以这么做,但这违背了“一个文件定义一个类”的良好实践。
1 2 3 4 5 6 7 8 | <?php namespace Foo { //code } namespace Bar { //code } |
全局命名空间
想要使用PHP原生的Exception类,需要在类名前加 \ 符号。
1 2 3 4 5 6 7 8 9 10 | <?php namespace My\App; class Foo { public function doSomething() { $exception = new \Exception(); } } |
如果Exception前不加 \ 符号,会在My\App命名空间下寻找Exception类。
2. 使用接口
使用接口编写的代码更灵活,能委托其他人实现细节。使用的人只需要关心有什么接口,而不需要关心实现。能够很好地解耦代码,方便扩展,比较常用就不说啦。
3. 性状
在学习laravel框架之前都没弄清楚性状(trait)。这是PHP5.4.0引入的新概念,既像类又像接口。但它两个都不是。
性状是类的部分实现,可以混入一个或多个现有PHP类中。类似Ruby的组合模块活混入(mixin)。
为什么使用性状
举个具体的例子,比如有两个类,Car 和 Phone,他们都需要GPS功能。为了解决这个问题,第一反应创建一个父类,然后让Car和Phone继承它。但因为很明显,这个祖先不属于各自的继承层次结构。
第二反应创建一个GPS的接口,定义好GPS的功能接口,然后让Car和Phone两个类都实现这个接口。这样做能实现功能,同时也能保持自然的继承层级结构。不过,这就使得在两个都要实现重复的GPS功能,这不符合DRY(dont repeat yourself)原则。
第三反应创建实现GPS功能的性状(trait),然后在Car和Phone类中混入这个性状。能实现功能,不影响继承结构,不重复实现,完美。
创建与使用性状
创建trait
1 2 3 4 | <?php trait MyTrait{ //实现 } |
使用trait
1 2 3 4 5 6 | <?php class MyClass { use MyTrait; // 类的实现 } |
4. 生成器
PHP生成器(generator)是PHP5.5.0引入的新功能,很多PHP开发者生成器不了解。生成器是个简单的迭代器,但生成器不要求实现Iterator接口。生成器会根据需要计算并产生要迭代的值。如果不查询,生成器永远不知道下一个要迭代的值是什么,在生成器中无法后退或快进。具体看如下两个例子:
简单的生成器
1 2 3 4 5 6 7 8 9 10 | <?php function makeRange( $length ) { for ( $i = 0; $i < $length ; $i ++) { yield $i ; } } foreach (makeRange(1000000) as $i ) { echo $i , PHP_EOL; } |
具体场景:使用生成器处理CSV文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php function getRows( $file ) { $handle = fopen ( $file , 'rb' ); if ( $handle === false) { throw new Exception(); } while ( feof ( $handle ) === false) { yield fgetcsv ( $handle ); } } foreach (getRows( 'data.csv' ) as $row ) { print_r( $row ); } |
处理这种场景,习惯的处理方法是先读取文件的所有内容放到数组中,然后再做处理等等。这种的处理存在的问题是:当文件特别大,一次读取就占用很多内存资源。而生成器最适合这种场景,因为这样占用的系统内存量极少。
5. 闭包
理论上,闭包和匿名函数是不同的概念。不过,PHP将其视作相同的概念。
简单闭包
1 2 3 4 5 6 7 | <?php $closure = function ( $name ) { return sprintf( 'Hello %s' , $name ); } echo $closure ( "Beck" ); // 输出 --> “Hello Beck” |
注意:我们之所以能调用$closure变量,是因为这个变量的值是个闭包,而且闭包对象实现了__invoke()魔术方法。只要变量名后有(),PHP就会查找并调用__invoke()方法。
附加状态
使用use关键字可以把多个参数传入闭包,此时要像PHP函数或方法的参数一样,使用逗号分隔多个参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php function enclosePerson( $name ) { return function ( $doCommand ) use ( $name ) { return sprintf( '%s, %s' , $name , $doCommand ); }; } // 把字符串“Clay”封装在闭包中 $clay = enclosePerson( 'Clay' ); // 传入参数,调用闭包 echo $clay ( 'get me sweet tea!' ); // 输出 --> "Clay, get me sweet tea!" |
使用bindTo()方法附加闭包的状态
PHP框架经常使用bindTo()方法把路由URL映射到匿名回调函数上,框架会把匿名函数绑定到应用对象上,这么做可以在这个匿名函数中使用$this关键字引用重要的应用对象。例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <?php class App { protected $routes = array (); protected $responseStatus = '200 OK' ; protected $responseContentType = 'text/html' ; protected $responseBody = 'Hello world' ; public function addRoute( $routePath , $routeCallback ) { $this ->routes[ $routePath ] = $routeCallback ->bindTo( $this , __CLASS__ ); //重点 } public function dispatch( $currentPath ) { foreach ( $this ->routes as $routePath => $callback ) { if ( $routePath === $currentPath ) { $callback (); } } header( 'HTTP/1.1' . $this ->responseStatus); header( 'Content-type:' . $this ->responseContentType); header( 'Content-length' . mb_strlen( $this ->responseBody)); echo $this ->responseBody; } } |
第11行是重点所在,把路由回调绑定到了当前的App实例上。这么做能在回调函数中处理App实例的状态:
1 2 3 4 5 6 7 | <?php $app = new App(); $app ->addRoute( '/users/josh' , function () { $this ->responseContentType = 'application/json;charset=utf8' ; $this ->responseBody = '{"name": "Josh"}' ; }); $app ->dispatch( '/users/josh' ); |
6. Zend OPcache
字节码缓存不是PHP的新特性,很多独立的扩展可以实现缓存。从PHP5.5.0开始,PHP内置了字节码缓存功能,名为Zend OPcache。
字节码缓存是什么
PHP是解释性语言,PHP解释器执行PHP脚本时会解析PHP脚本代码,把PHP代码编译成一系列Zend操作码,然后执行字节码。每次请求PHP文件都是这样,会消耗很多资源。字节码缓存能存储预先编译好的PHP字节码。这意味着,请求PHP脚本时,PHP解释器不用每次都读取、解析和编译PHP代码。这样能极大地提升应用的性能。
7. 内置的HTTP服务器
从PHP5.4.0起,PHP内置了Web服务器,这对众多使用Apache或nginx的php开发者来说,可能是个隐藏功能。不过,这个内置的服务器功能并不完善,不应该在生产环境中使用,但对本地开发来说是个便利的工具,可以用于快速预览一些框架和应用。
启动服务器
1 | php -S localhost:4000 |
配置服务器
1 | php -S localhost:8000 -c app/config/php.ini |
路由器脚本
与Apache和nginx不同,它不支持.htaccess文件。因此,这个服务器很难使用多数流行的PHP框架中常见的前端控制器。PHP内置的服务器使用路由器脚本弥补了这个遗漏的功能。处理每个HTTP请求前,会先经过这个路由器脚本,如果结果为false,返回当前HTTP请求中引用的静态资源URI。
1 | php -S localhost:8000 route.php |
是否为内置的服务器
1 2 3 4 | <?php if (php_sapi_name() === 'cli-server' ) { // php 内置的web服务器 } |
二、标准 如果你了解PHP-FIG和PSR可以跳过这部分
PHP组件和框架的数量很多,随之产生的问题就是:单独开发的框架没有考虑到与其他框架的通信。这样对开发者和框架本身都是不利的。
打破旧局面的PHP-FIG
多位PHP框架的开发者认识到了这个问题,在2009年的 php|tek(一个受欢迎的PHP会议)上谈论了这个问题。经过讨论后得出:我们需要一个标准,用来提高框架的互操作性。于是这几位在php|tek意外碰头的PHP框架开发者组织了PHP Framework Interop Group,简称PHP-FIG。
PHP-FIG是框架代表自发组织的,其成员不是选举产生的,任何人都可以申请加入PHP-FIG,并且能对处于提议阶段的推荐规范提交反馈。另外,PHP-FIG发布的是推荐规范,而不是强制规定。
1.PSR是什么?
PSR是PHP Standards Recommendation(PHP推荐标准)的简称。截至今日,PHP-FIG发布了五个推荐规范:
你会发现只有四个,没错,因为第一份推荐规范PSR-0废弃了,新发布的PSR-4替代了。
2.PSR-1:基本的代码风格
如果想编写符合社区标准的PHP代码,首先要遵守PSR-1。遵守这个标准非常简单,可能你已经再使用了。标准的细节就不写啦,点链接就能看。
3.PSR-2:严格的代码风格
PSR-2是在PSR-1的基础上更进一步的定义PHP代码规范。这个标准解决了很多世纪问题哈,比如缩进,大括号等等。细节也不多记录啦。
另外,现在很多IDE(比如,PHPStorm)会有代码格式化功能,设置代码格式化的标准,编写完代码,然后全部格式化,可以帮助你遵循推荐规范,修复一些换行、缩进、大括号等细节。
4.PSR-3:日志记录器接口
这个推荐规范与前两个不同,这是一个接口,规定PHP日志记录器组件可以实现的方法。符合PSR-3推荐规范的PHP日志记录器组件,必须包含一个实现Psr\Log\LoggerInterface接口的PHP类。PSR-3接口复用了RFC 5424系统日志协议,规定要实现的九个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php namespace Psr\Log; interface LoggerInterface { public function emergency( $message , array $context = array ()); public function alert( $message , array $context = array ()); public function critical( $message , array $context = array ()); public function error( $message , array $context = array ()); public function warning( $message , array $context = array ()); public function notice( $message , array $context = array ()); public function info( $message , array $context = array ()); public function debug( $message , array $context = array ()); public function log( $level , $message , array $context = array ()); } |
每个方法对应RFC 5424协议的一个日志级别。
使用PRS-3日志记录器
如果你正在编写自己的PSR-3日志记录器,可以停下来了。因为已经有一些十分出色的日志记录器组件。比如:monolog/monolog,直接用就可以了。如果不能满足要求,也建议在此基础上做扩展。
5.PSR-4:自动加载器
这个推荐规范描述了一个标准的自动加载器策略。自动加载器策略是指,在运行时按需查找PHP类,接口或性状,并将其载入PHP解释器。
为什么自动加载很重要
在PHP文件的顶部你是不是经常看到类似下面的代码?
1 2 3 4 | <?php include 'path/to/file1.php' ; include 'path/to/file2.php' ; include 'path/to/file3.php' ; |
如果只需载入几个PHP脚本,使用这些函数(include()、include_once()、require()、require_once())能很好的完成工作。可是如果你要引入一千个PHP脚本呢?
在PSR-4推荐规范之前,PHP组件和框架的作者使用__autoload()和spl_autoload_register()函数注册自定义的自动加载器策略。可是,每个PHP组件和框架的自动加载器都使用独特的自动加载器。因此,使用的组件多的时候,也是很麻烦的事情。
推荐使用PSR-4自动加载器规范,就是解决这个问题,促进组件实现互操作性。
PSR-4自动加载器策略
PSR-4推荐规范不要求改变代码的实现方式,只建议如何使用文件系统目录结构和PHP命名空间组织代码。PSR-4的精髓是把命名空间的前缀和文件系统中的目录对应起来。比如,我可以告诉PHP,\Oreilly\ModernPHP命名空间中的类、接口和性状在物理文件系统的src/目录中,这样PHP就知道,前缀为\Oreilly\ModernPHP的命名空间中的类、接口和性状对应的src/目录里的目录和文件。
如何编写PSR-4自动加载器
如果你在写自己的PSR-4自动加载器,请停下来。我们可以使用依赖管理器Composer自动生成的PSR-4自动加载器。
三、良好实践
1.过滤、验证和转义
过滤HTML
使用htmlentities()函数过滤输入。
1 2 3 | <?php $input = '<p><script>alert("You won the Nigerian lottery!");</script></p>' ; echo htmlentities( $input , ENT_QUOTES, 'UTF-8' ); |
需要注意的是:默认情况下,htmlentities()函数不会转义单引号,而且也检测不出输入字符串的字符集。正确的使用方式是:第一个参数输入字符串;第二个参数设为ENT_QUOTES常量,转移单引号;第三个参数设为输入字符串的字符集。
更多过滤HTML输入的方式,可以使用HTML Purifier库。这个库强健且安全,缺点:慢,且可能难以配置。
SQL查询
构建SQL查询不好的方式:
1 2 3 4 5 | $sql = sprintf( 'UPDATE users SET password = "%s" WHERE id = %s' , $_POST [ 'password' ], $_GET [ 'id' ] ); |
如果 psasword=abc";--
,则导致修改了整个users表的记录password都未abc。如果需要在SQL查询中使用输入数据,要使用PDO预处理语句。
用户资料信息
A.过滤用户资料中的电子邮件地址
这里会删除除字符、数字和!#$%&'*+-/=?^_{|}~@.[]`之外的所有其他符号。
1 2 3 | <?php $email = 'beckjiang@meijiabang.cn' ; $emailSafe = filter_var( $email , FILTER_SANITIZE_EMAIL); |
B.过滤用户资料中的外国字符
1 2 3 4 5 6 7 | <?php $string = "外国字符" ; $safeString = filter_var( $string , FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW|FILTER_FLAG_ENCODE_HIGH ); |
验证数据
验证数据与过滤不同,验证不会从输入数据中删除信息,而是只确认输入数据是否符合预期。
验证电子邮件地址
我们可以把某个FILTER_VALIDATE_*标志传给filter_var()函数,除了电子邮件地址,还可以验证布尔值、浮点数、整数、IP地址、正则表达式和URL。
1 2 3 4 5 6 7 8 | <?php $input = 'beckjiang@meijiabang.cn' ; $isEmail = filter_var( $input , FILTER_VALIDAE_EMAIL); if ( $isEmail !== false) { echo "Success" ; } else { echo "Fail" ; } |
2.密码
哈希算法有很多种,例如:MD5、SHA1、bcrypt和scrypt。有些算法的速度很快,用于验证数据完整性;有些算法速度则很慢,旨在提高安全性。生成密码和存储密码时需要使用速度慢、安全性高的算法。
目前,经同行审查,最安全的哈希算法是bcrypt。与MD5和SHA1不同,bcrypt是故意设计的很慢。bcrypt算法会自动加盐,防止潜在的彩虹表攻击。bcrypt算法永不过时,如果计算机的运算速度变快了,我们只需提高工作因子的值。
重新计算密码的哈希值
下面是登录用户的脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | <?php session_start(); try { // 从请求主体中获取电子邮件地址 $email = filter_input(INPUT_POST, 'email' ); // 从请求主体中获取密码 $password = filter_input(INPUT_POST, 'password' ); // 使用电子邮件地址获取用户(注意,这是虚构代码) $user = User::findByEmail( $email ); // 验证密码和账户的密码哈希值是否匹配 if (password_verify( $password , $user ->password_hash) === false) { throw new Exception( 'Invalid password' ); } // 如果需要,重新计算密码的哈希值 $currentHashAlgorithm = PASSWORD_DEFAULT; $currentHashOptions = array ( 'cost' => 15); $passwordNeedRehash = password_needs_rehash( $user ->password_hash, $currentHashAlgorithm , $currentHashOptions ); if ( $passwordNeedsRehash === true) { // 保存新计算得出的密码哈希值(注意,这是虚构代码) $user ->password_hash = password_hash( $password , $currentHashAlgorithm , $currentHashOptions ); $user ->save(); } // 把登录状态保存到回话中 ... // 重定向到个人资料页面 ... } catch (Exception $e ) { //异常处理 ... } |
值得注意的是:在登录前,一定要使用password_needs_rehash()函数检查用户记录中现有的密码哈希值是否过期。如果过期了,要重新计算密码哈希值。
PHP5.5.0之前的密码哈希API
如果无法使用PHP5.5.0或以上版本,可以使用安东尼·费拉拉开发的ircmaxell/password-compat组件。这个组件实现了PHP密码哈希API中的所有函数:
- password_hash()
- password_get_info()
- password_needs_rehash()
- password_verify()
3.日期、时间和时区
DateTime类
DateTime类提供一个面向对象接口,用于管理日期和时间。
没有参数,创建的是一个表示当前日期和时间的实例:
1 2 | <?php $datetime = new DateTime(); |
传入参数创建实例:
1 2 | <?php $datetime = new DateTime( '2017-01-28 15:27' ); |
指定格式,静态构造:
1 2 | <?php $datetime = DateTime::createFromFormat( 'M j, Y H:i:s' , 'Jan 2, 2017 15:27:30' ); |
DateInterval类
DateInterval实例表示长度固定的时间段(比如,“两天”),或者相对而言的时间段(比如,“昨天”)。DateInterval实例用于修改DateTime实例。
使用DateInterval类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php // 创建DateTime实例 $datetime = new DateTime(); // 创建长度为两周的间隔 $interval = new DateInterval( 'P2W' ); // 修改DateTime实例 $datetime ->add( $interval ); echo $datetime ->format( 'Y-m-d H:i:s' ); 创建反向的DateInterval实例: <?php // 过去一天 $interval = new DateInterval( '-1 day' ); |
DateTimeZone类
如果应用要迎合国际客户,可能要和时区斗争。
创建、使用时区:
1 2 3 | <?php $timezone = new DateTimeZone( 'America/New_York' ); $datetime = new DateTime( '2017-01-28' , $timezone ); |
实例化之后,也可以使用setTimeZone()
函数设置市区:
1 | $datetime ->setTimeZone( new DateTimeZone( 'Asia/Hong_Kong' )); |
DatePeriod类
有时我们需要迭代处理一段时间内反复出现的一系列日期和时间,DatePeriod类可以解决这种问题。DatePeriod类的构造方法接受三个参数,而且都必须提供:
- 一个Datetime实例,表示迭代开始时的日期和时间。
- 一个DateInterval实例,表示到下个日期和时间的间隔。
- 一个整数,表示迭代的总次数。
DatePeriod实例是迭代器,每次迭代时都会产出一个DateTime实例。
使用DatePeriod类:
1 2 3 4 5 6 7 8 | <?php $start = new DateTime(); $interval = new DateInterval( 'P2W' ); $period = new DatePeriod( $start , $interval , 3); foreach ( $period as $nextDateTime ) { echo $nextDateTime ->format( 'Y-m-d H:i:s' ), PHP_EOL; } |
4.数据库
PHP应用可以在很多种数据库中持久保存信息,比如:MySQL、SQLite、Oracle等。如果在项目中使用多种数据库,需要安装并学习多种PHP数据库扩展和接口,这增加了认知和技术负担。
正是基于这个原因,PHP原生提供了PDO扩展(PHP Data Objects,意思是PHP数据对象),PDO是一系列PHP类,抽象了不同数据库的具体实现。PDO的介绍和使用就不写了,比较常用。
5.流
在现代的PHP特性中,流或许是最出色但最少使用的。虽然PHP4.3.0就引入了流,但很多开发者不知道流的存在,因为很少人提及流,而且流的文档也匮乏。官方的解释比较难理解,一句话说就是:流的作用是在出发地和目的地之间传输数据。
我把流理解为管道,相当于把水从一个地方引到另一个地方。在水从出发地流到目的地的过程中,我们可以过滤水,可以改变水质,可以添加水,也可以排出水(提示:水是数据的隐喻)。
流封装协议
流式数据的种类各异,每种类型需要独特的协议,以便读写数据。我们称这些协议为流封装协议。比如,我们可以读写文件系统,可以通过HTTP、HTTPS或SSH与远程Web服务器通信,还可以打开并读写ZIP、RAR或PHAR压缩文件。这些通信方式都包含下述相同的过程:
- 开始通信。
- 读取数据。
- 写入数据。
- 结束通信。
虽然过程一样的,但是读写文件系统中文件的方式与手法HTTP消息的方式有所不同。流封装协议的作用是使用通用的几口封装这些差异。
每个流都有一个协议和一个目标。格式如下:
1 | <scheme>: //<target> |
说这么多有点懵,先看例子,使用HTTP流封装协议与Flickr API通信:
1 2 3 4 | <?php $json = file_get_contents ( 'http://api.flickr.com/services/feeds/photos_public.gne?format=json' ); |
不要误以为这是普通的网页URL,file_get_contents()函数的字符串参数其实是一个流标识符。http协议会让PHP使用HTTP流封装协议。看起来像是普通的网页URL,是因为HTTP流封装协议就是这样规定的:)。其他流封装协议可能不是这样。
file://流封装协议
我们使用file_get_contents()
,fopen()
,fwrite()
和fclose()
函数读写文件系统。因为PHP默认使用的流封装协议是file://
,所以我们很少认为这些函数使用的是PHP流。
隐式使用file://
流封装协议:
1 2 3 4 5 6 | <?php $handle = fopen ( '/etc/hosts' , 'rb' ); while ( feof ( $handle ) !== true) { echo fgets ( $handle ); } fclose( $handle ); |
显式使用file://
流封装协议:
1 2 3 4 5 6 | <?php $handle = fopen ( 'file:///etc/hosts' , 'rb' ); while ( feof ( $handle ) !== true) { echo fgets ( $handle ); } fclose( $handle ); |
流上下文
有些PHP流能接受一些列可选的参数,这些参数叫流上下文,用于定制流的行为。流上下文使用stream_context_create()
函数创建。
比如,你知道可以使用file_get_contents()
函数发送HTTP POST请求吗?如果想这么做,可以使用一个流上下文对象:
1 2 3 4 5 6 7 8 9 10 11 | <?php $requestBody = '{"username": "beck"}' ; $context = stream_context_create( array ( 'http' => array ( 'method' => 'POST' , 'header' => "Content-Type: application/json;charset=utf-8;\r\n" . "Content-Length: " . mb_strlen( $requestBody ), "content" => $requestBody ) )); $response = file_get_contents ( 'https://my-api.com/users' , false, $context ); |
流过滤器
关于PHP的流,其实真正强大的地方在于过滤、转换、添加或删除流中传输的数据。
注意:PHP内置了几个流过滤器:string.rot13、string.toupper、string.tolower和string.strp_tags。这些过滤器没什么用,我们要使用自定义的过滤器。
若想把过滤器附加到现有的流上,要使用stream_filter_append()
函数。比如,想要把文件中的内容转换成大写字母,可以使用string.toupper过滤器。书中不建议使用这个过滤器,这里只是演示如何把过滤器附加到流上:
1 2 3 4 5 6 7 | <?php $handle = fopen ( 'data.txt' , 'rb' ); stream_filter_append( $handle , 'string.toupper' ); while ( feof ( $handle ) !== true) { echo fgets ( $handle ); // <-- 输出的全是大写字母 } fclose( $handle ); |
使用php://filter
流封装协议把过滤器附加到流上:
1 2 3 4 5 6 | <?php $handle = fopen ( 'php://filter/read=string.toupper/resource=data.txt' , 'rb' ); while ( feof ( $handle ) !== true) { echo fgets ( $handle ); // <-- 输出的全是大写字母 } fclose( $handle ); |
来看个更实际的流过滤器示例,假如我们nginx访问日志保存在rsync.net,一天的访问情况保存在一个日志文件中,而且会使用bzip2压缩每个日志文件,名称格式为:YYYY-MM-DD.log.bz2。某天,领导让我提取过去30天某个域名的访问数据。使用DateTime类和流过滤器迭代bzip压缩的日志文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php $dateStart = new \DateTime(); $dateInterval = \DateInterval::createFromDateString( '-1 day' ); $datePeriod = new \DatePeriod( $dateStart , $dateInterval , 30); //创建迭代器 foreach ( $datePeriod as $date ) { $file = 'sftp://USER:PASS@rsync.net/' . $date ->format( 'Y-m-d' ) . 'log.bz2' ; if ( file_exists ( $file )) { $handle = fopen ( $file , 'rb' ); stream_filter_append( $handle , 'bzip2.decompress' ); while ( feof ( $handle ) !== true) { $line = fgets ( $handle ); if ( strpos ( $line , 'www.example.com' ) !== false) { fwrite(STDOUT, $line ); } } fclose( $handle ); } } |
计算日期范围,确定日志文件的名称,通过FTP连接rsync.net,下载文件,解压缩文件,逐行迭代每个文件,把相应的行提取出来,然后把访问数据写入一个输出目标。使用PHP流,不到20行代码就能做完所有这些事情。
自定义流过滤器
其实大多数情况下都要使用自定义的流过滤器。自定义的流过滤器是个PHP类,继承内置的php_user_filter类。这个类必须实现filter()
、onCreate()
和onClose()
方法。而且,必须使用stream_filter_register()
函数注册自定义的流过滤器。
1 | PHP流会把数据分成按次序排列的桶,一个桶中盛放的流数据量是固定的。一定时间内过滤器接收到的桶叫做桶队列。桶队列中的每个桶对象都有两个公开属性:data和datalen,分别是桶中的内容和内容的长度。 |
下面定义一个处理脏字的流过滤器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <?php class DirtyWordsFilter extends php_user_filter { /** * @param resource $in 流来的桶队列 * @param resource $out 流走的桶队列 * @param resource $consumed 处理的字节数 * @param resource $closing 是流中最后一个桶队列吗? */ public function filter() { $words = array ( 'grime' , 'dirt' , 'grease' ); $wordData = array (); foreach ( $words as $word ) { $replacement = array_fill (0, mb_strlen( $word ), '*' ); $wordData [ $word ] = implode( ' ' , $replacement ); } $bad = array_keys ( $wordData ); $goods = array_values ( $wordData ); // 迭代流来的桶队列中的每个桶 while ( $bucket = stream_bucket_make_writeable( $in )) { // 审查桶数据中的脏字 $bucket ->data = str_replace ( $bad , $goods , $bucket ->data); // 增加已处理的数据量 $consumed += $bucket ->datalen; // 把桶放入流向下游的队列中 stream_bucket_append( $out , $bucket ); } return PSFS_PASS_ON; } } |
filter()
方法的作用是接受、处理再转运桶中的流数据。这个方法的返回值是PSFS_PASS_ON
常量,表示操作成功。
注册流过滤器
接着,我们必须使用stream_filter_register()
函数注册这个自定义的DirtWordsFilter流过滤器:
1 2 | <?php stream_filter_register( 'dirty_words_filter' , 'DirtWordsFilter' ); |
第一个参数是用于识别这个自定义过滤器的过滤器名,第二个参数是自定义过滤器的类名。
使用DirtWordsFilter流过滤器
1 2 3 4 5 6 7 | <?php $handle = fopen ( 'data.txt' , 'rb' ); stream_filter_append( $handle , 'dirty_words_filter' ); while ( feof ( $handle ) !== true) { echo fgets ( $handle ); // <-- 输出审查后的文本 } fclose( $handle ); |
6.错误与异常
对错误和异常的处理,一定要遵守四个规则:
- 一定要让PHP报告错误。
- 在开发环境中要显示错误。
- 在生产环境中不能显示错误。
- 在开发环境和生产环境中都要记录错误。
错误与异常在日常使用的比较多,就不记录啦!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET 9 new features-C#13新的锁类型和语义
· Linux系统下SQL Server数据库镜像配置全流程详解
· 现代计算机视觉入门之:什么是视频
· 你所不知道的 C/C++ 宏知识
· 聊一聊 操作系统蓝屏 c0000102 的故障分析
· DeepSeek V3 两周使用总结
· 回顾我的软件开发经历(1)
· C#使用yield关键字提升迭代性能与效率
· 低成本高可用方案!Linux系统下SQL Server数据库镜像配置全流程详解
· 4. 使用sql查询excel内容