PHP7 Zend 认证学习指南(四)

原文:PHP 7 ZEND CERTIFICATION STUDY GUIDE

协议:CC BY-NC-SA 4.0

七、数据格式和类型

本章分为六大部分:

  • 可扩展置标语言
  • 肥皂
  • REST web 服务
  • 数据
  • 日期和时间
  • PHP SPL 数据结构

虽然这个主题不是 Zend 考试的三个高度重要的领域之一,但是你可以期待从这个部分中被问到一些相对详细的问题。

可扩展置标语言

XML 代表可扩展标记语言,是一种以结构化方式存储数据的方法。使用 XML 的一个优点是,它是一种公认的数据标准,因此是一种在系统之间交换数据的便捷方式。

在业界,作为数据交换过程,已经从 XML 转向 JSON,但是 XML 仍然与日常实践相关,并且是 Zend 考试的一部分。

XML 的基础知识

这不是一本关于 PHP 的入门书,所以我不会极其详细地介绍 XML 的所有元素。如果我们深入到那个细节层次,这本书就太长了。确保您至少熟悉下表中的所有术语,因为我们在研究 PHP 的 XML 处理能力时会用到它们。

学期描述
标准通用标识语言标准化通用标记语言。XML 是它的一个子集。
文档类型声明DTD 用合法元素和属性的列表定义了 XML 文档结构的合法构件。
实体实体可以声明在 XML 文档的其余部分中不允许的名称和值。例如,HTML 将<声明为一个实体来表示小于符号<。这些声明也可以用作快捷方式,并在整个文档中保持拼写和值的一致性。
元素元素是 XML 文档的基本构造块。元素可以嵌套并包含元素,也可以包含值。元素可能有属性。
格式良好的XML 格式良好的文档是符合 XML 1.0 规范规定的语法规则的文档,因为它必须满足物理和逻辑结构。 1
有效的根据 DTD 验证的 XML 文档既“格式良好”又“有效”。

如果您对这些定义不太确定,那么请务必阅读关于 XML 的全面教程,并阅读本节的链接脚注。

格式良好且有效的

让我详细解释一下这些术语的意思,因为知道它们的区别很重要。

一个文件是良构的,如果:

  • 它只有一个根元素
  • 标签被正确地打开和关闭
  • 根据以下列表,它的所有实体都是格式良好的:
    • 它们只包含正确编码的 Unicode 字符
    • 不会出现像<&这样的语法标记
    • 标记名必须完全匹配,并且不能包含符号

如果文档格式良好并且符合 DTD,则该文档是有效的。

Note

PHP 不要求 XML 文档是有效的,但是它要求 XML 文档格式良好,以便用标准库来解析它们。

XML 处理指令

处理指令允许文档包含应用的指令。它们包含在<??>标记中,看起来像这样,例如:

<?PITarget PIContent?>

一个用例可能是通知应用某个元素是特定的数据类型,如下例所示:

<?var type="string" ?>

最常见的用法是包含 XSLT 或 CSS 样式表,如下所示:

<?xml-stylesheet type="text/xsl" href="style.xsl"?>
<?xml-stylesheet type="text/css" href="style.css"?>

用 PHP XSL 进行 XML 转换

PHP XSL 扩展允许 PHP 应用 XSLT 转换。

尽管这通常用于应用样式表,但重要的是要知道许多其他形式的转换也是可能的。

XSL 是一种表达 XML 文档样式表的语言。它类似于 CSS,因为它描述了如何显示 XML 文档。

XSL 定义了 XSLT,它是 XML 文档的转换语言,允许将 XML 文档处理成其他文档。

XSLT 处理器接收一个输入 XML 文件、一些 XSLT 代码,然后生成一个新文档。摘自维基百科知识共享的图 7-1 说明了这一点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-1。

XSLT processor

这方面的一个用例可以是创建一个可以由浏览器呈现的 XHTML 文档。

输入 XML 将从一个 PHP 程序接收,该程序包含关于从哪里检索 XSL 样式表的处理指令。浏览器将检索这个样式表,并在其中应用 XSLT 代码来生成 XHTML。

首字母缩略词事实真相
可扩展样式表语言(Extensible Stylesheet Language 的缩写)表达样式表的语言
可扩展样式表语言转换将 XML 处理成另一个 XML 文档的转换语言

PHP 手册 2 中有一个简单的例子,说明如何使用 PHP 通过 XSL 转换 XML 文件:

<?php

$xslDoc = new DOMDocument();
$xslDoc->load("collection.xsl");

$xmlDoc = new DOMDocument();
$xmlDoc->load("collection.xml");

$proc = new XSLTProcessor();
$proc->importStylesheet($xslDoc);
echo $proc->transformToXML($xmlDoc);

用 PHP 解析 XML

PHP 中有两种类型的 XML 解析器。有几个解析 XML 的 PHP 扩展,但是它们都属于这两种类型之一。

所有的 PHP XML 扩展都使用相同的底层库,因此可以在它们之间传递数据。

所有 XML 例程都需要启用LibXML扩展和Expat库。这两者在 PHP 中都是默认启用的。

树形分析器

树解析器试图一次解析整个文档,并将其转换成树结构。很明显,如果您试图解析一个非常大的文档,这可能会带来问题。

PHP 中有两种树解析器:

  • SimpleXML
  • 数字正射影像图
基于事件的分析器

这些解析器比树解析器更快,消耗的内存更少。它们通过一个节点一个节点地读取 XML 文档来工作,并为您提供了一个机会来挂钩与这个读取过程相关的事件。

基于事件的解析器的两个例子是:

  • 的成员
  • XML Expat 解析器

XML Expat 解析器是一个基于非验证事件的解析器,也内置在 PHP 的核心中。它不需要 DTD,因为它不验证 XML,只要求 XML 格式良好。

错误代码

PHP 手册 3 列出了几个 XML 错误代码。这个列表是底层libxml库的 733 个错误代码的子集。

这里是您应该熟悉的 XML 常量的部分列表,因为它们比其他代码更常见。

字首码描述
XML_ERROR_SYNTAXXML 的格式不正确。
XML_ERROR_INVALID_TOKEN您在 XML 中使用了无效字符。
XML_ERROR_UNKNOWN_ENCODING无法解析您的 XML,因为无法确定编码方案。
XML_OPTION_CASE_FOLDING默认情况下启用,并将元素名称设置为大写。
XML_OPTION_SKIP_WHITE跳过源文档中多余的空白。
字符编码

当 PHP 解析 XML 文档时,它会执行一个称为源代码编码的过程来读取文档。

支持三种编码形式:

  • UTF-8
  • ISO-8859-1(默认)
  • 美国-阿斯凯

UTF-8 是一种多字节编码方案,这意味着单个字符可以由多个字节表示。其他两种方案都是单字节的。

PHP 在内部存储数据,然后在将数据传递给函数时执行目标编码。

默认情况下,目标编码设置为与源编码相同,但这是可以更改的。但是,在创建解析对象后,不能更改源编码。

如果解析器遇到源编码不能表示的字符,它将返回一个错误。

如果目标编码方案不能包含字符,那么该字符将被降级以适应编码方案。实际上,这意味着它们被一个问号所取代。

XML 扩展

XML 扩展允许您创建 XML 解析器和定义处理程序。您应该熟悉以下功能。

功能使用
xml:parser_create($encoding)用指定的编码创建 XML 分析器。
xml:parser_create_ns($encoding, $separator=":")使用支持 XML 命名空间的指定编码创建 XML 分析器。
xml:parser_free($xmlparser)释放 XML 解析器。
xml:set_element_handler($xmlparser, $start, $end)这告诉解析器在 XML 文档中每个元素的开头和结尾调用哪个函数。您可以通过FALSE来禁用特定的处理程序。$start$end都必须是可调用的,并且通常是存在于作用域中的函数的字符串名称。

处理元素开始的函数必须接受三个参数:

  • XML 解析器资源
  • 一个字符串,它将包含正在分析的元素的名称
  • 元素具有的属性数组

结束处理函数必须接受两个参数:

  • XML 解析器资源
  • 一个字符串,它将包含正在分析的元素的名称

xml:set_object($xmlparser, $object)函数允许在对象中使用 XML 解析器。这意味着您可以将对象的方法设置为用于设置元素处理程序的函数。

xml:parse_into_struct($parser, $xml, $valueArr, $indexArr)函数将 XML 字符串解析成两个并行的数组结构,一个(index)包含指向 values 数组中适当值的位置的指针。这最后两个参数必须通过引用传递。

数字正射影像图

DOM 是文档对象模型的缩写。DOMDocument类对于处理 XML 和 HTML 很有用。

它使用 UTF-8 编码,需要libxml2扩展(Gnome XML 库)和expat库。它是一个树解析器,在创建内部树表示之前将整个文档读入内存。

下面是一些DOMDocument语法的基本示例:

<?php
$domDoc = new DomDocument();
$domDoc->load("library.xml");
// $domDoc->loadXML($xmlString);
// $domDoc->loadHTMLFile("index.html");
// $domDoc->loadHTML($htmlDocumentString);
$domDoc->save(); // (to a file in XML format)
$xmlString = $domDoc->saveXML();
$htmlDocumentString = $domDoc->saveHTML();
$domDoc->saveHTMLFile(); // (to a file in HTML format)
$xpath = new DomXpath($dom);
$elements = $xpath->query("//*[@id]"); // find all elements with an id
echo "I found {$result->length} elements<br>";
if (!is_null($elements)) {
    foreach ($elements as $element) {
        echo "<br/>[". $element->nodeName. "]";

        $nodes = $element->childNodes;
        foreach ($nodes as $node) {
            echo $node->nodeValue. "\n";
        }
    }
}

您应该熟悉 DOM 类的以下方法:

方法描述
createElement创建一个节点元素,该元素可以用 node 类的appendChild方法追加。
createElementNScreateElement一样,但是支持带有名称空间的文档。
saveXML将 XML 树转储回字符串。
save将 XML 树转储回文件中。
createTextNode创建类DOMText的新实例。
DOM 节点

DOMNode类用于处理 DOM 树中的节点。

您可以通过调用DOMDocument的这些方法之一来检索节点:

  • getElementById
  • getElementsByTagName
  • getElementsByTagNameNS

这些方法返回一个DOMNodeList对象,可以使用foreach()遍历该对象。

getElementById()函数要求您指定哪个属性属于类型id。你可以通过包含一个定义它的 DTD 或者通过调用setIdAttribute()函数来做到这一点。无论是哪种情况,都必须验证文档才能调用函数。

当使用insertBefore()插入一个节点作为兄弟节点时,您需要引用父节点并指定您想要在之前插入新节点的兄弟节点。此示例显示了语法:

<?php
$xmlString = <<<XML
<root>
<teams>
<team>Silverbacks</team>
<team foo="winner">Golden Eyes</team>
</teams>
</root>
XML;

$domDoc = new DOMDocument();
$domDoc->loadXML($xmlString);
$xpath = new DomXPath($domDoc);
$team2 = $xpath->query('teams/team[2]');
$parent = $xpath->query('teams');
$textElement = $domDoc->createElement('team', 'Bearhides');
$parent->item(0)->insertBefore($textElement, $team2->item(0));

在本例中,我们希望在两个现有团队之间插入一个新团队。为此,我们找到了团队和家长。

Note

这些变量包含DOMElements。我们不能使用parent()方法,因为它是在DOMNode类上定义的。

您应该熟悉DOMNode类的这些方法。

