PHP8-快速脚本参考-二-

66 阅读21分钟

PHP8 快速脚本参考(二)

原文:PHP 8 Quick Scripting Reference

协议:CC BY-NC-SA 4.0

十一、访问级别

每个类成员都有一个可访问性级别,它决定了该成员在哪里可见。PHP 中有三个可用的:publicprotectedprivate

class MyClass
{
  public    $myPublic;    // unrestricted access
  protected $myProtected; // enclosing or child class
  private   $myPrivate;   // enclosing class only
}

私有访问

无论访问级别如何,所有成员都可以在声明它们的类(封闭类)中访问。这是唯一可以访问私有成员的地方。

class MyClass
{
  public    $myPublic    = 'public';
  protected $myProtected = 'protected';
  private   $myPrivate   = 'private';

  function test()
  {
    echo $this->myPublic;    // allowed
    echo $this->myProtected; // allowed
    echo $this->myPrivate;   // allowed
  }
}

与属性不同,方法不必指定显式的访问级别。除非设置为其他级别,否则它们默认为公共访问。

受保护的访问

受保护的成员可以从声明它们的类内部访问,也可以从扩展它们的任何类访问。

class MyChild extends MyClass
{
  function test()
  {
    echo $this->myPublic;    // allowed
    echo $this->myProtected; // allowed
    echo $this->myPrivate;   // inaccessible
  }
}

公共访问

公共成员可以不受限制地访问。除了可以访问受保护成员的任何地方之外,还可以通过对象变量访问公共成员。

$m = new MyClass();
echo $m->myPublic;    // allowed
echo $m->myProtected; // inaccessible
echo $m->myPrivate;   // inaccessible

做个关键词

在 PHP 5 之前,var关键字用于声明属性。为了保持向后兼容性,这个关键字仍然是可用的,并提供公共访问,就像public修饰符一样。

class MyVars
{
  var $x, $y; // deprecated property declaration
}

对象访问

在 PHP 中,同一类的对象可以访问彼此的私有和受保护成员。这种行为不同于许多其他不允许这种访问的编程语言。

class MyClass
{
  private $myPrivate;

  function setPrivate($obj, $val) {
    $obj->myPrivate = $val; // set private property

  }
}
$a = new MyClass();
$b = new MyClass();
$a->setPrivate($b, 10);

访问级别指南

作为指南,在选择访问级别时,通常最好尽可能使用最严格的级别。这是因为成员可以被访问的位置越多,它可以被错误访问的位置就越多,这使得代码更难调试。使用限制性访问级别还使得修改该类变得更加容易,而不会破坏使用该类的任何其他开发人员的代码。

附件

应该避免使用公共属性,因为它暴露了一个类的内部,这使得将来对该类的更改更加困难。一种更好的方法是通过 get 和 set 访问器方法提供对此类属性的访问。get 访问器检索属性,通常以“get”前缀命名。同样,set 访问器设置属性的值,通常以“set”前缀命名。

class Time
{
  private $minutes;

  function getMinutes() {
    return $this->minutes;
  }

  function setMinutes($val) {
    $this->minutes = $val;
  }
}

通过不直接公开属性,该类更加灵活。例如,该类中表示时间的底层属性可以从分钟更改为秒,而不会破坏使用该类的任何人的代码。set 访问器还可以验证输入,以验证它是该属性的有效值,从而使该类使用起来更安全。

class Time
{
  private $seconds;

  function getMinutes() {
    return $this->seconds / 60;
  }

  function setMinutes($val) {
   if ($val > 0) { $this->seconds = $val * 60; }
   else { $this->seconds = 0; }
  }
}

访问器的另一个优点是可以限制属性的读写访问。可以通过省略 set 访问器将属性设置为只读,或者通过省略 get 访问器将属性设置为只写。

十二、静态

static关键字可用于声明无需创建类实例就能访问的属性和方法。静态(类)成员只存在于一个副本中,该副本属于类本身,而实例(非静态)成员是作为每个新对象的新副本创建的。

class MyCircle
{
  // Instance members (one per object)
  public $r = 10;
  function getArea() {}

  // Static/class members (only one copy)
  static $pi = 3.14;
  static function newArea($a) {}
}

静态方法不能使用实例成员,因为这些方法不是实例的一部分。但是,它们可以使用其他静态成员。

引用静态成员

与实例成员不同,静态成员不能使用单箭头操作符(->)来访问。相反,要引用一个类中的静态成员,该成员必须以self关键字为前缀,后跟范围解析操作符(::)。self关键字是类名的别名,因此也可以使用类名。

static function newArea($a)
{
  return self::$pi * $a * $a; // ok
  return MyCircle::$pi * $a * $a; // alternative
}

同样的语法也用于从实例方法中访问静态成员。请注意,与静态方法不同,实例方法可以使用静态和实例成员。

function getArea()
{
  return self::newArea($this->r);
}

要从类外部访问静态成员,需要使用类名,后跟范围解析操作符(::)。

class MyCircle
{
  static $pi = 3.14;

  static function newArea($a)
  {
    return self::$pi * $a * $a;
  }
}

echo MyCircle::$pi; // "3.14"
echo MyCircle::newArea(10); // "314"

这里可以看出静态成员的优势;无需创建类的实例就可以使用它们。因此,如果方法独立于实例变量执行泛型函数,则应该将其声明为静态的。同样,如果只需要变量的一个实例,属性应该声明为静态的。

静态变量

局部变量可以声明为静态的,以使函数记住它的值。这样的静态变量只存在于局部函数的作用域中,但是当函数结束时,它不会丢失它的值。例如,这可以用来计算一个函数被调用的次数。

function add()
{
  static $val = 0;
  echo $val++;
}

add(); // "0"
add(); // "1"
add(); // "2"

静态变量的初始值只设置一次。请记住,静态属性和静态变量只能用常量初始化,而不能用表达式初始化,如另一个变量或函数返回值。

后期静态绑定

如前所述,self关键字是封闭类的类名的别名。这意味着关键字引用它的封闭类,即使它是从子类的上下文中调用的。

class MyParent
{
  protected static $val = 'parent';

  public static function getVal()
  {
    return self::$val;
  }
}

class MyChild extends MyParent
{
  protected static $val = 'child';
}

echo MyChild::getVal(); // "parent"

要获取类引用以评估实际调用该方法的类,需要使用static关键字而不是self关键字。这个特性被称为后期静态绑定,它是在 PHP 5.3 中添加的。

class MyParent
{
  protected static $val = 'parent';

  public static function getLateBindingVal()
  {
    return static::$val;
  }
}

class MyChild extends MyParent
{
  protected static $val = 'child';
}

echo MyChild::getLateBindingVal(); // "child"

十三、常量

