一、PHP 中的一等函数

函数式编程,顾名思义,是围绕函数展开的。为了有效地应用函数技术,一种语言必须支持作为一等公民的函数,或者支持第一函数

这意味着函数被视为与任何其他值一样。它们可以创建并作为参数传递给其他函数,还可以用作返回值。幸运的是,PHP 就是这样一种语言。本章将演示创建和使用函数的各种方式。

在本章中,我们将介绍以下主题:

  • 声明函数和方法
  • 标量类型提示
  • 匿名函数
  • 闭包
  • 使用对象作为函数
  • 高阶函数
  • 可调用类型提示

在我们开始之前

由于 PHP7 的第一个版本发生在 2015 年 12 月,因此本书中的示例将使用该版本。

然而,由于它是一个相当新的版本,每次我们使用一个新特性时,它都会被清楚地概述和解释。此外,由于不是每个人都能够立即迁移,因此只要有可能,就会提出在 PHP5 上运行代码所需的更改。

撰写本文时可用的最新版本是 7.0.9。所有代码和示例都使用此版本进行验证。

编码标准

本书中的示例将尊重PSR-2PHP 标准建议 2)及其父级建议 PSR-1 的编码风格,正如所展示的大多数库一样。对于不熟悉它们的人,以下是最重要的部分:

  • 类位于命名空间中,并使用大写的第一个字母作为大小写
  • 方法不使用首字母大写的 CamelCase
  • 常量用大写字母书写
  • 类和方法的大括号位于新行,其他大括号位于同一行

此外,尽管 PSR-2 中没有定义,但做出了以下选择:

  • 函数名以 snake_ 的形式出现
  • 参数、变量和属性名称以 snake_ 为例
  • 只要可能,财产都是私有的

自动加载和编写

这些示例还将假设存在与 PSR-4 兼容的自动装弹机。

由于我们将使用 Composer dependency manager 安装提供的库,因此建议将其用作自动加载程序。

功能与方法

虽然这本书不是为 PHP 初学者设计的,但为了确保我们共享一个共同的词汇表,我们将快速介绍一些基础知识。

在 PHP 中,通常使用function关键字声明函数:

<?php 

function my_function($parameter, $second_parameter) 
{ 
    // [...] 
} 

在类中声明的函数称为方法。它不同于传统函数,因为它可以访问对象属性,具有可见性修饰符,并且可以声明为静态。由于我们将尝试编写尽可能纯净的代码,因此我们的属性将为private类型:

<?php 

class SomeClass 
{ 
   private $some_property; 

   // a public function 
   public function some_function() 
   { 
       // [...] 
   } 

   // a protected static function 
   static protected function other_function() 
   { 
       // [...] 
   } 
} 

PHP7 标量类型提示

您已经能够在 PHP5 中声明类、可调用项和数组的类型提示。PHP7 引入了标量类型提示的思想。这意味着您现在可以说您想要一个string、一个int、一个float或一个bool数据类型,用于参数和返回类型。语法与其他语言中的语法大致相似。

与类类型提示相反,您还可以选择两种模式:严格模式和非严格模式,后者是默认模式。这意味着 PHP 将尝试将值强制转换为所需的类型。如果没有信息丢失,则强制转换将以静默方式进行,否则将发出警告。这可能会导致与字符串到数字转换或真值和假值相同的奇怪结果。

以下是此类类型转换的一些示例:

<?php 

function add(float $a, int $b): float { 
    return $a + $b; 
} 

echo add(3.5, 1); 
// 4.5 
echo add(3, 1); 
// 4 
echo add("3.5", 1); 
// 4.5 
echo add(3.5, 1.2); // 1.2 gets casted to 1 
// 4.5 
echo add("1 week", 1); // "1 week" gets casted to 1.0 
// PHP Notice:  A non well formed numeric value encountered 
// 2 
echo add("some string", 1); 
// Uncaught TypeError Argument 1 passed to add() must be of the type float, string given 

function test_bool(bool $a): string { 
    return $a ? 'true' : 'false'; 
} 