方法描述
appendChild在子节点的末尾添加一个新的子节点。
insertBefore在引用节点前添加新的子节点。
parentNode节点的父节点,如果没有父节点,则为 null。
cloneNode克隆一个节点,也可以克隆它的所有后代节点。
setAttributeNS将名称空间为namespaceURI且名称为name的属性设置为给定值。如果该属性不存在,将会创建它。

Note

您需要将一个节点作为参数传递给这些函数。

如果你试图使用appendChild(),那么你必须首先使用一个类似DOMDocument::createElement()的函数来创建节点。

SimpleXML

SimpleXML 是一个扩展,它牺牲了对复杂需求的健壮处理,而支持提供一个简单的接口。它需要 simpleXML 扩展,并且只支持 XML 规范的 1.0 版本。

Caution

是一个树解析器,在解析文档时将整个文档加载到内存中。这可能使它不适合非常大的文档。

SimpleXML 提供了一种面向对象的方法来访问 XML 数据。它创建的所有对象都是SimpleXMLElement类的实例。元素成为这些对象的属性,属性可以作为关联数组来访问。

创建 SimpleXML 对象

您可以使用过程方法或通过面向对象的方法创建 SimpleXML 对象:

<?php
// procedural from string variable
$xml = simple_xml:load_string($string_of_xml);
// procedural from file
$xml = simple_xml:load_file('filename.xml');
// object oriented from variable
$xml = new SimpleXMLElement($string_of_xml);

迭代 SimpleXML 对象

children()方法返回子对象的可遍历数组。

您可以创建一个算法来检查节点的子节点,然后递归地遍历它们。PHP 手册页上有这样一个例子。

检索信息
功能行动
SimpleXMLElement::construct()创建一个新的SimpleXMLElement对象。
SimpleXMLElement::attributes()标识元素的属性。
SimpleXMLElement::getName()检索元素的名称。
SimpleXMLElement::children()返回给定节点的子节点。
SimpleXMLElement::count()返回一个节点有多少个子节点。
SimpleXMLElement::asXML()以格式良好的 XML 字符串形式返回元素。
SimpleXMLElement::xpath()在当前节点上运行xpath查询。
语言

XPath 4 是一种定义 XML 文档各部分的语言。它将 XML 文档建模为一系列节点,并使用路径表达式在文档中导航和选择节点。

SimpleXMLElement::xpath()对 XML 数据运行 XPath 查询,并返回与指定路径匹配的子数组。

W3Cschools 的网站上有 XPath 用法的例子。 5

应该注意,与 PHP 结构不同,XPath 结果不是从零开始的。XPath /college/student[1]/name将返回第一个学生,而不是第二个学生,如果它是从零开始的,就会出现这种情况。

包含xpath结果的 PHP 数组是从零开始的。换句话说,如果您将结果存储在名为$array的数组变量中,那么$array[0]将对应于上例中的college/student[1]/名称。

您可以使用这样的 XPath 来检索文本值:/college/student/name[text()]

您可以像这样指定范围:/college/student[attendance<80]/name

在 DOM 和 SimpleXML 之间交换数据

函数simple_xml:import_dom()将把一个 DOM 节点转换成一个SimpleXML对象。

您可以使用dom_import_simplexml()SimpleXML对象转换为 DOM。

肥皂

SOAP 6 最初是简单对象访问协议的首字母缩写。业界发布了 1.0 和 1.1 版本。从版本 1.2 开始,该标准由 W3C 控制,缩写已经消失,使得 SOAP 只是一个普通的名称。

PHP SOAP 扩展用于编写 SOAP 服务器和客户端。它要求启用libxml,这是默认 PHP 安装的情况。

SOAP 缓存功能在php.ini文件中用soap.wsdl_cache_*设置进行配置。

如果 SOAP 可用,那么它会提供一组预定义的常量。这些常量与 SOAP 版本、编码、身份验证、缓存和持久性相关。

只有两个 SOAP 函数:

  • is_soap_fault()返回 SOAP 调用是否失败。
  • use_soap_error_handler()用于 SOAP 服务器,并设置 PHP 是否应该使用 SOAP 错误处理程序。如果设置为 false,则使用 PHP 错误处理程序,而不是向客户端发送 SOAP 错误。

SOAP 的其余功能在类中提供。

肥皂有什么作用

SOAP 允许定义和交换复杂的数据类型,并为各种消息传递模式提供了一种机制,其中最常见的是远程过程调用(RPC)。

这实际上允许开发人员在服务器上执行一个函数,将复杂数据作为参数传递给它,并接收复杂数据。

SOAP web 服务是由 WSDL (Web 服务描述语言)定义的。大多数人把这个首字母缩略词读作“whizz-dill”。

WSDL 使用 XML 结构定义数据类型。它还描述了可以远程调用的方法,指定了它们的名称、参数和返回类型。

服务器和客户端之间的 SOAP 消息以称为 SOAP 信封的 XML 结构发送。

使用 SOAP 服务

SoapClient类用于连接和使用 SOAP 服务。

可以解析一个 WSDL 文件来发现哪些方法是可用的,然后以一种易于使用的方式呈现给你。

<?php
$client = new SoapClient("http://example.com/login?wsdl");
$params = array('username'=>'name', 'password'=>'secret');
// call the login method directly
$client->login($params);

// If you want to call __soapCall, you must wrap the arguments in another array as follows:
$client->__soapCall('login', array($params));

在前面的示例中,我们连接到一个示例 WSDL,并使用两种不同的方法调用 login 方法。注意,使用SoapClient::__soapCall()方法需要将参数包装在一个数组中。

SOAP 服务提供 WSDL 并不是强制性的。如果需要使用这样的服务,可以将 null 作为 WSDL 文件传递,但是需要提供关于服务端点的信息。您必须提供位置和 URI 选项,并且可以选择提供有关 SOAP 服务版本的其他信息,如下例所示:

<?php
$client = new SoapClient(null,
    ['location' => 'http://example.com/soap.php',
        'uri' => 'http://test-uri/',
        'style'    => SOAP_DOCUMENT,
        'use'      => SOAP_LITERAL));
    ]);

当构造SoapClient类时,可以将 trace 参数设置为 true,以便调试原始 SOAP 信封头和主体。

以下两个调试命令要求跟踪为真,并允许您检查请求的详细信息:

  • SoapClient::__getLastRequestHeaders()
  • SoapClient::__getLastRequest()

提供肥皂服务

SoapServer类提供一个 SOAP 服务器。它支持版本 1.1 和 1.2,并且可以在有或没有 WSDL 服务描述的情况下使用。

下面是一个设置 SOAP 服务器的示例:

<?php
$options = ['uri'=>'http://localhost/test'];
$server = new SoapServer(NULL, $options);
$server->setClass('MySoapServer');
$server->handle();

我们可以看到,我们首先创建了带有一系列选项的服务器。在本例中,我们没有在第一个参数中提供 WSDL,因此我们必须在选项数组中提供服务器名称空间的 URI。

一旦我们有了一个SoapServer类的实例,我们就传入它将用来服务请求的类名。连接到服务器的 SOAP 客户端可以调用该类中的方法。

除了设置一个类之外,你还可以使用一个具体的对象来处理 SOAP 请求,方法是用SoapServer::setObject()函数将它作为一个参数传递。

REST Web 服务

REST 是表述性状态转移的缩写,是一种架构风格,而不是 PHP 扩展或命令集。REST 有几个旨在提高 web 服务性能和可维护性的约束。

Tip

将“面向服务的架构”与“微服务架构”进行比较,前者通常在 SOAP 中实现,后者通常在 REST 中实现。

REST 有几个类似于 HTTP 请求类型的动词。这导致了一些混乱,但是需要注意的是,REST 并不一定要使用 HTTP 作为传输层来进行通信。HTTP 恰好对 REST 非常方便,因为它是无状态的,请求类型可以很好地转换成 REST 动词。

REST 公开了链接到资源的统一资源标识符(URI)。这些链接被称为 REST 端点。根据用来访问它们的 HTTP 类型,它们将对资源执行一个动作(改变它的状态)。HTTP 类型用于通知要执行的 REST 动词。

REST 关注资源并提供对这些资源的访问。资源可以是类似于“用户”的东西。就像数据库模式表示用户实体一样,REST 将在 JSON 或 XML 结构中表示用户。

一个表示应该是服务器和客户端都可读的。REST 可以用来传输 JSON 和/或 XML。我们稍后会更详细地讨论这个问题。

在 PHP 中,REST APIs 最常见的用途之一是为支持 AJAX 的前端提供服务,比如用 Angular 或 ReactJS 编写的前端。

应用和资源状态

REST 服务器不应该记住应用的状态,客户机应该发送执行所需的所有信息。

这意味着对服务器的每个请求都是独立的。如果对服务器的请求失败,不会影响其他请求的成功或失败。这提高了应用的可靠性。

服务器不负责记住应用处于什么状态,而是依赖客户端发送处理请求所需的所有信息。这意味着客户端存储和维护应用状态(而不是服务器)。

应用无状态对水平伸缩有着重要的影响。因为没有单独的服务器维护状态,所以请求可以到达组中的任何服务器并被正确处理。

REST 提供访问的资源的状态应该在请求之间持续。资源状态在服务器上维护。

休息动词

REST 有几个用于改变服务器上资源状态的动词。

动词既可以作用于单个资源,也可以作用于一组资源。

| 资源 | GET | PUT | POST | DELETE |
| 募捐 | 列出您可以从中检索成员的 URIs | 用另一个集合替换该集合 | 在集合中创建新条目 | 删除整个集合 |
| 单一实体 | 检索单个元素的表示 | 替换该元素,如果它不存在,则创建它 | 创建新成员 | 删除成员 |

PUTPOST看起来很相似,但是有一个重要的区别。POST要求您为一个元素指定所有必需的属性,并将创建一个新元素。PUT将替换您为现有记录指定的属性,除非您正在创建新记录,否则您不需要提供所有属性。

为了举例说明,让我们考虑一个有名字和头衔的用户。首先,我们POST创建一个名为“Alice”、头衔为“Mrs”的新用户。然后爱丽丝毕业了,成为了一名医生,所以我们PUT到她的记录中,只包括了“博士”这个头衔。我们不需要指定她的名字,因为我们不需要,她的名字也不会被改变。

恨死我了

HATEOAS 代表“超文本是国家的引擎”。在这个概念中,来自服务器的响应将包括关于客户端接下来可以采取什么动作的信息。这些选项将用超文本标记出来。

目的是让客户不需要预先知道REST服务的端点。相反,当他们进行查询时,将为他们提供通过应用所需的端点。

让我们考虑一个例子:

GET /account/12345 HTTP/1.1

HTTP/1.1 200 OK
<?xml version="1.0"?>
<account>
<account_number>12345</account_number>
<balance currency="usd">100.00</balance>
<link rel="deposit" href="/account/12345/deposit" />
<link rel="withdraw" href="/account/12345/withdraw" />
<link rel="transfer" href="/account/12345/transfer" />
<link rel="close" href="/account/12345/close" />
</account>

在前面的例子中,从 HATEOAS, 7 上的维基百科页面,我们正在检索一个银行账户的信息。服务器用可用于进一步操作的 URIs 列表进行响应。例如,如果帐户余额为负,服务器可能不包括取款的链接。服务器通过暴露与上一次操作相关的附加 URIs 来引导客户端通过 API。

请求标题

HTTP 允许在其请求中传递标头。REST 客户机将使用这些来指示服务器它们正在提供什么以及它们期望得到什么。

REST 客户机应该使用 accept 头向服务器指出它想要返回哪种内容(表示)。例如,如果客户机将 accept 头设置为text/xml,它就告诉服务器它需要一个 XML 格式的响应。