一个常量是一个变量,它的值不能被脚本改变。因此,必须在创建常量的同时分配这样的值。PHP 提供了两种创建常量的方法:const修饰符和define函数。

常量

const修饰符用于创建类常量。与常规属性不同,类常量没有指定的访问级别,因为它们总是公开可见的。它们也不使用美元符号解析器标记($)。常量的命名约定都是大写,用下划线分隔每个单词。

class MyCircle
{
  const PI = 3.14;
}

常量在创建时必须赋值。像静态属性一样,常量只能用常量值初始化,而不能用表达式或变量初始化。类常量的引用方式与静态属性相同,只是它们不使用美元符号。

echo MyCircle::PI; // "3.14"

const修饰符可能不适用于局部变量或参数。但是,从 PHP 5.3 开始,const可以用来创建全局常量。这种常量是在全局范围内定义的,可以在脚本中定义它之后的任何地方访问。

const PI = 3.14;
echo PI; // "3.14"

规定

define函数可以创建全局和局部常量,但不能创建类常量。这个函数的第一个参数是常量的名称,第二个参数是它的值。

define('DEBUG', 1);

就像用const创建的常量一样,define常量在使用时没有美元符号,它们的值不能修改。

echo DEBUG; // "1"

像用const生成的常量一样,define的值可以是任何标量数据类型:整数、浮点数、字符串或布尔值。然而,与const不同的是,define函数允许在赋值中使用表达式,比如变量或数学表达式的结果。

define('ONE', 1);     // 1
define('TWO', ONE+1); // 2

默认情况下,常量区分大小写。然而,define函数接受第三个可选参数,该参数可以设置为true来创建一个不区分大小写的常量。

define('DEBUG', 1, true);
echo debug; // "1"

要检查常量是否已经存在,可以使用defined功能。该功能适用于用constdefine创建的常量。

if (!defined('PI'))
  define('PI', 3.14);

PHP 7 使得使用define函数创建常量数组成为可能。对用const创建的常量数组的支持从 PHP 5.6 开始就存在了。

const CA = [1, 2, 3];    // PHP 5.6 or later
define('DA', [1, 2, 3]); // PHP 7 or later

构建和定义

const修饰符创建了一个编译时常量,所以编译器用它的值替换了常量的所有用法。相反,define创建一个运行时常量,直到运行时才设置。这就是为什么定义常量可以被赋予表达式值,而const需要在编译时已知的常量值。

const PI = 3.14;   // compile-time constant
define('E', 2.72); // run-time constant

只有const可用于类常量,只有define可用于局部常量。然而,当创建全局常量时,constdefine都是允许的。在这些情况下,使用const通常更好,因为编译时常量比运行时常量稍快。主要的例外是当常量是有条件定义的,或者需要一个表达式值时,在这种情况下必须使用define

不变准则

通常,如果常量的值不需要更改,那么创建常量而不是变量是一个好主意。这确保了脚本中的变量不会被错误地更改,从而有助于防止错误。

魔法常量

PHP 提供了八个预定义的常量,如表 13-1 所示。这些被称为魔法常量,因为它们的值会随着使用场合的不同而变化。

表 13-1

魔法常量

|

名字

|

描述

| | --- | --- | | __ 行 _ _ | 文件的当前行号。 | | FILE | 文件的完整路径和文件名。 | | DIR | 文件的目录。 | | __ 函数 _ _ | 函数名。 | | CLASS | 包括命名空间的类名。 | | __ 性状 _ _ | 包括名称空间的性状名称。 | | __ 方法 _ _ | 类方法名。 | | __ 名称空间 _ _ | 当前名称空间。 |

幻常量对于调试特别有用。例如,__LINE__的值取决于它出现在脚本中的哪一行。

if(!isset($var))
{
  echo '$var not set on line ' . __LINE__;
}

十四、接口

接口指定使用该接口的类必须实现的方法。它们用interface关键字定义,后跟一个名称和一个代码块。一个常见的命名约定是以一个小“i”开始命名接口,然后让每个单词最初大写。

interface iMyInterface {}

接口签名

接口的代码块可以包含实例方法的签名。这些方法不能有任何实现。相反,他们的身体被分号代替。接口方法必须总是公共的。

interface iMyInterface
{
  public function myMethod();
}

此外,接口可以定义常量。这些接口常量的行为就像类常量一样,只是它们不能被重写。

interface iMyInterface
{
  const PI = 3.14;
}

一个接口可能不从一个类继承,但它可能从另一个接口继承,这有效地将两个接口合并为一个。

interface i1 {}
interface i2 extends i1 {}

界面示例

下面的例子展示了一个名为iComparable的接口,它有一个名为Compare的方法。请注意,该方法利用类型提示来确保使用正确的类型调用该方法。这一功能将在后面的章节中介绍。

interface iComparable
{
  public function compare(iComparable $o);
}

Circle类通过在类名后使用implements关键字,后跟接口名来实现这个接口。如果这个类也有一个extends子句,那么implements子句需要放在它的后面。请记住,尽管一个类只能从一个父类继承,但它可以通过在逗号分隔的列表中指定接口来实现任意数量的接口。

class Circle implements iComparable
{
  public $r;
}

因为Circle实现了iComparable,所以它必须定义compare()方法。对于此类,该方法返回圆半径之间的差值。除了具有与接口中定义的方法相同的签名之外,实现的方法必须是公共的。它也可以有更多的参数,只要它们是可选的。

class Circle implements iComparable
{
  public $r;

  public function compare(iComparable $o)
  {
    return $this->r - $o->r;
  }
}

接口用法

接口允许类设计的多重继承,而没有与允许功能的多重继承相关的复杂性。要求特定类设计的主要好处可以从iComparable接口中看出,它定义了类可以共享的特定功能。它允许开发人员使用接口成员,而不必知道类的实际类型。为了说明这一点,下面的例子展示了一个简单的方法,它接受两个iComparable对象并返回最大的一个。

function largest(iComparable $a, iComparable $b)
{
  return ($a->compare($b) > 0) ? $a : $b;
}

这个方法适用于任何两个实现了iComparable接口的相同类型的对象。不管对象是什么类型,它都可以工作,因为该方法只使用通过该接口公开的功能。

界面指南

接口为没有任何实现的类提供设计。它是一个契约,通过它实现它的类同意提供某些功能。这有两个好处。首先,它提供了一种方式来确保开发人员实现某些方法。第二,因为这些类保证有某些方法,所以即使不知道类的实际类型也可以使用它们,这使得代码更加灵活。

十五、抽象

抽象类提供了部分实现,其他类可以在此基础上构建。当一个类被声明为抽象类时,这意味着除了正常的类成员之外,该类还可以包含必须在子类中实现的不完整方法。

抽象方法