echo test_bool(true); 
// true 
echo test_bool(false); 
// false 
echo test_bool(""); 
// false 
echo test_bool("some string"); 
// true 
echo test_bool(0); 
// false 
echo test_bool(1); 
// true 
echo test_bool([]); 
// Uncaught TypeError: Argument 1 passed to test_bool() must be of the type Boolean 

如果你想避免铸造问题,你可以选择严格模式。这样,每次值不完全符合所需类型时,PHP 都会引发错误。为此,必须将declare(strict_types=1)指令添加到文件的第一行。没有任何事情必须在它之前。

PHP 允许自己进行的唯一转换是从intfloat添加.0,因为绝对没有数据丢失的风险。

以下是与之前相同的示例,但已激活严格模式:

<?php 

declare(strict_types=1); 

function add(float $a, int $b): float { 
    return $a + $b; 
} 

echo add(3.5, 1); 
// 4.5 
echo add(3, 1); 
// 4 
echo add("3.5", 1); 
// Uncaught TypeError: Argument 1 passed to add() must be of the type float, string given 
echo add(3.5, 1.2); // 1.2 gets casted to 1 
// Uncaught TypeError: Argument 2 passed to add() must be of the type integer, float given 
echo add("1 week", 1); // "1 week" gets casted to 1.0 
// Uncaught TypeError: Argument 1 passed to add() must be of the type float, string given 
echo add("some string", 1); 
// Uncaught TypeError: Argument 1 passed to add() must be of the type float, string given 

function test_bool(bool $a): string { 
    return $a ? 'true' : 'false'; 
} 

echo test_bool(true); 
// true 
echo test_bool(false); 
// false 
echo test_bool(""); 
// Uncaught TypeError: Argument 1 passed to test_bool() must be of the type boolean, string given 
echo test_bool(0); 
// Uncaught TypeError: Argument 1 passed to test_bool() must be of the type boolean, integer given 
echo test_bool([]); 
// Uncaught TypeError: Argument 1 passed to test_bool() must be of the type boolean, array given 

虽然这里没有演示,但相同的强制转换规则适用于返回类型。根据模式的不同,PHP 将愉快地执行与参数提示相同的强制转换,并显示相同的警告和错误。

另外,另一个微妙之处是应用的模式是在进行函数调用的文件顶部声明的模式。这意味着,当您调用在另一个文件中声明的函数时,不会考虑该文件所处的模式。只有当前文件顶部的指令才起作用。

关于类型引起的错误,我们将在第 3 章PHP 中的函数基础中看到 PHP7 中异常和错误处理是如何改变的,以及如何使用它来捕获这些异常和错误。

从现在开始,每次有意义的时候,我们的示例都将使用标量类型提示,以使代码更加健壮和可读。

强加于人的类型可能会被认为是很麻烦的,当你开始使用它们时,可能会导致一些不愉快,但从长远来看,我可以向你保证,它会让你避免一些讨厌的 bug。解释器可以完成的所有检查都不需要自己测试。

它还使您的功能更易于理解和推理。查看代码的人不必问自己值可能是什么,他们可以肯定地知道必须作为参数传递什么样的数据以及将得到什么。结果是,认知负担减轻了,您可以利用时间思考问题,而不是记住代码的琐碎细节。

匿名函数

您可能很清楚我们看到的声明函数的语法。您可能不知道的是,函数不一定需要有名称。

匿名函数可以分配给变量,用作回调并具有参数。

在 PHP 文档中,术语匿名函数可与术语闭包互换使用。正如我们将在下面的代码片段中看到的,匿名函数甚至是Closure类的一个实例,我们将对此进行讨论。根据学术文献,这两个概念虽然相似,但有点不同。术语闭包的首次使用是在 1964 年由 Peter Landin 在表达式的机械评估中提出的。本文将闭包描述为具有环境部分和控制部分。我们将在本节中声明的函数没有任何环境,因此严格来说,它们不是闭包。