客户端还将设置一个Content-Type头,通知服务器其有效负载的 MIME 类型。有关更多详细信息,请参见响应标题中的部分。

回应标题和代码

Content-Type头是由服务器发送的,它定义了被发送的主体的 MIME 类型。例如,服务器可以将content-type设置为application/json,以表明响应的主体包含 JSON 格式的文本。

服务器还将设置一个状态代码,通知客户端请求的结果。这里列出了一些常见的代码,但还有更多。 8

密码意义
200请求处理成功
201资源已创建
202资源已接受处理,但尚未处理
400错误的请求(客户端错误)
401未经授权;客户端在访问该资源之前必须进行自我验证
403禁止;客户端已经对自己进行了身份验证,但是没有权限访问该资源
500服务器或应用错误

Tip

在响应正文中发送与 HTTP 响应代码相矛盾的消息是非常糟糕的做法。

在 Zend 框架中,术语“上下文切换”指的是根据程序是响应 REST 请求还是其他请求来改变程序的输出。

例如,对于普通请求,您可以用 HTML 页面来响应,如果请求是通过XMLHttpRequest (AJAX)发出的,您可以用 JSON 来响应。

您也可以用 XML 或 JSON 来响应,这取决于客户机指示它需要什么类型的内容作为响应。

另一个例子是用不同的布局来响应,这取决于使用的是哪种浏览器(例如,移动设备还是桌面设备)。

您应该熟悉服务器对同一 URL 的调用做出不同响应的概念,这取决于客户端如何设置其请求。

发送请求

curl扩展是 PHP 中发送 REST 请求的常用方式。Curl 允许您指定头和请求类型。

有包装了curl函数的库。其中比较受欢迎的是 Guzzle, 9 这款软件安装和使用都很简单。它提供了非常广泛的特性,在我撰写本文时,我认为它是 PHP 请求客户端的最佳选择。

数据

JSON 是 JavaScript 对象符号的首字母缩写。在 PHP 中,它经常与 Ajax 一起使用,Ajax 是异步 JavaScript 和 XML 的缩写。

JSON 允许您将对象序列化为字符串,以便它可以在服务之间传输。Ajax 是传输字符串的一种方式。

这些技术一起允许您在浏览器中的 JavaScript 应用和服务器上的 PHP 应用之间进行通信。

缺省情况下,JSON 扩展加载在 PHP 中,并提供处理与 JSON 相互转换的方法。

它提供了许多常量,包括:

常数意义
JSON_ERROR_NONE确认是否发生了 JSON 错误。
JSON_ERROR_SYNTAX确认解析 JSON 时是否有语法错误,并帮助检测编码错误。
JSON_FORCE_OBJECT如果一个空的 PHP 数组被编码,这个选项将强制它被编码为一个对象。

该扩展提供了三个功能。

json_decode()将一个字符串作为第一个参数,并返回一个对象。如果第二个参数设置为 true,它将返回一个关联数组。

从 PHP 5.3 开始,提供了两个额外的选项— $depth$options。深度是指递归深度,目前唯一的选择是JSON_BIGINT_AS_STRING,它将大整数的浮点数转换为字符串。

如果超过递归深度,json_decode()将返回NULLjson_last_error_msg()将返回"Maximum stack depth exceeded"。如果阵列的层数超过您指定的可接受深度,就会发生这种情况。

例如,考虑以下代码:

<?php
$arr = [
    "fruits" => [
        "apple" => ["taste" => "sweet", "color" => "yellow"],
        "banana" => ["taste" => "sour", "color" => "green"],
        "cherry" => ["taste" => "sweet", "color" => "red"]
    ],
    "vegetables" => "yuck"
];
$str = json_encode($arr);
$decode = json_decode($str, true, 1);
echo json_last_error_msg(); // Maximum stack depth exceeded

该数组有两层,因为每个水果都包含一个数组。我们指定只解码一个深度级别,因此$decode将是NULL,脚本将输出"Maximum stack depth exceeded"

json_encode()将任何类型的变量(除了资源)作为参数,并返回 JSON 表示。它有两个可选参数——$depth$options——与前面描述的相同。

json_last_error()返回前一个函数中出现的最后一个错误代码,而json_last_error_msg()返回一个字符串消息。

Tip

记住第六章的内容,JSON 是序列化传输到客户端的数据的首选方式。

日期和时间

PHP 提供了几个从服务器获取日期和时间的函数。您应该在配置中设置一个默认时区,或者在运行时在脚本中设置它。您应该设置时区来匹配您的服务器所在的时区,以便 PHP 可以正确地解释服务器时间。这也让您的脚本知道夏令时等调整。

PHP 5.2 引入了DateTime类,它处理大范围的日期和时间计算。建议使用这个类,而不要使用像date()time()这样的函数。

要创建一个新的DateTime对象,您可以向它传递一个它可以解析的字符串。它可以理解多种字符串格式,如下例所示:

<?php
$strings = [
    'Next monday',
    'Yesterday',
    '', // now
    '2016-12-25',
    '25 December 2016',
    '-1 week',
    '+1 days'
];
foreach ($strings as $example) {
    $dateTime = new DateTime($example);
    echo $dateTime->format(DateTime::COOKIE) . PHP_EOL;
}

本例中数组中的所有字符串都将被理解。

如果日期格式不明确,那么您可以使用DateTime::createFromFormat()命令来创建对象。

例如,日期 2013 年 6 月 3 日将被一个美国人写成 06-03-2013,而世界上的其他人将写成 03-06-2013。如果您向 PHP 提供这些字符串中的任何一个,它都不知道您是指 2013 年 6 月 3 日还是 2013 年 3 月 6 日。

要解决这种不确定性,您可以指定在字符串中使用哪种格式,如下所示:

<?php
$dateTime = DateTime::createFromFormat('d-m-Y', '06-03-2013');
echo $dateTime->format(DateTime::COOKIE);

该脚本将输出类似于欧洲中部时间 2013 年 3 月 6 日星期三 12:56:42 的内容。注意,如果您在创建一个DateTime类时省略了时间,将使用脚本运行的时间。

格式化日期

在这些例子中,我们使用了由DateTime提供的一个类常量来格式化我们的日期。

手册中列出了这些常量,它们是日期显示或存储的常见用例。它们出现在下表中:

常数格式
ATOMY-m-dTH:i:sP
COOKIEl, d-M-Y H:i:s T
ISO8601Y-m-dTH:i:sO
RFC822D, d M y H:i:s O
RFC850l, d-M-y H:i:s T
RFC1036D, d M y H:i:s O
RFC1123D, d M Y H:i:s O
RFC2822D, d M Y H:i:s O
RFC3339Y-m-dTH:i:sP
RSSD, d M Y H:i:s O
W3CY-m-dTH:i:sP

这些是字符串常量,包含日期和时间格式代码。格式化代码被替换为一个由DateTime类生成的值。例如,符号“Y”被替换为存储日期的四位数年份。

显然,声明常量的目的是让你不必去记忆字符串,所以不用担心学习格式。我包含了格式化字符串,因为它们很好地表明了常用的格式。

日期和时间格式代码区分大小写。例如,“Y”是两位数的年份,“Y”是四位数的年份。

格式化字符串中未被识别为格式化字符的字符将被原封不动地放入输出中。因此,字符串“Y-m-d”在输出时将包括年、月和日之间的连字符,就像这样“2015-12-25”。

您可以在手册页上找到 PHP 日期和时间格式代码的列表, 10 ,但这里是上表中的那些:

密码用…替换示例
Y整整四位数的年份One thousand nine hundred and ninety-nine
M两位数的月份,带前导零06
d一个月中的某一天,带前导零的两位数Fourteen
D三个字母的文本日杀了他,Wed
H带前导零的 24 小时制小时00, 09, 12, 23
i两位数的分钟,带前导零05,15,25,45
s两位数秒,带前导零05,15,25,45
P与格林威治时间(GMT)的差异,小时和分钟之间有冒号(PHP 5.1.3+)+02:00
O与格林威治时间(GMT)的时差(小时)+0200
T时区缩写是啊,这个

日期计算

使用DateTime类方法modify()可以执行最简单的计算。例如,要查找一个月后的日期和时间,您可以执行以下操作:

<?php
$dateTime = new DateTime();
$dateTime->modify('+1 month');
echo $dateTime->format(DateTime::COOKIE) . PHP_EOL;

然而,PHP 提供了一种更加灵活的方式来处理日期计算。

DateInterval类用于存储固定的时间量(年、月、日、小时等)。)或一个相对时间字符串,其格式为DateTime的构造函数支持的格式。

DateTime类允许你从DateTimeadd()sub()出一个DateInterval。它将处理闰年和其他时间调整,同时这样做。

为了在创建一个DateInterval对象时指定一个固定的时间量,我们向它的构造函数传递一个字符串。该字符串总是以P开头,然后以降序列出每个日期单元的编号。可选地,出现字母T,然后包括时间单位。

举几个例子来说明这一点更有意义:

线描述
P14D14 天
P2W两周
P2W5D这是无效的;不能在一个字符串中同时指定周和日;这些星期将被忽略
P2WT5H两周零五个小时
P1Y2M3DT4H5M一年,两个月,三天,四小时,五分钟

请注意:

  • 每个字符串都以 P 开头
  • 单位数位于表示单位的字母之前
  • 时间单位通过字母 T 与日期单位分开
  • 单位按降序排列

下面是一个代码示例:

<?php
$dateTime = DateTime::createFromFormat('d-m-Y H:i:s', '01-12-2016 13:14:15');
$dateInterval = new DateInterval('P1M2DT3H4M5S');
$dateTime->add($dateInterval);
echo $dateTime->format(DateTime::COOKIE) . PHP_EOL;

此代码输出日期和时间,即 12 月 1 日 13:14:15 之后的一个月、两天、三小时、四分钟和五秒。

手动日期计算

有时,您需要使用 UNIX 风格的时间戳。这个时间戳是一个数字,它保存了自 UNIX 纪元 1970 年 1 月 1 日以来经过的秒数。时间戳的一个优点是它不受时区的限制。

有几个 PHP 函数可以让你创建一个时间戳。strtotime()函数是一种非常灵活的将日期时间描述转换成时间戳的方法。它足够智能,可以识别像“下周一”或“+1 年”这样的短语,以及像“2017 年 4 月 1 日”这样更普通的字符串。

mktime()函数接受每个小时、分钟、秒、月、日或年的参数。mktime()返回给定参数的 UNIX 时间戳。如果参数无效,函数返回FALSE

请注意,参数的顺序不会随着单元大小的增加而增加,而是按照“h i s m d y”的顺序。

您可以从右到左省略参数,在这种情况下,它们将默认为当前日期值。所以如果当前年份是 2016 年,你在没有指定年份的情况下调用mktime(),PHP 会假设你指的是 2016 年。

如果您传递给mktime()的参数大于允许的值,mktime()会认为您引用的是下一个周期。

例如,十二月有 31 天。如果您调用mktime(0, 0, 0, 12, 32, 2016),那么您将获得下个月第一天的时间戳;换句话说,2017 年 1 月 1 日。

比较日期

DateTime::diff()方法允许您比较两个DateTime对象之间的差异。它返回一个DateInterval,包含所表示的两个日期之间的时间段。

注意,DateTime类为您处理时区和夏令时转换。

让我们试着找出离圣诞节还有多长时间。