在抽象类中,任何方法都可以被声明为抽象的。这些方法没有实现,只指定了它们的签名,而它们的代码块被分号替换。

abstract class Shape
{
  abstract public function myAbstract();
}

抽象示例

举个例子,下面的类有两个属性和一个抽象方法。

abstract class Shape
{
  private $x = 100, $y = 100;
  abstract public function getArea();
}

如果一个类从这个抽象类继承,那么它将被强制重写抽象方法。方法签名必须匹配,但访问级别除外,因为访问级别的限制较少。

class Rectangle extends Shape
{
  public function getArea()
  {
    return $this->x * $this->y;
  }
}

不可能实例化抽象类。他们只是作为其他类的家长,在一定程度上决定他们的执行。

$s = new Shape(); // compile-time error

然而,抽象类可以从非抽象(具体)类继承。

class NonAbstract {}
abstract class MyAbstract extends NonAbstract {}

抽象类和接口

抽象类在许多方面类似于接口。它们都可以定义派生类必须实现的成员签名,并且它们都不能被实例化。关键的区别是,首先,抽象类可以包含非抽象成员,而接口不能。第二,一个类可以实现任意数量的接口,但只能从一个类继承,不管是不是抽象的。

// Defines default functionality and definitions
abstract class Shape
{
  public $x = 100, $y = 100;
  abstract public function getArea();
}
// Class is a Shape
class Rectangle extends Shape { /*...*/ }

// Defines a specific functionality
interface iComparable
{
  function compare(iComparable $o);
}
// Class can be compared
class MyClass implements iComparable { /*...*/ }

抽象指南

抽象类提供了部分实现的基类,指示子类必须如何行为。当子类有一些相似之处,但在需要子类定义的其他实现中有所不同时,它们最有用。就像接口一样,抽象类是面向对象编程中有用的构造,可以帮助开发人员遵循良好的编码标准。

十六、性状

性状是一组可以插入到类中的方法。它们是在 PHP 5.4 中添加的,以支持更大的代码重用,而不会因为允许多重继承而增加复杂性。性状是用关键字trait定义的,后跟一个名称和一个代码块。命名约定通常与类相同,每个单词最初都是大写的。代码块只能包含静态方法和实例方法。

trait PrintFunctionality
{
  public function myPrint() { echo 'Hello'; }
}

需要 trait 提供的功能的类可以用关键字use包含它,后跟 trait 的名称。然后性状的方法的行为就好像它们是直接在那个类中定义的一样。

class MyClass
{
  // Insert trait methods
  use PrintFunctionality;
}

$o = new MyClass();
$o->myPrint(); // "Hello"

一个类可以使用多个性状,方法是将它们放在一个逗号分隔的列表中。类似地,性状可以由一个或多个其他性状组成。

遗传和性状

Trait 方法覆盖继承的方法。同样,类中定义的方法会覆盖由性状插入的方法。

class MyParent
{
  public function myPrint() { echo 'Base'; }
}

class MyChild extends MyParent
{
  // Overrides inherited method
  use PrintFunctionality;
  // Overrides trait inserted method
  public function myPrint() { echo 'Child'; }
}

$o = new MyChild();
$o->myPrint(); // "Child"

特质指南

单一继承有时会迫使开发人员在代码重用和概念清晰的类层次结构之间做出选择。为了实现更好的代码重用,可以将方法移动到类层次结构的根附近,但是这样一来,类就开始有了它们不需要的方法,这就降低了代码的可理解性和可维护性。另一方面,在类层次结构中加强概念上的整洁常常会导致代码重复,这可能会导致不一致。Traits 通过支持独立于类层次结构的代码重用,提供了一种避免单一继承缺点的方法。

十七、导入文件

同一个代码经常需要在多个页面上调用。这可以通过首先将代码放在一个单独的文件中,然后使用include语句包含该文件来实现。该语句获取指定文件中的所有文本,并将其包含在脚本中,就像代码已被复制到该位置一样。就像echoinclude是一个特殊的语言构造,而不是一个函数,所以不应该使用括号。

<?php
include 'myfile.php';
?>

当包含一个文件时,解析在目标文件的开头变为 HTML 模式,在结尾再次恢复 PHP 模式。因此,包含文件中需要作为 PHP 代码执行的任何代码都必须包含在 PHP 标记中。

<?php
// myfile.php
?>

包括路径

一个include文件可以被指定一个相对路径、一个绝对路径或者没有路径。相对文件路径相对于导入文件的目录。一个绝对文件路径包括完整的文件路径。

// Relative path
include 'myfolder/myfile.php';

// Absolute path
include 'C:/xampp/htdocs/myfile.php';

请注意,在 Windows 和 Linux 下,正斜杠“/”用作目录分隔符。至于大小写,默认情况下,文件路径在 Windows 上不区分大小写,在 Linux 上区分大小写。

当指定相对路径或未指定路径时,include首先在当前工作目录中搜索文件,默认为导入脚本的目录。如果在那里没有找到文件,include在失败前检查php.ini中定义的include_path1指令指定的文件夹。

// No path
include 'myfile.php';

除了include之外,还有另外三种语言结构可以将一个文件的内容导入到另一个文件中:requireinclude_oncerequire_once

需要

require构造包含并评估指定的文件。它与include相同,除了它如何处理故障。当文件导入失败时,require会暂停脚本并显示错误,而include只会发出警告。导入失败的原因可能是找不到文件,或者是运行 web 服务器的用户没有读取该文件的权限。

require 'myfile.php'; // halt on error

一般来说,对于任何复杂的 PHP 应用或 CMS 站点,最好使用require。这样,当密钥文件丢失时,应用不会试图运行。对于不太重要的代码段和简单的 PHP 网站来说,include可能就足够了,在这种情况下,即使包含的文件丢失,PHP 也会显示输出。

包含一次

include_once语句的行为类似于include,除了如果指定的文件已经被包含,它将不再被包含。

include_once 'myfile.php'; // include only once

需要一次

require_once语句的工作方式类似于require,但是如果文件已经被导入,它就不会导入该文件。

require_once 'myfile.php'; // require only once

在脚本的特定执行过程中,如果一个文件可能被多次导入,那么最好使用include_oncerequire_once语句。例如,这避免了由函数和类重定义引起的错误。

返回

可以在导入的文件中执行return语句。这将停止执行并返回到调用文件导入的脚本。

<?php
// myimport.php
return 'OK';
?>

如果指定了返回值,import 语句将计算该值,就像普通函数一样。

<?php
// myfile.php
if ((include 'myimport.php') == 'OK')
  echo 'OK';
?>

_ 自动装载

对于大型 web 应用,每个脚本中所需的包含数量可能相当大。这可以通过定义一个__autoload函数来避免。当一个未定义的类或接口被用来加载那个定义时,这个函数被自动调用。它接受一个参数,即 PHP 正在寻找的类或接口的名称。