为了避免阅读其他作品时产生混淆,本书将使用术语匿名函数来描述一个没有名字的函数,如本节所示:

<?php 

$add = function(float $a, float $b): float { 
    return $a + $b; 
}; 
// since this is an assignment, you have to finish the statement with a semicolon 

前面的代码段声明了一个匿名函数,并将其分配给一个变量,以便我们以后可以将其作为另一个函数的参数重用或直接调用它:

$add(5, 10); 
$sum = array_reduce([1, 2, 3, 4, 5], $add, 0); 

如果不打算重用匿名函数,也可以将其直接声明为参数:

<?php 
$uppercase = array_map(function(string $s): string { 
  return strtoupper($s); 
}, ['hello', 'world']); 

也可以像返回任何类型的值一样返回函数:

<?php 

function return_new_function() 
{ 
  return function($a, $b, $c) { /* [...] */}; 
} 

关闭

正如我们前面看到的,学术界对闭包的描述是一个可以访问外部环境的函数。在本书中,我们将继续使用这种语义,尽管 PHP 使用后面的术语调用匿名函数和闭包。

您可能熟悉 JavaScript 闭包,在这里您可以简单地使用外部范围中的任何变量,而无需执行任何特定操作。在 PHP 中,需要使用use关键字将现有变量导入匿名函数的作用域:

<?php 

$some_variable = 'value'; 

$my_closure = function() use($some_variable) 
{ 
  // [...] 
}; 

PHP 闭包使用早期绑定方法。这意味着闭包内的变量将具有该变量在创建闭包时所具有的值。如果随后更改变量,则不会从闭包内部看到更改:

<?php 

$s = 'orange'; 

$my_closure = function() use($s) { echo $s; }; 
$my_closure(); // display 'orange' 

$a = 'banana'; 
$my_closure(); // still display 'orange' 

您可以通过引用传递变量,以便在闭包中传播对变量的更改,但由于这是一本关于函数式编程的书,我们试图使用不可变的数据结构并避免出现状态,因此如何实现这一点留给读者作为练习。

请注意,将对象传递给闭包时,对对象中的属性所做的任何修改都可以在闭包内访问。当传递给闭包时,PHP 不会复制对象。

类内的闭包

如果在类中声明任何匿名函数,它将通过常用的$this变量自动访问实例引用。为了保持词汇表的连贯性,函数将自动成为一个闭包:

<?php 

class ClosureInsideClass 
{ 
    public function testing() 
    { 
        return function() { 
            var_dump($this); 
        }; 
    } 
} 

$object = new ClosureInsideClass(); 
$test = $object->testing(); 

$test(); 

如果要避免此自动绑定,可以声明静态匿名函数:

<?php 

class ClosureInsideClass 
{ 
    public function testing() 
    { 
        return (static function() { 
            // no access to $this here, the following line 
            // will result in an error. 
            var_dump($this); 
        }); 
    } 
}; 

$object = new ClosureInsideClass(); 
$test = $object->testing(); 

$test(); 

使用对象作为函数

有时,您可能希望将函数拆分为更小的部分,但不让每个人都可以访问这些部分。在这种情况下,您可以在任何对象上利用__invoke魔术方法,让您将实例用作函数,并将该助手函数作为私有方法隐藏在对象中:

<?php 

class ObjectAsFunction 
{ 
    private function helper(int $a, int $b): int 
    { 
        return $a + $b; 
    } 

    public function __invoke(int $a, int $b): int 
    { 
      return $this->helper($a, $b); 
    } 
} 

$instance = new ObjectAsFunction(); 
echo $instance(5, 10); 

将使用传递给实例的任何参数调用__invoke方法。如果需要,还可以向对象添加构造函数,并使用其中包含的任何方法和属性。只要尽量保持它的纯净,因为一旦你使用可变属性,你的函数就会很难理解。

闭包类