<?php
$now = new DateTime();
$christmas = new DateTime('25 december');
if ($now > $christmas) {
    $christmas = new DateTime('25 december next year');
}
$interval = $christmas->diff($now);
// 97 days until Christmas
echo  $interval->days . ' days until Christmas' . PHP_EOL;

请注意这段代码中的以下内容:

  • 不向构造传递任何参数都使用当前日期和时间。
  • 我们可以使用像><==这样的数学运算符来比较DateTime对象。
  • 当创建一个DateTime时,我们可以使用相当灵活的语言,例如“明年 12 月 25 日”用于当前日期在圣诞节和新年之间的情况。
  • diff()方法返回一个DateInterval
  • 对象有许多公共属性,可以访问这些属性来度量年、月,在本例中是天。

PHP SPL 数据结构

标准 PHP 库(SPL)是用来解决常见问题的接口和类的集合。它包括几个帮助你处理标准数据结构的类。

与数据结构相关的接口

在我们看 SPL 数据结构类之前,有必要看一下它们实现的一些接口。这使得记住这些类有什么功能变得相当容易。

迭代程序

Iterator接口扩展了Traversable接口。

Iterator接口 11 定义了用于在集合中移动的五种方法。

方法目的
current返回当前元素
key返回当前元素的键
next向前移动到下一个元素
rewind将迭代器倒回到第一个元素
valid检查当前位置是否有效
可穿越

实现可遍历接口 12 的类可以使用foreach()进行循环。

这个接口不能自己实现,它只能通过实现一个告诉类如何迭代集合的接口来实现。

实际上,这意味着要实现可遍历接口,您必须实现IteratorIteratorAggregate接口。

数组式访问

该接口提供了以数组形式访问对象的能力。为此,您需要实现四种方法:

方法目的
offsetExists偏移是否存在
offsetGet要检索的偏移量
offsetSet为指定的偏移量赋值
offsetUnset取消偏移设置

如果您的类实现了此接口,那么您将能够在引用从它实例化的对象时使用数组语法。

可数的

如果您的类实现了Countable接口,您将能够使用count()函数来找出它有多少个元素。

Countable接口有一个名为count的抽象方法。当你在一个对象上调用 PHP 函数count()时,这个方法将被调用,这个对象是从一个实现接口的类实例化而来的。

<?php
class BadCount implements Countable
{
  public function count()
  {
    return 42;
  }
}
$a = new BadCount;
echo count($a);  // 42

在这个简单的例子中,这个类中的count()方法总是返回数字42。在一个更复杂的例子中,我们可以在这里实现逻辑,定义如何返回对象的计数。

列表

列表是元素的有序集合。同一个值可能在一个列表中出现多次。双向链表中的每个元素都包含一个到链中上一个和下一个元素的链接。

SplDoublyLinkedList 13 类实现了IteratorArrayAccessCountable接口。此外,它还实现了一些方法,让您可以更改迭代器的行为,以及在列表的前面或后面添加或删除条目。

SplStack14 扩展了SplDoublyLinkedList类。它本质上是一个SplDoublyLinkedList,你调用了setIteratorMode() 15 ,并设置列表使用IT_MODE_LIFO进行迭代,并在模式IT_MODE_KEEP下运行。这告诉迭代器像堆栈一样遍历列表(后进先出),遍历元素而不是删除它们。

SplQueue16 也扩展了SplDoublyLinkedList类。它实现了方法enqueuedequeue,这两个方法分别将一个元素添加到队列的末尾或删除队列前面的元素。

Caution

SplStackSplQueue类都继承自SplDoublyLinkedList类,所以你可能会错误地调用它们的错误方法。

这里有一个使用堆栈的例子,展示了一些你可以使用的方法。下表显示了堆栈中包含的值。

密码堆栈包含
<?php
$stack = new SplStack();Null
$stack->push(5);5
// this uses array syntax to add a new element
$stack[] = 4;5, 4
// now we push another number to the end of queue
$stack->push(3);5, 4, 3
// this inserts the number 100 into position 1
// elements below it are shuffled down
$stack->add(1, 100);5, 100, 4, 3
// this returns the last value in the queue
echo "Pop: " . $stack->pop() . PHP_EOL;0, 100, 4
foreach ($stack as $key => $value) {
echo "$key => $value" . PHP_EOL;
}

这段代码的输出如下:

Pop: 3
2 => 4
1 => 100
0 => 5

Note

密钥以降序(2,1,0)包含在堆栈中。

堆是树状结构,其中父节点可以有零个、一个或多个子节点。堆定义了一个比较规则,允许您确定一个节点是大于还是小于另一个节点。在堆中,父节点总是等于或大于其子节点。比较函数用于确定一个节点是大于还是小于另一个节点。

Note

SplHeap类是一个抽象类。使用时需要实现compare函数。

SplHeap类实现了Iterator 17 接口,这意味着您可以使用foreach()在其中移动。

SplMaxHeap类从SplHeap扩展而来,在顶部保持最大值。它通过为您实现compare()功能来实现这一点。同样,SplMinHeap类将最小值保持在顶部。

Note

SplMinHeapSplMaxHeap只是扩展了SplHeap并实现了compare()来提供定向排序的类。

让我们看一个简单的堆的例子:

<?php
class MyHeap extends SplHeap
{
  function compare($a, $b)
  {
    return $a <=> $b;
  }
}

$heapExample = new MyHeap;
$heapExample->insert(10);
$heapExample->insert(5);
$heapExample->insert(15);

while ($heapExample->valid()) {
  echo $heapExample->current() . PHP_EOL;
  $heapExample->next();
}

这段代码按照降序输出数字,因为当我们插入数字时,它会应用compare()函数来确定将它们放在哪里。

Note

如果我们修改代码并扩展SplMinHeapSplMaxHeap而不是SplHeap,输出与之前的代码相同!

我可以听到你烦恼地说SplMinHeap应该把最低的值放在最上面,那么为什么输出显示 15 仍然在最上面呢?答案是因为SplMinHeapSplMaxHeap类提供的只是compare()函数的默认实现,我们在类定义中覆盖了它。

您可以扩展SplMinHeap,但是只要您的compare()函数保持不变,就像前面的例子一样,您将始终拥有一个最大堆。为了获得一个最小堆的工作实现(在我们的例子中),你需要或者交换飞船操作符的操作数,或者完全避免实现compare()函数,而使用在SplMinHeap中声明的函数。

数组

SplFixedArray 18 结构以连续的方式存储数据,可通过索引访问。它比普通的 PHP 数组更快,但也不太灵活,因为它是固定长度的,只能使用整数作为索引。

SplFixedArray类实现了Iterator接口和ArrayAccess接口。

地图

映射是一种保存键值对的结构。PHP 数组是一种映射,因为它存储整数(或字符串)键的值。

SplObjectStorage提供了从对象到数据的映射,或者如果您忽略数据,它可以作为一个对象集。

SplObjectStorage不是抽象类,可以直接实例化。它实现了CountableIteratorSerializableArrayAccess接口。

因为它实现了ArrayAccess接口,所以您可以使用数组语法来引用结构内部对象的数据,如下所示:

<?php
$bucket = new SplObjectStorage();
$file = new StdClass;
$metaData = ['name' => 'passwords.xslx', 'size' => '102400'];
$bucket[$file] = $metaData;

在本例中,我们将数据(元数据)映射到一个对象的特定实例(文件)。

SPL 数据结构概述

| SplHeap | 堆是一个树集合,其中父级的子级必须始终具有低于其父级的值。有不同类型的堆。 |
| SplMaxHeap | 这是一种最大值保存在堆顶部的堆。 |
| SplMinHeap | 在这种类型的堆中,最小值保存在顶部。 |
| SplPriorityQueue | 这是一个队列,其中每个元素都有一个与之关联的“优先级”。一个用例的例子是带宽管理,其中特定类型的流量比其他流量具有更高的优先级。 |
| SplFixedArray | 这是一个更快的数组实现,但是它限制了您使用一个固定长度的数组,该数组只包含整数。 |
| SplObjectStorage | 这个类提供了一种方便的方法来映射对象及其数据。 |

还有一个名为DS的扩展,它提供了可选的数据结构。你可以在 PHP 网站 19 上找到它的文档,在 GitHub 上找到它的源代码。你不需要在 Zend 考试中知道它。

Chapter 7 Quiz

Q1:对还是错?无法在目标 XML 编码方案中编码的字符会生成错误。

| 真实的 |
| 假的;他们会发出警告 |
| 假的;它们符合编码方案(转换成问号) |
| 以上都不是 |

Q2:对还是错?如果请求失败,服务器不可能发送带有 HTTP 状态代码 200 的REST响应。

| 真实的 |
| 错误的 |

Q3:这段代码会输出什么?

| 语法错误;它不会运行 |
| 没什么;没有错误消息,所以echo语句不输出任何内容 |
| 超过了最大堆栈深度 |
| 致命错误,json_decode的第二个参数不能是" " " |

<?php
$arr = [
  "fruits" => [
    "apple" => ["taste" => "sweet", "color" => "yellow"],
    "banana" => ["taste" => "sour", "color" => "green"],
    "cherry" => ["taste" => "sweet", "color" => "red"]
  ],
  "vegetables" => "yuck"
];
$str = json_encode($arr);
$decode = json_decode($str, true, 1);
echo json_last_error_msg();

Q4:您应该为您的 PHP 应用设置默认时区。您可以使用以下哪种方法来实现这一点?选择尽可能多的适用项。

| 使用功能set_date_default_timezone() |
| 编辑php.ini |
| 在 PHP 上使用 Linux time()命令 |
| 使用 PHP 的ini_set()函数,像这样:ini_set('date.timezone', 'Europe/Edinburgh'); |

Q5:这段代码会输出什么?

| 4 |
| 5 |
| A fatal error will occur |

<?php
$stack = new SplStack();
$stack->push(5);
$stack[1] = 4;
echo $stack->pop();

Q6:下面的 PHP 代码有什么问题?

| 语法错误;它根本不会跑 |
| 登录方法的参数需要像这样传递:$client->login([$params]); |
| 你不能直接调用SoapClient上的方法 |
| 没什么不对;这会有用的 |

<?php
$client = new SoapClient("http://example.com/login?wsdl");
$params = array('username'=>'name', 'password'=>'secret');
// call the login method directly
$client->login($params);

Q7:这段代码会输出什么?

| 语法错误;它不会跑 |
| 银背企鹅 |
| 金色的眼睛 |
| 它将生成一个警告,因为xpath将无法评估 |

<?php
$xmlString = <<<XML
<root>
<teams>
<team>Silverbacks</team>
<team>Golden Eyes</team>
</teams>
</root>
XML;
$xml = new SimpleXMLElement($xmlString);
$result = $xml->xpath('teams/team[1]');
echo $result[0];

Q8:可以用 ______ 函数将 SimpleXML 对象转换成 DOM。

| dom_import_simplexml() |
| 简单 xml:导入 dom() |
| 简单 xml:导出 dom() |
| 以上都不是 |

Q9:这个脚本的输出是什么?

| 这将产生致命的错误 |
| 在团队列表的开头有一个新团队的 XML 文档 |
| 两个团队之间有一个新团队的 XML 文档 |
| 以上都不是 |

<?php
$xmlString = <<<XML
<root>
<teams>
<team>Silverbacks</team>
<team foo="winner">Golden Eyes</team>
</teams>
</root>
XML;
$domDoc = new DOMDocument();
$domDoc->loadXML($xmlString);
$textElement = $domDoc->createElement('team', 'Bearhides');
$result = $domDoc->xpath('teams/team[2]');
$result[1]->insertBefore($textElement);
echo $domDoc->saveXML();