function __autoload($class_name)
{
  include $class_name . '.php';
}

// Attempt to auto include MyClass.php
$obj = new MyClass();

编写面向对象的应用时,一个好的编码实践是为每个类定义准备一个源文件,并根据类名来命名文件。遵循这个约定,__autoload函数能够加载这个类——只要它与需要它的脚本文件在同一个文件夹中。

<?php
// MyClass.php
class MyClass {}
?>

如果文件位于子文件夹中,类名可以包含下划线字符来表示。然后需要在 __ autoload函数中将下划线字符转换成目录分隔符。

Footnotes 1

www.php.net/manual/en/ini.core.php#ini.include-path

 

十八、类型声明

类型声明允许为属性、函数参数和函数返回类型指定类型。这允许 PHP 引擎强制使用指定的类型。

参数类型声明

PHP 的早期版本完全依赖于适当的函数文档,以便开发人员知道函数接受什么参数。为了让函数更加健壮,PHP 5 开始引入参数类型声明,允许指定函数参数的类型。表 18-1 显示了类型声明的有效类型,以及添加了这些类型的 PHP 版本。

表 18-1

类型声明

|

名字

|

描述

|

版本

| | --- | --- | --- | | 类别名 | 参数必须是该类的对象或子对象。 | PHP 5.0 | | 接口名称 | 值必须是实现此接口的对象。 | PHP 5.0 | | Self | 值必须是定义该方法的类的实例,或者是它的一个子类。仅适用于类和实例方法。 | PHP 5.0 | | parent | 值必须是定义该方法的类的父类的实例,或者是它的一个子类。仅适用于类和实例方法。 | PHP 5.0 | | array | 值必须是数组。 | PHP 5.1 | | iterable | 值必须是实现可遍历接口的数组或对象。它可以与 foreach 循环一起使用。 | PHP 5.1 | | callable | 值必须可以作为函数调用。 | PHP 5.4 | | Bool | 值必须为 true 或 false。 | PHP 7.0 | | Float | 值必须是浮点数。 | PHP 7.0 | | Int | 值必须是整数。 | PHP 7.0 | | string | 值必须是字符串。 | PHP 7.0 | | mixed | 允许的任何值。 | PHP 8.0 |

通过在函数签名中将类型作为参数的前缀来设置类型声明。下面是一个使用 PHP 5.1 中引入的array伪类型的例子。

function myPrint(array $a)
{
  foreach ($a as $v) { echo $v; }
}

myPrint( array(1,2,3) ); // "123"

不满足类型提示会导致致命错误。这为开发人员提供了一种快速检测何时使用了无效参数的方法。

myPrint('Test'); // error

PHP 5.4 中增加了callable伪类型。有了这个类型提示,参数必须是可调用的函数、方法或对象。不允许使用像echo这样的语言结构,但是可以使用匿名函数,如下例所示。

function myCall(callable $callback, $data)
{
  $callback($data);
}

$say = function($s) { echo $s; };
myCall( $say, 'Hi' ); // "Hi";

要将一个方法作为一个callback函数传递,对象和方法名都需要作为一个数组组合在一起。

class MyClass {
  function myCallback($s) {
    echo $s;
  }
}

$o = new MyClass();
myCall( array($o, 'myCallback'), 'Hi' ); // "Hi"

PHP 7 中增加了标量类型的类型声明,包括 bool、int、float 和 string。下面是一个使用 bool 类型的简单示例。

function isTrue(bool $b)
{
  return ($b === true);
}

echo isTrue(true);  // "1"
echo isTrue(false); // ""

对于依赖于特定类型参数的函数,最好使用类型声明。这样,如果这个函数被错误地传递了一个不正确类型的参数,它会立即触发一个错误。如果没有类型声明,函数可能会无声无息地失败,使错误更加难以检测。

返回类型声明

PHP 7 中增加了对返回类型声明的支持,作为防止意外返回值的一种方式。返回类型在参数列表后声明,以冒号(:)为前缀。允许与参数类型声明相同的类型。

function f(): array {
  return [];
}

当与接口一起使用时,类型声明强制实现类匹配相同的类型声明。

interface I {
  static function myArray(array $a): array;
}

class C implements I {
  static function myArray(array $a): array {
    return $a;
  }
}

从 PHP 7.1 开始,void类型可以用作返回类型声明,指定函数不返回值。这样的函数可以完全省略 return 语句,或者使用空的 return 语句来退出函数。

function myFunc(): void {
  return; // empty return statement
}

PHP 8 在方法允许的返回类型列表中增加了static。这种方法必须返回定义该方法的类的对象。

class MyTest

{
  public function getNewObject(): static
  {
    return new MyTest();
  }
}

严格打字

PHP 中的默认行为是试图将不正确类型的标量值转换成期望的类型。例如,需要字符串的函数仍然可以用整数参数调用,因为整数可以转换成字符串。

function showString(string $s) {
  echo $s;
}

showString(5); // "5"

通过将以下声明作为特定源文件中的第一条语句,可以在该文件中启用强类型检查。

declare(strict_types=1);

这将影响标量类型的参数和返回类型声明,它们必须与函数中声明的类型完全相同。

showString(5); // Fatal error: Uncaught TypeError

可空类型

从 PHP 7.1 开始,一个类型声明可以通过在类型前加一个问号来标记为可空。除了其指定类型的任何常规值之外,可空类型还允许保存 null。

function nullOrInt(?int $i)
{
  echo $i === null ? 'null' : 'int';
}

echo nullOrInt(3); // "int"
echo nullOrInt(null); // "null"

工会类型

PHP 8 引入了联合类型,允许一个类型声明包含多个类型。声明联合类型时,每个允许的类型由竖线(|)分隔。

function squared(int|float $i): int|float
{
  return $i ** 2;
}

echo squared(2); // "4"
echo squared(1.5); // "2.25"

null这样的伪类型和像truefalse这样的子类型也可以用来组成联合类型。

function trueOrNull(true|null $j): true|null
{
  return $j === null ? true : null;
}

属性类型声明

PHP 7.4 中增加了类型化属性,允许属性指定它可以保存什么类型的值。在声明这样的属性时,可以使用任何类型(除了callable and void)。

class MyClass
{
  public int $a = 3;
}

如此处所示,PHP 引擎将阻止其他类型被赋给类型化属性。

$o = new MyClass;
$o->a = 5; // allowed
$o->a = 'string'; // fatal error: TypeError

未赋值的常规非类型化属性将为 null。相比之下,类型化属性将是未初始化的,这是一种不同于 null 的新的变量状态。

class MyType
{
  public int $a; // uninitialized
  public $b; // null
}

