在过去的一个月里,我和我的团队开始着手开发ZendFramework2.0,我一直沉浸在PHP5.3中。PHP5.3提供了大量新的语言特性,其中许多是为了帮助框架和库开发人员而开发的。大多数时候,这些功能都很简单,您可以简单地使用它们;然而,在其他情况下,我们会遇到意想不到的行为。这篇文章将详细介绍其中的几个,因此您要么不会遇到相同的问题,要么可以利用我们的一些发现。
闭包、匿名函数和Lambda,我的天啊!
简而言之,这些都是单个PHP结构匿名函数的同义词(上下文略有不同):
$callback = function ($param) { // do something };
您可以将匿名函数分配给变量,或将其作为回调参数内联传递给函数或方法调用。该构造实现了一些非常灵活的设计,并且对于各种数组函数和preg_replace_callback()
特别有用。如果您在代码库中看到任何create_function
构造,请立即用匿名函数替换它们;它们不仅更易于阅读(在create_function()
中转义代码内容总是很痛苦),而且速度更快,并且如果可用的话还可以从操作码缓存中获益。
不过,我们发现了一个有趣的问题。PHP不喜欢序列化闭包;这样做会引发异常(“不允许序列化‘Closure’”)。这有很多含义:
- 如果您需要更改SPL自动加载器堆栈,请小心使用闭包。例如,我们的测试台通过存储
spl_autoload_functions()
的返回值来缓存自动加载器,然后在测试期间重置它。不幸的是,如果您使用spl_autoload_register
注册闭包,您可能会在执行此操作时遇到错误。(注意:这似乎已在5.3.2及更高版本中修复。) - 如果您正在序列化具有引用闭包属性的类,则需要向
__sleep()
和__wakeup()
以确保这些属性未被序列化,并在反序列化时重新创建它们。
此外,即使内部匿名函数通过Closure
类表示,您也不能对该类进行类型提示;测试变量是否为闭包的唯一方法是使用is_callable()
。
可调用对象
PHP5的一个有趣的新特性是魔法方法__invoke()
,它允许您像调用函数一样调用对象:
class Greeting { public function __invoke($name) { return "Hello, $name"; } } $greeting = new Greeting; echo $greeting('world'); // "Hello, world"
与其他魔术方法不同,它实际上比替代方法更快。当简单地返回一个值时,它比在同一对象上调用方法快25%;当与call_user_func_array()
一起使用时,它比使用普通的数组式回调(例如,array($o,'greet')
)快30%—即使它“代理到另一种方法!
所以,听起来像是一个很棒的新功能,对吧?是的……但是有些事情你应该知道。
-
像闭包一样,您不能为
__invoke()
显式键入提示;您必须使用is_callable()
或创建定义它的接口:interface Filter { public function filter($value); } interface CallableFilter { public function __invoke($value); } class IntFilter implements Filter, CallableFilter { public function filter($value) { return (int) $value; } public function __invoke($value) { return $this->filter($value); } } $filter = new IntFilter; if ($filter instanceof CallableFilter) { // matches }
-
小心关于使用实现
__invoke()
的对象作为对象属性;他们不做你期望的。例如,请考虑以下内容:class Foo { public function __invoke() { return 'foo'; } } class Bar { public $foo; public function __construct() { $this->foo = new Foo; } } $bar = new Bar; echo $bar->foo();
您可能希望它回显“foo”——但它不会。相反,它会引发一个
E_FATAL
,声称“调用未定义的方法Bar::foo()”。如果要执行该属性,则必须先将其分配给一个临时变量,或者显式调用__invoke()
:$foo = $bar->foo; echo $foo(); // or: $bar->foo->__invoke();
命名空间的乐趣和利润
请搁置您对在PHP中选择名称空间分隔符的意见;此时它是桥下的水,并且有很好的选择技术原因。我们有一个实现,所以让我们使用它吧。
首先,您在文件顶部声明您的命名空间:
namespace Zend\Filter;
或者你可以在同一个文件中有多个命名空间,只要你没有松散的代码:
namespace Zend\Filter; // some namespaced code here... namespace Zend\Validator; // some namespaced code here...
虽然上述内容有效,但如果您在单个文件中使用多个命名空间,PHP手册建议使用大括号:
namespace Zend\Filter { // some namespaced code here... } namespace Zend\Validator { // some namespaced code here... }
您可以使用use
构造从其他命名空间导入代码。此结构还允许您使用as
修饰符别名命名空间(或命名空间中的类、常量或函数):
namespace Foo; use Zend\Filter; use Zend\Validator\Int as IntValidator; $validator = new IntValidator; // Zend\Validator\Int if ($validator->isValid($foo) { $filter = new Filter\Int(); // Zend\Filter\Int echo $filter($foo); }
关于命名空间的一些快速规则:
-
完全限定的命名空间(FQN)以命名空间分隔符(
\\
)开头。类、函数、常量和静态成员引用使用FQN将始终解析。 -
命名空间声明始终被认为是完全限定的,并且不应不以命名空间分隔符作为前缀。
-
use
语句中引用的命名空间始终被认为是完全合格的;您可以使用命名空间分隔符作为前缀,但这不是必需的。 -
在命名空间中引用命名空间类时,请注意来源:如果您不这样做’完全限定命名空间,假设将是:
- 当前命名空间的子命名空间
- 对导入时定义的别名之一的引用
例如,考虑以下代码:
namespace Foo; use Zend\Filter; // imports are always considered FQN $foo = new Bar\Baz; // actual; Foo\Bar\Baz $filter = new Filter\Int; // actual; Zend\Filter\Int $validator = new Zend\Validator\Int; // actual: Foo\Zend\Validator\Int $validator = new \Zend\Validator\Int; // actual: Zend\Validator\Int
我们的一个发现是,您可以拥有一个名称空间,该名称空间与类的接口共享相同的名称。例如:
namespace Foo { interface Adapter { // definition here... } } namespace Foo\Adapter { use Foo\Adapter as FooAdapter; class Concrete implements FooAdapter { // ... } }
这一发现使我们能够在组件中定义更多“顶级”接口,并在与接口匹配的名称空间中实现具体实现。这减少了一些冗长的文字,定义了更好的类层次结构,并使代码关系更加语义化。
最后,我们发现命名空间的一个巨大好处是在单元测试时:我们可以为单元测试定义一个单独的命名空间,以及为每个组件定义单独的命名空间。如果我们将这些命名空间用于测试工件——单元测试使用的类和模拟适配器——我们确保每个测试套件都被完全封装。这减少了命名冲突问题。
在结束…
PHP5.3提供了大量新功能——我在这里介绍的只是一些比较突出的功能。如果您还没有开始使用5.3,您应该开始—它绝对是PHP的未来,您将看到越来越多的库和框架使用它。