Q10:下面的代码会输出什么?

| 这将产生致命的错误 |
| 未来一年、两个月、三天、四小时零五分钟的日期 |
| 以上都不是 |

<?php
$dateTime = new DateTime();
$interval = new DateInterval('P1Y2M3D4H5M');
$dateTime->add($interval);
echo $dateTime->format(DateTime::COOKIE);

Footnotes 1

https://en.wikipedia.org/wiki/Well-formed_document

2

https://php.net/manual/en/xsltprocessor.transformtoxml.php

3

https://php.net/manual/en/xml.error-codes.php

4

https://en.wikipedia.org/wiki/XPath

5

https://www.w3schools.com/xml/xml:xpath.asp

6

https://en.wikipedia.org/wiki/SOAP

7

Https://en.wikipedia.org/wiki/HATEOAS

8

https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

9

http://docs.guzzlephp.org/en/stable/

10

https://php.net/manual/en/function.date.php

11

https://php.net/manual/en/class.iterator.php

12

https://php.net/manual/en/class.traversable.php

13

https://php.net/manual/en/class.spldoublylinkedlist.php

14

https://php.net/manual/en/class.splstack.php

15

https://php.net/manual/en/spldoublylinkedlist.setiteratormode.php

16

https://php.net/manual/en/class.splqueue.php

17

https://php.net/manual/en/class.iterator.php

18

https://php.net/manual/en/class.splfixedarray.php

19

https://docs.php.net/manual/en/book.ds.php

八、输入输出

在这一章中,我们将会看到 PHP 是如何管理输入输出的。我们将研究如何读写文件系统和网络。

文件

有两组主要的函数来处理文件:一组处理文件资源,另一组处理文件名。

请记住,资源是一种不能直接存储在 PHP 中的变量。文件资源是操作系统文件句柄。

所有处理文件资源的函数都以一个字母f开始,然后有一个动词描述它们的功能。例如,fopen()打开一个文件资源。

使用文件字符串名称的函数都以单词file开头,后面跟一个描述它们做什么的动词。例如,file_get_contents()接受一个字符串文件名并返回该文件的内容。

打开文件

功能fopen()用于打开文件。它返回一个资源变量,该变量是文件的句柄。

您必须向fopen()传递两个参数:

  • 文件系统中文件的名称
  • 您要用来打开它的文件模式

文件模式

文件可以在不同的模式下打开。文件模式描述了我们将如何与文件交互。

文件模式与操作系统文件权限相关。例如,如果 PHP 用户对一个文件只有读权限,那么以写模式打开它的尝试将被操作系统拒绝。如果我们尝试使用较低的权限(比如只读),那么操作系统将为我们创建一个文件句柄。

当我们指定一个模式时,我们传达两条关于我们打算如何使用文件的信息:

  • 无论我们是在阅读,写作,还是两者都有
  • 我们是想把文件指针放在文件的开头还是结尾

文件指针就像一个迭代器光标。它存储下次读取时将返回的文件位置。

下表总结了常见的文件模式。

方式读/写指针行为
rR开始
r+RW开始
wW开始截断现有文件或创建一个新文件(如果它不存在)
w+RW开始
aW开始如果文件不存在,则创建一个新文件,如果存在,则保留当前文件
a+RW开始
xW不适用的尝试创建新文件进行写入;如果文件已经存在,返回FALSE并生成E_WARNING
x+RW
cW开始如果文件不存在,则尝试创建该文件;如果存在,将光标放在文件的前面
c+RW

您会注意到在文件模式中添加一个+符号的效果是表明您也想执行与默认模式相反的模式。因此,当我们覆盖一个文件时,如果我们添加一个+符号,那么我们就表示我们也想读取这个文件。然而,行为保持不变,因此我从表中省略了它,以便于阅读。

当使用w模式覆盖一个文件时,PHP 会将文件截断为零字节。如果您希望文件被新数据覆盖,这很有用。

如果文件已经存在,x模式将返回FALSE并生成警告。如果您想要避免覆盖您想要保留的数据,这是很有用的。

c模式将创建一个文件(如果存在)或打开一个现有文件。对于现有文件,指针将被设置到文件的开头。

文件模式标志

通过将两个标志添加到模式字符串的末尾,可以指定这两个标志。默认标志是由您的 SAPI 和您正在使用的 PHP 版本定义的,所以出于兼容性的考虑,您应该指定它们。

您可以指定一个b标志来表明您正在处理二进制文件。这意味着不会翻译任何字符。当您处理图像或其他二进制文件时,这是必要的。

在 Windows 服务器上,您可以指定一个t标志来将\n转换为\r\n

Tip

为了保持代码的可移植性,您应该使用b标志并确保您的代码使用正确的行尾。

读取文件

您可以使用fread()函数读取文件资源。

<?php
$handle = fopen('info.txt', 'r');
while (!feof($handle)) {
    echo fread($handle,1024);
}

在这个例子中,我们使用了文件函数feof(),当文件指针在文件末尾时,这个函数返回TRUE,否则返回FALSE。在一个while循环中使用它的效果是继续循环,直到我们到达文件的末尾。

fread()函数有两个参数。第一个是保存文件资源的变量,第二个是要读取的字节数。如果到达文件的末尾,那么fread()将停止读取。

下面是另外四个 PHP 函数,可以更方便地读取文件:

功能习惯
fgetcsv()从文件指针中读取一行并解析 CSV 字段
file_get_contents()获取一个字符串文件名并将结果读入一个字符串
readfile()读取一个字符串文件名并将内容写入输出缓冲区
file()将整个文件读入一个数组

写入文件

写入文件是通过二进制安全的fwrite()函数完成的。fputs()是该函数的别名。

fwrite()函数有两个参数——要写入的文件资源和要写入文件的字符串。

对于fgetcsv()函数有一个写对应物,即fputcsv(),它将一个数组格式化为 CSV,并将该行写入一个文件。除了文件资源和数组的参数之外,还需要可选参数来定义 CSV 格式。

如果你想把格式化的字符串写到一个文件中,你应该使用fprintf(),它的工作方式类似于printf()命令。

如果您想将文件的内容转储到连接的客户端,您可以使用fpassthru()。该函数将从当前文件位置开始,将文件的其余部分写入输出缓冲区。

最后,有一个方便的函数可以快速地将字符串写入文件。函数file_put_contents()不需要你提供文件资源,只需要文件名和你想写的字符串。

下面是一些正在使用的函数的简单示例:

<?php
$filename = 'test.csv';
$dataString = '1,2,3,4,5';
file_put_contents($filename, $dataString);
$handle = fopen($filename, 'r');
$myData = fgetcsv($handle);
echo gettype($myData); // array
echo count($myData); // 5

在示例中,我们使用快捷函数file_put_contents()将字符串输出到文件中。我们不需要一个可用的文件资源,因为file_put_contents()为我们处理这个。在我们写完字符串后,我打开一个文件资源从中读取,并使用fgetcsv()从文件中读取。该字符串是一个有效的 CSV 列表,因此$myData包含一个包含五个元素的数组。

文件系统功能

PHP 有一个将你连接到文件系统的函数的扩展列表。我们将在这一章中讨论其中的一些,但是正如我经常做的那样,我将让你参考 PHP 手册以获得详尽的列表。

目录

这组函数允许您遍历、创建和删除目录。

功能使用
chdir()改变 PHP 当前的工作目录。
chroot()将正在运行的进程的根目录更改为指定的目录,并将 PHP 的工作目录设置为/
rmdir()删除目录。
readdir()返回作为参数传递的目录句柄中下一个条目的名称。条目按照文件系统存储它们的顺序返回。
scandir()读取由 string 参数指定的目录,并返回它包含的文件和目录的列表。