添加了未初始化,因为否则就不可能区分设置为 null 的可空属性和错误地未赋值的属性。从 PHP 8 开始,试图读取未初始化的类型化属性会导致致命错误。

$t1 = new MyType;
$o->t1 = 5;
$i = $o->t1; // ok

$t2 = new MyType;
$i = $o->t2; // error

十九、类型转换

给定变量的使用环境,PHP 会根据需要自动转换变量的数据类型。因此,很少需要显式类型转换。尽管如此,变量或表达式的类型可以通过执行显式类型转换来更改。

显式强制转换

显式强制转换是通过将所需的数据类型放在要计算的变量或值前面的括号中来执行的。在下面的示例中,显式强制将 bool 变量计算为 int。

$myBool = false;
$myInt = (int)$myBool; // 0

当 bool 变量作为输出发送到页面时,可以看到显式强制转换的一种用法。由于自动类型转换,false值变成空字符串;因此,它不会显示。首先将它转换成一个整数,false值显示为0

echo $myBool;      // ""
echo (int)$myBool; // "0"

表 19-1 中列出了允许的造型。

表 19-1

允许的类型转换

|

名字

|

描述

| | --- | --- | | (整数),(整数) | 转换为整数。 | | (布尔值)、(布尔值) | 转换为布尔型。 | | (浮点)、(双精度)、(实数) | 抛来抛去。 | | (字符串) | 转换为字符串。 | | (数组) | 转换为数组。 | | (对象) | 转换为对象。 | | (未设置) | 转换为 null。 |

举几个例子,数组转换将标量类型转换为只有一个元素的数组。它执行与使用数组构造函数相同的功能。

$myInt = 10;
$myArr = (array)$myInt;
$myArr = array($myInt); // same as above
echo $myArr[0]; // "10"

如果一个标量类型(比如 int)被转换成 object,它就成为内置stdClass类的一个实例。变量值存储在这个类的一个属性中,称为scalar

$myObj = (object)$myInt;
echo $myObj->scalar; // "10"

未设置的强制转换使变量的值为 null。尽管它的名字,它实际上并没有取消变量的设置。强制转换仅仅是为了完整性而存在,因为 null 被认为是一种数据类型。它在 PHP 7.2 中被弃用,不应再使用。

$myNull = (unset)$myInt;
$myNull = null; // same as above

集合类型

显式强制转换不会改变它前面的变量的类型,只会改变它在表达式中的求值方式。要改变变量的类型,可以使用settype函数,它有两个参数。第一个是要转换的变量,第二个是以字符串形式给出的数据类型。

$myVar = 1.2; // float variable
settype($myVar, 'int'); // convert variable to int

或者,可以通过将显式强制转换的结果存储回同一个变量来执行类型转换。

$myVar = 1.2;
$myVar = (int)$myVar; // 1

获取类型

settype相关的是gettype函数,它以人类可读的字符串形式返回所提供参数的类型。

$myBool = true;
echo gettype($myBool); // "boolean"

二十、变量测试

作为一种以 web 为中心的语言,在 PHP 中处理用户提供的数据是很常见的。在使用此类数据之前,需要对其进行测试,以确认其存在并具有有效值。PHP 为此提供了许多内置的构造。

没错

如果变量存在并且被赋予了一个非空值,那么isset语言构造返回true

isset($a); // false

$a = 10;
isset($a); // true

$a = null;
isset($a); // false

空的

empty构造检查指定的变量是否有空值——比如 null、0、false 或空字符串——如果是,则返回 true。如果变量不存在,它也返回true

empty($b); // true

$b = false;
empty($b); // true

Is_null

is_null构造可以用来测试一个变量是否被设置为空。

$c = null;
is_null($c); // true

$c = 10;
is_null($c); // false

从 PHP 8 开始,如果变量不存在,is_null构造会发出一个错误,因为它不应该与未初始化的变量一起使用。在 PHP 8 之前,is_null返回 true,并附带一个非致命错误通知。

// Prior to PHP 8
is_null($d); // true (undefined variable notice)

// As of PHP 8
is_null($d); // error (TypeError)

针对 null 的严格相等检查在功能上等同于使用is_null构造。使用该操作符通常是首选,因为它可读性更好,速度也稍快,因为它不涉及函数调用开销。

$c = null;
$c === null; // true

未设置

了解另一个有用的语言构造是unset,它从当前作用域中删除一个变量。

$e = 10;
unset($e); // delete $e

当通过使用global关键字在函数中访问一个全局变量时,这段代码实际上在$GLOBALS数组中创建了一个对全局变量的局部引用。因此,试图在函数中取消设置全局变量只会删除局部引用。要从函数的作用域中删除全局变量,必须直接在$GLOBALS数组上取消设置。

$o = 5; // global variable

function myUnset()
{
  // Make $o a reference to $GLOBALS['o']
  global $o;

  // Remove the local reference variable
  unset($o);

  // Remove the global variable
  unset($GLOBALS['o']);
}

取消设置变量与将变量设置为 null 略有不同。当一个变量被设置为 null 时,该变量仍然存在,但是它保存的内容立即被释放。相反,不设置变量会删除变量,但在垃圾收集器清除它之前,内存仍被认为是在使用中。撇开性能问题不谈,推荐使用unset,因为它使代码的意图更加清晰。

$var = null; // free memory
unset($var); // delete variable

请记住,大多数情况下,没有必要手动取消设置变量,因为当变量超出范围时,PHP 垃圾收集器会自动删除变量。但是,如果服务器执行内存密集型任务,那么手动取消设置这些变量将允许服务器在耗尽内存之前处理更多的并发请求。

零合并算子

PHP 7 中添加了空合并操作符(??),作为使用三元组和isset的常见情况的快捷方式。如果存在并且不为空,则返回第一个操作数;否则,它返回第二个操作数。

$x = null;
$name = $x ?? 'unknown'; // "unknown"

该语句相当于下面的三元运算,它使用了isset构造。

$name = isset($x) ? $x : 'unknown';

PHP 7.4 增加了空合并赋值操作符(??=)。该操作符提供了一种简洁的方法,仅当变量未赋值时才给变量赋值(null)。它也可以用于未定义的变量。

// Assign value if $name is unassigned
$name ??= 'unknown';

// Same as above
if(!isset($name)) { $name = 'unknown'; }

// Functionally same as above

$name = $name ?? 'unknown';

空安全运算符

在调用方法之前,有必要首先检查以确保其对象存在。这可以通过使用三元运算符来实现。

$result = $obj ? $obj->myMethod() : null;

PHP 8 引入了 nullsafe 操作符(?->)作为实现这一功能的更简洁的方法。如果对象不存在,操作符返回null

$result = $obj?->myMethod();

运算符可以在调用方法或获取属性时使用,但不能在写入属性时使用。

确定类型