所有匿名函数实际上都是Closure类的实例。但是,如文件(中所述 http://php.net/manual/en/class.closure.php ),此类不使用上述__invoke方法;这是 PHP 解释器中的一个特例:

除了这里列出的方法外,这个类还有一个 _invoke 方法。这是为了与实现调用 magic 的其他类保持一致,因为此方法不用于调用函数。

类上的此方法允许您更改$this变量将绑定到闭包内的对象。甚至可以将对象绑定到在类外部创建的闭包。

如果您开始使用Closure类的特性,请记住call方法是最近在 PHP7 中添加的。

高阶函数

PHP 函数可以将函数作为参数,将返回函数作为返回值。执行上述任一操作的函数称为高阶函数。就这么简单。

事实上,如果您阅读以下代码示例,您将很快看到我们已经创建了多个高阶函数。您还将毫不奇怪地发现,您将学习的大多数函数技术都是围绕高阶函数展开的。

什么是可调用的?

callable是一种类型提示,可以用来强制函数的参数是可以调用的,比如函数。从 PHP 7 开始,它还可以用作返回值的类型提示:

<?php 

function test_callable(callable $callback) : callable { 
    $callback(); 
    return function() { 
        // [...] 
    }; 
} 

但是,类型提示不能强制执行的是可调用参数的数量和类型。但是保证有你可以打电话的东西已经很好了。

可调用文件可以采用多种形式:

  • 用于命名函数的字符串
  • 类方法或静态函数的数组
  • 匿名函数或闭包的变量
  • 具有__invoke方法的对象

让我们看看如何利用所有这些可能性。让我们从按名称调用简单函数开始:

$callback = 'strtoupper'; 
$callback('Hello World !'); 

我们也可以对类内的函数执行同样的操作。让我们用一些函数声明一个A类,并使用数组来调用它。

class A { 
    static function hello($name) { return "Hello $name !\n"; } 
    function __invoke($name) { return self::hello($name); } 
} 

// array with class name and static method name 
$callback = ['A', 'hello']; 
$callback('World'); 

使用字符串只适用于静态方法,因为其他方法需要使用对象作为上下文。在静态方法的情况下,您也可以直接使用一个简单的字符串,但是,这只能从 PHP7 开始工作;以前的版本不支持此语法:

$callback = 'A::hello'; 
$callback('World'); 

您可以很容易地在类实例上调用方法:

$a = new A(); 

$callback = [$a, 'hello']; 
$callback('World'); 

由于我们的A类有__invoke方法,我们可以直接将其作为callable使用:

$callback = $a; 
$callback('World'); 

您还可以使用匿名函数指定为callable的任何变量:

$callback = function(string s) { 
    return "Hello $s !\n"; 
} 
$callback('World'); 

PHP 还为您提供了两个助手,以call_user_func_arraycall_user_func的形式调用函数。它们将可调用作为参数,您也可以传递参数。对于第一个辅助对象,传递一个包含所有参数的数组;对于第二个,分别传递它们:

call_user_func_array($callback, ['World']); 

最后一点要注意的是,如果您使用的是callable类型提示:任何包含已声明函数名的字符串都被认为是有效的;这有时会导致一些意想不到的行为。

一个有点做作的例子是一个测试套件,您可以通过传递一些字符串并捕获结果异常来检查某些函数是否只接受有效的可调用项。在某个时候,您引入了一个库,而这个测试现在失败了,尽管两者应该是不相关的。所发生的事情是,所讨论的库使用字符串包含的确切名称声明了一个函数。现在,函数已存在,不再引发任何异常。

总结

在本章中,我们了解了如何创建新的匿名函数和闭包。您现在也熟悉了传递这些信息的各种方式。我们还了解了新的 PHP7 标量类型提示,这些提示帮助我们使程序更加健壮,以及callable类型提示,以便我们可以强制使用有效函数作为参数或返回值。

对于已经使用 PHP 一段时间的人来说,本章可能没有什么新内容。但我们现在有一个共同点,这将有助于我们进入功能性世界。

在介绍了 PHP 函数的基础知识之后,我们将在下一章中进一步了解与函数编程相关的基本概念。我们将看到,为了在函数代码库中真正有用,函数必须遵守某些规则。