scandir()和readdir()的区别在于它们取的参数。其中readdir()使用目录句柄,scandir(接受目录名作为字符串。

Caution

这可能令人困惑,因为文件函数的命名约定(f*file*)似乎不适用于目录。

文件信息

我们在安全性一章中提到了这些函数,但是还有其他需要获取文件信息的用例。

PHP 提供了finfo_open()函数,该函数返回一个fileinfo资源的新实例。您为它提供了两个参数——一个预定义的选项常量和一个 magic 数据库文件的字符串位置。

魔术数据库文件是一种用于描述文件类型的格式,也由 Unix 标准命令使用,file. 1 如果您不提供魔术数据库的路径,那么 PHP 将使用它附带的路径。

一旦 PHP 知道如何识别文件,你就可以使用finfo_file()函数来获得关于文件的信息。它至少需要两个参数——您刚刚创建的fileinfo资源和您想要检查的文件的字符串名称。

下面是 PHP 手册 2 中的一个例子:

<?php
$finfo = finfo_open(FILEINFO_MIME_TYPE);
foreach (glob("*") as $filename) {
    echo finfo_file($finfo, $filename) . "\n";
}
finfo_close($finfo);

这两个函数都具有面向对象的使用风格,如 PHP 手册中的示例所示:

<?php
// finfo will return the mime type
$finfo = new finfo(FILEINFO_MIME, "/usr/share/misc/magic");

/* get mime-type for a specific file */
$filename = "/usr/local/something.txt";
echo $finfo->file($filename);

管理文件

你可以用 PHP 来管理文件。下表列出了一些常用功能。

功能目的
copy复制文件。
unlink删除文件。
rename重命名文件。你可以用它在目录间移动文件。
chmod设置文件权限。
chgrp更改文件的组。
chown更改文件的所有者(仅限超级用户)。
umask更改当前的 umask。

确定文件系统对象的类型

验证文件和目录是否存在,以及您是否有适当的权限以您想要的方式使用它们,这是一个很好的编程实践。

PHP 提供了一些函数,如果匹配作为参数传递的字符串的对象满足测试,这些函数将返回布尔值。这些函数接受一个字符串参数,该参数是文件或目录的名称。

在下表中,检查针对的是找到的与参数中给定的名称相匹配的对象。

功能检查
is_dir是一个目录
is_file是一个文件
is_readable是一个文件或目录,可以读取
is_writeable是一个文件或目录,可以写入
is_executable是一个文件或目录,可以被执行
is_link是一个符号链接
is_uploaded_file由一个POST请求上传

如果没有找到与参数中给出的名称相匹配的文件系统对象,所有函数都将返回FALSE

魔法文件常量

PHP 有几个神奇的常数,你可以用在当前执行的文件中。

常数涉及
__LINE__当前正在执行的文件的行
__FILE__文件的完整路径和文件名
__FUNCTION__当前函数名
__CLASS__范围内的类的名称
__METHOD__正在执行的方法的名称

这些常量在编写调试日志时非常有用。例如,我通常以__METHOD__标记开始我的所有日志消息,这样就可以立即清楚日志消息是在哪个类和方法中生成的。

PHP 中的流是一种概括文件、网络、数据压缩和其他共享一组公共功能和用途的操作的方式。

溪流几乎就像一条传送带,一件一件地向你涌来。在 PHP 中,你也可以沿着传送带跳跃,寻找一个位置,而不是等待它来找你。

流是以您可能认识的格式引用的:

scheme://target

例如, http://www.php.nethttp方案和target指定为 PHP 网站的 URL。

流包装

包装器是将流翻译成特定编码或协议的代码对象。PHP 手册 3 中有一个在该语言中实现的包装器列表,stream_wrapper_register()函数让你定义自己的包装器。

草案使用
file://访问本地文件系统
http://访问 HTTP(s)URL
ftp://访问 FTP URL
php://访问各种 I/O 流
compress.zlib://压缩流
data://数据(RFC 2397)
glob://查找与模式匹配的路径名
phar://PHP 存档
ssh2://安全外壳 2
rar://压缩包
ogg://音频流
expect://流程交互流

您可以访问的 PHP 流有stdinstdoutstderrinputoutput、fd、memorytempfilter

注意,为了提高可读性,我省略了所有这些流的协议。当你使用它们的时候,它们都应该以php://协议为前缀,比如stdin就是php://stdin

作为读取流的例子,让我们看看如何读取一个PUT请求的主体。在您职业生涯的某个时候,您将编写一个 REST API,并且需要读取和解析客户端向您的服务器发出的PUT请求的主体。这种请求类型没有像GETPOST那样的超级全局,那么它是如何实现的呢?答案就在php://input流里!

<?php
// reads the PUT body
$input = file_get_contents('php://input');
// parses the input into an array
parse_str($input, $params);
print_r($params);

过滤

流过滤器可以应用于流,并对离开流的数据执行转换操作。

过滤器功能
string.rot13用 ROT13 编码数据
string.toupper将字符串转换为大写
string.tolower将字符串转换为小写
string.strip_tags从字符串中去除 XML 标记
convert.*例如,根据算法转换数据
mcrypt.*使用libmcrypt提供对称加密
mdecrypt.*使用libmcrypt的解密过滤器
zlib.*使用 ZLIB 库来压缩和解压缩数据

使用stream_filter_append()函数将这些过滤器附加到流上。您可以独立地将筛选器应用于流的读取和写入方向。

<?php
$handle = fopen("files.php", 'a+');
stream_filter_append($handle, 'string.rot13');
while (!feof($handle)) {
    echo fread($handle,1024);
}

您可以向stream_filter_append()提供第三个参数,将其附加到读或写流中。该参数是预定义常数STREAM_FILTER_READSTREAM_FILTER_WRITESTREAM_FILTER_ALL之一。默认情况下,筛选器附加到读取和写入。

该示例将输出如下内容:

<?cuc
$unaqyr = sbcra("svyrf.cuc", 'n+');
fgernz_svygre_nccraq($unaqyr, 'fgevat.ebg13');
juvyr (!srbs($unaqyr)) {
    rpub sernq($unaqyr,1024);
}

流上下文

流上下文是一组选项的包装器,可以修改流的行为。

您用stream_context_create()函数创建一个上下文。您向它传递两个可选参数,这两个参数都是关联数组。第一个参数是选项,第二个是上下文参数数组。

每种类型的流都有自己的一组上下文选项。PHP 手册中有详尽的列表。

当前唯一可用的参数是一个 callable,当流上发生事件时将调用该参数。这些事件都是预定义的STREAM_NOTIFY_*常量。

回调函数的原型在 PHP 手册中,还有一个 HTTP 流的 notify 事件的例子。

举个例子,如果你正在下载一个文件,你可以设置你的回调函数来响应STREAM_NOTIFY_FILE_SIZE_IS事件,如果文件太大就中止下载。这个例子阻止我们下载大于一千字节的 www.example.com 的主页。

<?php
function callback($notification_code,
    $severity,
    $message,
    $message_code,
    $bytes_transferred,
    $bytes_max)
{  
    if ($notification_code == STREAM_NOTIFY_FILE_SIZE_IS) {
        if ($bytes_max > 1024) {
            die("Download too big!");
        }
    }
}

$context = stream_context_create();

stream_context_set_params($context,
    ["notification" => "callback"]);

$handle = fopen('http://www.example.com', 'r', false, $context);

fpassthru($handle);

您可以使用stream_context_set_params()功能更改选项和参数,而stream_context_get_params()将返回流的当前参数。

Chapter 8 Quiz

Q1:假设 web 服务器用户拥有data.csv文件,并且在这个脚本运行之前它包含字符串"Hello World"。这段代码的输出会是什么?

| 字符串(0)" " |
| 字符串(1)“,” |
| 字符串(1) "1 " |
| 字符串(1) "2 " |
| 这将产生一个错误 |

<?php
file_put_contents('data.csv', '1,2,3,4,5');
$handle = fopen('data.csv', 'c+');
$data = fgetcsv($handle, 2);
var_dump($data[1]);

Q2:这段代码会输出什么?

| 这将产生一个错误 |
| 1,2,3,4,5,6,7,8 |
| 6,7,8,1,2,3,4,5 |
| 6,7,8 1,2,3,4,5 |
| 6,7,8,1,2,3,4,5 |
| 1,2,3,4,5 6,7,8 |

<?php
file_put_contents('test.csv', '1,2,3,4,5');
$handle = fopen('test.csv', 'c');
fputcsv($handle, ['6', '7', '8']);
fclose($handle);
echo file_get_contents('test.csv');

Q3:如果你正在编写一个 REST 接口,并且需要读取在一个PUT请求中发送的参数,你怎么做?

| 参考$_REQUEST超级全球 |
| 参考$_POST超级全球 |
| 阅读php://input stream |
| 阅读http://input stream |

Q4:我想在我的 PHP 程序运行时将日志条目写入一个文件。我不想丢失旧的日志条目,我需要我的日志条目有适当的日期顺序,最新的条目在旧的条目之后。我应该用什么文件模式打开我的文件?

| r |
| a |
| x |
| c |

Q5:假设我获取的文件是一个有效的 GIF 格式图像,并且我在 Linux 中运行 PHP。这段代码的输出会是什么?

| 这会产生一个错误 |
| GIF 图像数据,版本 89a,400x400 |
| JPEG 图像数据,400x400 |
| image/gif |
| image/jpeg |
| 无法重命名文件 |
| 以上都不是 |

<?php
// This is a valid GIF image
$url = ' https://goo.gl/QycgqH';
file_put_contents('earth.gif', file_get_contents($url));
if (!rename('earth.gif', 'earth.jpeg')) {
    throw new RuntimeException('Could not rename the file.');
}
$finfo = new finfo();
echo $finfo->file('earth.jpeg') . PHP_EOL;

Footnotes 1

[en.wikipedia.org/wiki/File_(command)](https://en.wikipedia.org/wiki/File_(command)

2

https://php.net/manual/en/function.finfo-file.php

3

https://php.net/manual/en/wrappers.php

九、Web 功能

PHP 是一种为网络而创造的语言。它最初的目的是让制作网页变得更容易,现在它仍然非常关注服务器端脚本。本章着眼于使它成为世界上最流行的服务器端 web 编程语言之一的一些语言特性。

请求类型

HTTP 有几种不同的请求方法 1 ,它们通常被称为 HTTP 动词。HTTP 规范 2 相当详细地列出了每个动词的用途。您的应用应该遵守这个规范,以便与使用它的客户端兼容。

动词习惯
GET检索指定资源的表示形式
HEADGET相同,但没有任何响应体
POST向服务器提交一个条目,通常会导致诸如添加新资源之类的更改
PUT用请求负载中的资源替换指定的资源
PATCH对指定的资源应用部分修改
DELETE删除指定的资源
CONNECT发起一个 HTTP 隧道 3
选择描述目标资源的通信选项
TRACE对目标资源执行消息环回测试

请求日期

在典型的生产 web 环境中,PHP 接受 web 服务器传递给它的请求。它运行并处理请求,然后终止并等待下一个请求。web 服务器可以随请求传递数据,这些数据构成了 PHP 运行的上下文 4 的一部分。

HTTP 请求由三部分组成:URL、头部和主体。数据可以包含在请求的任何部分中,并且可以用于 PHP 应用,如下所示:

来源进来了可用于
GET请求 URL 中的参数$_GET
POST请求的正文$_POST
PUT请求的正文php://input阅读
PATCH请求的正文php://input阅读
饼干“cookie”标题$_COOKIE
上传的文件请求的正文$_FILES

如果 PHP 正在从命令行处理一个请求,那么$_SERVER['argv']包含一个传递的参数数组,而$_SERVER['argc']包含传递的参数个数。

除了 HTTP 请求中包含的数据之外,PHP 还可以接受来自其运行环境的数据。例如,您可以在 docker 容器中运行 PHP,并设置一个包含数据库服务器地址的环境变量。您可以使用$_ENV超全局变量在 PHP 脚本中引用它。 5

请求超全局

$_REQUEST超全局是一个关联数组,默认包含$_GET$_POST$_COOKIE的内容。

php.ini设置variables_order决定了ENVGETPOSTCOOKIE变量中的哪一个出现在$_REQUEST数组中以及顺序。 6

如果同一变量存在于多个请求类型中,它将采用该设置值序列中最后一个的值。

例如,假设配置被设置为EGPCS,表示POSTGET之后。那么如果$_GET['action']$_POST['action']都被设置,那么$_REQUEST['action']将包含$_POST['action']的值。

因为您不能确定$_REQUEST中的数据到底来自哪里,所以您应该小心使用这个数组。在代码中引入不确定性会使测试变得复杂,并可能影响安全性。

邮政

按照惯例,POST请求用于向网站发送数据,并指示它创建一个新的实体。在 CRUD 范例中,这是一个写操作。

接收过帐数据

POST请求中发送的变量包含在主体中。与在 URL 中传递变量的GET请求形成对比。

如果用户提交一个表单,那么浏览器会将这些值编码到请求的正文中并发送给你。类似地,指向 API 端点的应用需要将变量编码到请求体中。PHP 将在$_POST变量中提供它们。

例如,下面是一个将POST作为 name 变量的值发送给网站的例子。这个请求将被用来添加一个叫做Ron的人到粉丝俱乐部。

POST  HTTP/1.1
Host: bieberfanclub.com
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache

name=Ronald

如果运行bieberfanclub.com的应用正在运行 PHP,那么$_POST数组将是一个数组,包含一个名为name的元素,其值为Ronald

POST发送变量有三个好处:

  • POST数据可以用特定的字符集编码,而GET则不是这种情况。
  • 因为变量是在消息体中发送的,所以可以发送的数据量不受 URL 长度的限制。
  • POST允许您上传文件,但GET不允许。

这两种方法在安全性上没有区别。

HTTP 协议中对 URL 的长度没有限制,但是对浏览器和其他客户端有限制。一般来说,不要创建超过 2000 个字符的 URL。

发送帖子数据

当您想向另一个应用发出一个POST请求时,您需要负责将变量编码到主体中。最简单的方法是使用curl扩展。 7 Curl 支持多种协议,可以轻松地按照您的需要设置您的请求。

使用curl包括以下过程:

  1. 初始化一个curl会话。
  2. 为会话设置选项。
  3. 执行会话(打电话)。
  4. 关闭会话并释放资源。

让我们看看如何使用curl来设置你之前看到的请求,其中你POST将包含值Ronald的变量name添加到比伯粉丝俱乐部。

<?php
// We specify the url when we initialize the curl resource
$curlResource = curl_init("https://requestb.in/13fkcqj1");
// This array contains the variables we want to POST
$postData = ['name' => 'Ron'];
// Tell curl to do a application/x-www-form-urlencoded POST
curl_setopt($curlResource, CURLOPT_POST, true);
// We specify the values to POST
curl_setopt($curlResource,CURLOPT_POSTFIELDS, $postData);
// Execute the request and store the response
$response = curl_exec($curlResource);
// If there is an error it will be stored in $err
$err = curl_error($curlResource);
// Close the handle
curl_close($curlResource);

如果您运行这段代码,您将能够在 https://requestb.in/13fkcqj1?inspect 看到结果。

Tip

可以传递一个你想要设置的所有选项的数组,而不是多次调用它。

得到

请求通常用于从服务器获取单个实体或一组实体。你可以把它想象成从服务器读取数据。

接收获取数据

GET请求中发送的变量被编码到 URL 中。以下是如何将变量编码到 URL 中的示例:

http://bieberfanclub.com/topfan.php?name=Ron&rank=cheerleader

变量以一个问号开始,并用和符号分隔。每个变量都是一个键值对,等号表示值。

PHP 会自动使 URL 中传递的变量在$_GET superglobal 中可用。

可以使用如下语法通过GET传递数组:

http://example.com/users.php?sort[col]=name&sort[order]=desc

您可以像这样访问这些变量:

<?php
echo $_GET['sort']['col'];
echo $_GET['sort']['order'];

发送获取数据

PHP 包含了一个函数,可以非常容易地构建 URL 字符串来传递您的GET数据。

<?php
$getData = ['fans' => ['Ron', 'Jonathan', 'Anne Frank']];
// fans%5B0%5D=Ron&fans%5B1%5D=Jonathan&fans%5B2%5D=Anne+Frank
echo http_build_query($getData);

函数将一个数组转换成一个正确的 URL 编码的查询字符串。

URL 的 HTTP 规范只允许使用非常有限的字符集。任何不在此集合中的字符都必须进行编码。 8 PHP 提供了urlencode()函数,它将正确地编码一个字符串以用作 URL 的一部分。urldecode()函数将把一个编码的字符串转换回它的原始表示。

<?php
$getData = ['fans' => ['Ron', 'Jonathan', 'Anne Frank']];
// fans%5B0%5D=Ron&fans%5B1%5D=Jonathan&fans%5B2%5D=Anne+Frank
$encodedString = http_build_query($getData);
// fans[0]=Ron&fans[1]=Jonathan&fans[2]=Anne Frank
echo urldecode($encodedString);

在这个例子中,我们正在解码由http_build_query()生成的正确 URL 编码的字符串,这样我们就可以看到一个数组是如何在一个参数中编码的。

请求用于替换整个实体或集合。通常,PUT请求会要求您指定一个实体的所有强制属性。这是一个写操作,因为它用您提供的状态替换了一个实体。

PATCH请求的相似之处在于它们都是用来替换数据的,但是PATCH请求只会替换你提供的实体的一部分。例如,如果一个用户有一个名字、姓氏和电子邮件字段,您将能够使用一个PATCH请求来更改其中一个字段,而保持其他字段不变。API 服务器通常不实现PATCH,而是要求你使用PUT

接收 PUT 数据

PHP 没有为PUT提供一个超级全局变量。要访问它,您需要读取php://input流。你可以使用parse_str()函数将它转换成一个数组:

<?php
$putVariables = [];
parse_str(file_get_contents("php://input"), $putVariables);

发送上传数据

PUT数据的传输和POST完全一样,所以curl是 PHP 中最简单的发送方式。

<?php
$data = ["fan" => "Ron"];
$curlResource = curl_init();
$options = [
    CURLOPT_URL => 'https://requestb.in/oxk2utox',
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_POSTFIELDS => $data
];
curl_setopt_array($curlResource, $options);
$response = curl_exec($curlResource);

在前面的例子中,我们告诉curl发出一个PUT请求,我们规定传递的值与我们对POST所做的完全一样。

注意,我们使用curl_setopt_array()函数来一次设置多个curl选项,而不是多次调用curl_setopt()

会议

HTTP 是一种无状态协议,这意味着一旦事务结束,客户机和服务器之间的连接就会丢失。此外,当 PHP 处理完一个请求并且它的应用状态丢失时,它就会终止。

会话是服务器为访问者的连续请求保存应用状态的一种方式。

诸如用户是否登录之类的信息可以存储在会话中。使用会话的另一个例子是在线购物网站,访问者购物车中的内容可以存储在会话中。

会话信息存储在服务器上,并与唯一标识符相关联。对于每个请求,客户端将向服务器发送会话标识符,这允许服务器将请求与特定会话相关联。

如果您有多个 web 服务器,那么您需要找到一种方法,要么在它们之间共享会话信息,要么确保访问者总是被定向到保存其会话信息的服务器。

不需要记住用户是谁或保留任何偏好的网站不需要使用会话。这种网站的一个例子是提供对所有访问者都一样的静态内容。

PHP 默认支持会话,但是可以通过php.ini中的配置设置禁用它们。

开始会话

PHP 中的一个会话在您调用函数session_start()时启动,或者如果您的php.ini配置指定了session.auto_start = 1则自动启动。

如果您正在使用session_start(),那么您必须确保在任何输出被发送到客户端之前调用这个函数。

当会话开始时,用户被分配一个随机的唯一会话标识符,称为session id。如果启用了session.use_trans_sid配置设置,会话 ID 或者存储在客户端的 cookie 中,或者通过 URL 传递。

接受来自 URL 的会话可能有风险,最好通过session.use_only_cookies设置将 PHP 配置为只使用 cookies。第六章关于安全有更多的信息。

会话标识符和会话变量

会话扩展使保存会话标识符的 SID 预定义常数可用。您也可以使用session_id()函数来获取或设置它。

您可以使用函数session_regenerate_id()为客户端创建一个新的会话标识符。您应该在调用session_start()之后立即调用它,以帮助防止会话固定。

一旦会话开始,超全局$_SESSION就可以作为包含会话变量的关联数组使用。

结束会话

要正确结束会话,您应该做三件事:

  1. $_SESSION数组设置为空数组。
  2. 将会话 cookie 过期时间设置为过去。
  3. 调用函数session_destroy()

步骤 2 的作用是让客户端浏览器知道它可以删除包含会话标识符的 cookie。但是,不能保证客户端会这样做。当然,如果您没有使用基于 cookie 的会话,那么就没有必要这样做。

会话处理程序

PHP 支持创建自己的会话处理程序,但默认情况下,PHP 会话存储在磁盘上,并使用serialize()unserialize()命令来编码和解码数据。

除了基于磁盘的会话,PHP 还附带了一个 memcached 会话处理程序,可以在php.ini中配置。

如果你想写自己的会话处理程序,你应该实现SessionHandler接口。这将允许您使用存储会话的替代方法,并自定义如何编码和解码会话数据。

文件上传

在这一节中,我们将重点讨论文件上传是如何工作的以及与之相关的 PHP 语法。请务必结合本节学习第六章中关于文件上传的章节。

表单允许通过“多方”HTTP POST事务上传文件。

您可以通过声明如下所示的表单来指定您想要在 HTML 中使用多部分表单数据对您的POST进行编码:

<form enctype="multipart/form-data" action="" method="post">

请注意,我将“action”属性留空。默认情况下,HTML 表单将提交给提供它的 URI。

限制上传的大小

你不希望人们上传大量的文件来填满你的磁盘。要管理人们可以上载的文件的大小,您可以在浏览器和服务器上限制文件的大小。

要告诉客户端限制上传的大小,您可以像这样在表单中添加一个输入:

<input type="hidden" name="MAX_FILE_SIZE" value=" 1000000" />

限制浏览器的大小只是为了改善用户体验。用户很容易禁用或更改限制来绕过限制。

您应该配置 PHP 来限制POST操作的大小。post_max_size设置限制了任何POST可以包含的最大数据量。upload_max_filesize用于限制可以上传的文件大小。

临时文件

PHP 将上传的文件存储在一个临时位置,并让表单POST接收到的脚本可以使用它。

您可以在临时位置处理文件,然后有选择地将其移动到永久位置。当脚本运行结束时,PHP 会自动删除临时文件,所以如果你想保留它,你必须移动它。

除了创建临时文件,PHP 还将填充$_FILES超全局数组。表单中上传的每个文件在数组中都有一个条目。

您需要意识到,$_FILES数组中的信息很容易被欺骗,因此您应该手动验证每一条信息。

每个文件由一个数组表示在$_FILES超全局中,并将键入名称、类型、大小、临时文件名和错误代码。

钥匙描述
name存储在客户端上的文件的原始名称
type客户端提供的 MIME 类型
size文件的字节大小
tmp_name文件在其临时位置的名称
error错误代码,如果上传成功,则为UPLOAD_ERR_OK

Note

第六章有更多关于处理文件上传的信息。

形式

表单允许用户向 PHP 脚本提交数据。

当用 HTML 声明一个表单时,需要指定它用来向服务器发送信息的方法。虽然您可以选择GETPOST,但是您应该确保您选择的请求方法与您想要做的相匹配。

PHP 自动使表单数据在两个超级全局变量中的一个中对您的脚本可用,这两个超级全局变量是$_GET$_POST,这取决于表单用来发出请求的方法。

表单元素

这些超全局变量很容易被客户端编辑,所以应该小心过滤,不要相信它们。

表单域名中的点和空格被转换为下划线。例如,考虑 HTML 输入标签:

<input name="email.address" type="text">

它包含的值将根据 forms 方法放在$_GET['email_address']$_POST['email_address']中。

HTML 表单中的数组

可以使用 HTML 中的语法将表单数据转换为数组:

<form action="formhandler.php" method="POST">
<input type="text" name="name[first]">
        <input type="text" name="name[last]">
        <input type="submit">
</form>

这将导致$_POST$_GET成为如下所示的数组:

array(
  'name' => array(
    'first' => '',
    'last' => ''
  )
)

数组帮助的最有用的方式之一是将输入分组在一起。

考虑一个可以有多个值的复选框:

<h1>What pets do you want in your home?</h1>
<form action="formhandler.php" method="POST">
<input type="checkbox" name="pets[]" value="cats" id="lotsacats">
<label for="lotsacats">Lots of Cats</label>
<input type="checkbox" name="pets[]" value="dog" id="adog">
<label for="adog">Just a dog</label>
<input type="submit">
</form>

如果该人在提交表单前勾选了两个框,那么$_GET$_POST数组将包含以下内容:

array(
  'pets' => array('cats', 'dog')
)

这使得复选框更加整洁和易于使用。你可以在 PHP 手册中了解更多。 9

从列表中选择多个项目

最后,如果您希望用户能够从一个select列表中选择多个项目,您将需要使用一个数组:

<select name="var[]" multiple="yes">

注意,select的名称是一个数组,所以用户选择的每个值都将被添加到超级全局数组中的"var"数组中。

饼干

Cookies 允许您在客户端设备上存储少量(4 到 6 KB)数据。客户端将读取它们,并在每次请求时发送它们。

您可以在 cookie 中存储任何类型的信息,但是它们通常与会话相关联。PHP 可以将其会话标识符存储在 cookie 中。会话信息存储在服务器上,并通过 cookie 中的标识符与客户端匹配。当您开始会话时,PHP 会为您做这件事。默认情况下,PHP 会话 cookies 在用户关闭浏览器之前一直有效。

您无法控制客户端设备上的 cookies。客户可以随时编辑或删除它们。这意味着你既不应该相信与它们一起发送的信息,也不应该依赖它们的存在。您也不应该将敏感信息存储在 cookies 中。

如果您想删除 cookie,您可以设置一个过去的到期日期。这将让客户端知道不再需要 cookie,可以将其删除。你不能保证客户会尊重这一点。

服务器将使用Set-Cookie响应头设置一个 cookie。客户端将使用Cookie请求头将其包含在未来的请求中。

设置 Cookies

setcookie()函数用于设置一个 cookie。PHP 手册中解释了这些参数,并按照下表的顺序给出:

参数用于
value在 cookie 中存储标量值。
expirecookie 过期时的 Unix 纪元时间戳。在 cookie 过期之前,你不能依赖它,因为人们删除他们的 cookie 是很常见的。
pathcookie 将在其上可用的域的基本路径。如果设置为/,那么它在所有路径上都可用;否则,它将在该路径及其所有子路径上可用。
domain该 cookie 将在此域及其下的所有子域中可用。您只能设置与提供 cookie 的域相匹配的 cookie。
secure告诉客户端,如果 cookie 是通过 HTTPS 加密连接发送的,它应该只发送 cookie。
httponly告诉客户端应该只使用 HTTP 发送 cookie,而不要让 JavaScript 等脚本语言使用它。在一定程度上,这有助于减少对支持 XSS 和会话固定的客户端的攻击。

Cookies 只能存储标量值。但是,您可以使用如下所示的语法:

<?php
setcookie("user[name]", "Alice");
setcookie("user[email]", "alice@example.com");

下一次用户向站点发出请求时,$_COOKIE变量将包含如下内容:

Array
 (
        [PHPSESSID] => jlm5od9ngqi3krmu6fkjcebcb4
        [user] => Array
     (
         [name] => Alice
         [email] => alice@example.com
     )

)

注意user是一个数组,cookie 值也包含 PHP 会话标识符。

正在检索 Cookies

您可以使用$_COOKIE超级全局变量来访问 cookie 信息。

记住,这个数组是用客户机发送的 cookie 中的信息填充的。这意味着,如果您使用setcookie()来创建或更改 cookie,那么当客户端发出新请求时,$_COOKIE数组将只包含新信息。

HTTP 头

HTTP 头随客户端的请求和服务器的响应一起发送。它们用于传递关于 HTTP 消息的信息,比如提供了什么类型的信息,以及作为回报将接受什么。

HTTP 头采用明文字符串中的名称-值对的形式。每个标题后面都有一个回车符和换行符。标准中没有限制,但是大多数服务器和客户机对一个请求/响应中可以发送的报头长度和报头总数有限制。

PHP 将自动为您发出有效的头,但是在一些情况下,您可能想要发送自己的头。

发送邮件头

PHP 函数header()让你发送一个头到客户端。您只能在任何正常内容发送到客户端之前发送邮件头。在包含的 PHP 文件中省略结束标签?>的原因之一是为了避免在标签后出现换行符。该字符将作为 HTML 内容发送,并阻止您发送标题。

发送到header()的参数如下:

参数描述
标题字符串包含要设置的标头的字符串。比如"Cache-Control: no-cache, must-revalidate"
替换布尔值,指示此标头是否必须替换以前发送的同名标头。
响应代码与标头一起发送的 HTTP 响应代码。

头有两种特殊情况。第一种是以字符串"HTTP/"开头的头。这些可以用来显式设置 HTTP 响应代码,如 PHP 手册 10 中的这个例子:

<?php
header("HTTP/1.0 404 Not Found");

第二种特殊情况是使用"Location"标题。这个头向客户端表明他们正在寻找的文档在您指定的位置。

Note

如果您使用这个头,PHP 将自动设置一个 302 HTTP 状态码,除非您已经设置了一个 2xx 或 3xx 头。

这里有一个例子:

<?php
header("Location: http://www.example.com/");
exit;

在此示例中,服务器将使用状态代码 302 进行响应,客户端将被重定向到示例域。

注意发送重定向头后出口语言结构的用法。您的代码在发送标头后会继续运行,除非您停止它。这取决于客户端是否尊重您的重定向头。如果他们决定不尊重它,您的代码将继续输出,他们将看到它生成的任何输出。确保在发送重定向头后显式终止程序。

跟踪标题

headers_list()函数将返回一个准备发送或已经发送到客户端的头数组。您可以通过调用headers_sent()来确定报头是否已经发送。

如果您想阻止发送特定的标题,您可以使用headers_remove()功能从要发送的列表中取消设置标题。

HTTP 认证

PHP 可以向客户端发送一个头,让它弹出一个需要认证的对话框。当用户在对话框中输入用户名和密码时,PHP 脚本的 URL 会被再次调用。

在第二次调用时,PHP 将在$_SERVER数组中有三个预定义的变量。它们是PHP_AUTH_USERPHP_AUTH_PWAUTH_TYPE,分别被设置为用户名、密码和认证类型。

然后,您应该使用您认为合适的任何方法对用户进行身份验证,比如对照数据库检查用户和密码。

PHP 手册页 11 中给出了 HTTP 认证的例子:

<?php
 if (!isset($_SERVER['PHP_AUTH_USER'])) {
     header('WWW-Authenticate: Basic realm="My Realm"');
     header('HTTP/1.0 401 Unauthorized');
     echo 'Text to send if user hits Cancel button';
     exit;
 } else {
     echo "<p>Hello {$_SERVER['PHP_AUTH_USER']}.</p>";
     echo "<p>You entered {$_SERVER['PHP_AUTH_PW']} as your password.</p>";
}

在这个例子中,我们只是输出了$_SERVER数组中变量的内容,但是在现实生活中,我们会执行某种形式的认证。

客户端发送的密码是 base64 编码的,以标准化字符集,但没有执行哈希或加密。这是一种非常脆弱的保护网站的方式,除非你使用 HTTPS,否则在你的客户端和服务器之间的任何人都可以读取密码。

HTTP 状态代码

HTTP 状态代码与响应一起发送,并遵循互联网工程任务组制定的标准以及行业内使用的实际标准。

HTTP 状态代码被分成 100 个代码的范围。该范围内的所有状态代码将具有类似的含义,如下表所示。

范围一般含义
200请求成功
300请求需要被重定向
400客户端有一个错误
500服务器端有一个错误

你不需要为你的考试记住所有不同的状态代码。当你在现实生活中编写 API 时,你将能够访问维基百科 12 并为你的响应选择正确的代码。

对于你的考试,你只需要知道最重要的几个:

密码状态
200OK;请求成功。
201已创建;该请求导致创建新资源。
301永久移动;资源将总是在指定的位置找到。
400错误的请求;请求中的某些内容格式不正确或妨碍了它的执行。
401未经授权;客户端没有被授权进行此请求。
403禁止;(经过身份验证的)客户端不允许发出此请求。
418我是茶壶;客户端试图向服务器发送咖啡制作协议,服务器实际上是一个茶壶。好吧,这不是一个重要的身份代码,但了解它很有趣。
500内部服务器错误;服务器无法完成请求,无法做出更恰当的响应。通常与崩溃或错误配置相关联。

使用 API 时,HTTP 状态代码非常重要。如果你正在编写一个 API,你应该确保你为错误发送了正确的状态码。

例如,如果请求失败,您在正文中发送了一条错误消息,您应该确保 HTTP 状态代码是 400 而不是 200。

当您使用 PHP 时,您会对状态代码更加熟悉,但是如果有疑问,您应该查找一个列表,并确保您发送的是一个适当的响应。

输出控制功能

不是立即从脚本中输出,而是将输出存储在一个缓冲区中,然后立即刷新整个缓冲区,这通常非常有用。这对于在将输出发送给用户之前对输出进行转义,或者使用 PHP 内置的压缩例程来压缩发送给兼容浏览器的响应非常有用。

ob_start()功能用于启动输出缓冲。您的脚本通常会在响应正文中输出的任何内容都将被存储到缓冲区中。该函数采用一个可选参数,该参数是可调用的,当输出或丢弃缓冲区时将调用该参数。

下面是一个设置回调函数的例子。我将脚本的输出作为注释包含在内。

<?php
function escapeOutput(string $buffer): string {
    return htmlentities($buffer);
}

ob_start("escapeOutput");

// &lt;script&gt;alert(&quot;xss&quot;);&lt;/script&gt;
echo '<script>alert("xss");</script>';

在本例中,当脚本结束时,缓冲区被隐式刷新。您可以使用ob_flush()函数 14 显式刷新缓冲区并输出其内容。当您正在编写 CLI 脚本并希望能够看到进度时,这可能是有用的,而另一个(可能是不好的)用例可能是为长轮询 JavaScript 客户机编写服务器。

<?php
ob_start();
// this is a cli script
for ($i=1; $i<5; $i++) {
    echo $i;
    // each character is output one by one
    ob_flush();
    sleep(1);
}

ob_flush函数将输出缓冲区,并允许进一步缓冲输出。将其与ob_end_flush, 15 进行比较,后者将输出缓冲并禁用任何进一步的缓冲。

Note

如果您的 web 服务器正在缓冲输出,并且您想尝试覆盖这种行为并直接向浏览器发送内容,那么可以使用flush()函数。然而,这并不总是有效的。

PHP 有一个内置的方法来帮助你在通过网络发送数据之前压缩数据。如果您启用它,那么 PHP 将检测浏览器是否能够支持压缩数据,如果是,使用 GZIP 算法来减少响应的大小。要设置它,您需要指定压缩函数作为对ob_start()的回调。更多例子请看 PHP 手册 16 ,这里有一个简单的例子:

<?php
ob_start("ob_gzhandler");
?>
{"string": "my json api output is compressed now"}

Chapter 9 Quiz

Q1:假设variables_order被设置为 PHP 的默认值。对于下面的 HTTP 请求,$_REQUEST['biggestfan']的值是多少?

| Ron |
| Ronald |
| 别的东西 |
| 以上都不是 |

POST  HTTP/1.1
Host: thebeebfanclub.com?biggestfan=Ron
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache

biggestfan=Ronald

Q2:这些输入的每一个都有一个合适的构造形式,适合你的网站。将输入框中输入的名字放入一个变量的正确方法是什么,你可以像这样访问这个变量$_POST['justin]['numberonefan']

| <input type="text" name="justin.numberonefan"> |
| <input type="text" name="justin(numberonefan)"> |
| <input type="text" name="justin[numberonefan]"> |
| <input type="text" name="justin_numberonefan"> |
| 以上都不是 |

Q3:cookie 是一种存储信息的可靠方式,不会浪费服务器资源。选择最正确的选项。

| 是的,在客户端存储信息可以节省服务器磁盘空间 |
| 不,信息的副本仍保留在会话数据中 |
| 不,他们不可靠 |
| 是的,将所有会话数据存储在 cookies 中是很常见的 |

Q4:这段代码会输出什么?选择所有适用的选项

| 一个通知,因为$ a 未定义 |
| 警告,因为$a未定义 |
| 警告,因为您无法启动会话 |
| 包含session_id的随机字符串 |

<?php
echo $a;
session_start();
echo session_id();

q5:cookie 的domain属性用来做以下哪一项?

| 限制 cookie 对网站的哪一部分有效 |
| 指定您的网站的名称 |
| 阻止浏览器将此 cookie 发送到其他网站 |
| 以上都不是 |

Footnotes 1

https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods

2

https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9

3

https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT

4

[en.wikipedia.org/wiki/Context_(computing)](https://en.wikipedia.org/wiki/Context_(computing)

5

https://php.net/manual/en/reserved.variables.environment.php

6

https://php.net/manual/en/ini.core.php#ini.variables-order

7

https://php.net/manual/en/book.curl.php

8

https://en.wikipedia.org/wiki/Percent-encoding

9

https://secure.php.net/manual/en/faq.html.php#faq.html.arrays

10

https://php.net/manual/en/function.header.php

11

https://php.net/manual/en/features.http-auth.php

12

https://en.wikipedia.org/wiki/List_of_HTTP_status_codes

13

https://en.wikipedia.org/wiki/Hyper_Text_Coffee_Pot_Control_Protocol

14

https://php.net/manual/en/function.ob-flush.php

15

https://php.net/manual/en/function.ob-end-flush.php

16

https://secure.php.net/manual/en/function.ob-gzhandler.php

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值