PHP 有几个确定变量类型的有用函数。这些功能可以在表 20-1 中看到。

表 20-1

类型函数

|

名字

|

描述

| | --- | --- | | is_array() | 如果变量是数组,则为 True。 | | is_bool() | 如果变量是布尔值,则为 True。 | | is_callable() | 如果变量可以作为函数调用,则为 True。 | | is_float(), is_double(), is_real() | 如果变量是浮点型,则为 True。 | | is_int(), is_integer(), is_long() | 如果变量是整数,则为 True。 | | is_null() | 如果变量设置为 null,则为 True。 | | is_numeric() | 如果变量是数字或数字字符串,则为 True。 | | is_scalar() | 如果变量是 int、float、string 或 bool,则为 True。 | | is_object() | 如果变量是对象,则为 True。 | | is_resource() | 如果变量是资源,则为 True。 | | is_string() | 如果变量是字符串,则为 True。 |

举个例子,如果参数包含一个数字或一个可以计算为数字的字符串,is_numeric函数将返回true

is_numeric(10.5);   // true  (float)
is_numeric('33');   // true  (numeric string)
is_numeric('text'); // false (non-numeric string)

可变信息

PHP 有三个用于检索变量信息的内置函数:print_rvar_dumpvar_exportprint_r函数以人类可读的方式显示变量值。这对于调试非常有用。

$a = array('one', 'two', 'three');
print_r($a);

前面的代码产生以下输出。

Array ( [0] => one [1] => two [2] => three )

print_r类似的是var_dump,除了数值,还显示数据类型和大小。调用var_dump($a)显示这个输出。

array(3) {
  [0]=> string(3) "one"
  [1]=> string(3) "two"
  [2]=> string(5) "three"
}

最后是var_export函数,它以可以用作 PHP 代码的样式打印变量信息。下图显示了var_export($a)的输出。注意最后一个元素后面的逗号,这是允许的。

array ( 0 => 'one', 1 => 'two', 2 => 'three', )

var_export函数和print_r一起接受可选的第二个布尔参数。当设置为true时,该功能返回输出而不是打印输出。这给了var_export更多的用途,比如与eval语言结构结合使用。这个构造接受一个字符串参数,并将其作为 PHP 代码进行计算。

eval('$b = ' . var_export($a, true) . ';');

使用eval执行任意代码的能力是一个强大的特性,应该小心使用。不应该使用它来执行任何用户提供的数据,至少不应该在没有适当验证的情况下执行,因为这存在安全风险。不鼓励使用eval的另一个原因是,与goto类似,它使得代码的执行更加难以跟踪,这使得调试更加复杂。

二十一、重载

PHP 中的重载提供了在运行时添加对象成员的能力。这是通过让类实现重载方法__get__set__call__callStatic来实现的。请记住,PHP 中重载的含义不同于许多其他语言。

属性重载

__get__set方法提供了一种实现 getter 和 setter 方法的便捷方式,这些方法通常用于安全地读写属性。当使用不可访问的属性时,会调用这些重载方法,因为这些属性没有在类中定义,或者因为它们在当前范围内不可用。在下面的例子中,__set方法将所有不可访问的属性添加到$data数组中,__get安全地从该数组中检索元素。

class MyProperties
{
  private $data = array();

  public function __set($name, $value)
  {
    $this->data[$name] = $value;
  }

  public function __get($name)
  {
    if (array_key_exists($name, $this->data))
      return $this->data[$name];
  }
}

当设置一个不可访问的属性的值时,__set被调用,属性的名称和值作为它的参数。类似地,当访问一个不可访问的属性时,使用属性名作为参数调用__get

$obj = new MyProperties();

$obj->a = 1;  // __set called
echo $obj->a; // __get called

方法重载

有两种方法可以处理对一个类的不可访问方法的调用:__call__callStatic。对于实例方法调用,调用__call方法。

class MyClass
{
  public function __call($name, $args)
  {
    echo "Calling $name $args[0]";
  }
}

// "Calling myTest in object context"
(new MyClass())->myTest('in object context');

__call的第一个参数是被调用方法的名称,第二个参数是包含传递给该方法的参数的数值数组。这些参数对于__callStatic方法是相同的,它处理对不可访问的静态方法的调用。

class MyClass
{
  public static function __callStatic($name, $args)
  {
    echo "Calling $name $args[0]";
  }
}

// "Calling myTest in static context"
MyClass::myTest('in static context');

Isset 和 unset 重载

内置的构造issetemptyunset只作用于显式定义的属性,而不是重载的属性。这个功能可以通过重载__isset__unset方法添加到一个类中。

class MyClass
{
  private $data = array();

  public function __set($name, $value) {
    $this->data[$name] = $value;
  }
  public function __get($name) {
    if (array_key_exists($name, $this->data))
      return $this->data[$name];
  }

  public function __isset($name) {
    return isset($this->data[$name]);
  }

  public function __unset($name) {
    unset( $this->data[$name] );
  }
}

当在不可访问的属性上调用isset时,调用__isset方法。

$obj = new MyClass();
$obj->name = "Joe";

isset($obj->name); // true
isset($obj->age);  // false

当在不可访问的属性上调用unset时,__unset方法处理该调用。

unset($obj->name); // delete property
isset($obj->name); // false

如果同时实现了__isset__get,那么empty构造只对重载属性有效。如果__isset的结果为假,那么empty构造返回true。另一方面,如果__isset返回true,那么empty__get检索属性,并评估它是否有一个被认为是空的值。

empty($obj->name); // false
empty($obj->age);  // true

二十二、魔术方法

有许多方法可以在一个类中实现,以供 PHP 引擎内部调用。这些被称为魔术方法,它们很容易识别,因为它们都以两个下划线开头。表 22-1 列出了到目前为止已经讨论过的魔术方法。

表 22-1

魔术方法

|

名字

|

描述

| | --- | --- | | __construct(...) | 创建新实例时调用。 | | __destruct() | 当对象没有剩余引用时调用。 | | __call($name, $array) | 在对象上下文中调用不可访问的方法时调用。 | | __callStatic($name, $array) | 在静态上下文中调用不可访问的方法时调用。 | | __get($name) | 从不可访问的属性中读取数据时调用。 | | __set($name, $value) | 将数据写入不可访问的属性时调用。 | | __isset($string) | 当issetempty用于不可访问的属性时调用。 | | __unset($string) | 当unset用于不可访问的属性时调用。 |

除此之外,还有六个魔术方法,和其他方法一样,可以在类中实现以提供某些功能。

表 22-2

更多魔术方法

|

名字

|

描述

| | --- | --- | | __toString() | 为对象到字符串的转换调用。 | | __invoke(...) | 调用对象到函数的转换。 | | __sleep() | 由serialize调用。执行清理任务并返回要序列化的变量数组。 | | __wakeup() | 由unserialize调用以重建对象。 | | __set_state($array) | 由var_export调用。该方法必须是静态的,并且其参数包含导出的属性。 | | __clone() | 在对象被克隆后调用。 |

_toString

当在需要字符串的上下文中使用对象时,PHP 引擎会搜索名为__toString的方法来检索对象的字符串表示。

class MyClass
{
  public function __toString()
  {
    return 'Instance of ' . __CLASS__;
  }
}

$obj = new MyClass();
echo $obj; // "Instance of MyClass"

不可能定义一个对象在作为字符串以外的类型进行计算时的行为。

_invoke

方法允许一个对象被当作一个函数。调用对象时提供的参数被用作__invoke函数的参数。

class MyClass
{
  public function __invoke($arg)
  {
    echo $arg;
  }
}

$obj = new MyClass();
$obj('Test'); // "Test"

对象序列化

序列化是将数据转换为字符串格式的过程。这对于在数据库或文件中存储对象很有用。在 PHP 中,内置的serialize函数执行对象到字符串的转换,而unserialize将字符串转换回原始对象。serialize函数处理除资源类型以外的所有类型,例如,资源类型用于保存数据库连接和文件处理程序。考虑下面这个简单的数据库类。

class MyConnection
{
  public $link, $server, $user, $pass;

  public function connect()
  {
    $this->link = mysql_connect($this->server,
                                $this->user,
                                $this->pass);
  }
}

当这个类被序列化时,数据库连接丢失,保存连接的$link资源类型变量被存储为 null。

$obj = new MyConnection();
// ...

$bin = serialize($obj);   // serialize object
$obj = unserialize($bin); // restore object

为了更好地控制对象数据的序列化和非序列化,这个类可以实现__sleep__wakeup方法。

_sleep

__sleep方法由serialize调用,需要返回一个包含将被序列化的属性的数组。这个数组不能包含私有或受保护的属性,因为serialize不能访问它们。该方法还可以在串行化发生之前执行清理任务,诸如将任何未决数据提交给存储介质。

public function __sleep()
{
  return array('server', 'user', 'pass');
}

注意,属性以字符串形式返回到serialize$link资源类型指针不包含在数组中,因为它无法序列化。要重新建立数据库连接,可以使用__wakeup方法。

_wakeup

对序列化对象调用unserialize会调用__wakeup方法来恢复对象。它不接受任何参数,也不需要返回值。它用于重新建立资源类型变量,以及执行可能需要在对象取消序列化后完成的其他初始化任务。在本例中,它重新建立了 MySQL 数据库连接。

public function __wakeup()
{
  if(isset($this->server, $this->user, $this->$pass))
    $this->connect();
}

请注意,isset构造在这里用多个参数调用,在这种情况下,如果设置了所有参数,它只返回true

设置状态

var_export函数检索可用作有效 PHP 代码的变量信息。在下面的示例中,该函数用于对象。

class Fruit
{
  public $name = 'Lemon';
}

$export = var_export(new Fruit(), true);

因为对象是一个复杂类型,所以没有通用的语法来构造它和它的成员。相反,var_export创建以下字符串。

Fruit::__set_state(array( 'name' => 'Lemon', ))

为了构造对象,这个字符串依赖于为对象定义的静态__set_state方法。如图所示,__set_state方法采用一个包含每个对象属性的键值对的关联数组,包括私有和受保护成员。

static function __set_state(array $array)
{
  $tmp = new Fruit();
  $tmp->name = $array['name'];
  return $tmp;
}

有了在Fruit类中定义的这个方法,导出的字符串现在可以用eval构造来解析,以创建一个相同的对象。

eval('$MyFruit = ' . $export . ';');

对象克隆

将对象赋给变量只会创建对同一对象的新引用。要复制一个对象,可以使用clone操作符。

class Fruit {}

$f1 = new Fruit();
$f2 = $f1;       // copy object reference
$f3 = clone $f1; // copy object

克隆对象时,其属性会复制到新对象中。但是,它可能包含的任何子对象都不会被克隆,因此它们在副本之间共享。这就是__clone方法的用武之地。克隆完成后,在克隆的副本上调用它,它可用于克隆任何子对象。

class Apple {}

class FruitBasket
{
  public $apple;

  function __construct() { $apple = new Apple(); }

  function __clone()
  {
    $this->apple = clone $this->apple

;
  }
}

二十三、用户输入

当一个 HTML 表单被提交给一个 PHP 页面时,数据就可供该脚本使用了。

HTML 表单

HTML 表单有两个必需的属性:actionmethod。action 属性指定提交表单时表单数据传递到的脚本文件。例如,下面的表单向mypage.php脚本文件提交一个名为myString的输入属性。

<?php // myform.php ?>
<!doctype html>
<html>
<body>
  <form action="mypage.php" method="post">
    <input type="text" name="myString">
    <input type="submit">
  </form>
</body>
</html>

表单元素的第二个必需属性指定了发送方法,可以是 GET 或 POST。

通过邮件发送

提交表单时,浏览器加载 mypage.php 并传递表单数据。如果表单是使用 POST 方法发送的,那么接收脚本可以通过$_POST数组获得数据。表单输入属性的名称成为关联数组中的键。

<?php // mypage.php ?>
<!doctype html>
<html>
<body>
  <?php echo $_POST['myString']; ?>
</body>
</html>

用 POST 方法发送的数据在页面的 URL 上是不可见的,但是这也意味着页面的状态不能通过例如给页面添加书签来保存。

使用 GET 发送

POST 的替代方法是用 GET 方法发送表单数据,并使用$_GET数组检索它。然后变量显示在地址栏中,如果页面被书签标记并被重新访问,地址栏会有效地维护页面的状态。

// mypage.php
echo $_GET['myString'];

因为数据包含在地址栏中,所以变量不仅可以通过 HTML 表单传递,还可以通过 HTML 链接传递。然后可以使用$_GET数组相应地改变页面的状态。这提供了一种将变量从一页传递到另一页的方法,如下例所示。

<?php // sender.php ?>
<!doctype html>
<html>
<body>
  <a href="receiver.php?myString=Foo+Bar">link</a>
</body>
</html>

单击此网页上的链接时,receiver.php 文件可以访问传递给它的数据。以下示例显示页面上的数据。

<?php // receiver.php ?>
<!doctype html>
<html>
<body>
  <?php echo $_GET['myString']; // "Foo Bar" ?>
</body>
</html>

请求数组

如果使用 POST 或 GET 方法发送数据并不重要,那么可以使用$_REQUEST数组。这个数组通常包含$_GET$_POST数组,但也可能包含$_COOKIE数组。

echo $_REQUEST['myString']; // "Foo Bar"

数组$_REQUEST的内容可以在 PHP 配置文件 1 中设置,并且在不同的 PHP 发行版之间有所不同。出于安全考虑,通常不包含$_COOKIE数组。

安全问题

任何用户提供的数据都可以被操作;因此,在使用前应该对其进行验证和消毒。验证意味着您要确保数据在数据类型、范围和内容方面符合您的预期。例如,下面的代码验证一个电子邮件地址。

if(!filter_var($_POST['email'], FILTER_VALIDATE_EMAIL))
  echo "Invalid email address";

净化就是禁用用户输入中潜在的恶意代码。这是通过根据使用输入的语言规则对代码进行转义来实现的。例如,如果数据被发送到一个数据库,它需要用mysql_real_escape_string函数清理,以禁用任何嵌入的 SQL 代码。

// Sanitize for database use
$name = mysql_real_escape_string($_POST['name']);

// Execute SQL command
$sql = "SELECT * FROM users WHERE user='" . $name . "'";
$result = mysql_query($sql);

当用户提供的数据作为文本输出到网页时,应该使用htmlspecialchars函数。它禁用任何 HTML 标记,以便显示用户输入,但不解释。

// Sanitize for web page use
echo htmlspecialchars($_POST['comment']);

提交数组

通过在表单中的变量名后加上数组方括号,可以将表单数据分组到数组中。这适用于所有表单输入元素,包括<input><select><textarea>

<input type="text" name="myArr[]">
<input type="text" name="myArr[]">

元素也可以被赋予它们自己的数组键。

<input type="text" name="myArr[name]">

提交后,该数组就可以在脚本中使用了。

$val1 = $_POST['myArr'][0];
$val2 = $_POST['myArr'][1];
$name = $_POST['myArr']['name'];

表单<select>元素有一个允许从列表中选择多个项目的属性。

<select name="myArr[]" size="3" multiple="true">
  <option value="apple">Apple</option>
  <option value="orange">Orange</option>
  <option value="pear">Pear</option>
</select>

当表单中包含此多选元素时,数组括号对于在脚本中检索所选值是必需的。

foreach ($_POST['myArr'] as $item)
  echo $item . ' '; // ex "apple orange pear"

文件上传

HTML 表单提供了一种文件输入类型,允许将文件上传到服务器。为了上传文件,表单的可选属性enctype必须设置为"multipart/form-data",如下例所示。

<form action="mypage.php" method="post"
      enctype="multipart/form-data">
  <input name="myfile" type="file">
  <input type="submit" value="Upload">
</form>

关于上传文件的信息存储在$_FILES数组中。该关联数组的关键字见表 23-1 。

表 23-1

$_FILES 数组的键

|

名字

|

描述

| | --- | --- | | 名字 | 上传文件的原始名称。 | | tmp_name | 临时服务器副本的路径。 | | 类型 | 文件的 Mime 类型。 | | 大小 | 以字节表示的文件大小。 | | 错误 | 错误代码。 |

接收到的文件只是临时存储在服务器上。如果脚本没有保存它,它将被删除。下面显示了一个如何保存文件的简单示例。该示例检查错误代码以确保文件已成功接收,如果已成功接收,则将文件移出临时文件夹进行保存。实际上,您可能还希望检查文件大小和类型,以确定是否要保留该文件。

$dest = 'upload/' . basename($_FILES['myfile']['name']);
$file = $_FILES['myfile']['tmp_name'];
$err  = $_FILES['myfile']['error'];

if($err == 0 && move_uploaded_file($file, $dest))
  echo 'File successfully uploaded';

在这个例子中可以看到两个新函数。move_uploaded_file函数检查以确保第一个参数包含一个有效的上传文件,如果是,它将它移动到 path 并重命名为第二个参数指定的文件名。指定的文件夹必须已经存在,如果函数成功移动文件,则返回true。另一个新功能是basename。它返回路径的文件名部分,包括文件扩展名。

超级全球

正如在本章中看到的,有许多内置的关联数组使外部数据对 PHP 脚本可用。这些数组被称为超级全局变量,因为它们在每个作用域中都自动可用。PHP 中有九个超全局变量,每个都在表 23-2 中有简要描述。

表 23-2

超级全球

|

名字

|

描述

| | --- | --- | | $GLOBALS | 包含所有全局变量,包括其他超全局变量。 | | $_GET | 包含通过 HTTP GET 请求发送的变量。 | | $_POST | 包含通过 HTTP POST 请求发送的变量。 | | $_FILES | 包含通过 HTTP POST 文件上传发送的变量。 | | $_COOKIE | 包含通过 HTTP cookies 发送的变量。 | | $_SESSION | 包含存储在用户会话中的变量。 | | $_REQUEST | 包含$_GET$_POST,可能还有$_COOKIE变量。 | | $_SERVER | 包含有关 web 服务器及其请求的信息。 | | $_ENV | 包含由 web 服务器设置的所有环境变量。 |

变量$_GET$_POST$_COOKIE$_SERVER$_ENV的内容包含在phpinfo函数生成的输出中。这个函数还显示 PHP 配置文件php.ini的一般设置,以及其他关于 PHP 的信息。

phpinfo(); // display PHP information

Footnotes 1

www.php.net/manual/en/ini.core.php#ini.request-order

 

二十四、Cookie

一个 cookie 是一个保存在客户电脑上的小文件,可以用来存储与该用户相关的数据。

创建 Cookies

为了创建一个 cookie,使用了setcookie函数。在将任何输出发送到浏览器之前,必须调用此函数。通常用三个参数调用它,这三个参数包含 cookie 的名称、值和到期日期。

setcookie("lastvisit", date("H:i:s"), time() + 60*60);

这里的值是用date函数设置的,它返回一个根据指定的格式字符串格式化的字符串。到期日期以秒为单位,通常相对于通过time函数检索的当前时间(以秒为单位)进行设置。在本例中,cookie 在一小时后过期。

还可以提供两个可选参数:path 和 domain。这些用于限制可以从哪些页面访问 cookie。例如,只有在访问“fr.example.com”子域上的“foo”目录中的页面时,才会发送以下 cookie,例如在访问“fr.example.com/foo/example.php”时。

setcookie("lastvisit",
           date("H:i:s"),
           time() + 60*60,
           '/foo/',
           'fr.example.com');

Cookie 数组

一旦为用户设置了 cookie,该 cookie 将在用户下次查看同一域上的页面时发送。然后可以通过$_COOKIE数组访问它。

if (isset($_COOKIE['lastvisit']))
  echo "Last visit: " . $_COOKIE['lastvisit'];

删除 Cookies

可以通过使用旧的过期日期重新创建相同的 cookie 来手动删除 cookie。当浏览器关闭时,它将被删除。

setcookie("lastvisit", 0, 0);