四、社交媒体
社交媒体融合了技术和通信来创造社交互动和协作。Twitter 和脸书是两个最受欢迎的社交媒体网站,吸引了数百万忠实用户,引发了一些争议,而在脸书的情况下,导致了一部获奖电影。
自 2006 年推出以来,Twitter 一直是世界上最受欢迎的微博服务。它已经提供了数十亿条推文(140 个字符或更短的消息),通过网络和 SMS(短消息服务)设备共享。马克·扎克伯格的大宝宝脸书比以往任何时候都更加引人注目。新闻报道了脸书的隐私问题,时代杂志宣布扎克伯格为年度人物,电影社交网络获得了很多好评。
*Twitter 和脸书都通过 OAuth 认证。在本章中,我们将解释 OAuth 是什么以及如何与之连接。
Twitter 有三个应用编程接口(API)。有一个使用 GET 查询的公共搜索 API 和两个私有 REST APIs。一个 REST API 在您的个人网络中提供特定的用户信息和动作,而另一个提供低延迟和高容量的流。在这一章中,我们将展示如何使用公共搜索 API 和认证后的私有 API。
有了 Twitter,你可以拥有朋友,他们被定义为你正在关注的人或正在关注你的人。我们将向您展示如何生成您的 Twitter 好友及其状态的列表。我们还将讨论将 Twitter 登录绑定到您自己网站的身份验证的高级主题,使用数据库来存储用户凭证,以及使用数据库缓存来帮助消除过度 tying Twitter 和达到我们的请求限制。
脸书有一个开发良好的 API 和一个官方的 PHP SDK。在这一章中,我们将学习用脸书创建一个新的应用,认证,并进行一些 API 调用。
非统组织
OAuth 代表开放身份验证,它使用为特定应用生成的密钥/秘密令牌字符串,这些字符串持续固定的时间。OAuth 发生在消费者应用和服务提供者之间。使用 OAuth 进行身份验证的基本步骤如下:
- OAuth 应用将其消费者令牌发送给服务提供商(如脸书或 Twitter ),以换取请求令牌。
- 系统会提示用户获取权限,并授予用户权限。
- 使用回叫 URL 或个人识别码(PIN)来验证许可请求。
- 请求令牌和 PIN(或回叫)被交换为访问令牌。
- 用户现在可以使用带有访问令牌的应用。
注更多 OAuth 信息,请访问
[
oauth.net](http://oauth.net)
。对于 Twitter 内部的 OAuth 来说,[
dev.twitter.com/pages/auth](http://dev.twitter.com/pages/auth)
是一个有价值的资源。
在撰写本文时,PHP 最流行的两个 OAuth 模块是 PECL OAuth 和 Zend_Oauth。关于 PECL OAuth 的信息可以在[www.php.net/manual/en/book.oauth.php](http://www.php.net/manual/en/book.oauth.php)
找到,Zend_Oauth 在[
framework.zend.com/manual/en/zend.oauth.html](http://framework.zend.com/manual/en/zend.oauth.html)
找到。
Twitter 提供了三种使用 OAuth 的机制。如果您只需要连接到应用所有者的帐户,那么您将获得可以直接使用的访问令牌。这允许您绕过上面概述的前四个步骤。如果您的程序允许多个用户访问他们的 Twitter 帐户,那么您可以对客户端应用使用 PIN 验证方法,或者定义一个回调页面,这样可以绕过让用户进行验证。我们将在本章的后面更深入地讨论每一种方法。
推特
公共搜索 API
要搜索特定的推文,我们不需要认证。从http://dev.twitter.com/doc/get/search
的 Twitter 搜索文档中,我们可以看到搜索 Twitter 的 URL 是:
http://search.twitter.com/search.format
(其中format
是 JSON 或者 Atom)。
可以设置可选参数,例如语言、地理编码、开始和结束时间间隔以及区域设置。搜索查询本身由参数q
表示。标准的 Twitter 符号是有效的,比如用户的@
和关键字散列标签的#
。查询[
search.twitter.com/search.json?q=montreal&lang=en&until=2010-10-21](http://search.twitter.com/search.json?q=montreal&lang=en&until=2010-10-21)
将搜索在 2010 年 10 月 21 日之前发布的带有“蒙特利尔”一词的英语推文。
清单 4-1 显示了一个搜索 Twitter 帖子的简单表单。
***清单 4-1。*搜索示例:twitter_get_search.php
`
<?php error_reporting ( E_ALL ^ E_NOTICE );
$url = “http://search.twitter.com/search.json?lang=en&q=”;
if ( isset ( KaTeX parse error: Expected '}', got 'EOF' at end of input: … ) ) { full_query = $url . urlencode (
P
O
S
T
[
′
q
u
e
r
y
′
]
)
;
_POST ['query'] );
POST[′query′]); raw_json = file_get_contents (
f
u
l
l
q
u
e
r
y
)
;
full_query );
fullquery); json = json_decode ( $raw_json );
//uncomment to display the available keys
/* foreach ( $json->results[0] as $key => $value ) {
echo $key . “
”;
}
*/
echo “
”;echo “”;
foreach ( $json->results as $r ) {
user | tweet |
---|
echo ' ';
echo $r->from_user . ‘’;
echo ‘’ . $r->text . ‘’;
echo ‘’;
}
echo “”;
}
?>
清单 4-1 的第一部分显示了一个简单的表单,带有一个用于查询的文本字段和一个提交按钮。下一部分检查表单提交并对查询进行编码。然后从生成的 URL 获取输出。我们使用json_decode
将返回的 JSON 格式的对象转换成 PHP 对象。我们留下了一些注释掉的代码,可以用来标识可用的字段。最后,结果被循环并显示为一个 HTML 表。
注意,我们在脚本的顶部调用了error_reporting(E_ALL ^ E_NOTICE);
,它显示了除通知之外的所有错误消息。这将有助于我们在出现问题时进行调试。关于 JSON 的更多信息可以在第十五章中找到。
私有 REST API
许多 PHP 库被编写成与 Twitter API 接口。然而,大多数这些库使用凭证(用户名和密码的组合)作为连接的基本身份验证。截至 2010 年 8 月,Twitter 使用 OAuth,不再支持基本认证。总之,许多与 Twitter 接口的库现在都过时了,或者至少需要更新。从基本身份验证更改为 OAuth 的原因是为了增加安全性。
最常用的 PHP OAuth-Twitter 库之一是 twitteroauth,可在[
github.com/abraham/twitteroauth/downloads](https://github.com/abraham/twitteroauth/downloads)
获得。我们将在整章中使用这个库。Twitteroauth 由两个主要文件组成,Twitteroauth 和 oauth.php。对于我们的例子,将两者都放在相对于 webroot 的目录'/twitteroauth/'
中。
您还需要确保启用了 curl library for PHP。所需的库文件会因您使用的操作系统而异。在 Windows 上,库是php_curl.dll
,在你的php.ini
文件中,你可以添加或者取消注释行extension=php_curl.dll
。在 Linux 上,库是curl.so
,你需要在你的php.ini
文件中有一行extension=curl.so
。您还可以在 shell 中使用命令php –m
或者通过调用phpinfo()
函数来检查您安装的模块,
最重要的事情:获得一个 Twitter 账户
为了学习这些例子,你需要一个 Twitter 账户。在[
twitter.com/signup](https://twitter.com/signup)
注册过程快速简单。参见图 4-1 。您将收到一封确认电子邮件。一旦你确认了你的账户,前往 Twitter 的发展区[
dev.twitter.com/](http://dev.twitter.com/)
。我们将在[
dev.twitter.com/apps/new](http://dev.twitter.com/apps/new)
创建一个新的应用,用于 OAuth。
图 4-1。 Twitter 应用注册表单
注意 Twitter 要求填写应用网站字段。如果您正在本地测试您的应用,或者您的应用没有公共主页,您将需要假装您有。填写任何有效的网址,如
foobar.com。
对于我们的第一个演示,我们将使用以下设置:
Application Type: *Client* Default Access Type: *Read-only*
这两种类型的应用是桌面“客户端”和网络“浏览器”。web 浏览器应用使用公共回调 URL 在身份验证过程中接收信息。桌面客户端不需要外部访问就可以与 OAuth 服务提供者进行通信。相反,会给出一个 PIN,并要求用户返回到应用以完成身份验证。
访问类型可以是“只读”(默认)或“读写”只读仅允许您请求和查看信息。读写访问还允许您将数据发送回应用。
Twitter 将为我们生成一个消费者密钥和消费者秘密令牌,我们将在示例中使用它们。
图 4-2。 Twitter 生成的消费代币
本章中的大多数 Twitter 示例都需要使用我们的消费令牌,所以为了方便起见,我们将它们放在一个外部文件中。参见清单 4-2 。
***清单 4-2。*定义我们的消费代币:twitter_config.php
`<?php
define ( ‘CONSUMER_KEY’, ‘1bSbUBh***************’ );
define ( ‘CONSUMER_SECRET’, ‘M2FFf2k**********************************’ );
?>`
使用我的访问令牌进行认证
在我们的应用的右边菜单上,有一个到我的访问令牌的链接。这为我们提供了访问令牌和访问秘密令牌。这些令牌允许我们进行身份验证,而无需经过所有常见的 OAuth 步骤。
图 4-3。 Twitter 单用户直接访问令牌
使用我们的直接访问令牌,我们可以连接 twitteroauth。参见清单 4-3 。
***清单 4-3。*用twitteroauth
和我的接入令牌:twitter_direct_access.php
进行简单认证
`<?php
error_reporting ( E_ALL ^ E_NOTICE );
require_once (“twitteroauth/twitteroauth.php”);
require_once (“twitter_config.php”);
//My Access tokens
$accessToken = ‘ACTUAL_ACCESS_TOKEN’;
$accessTokenSecret = ‘ACTUAL_SECRET_ACCESS_TOKEN’;
//since we know our access tokens now, we will pass them into our constructor
$twitterOAuth = new TwitterOAuth ( CONSUMER_KEY, CONSUMER_SECRET, $accessToken, $accessTokenSecret );
//verify credentials through Twitter API call
$user_info = $twitterOAuth->get ( “account/verify_credentials” );
if ( KaTeX parse error: Expected 'EOF', got '&' at position 11: user_info &̲& !user_info->error ) {
print "Hello " . $user_info->screen_name . “!
”;
} else {
die ( “error verifying credentials” );
}
?>`
该脚本加载 twitteroauth 库,并传入我们的消费者、消费者秘密和“我的访问”令牌作为参数。你当然需要为'ACTUAL_ACCESS_TOKEN'
和'ACTUAL_SECRET_ACCESS_TOKEN'
行输入真实值。
在 http://localhost/Twitter _ direct _ access . PHP 上调用我们的脚本,成功输出:
Hello bdanchilla!
注意保护您的消费者和访问令牌不被潜在黑客窃取是非常重要的。
使用个人识别号(PIN)进行客户端认证
在本例中,我们假设另一个用户正在尝试进行身份验证。因此,我们没有访问令牌。我们将把我们的应用消费者令牌输入到 twitteroauth 构造函数中,获取请求令牌,然后重定向到 Twitter 开发区域。我们将被告知,我们的脚本正试图访问我们的应用,并拒绝或接受它。我们当然会接受,然后给一个 PIN。
将这个七位数的 PIN 输入第二个脚本将完成我们的激活。这只需要做一次。如果在非公共脚本中使用身份验证,例如桌面应用或没有公共可访问域的本地主机,则使用这种方法。
在 twitteroauth 库中,我们可以通过将 PIN 作为参数传递给函数getAccessToken
来使用它进行身份验证:
function getAccessToken($oauth_verifier = FALSE);
要获得 PIN,我们需要将消费者令牌交换为请求令牌,然后注册请求令牌。一旦我们有了 PIN,我们就使用它和请求令牌来获得访问令牌。当我们有了访问令牌,我们就可以认证和使用 Twitter APIs 了。
第一步:获取个人识别码
***清单 4-4。*推特注册:twitter_registration.php
`<?php
error_reporting ( E_ALL ^ E_NOTICE );
require_once (‘twitteroauth/twitteroauth.php’);
require_once (‘twitter_config.php’);
session_start (); //start a session
//the constructor takes our ‘consumer key’, ‘consumer secret’ as arguments
$twitterOAuth = new TwitterOAuth ( CONSUMER_KEY, CONSUMER_SECRET );
//returns the oauth request tokens {oauth_token, oauth_token_secret}
$requestTokens = $twitterOAuth->getRequestToken ();
//we write the tokens into the $_SESSION
$_SESSION [‘request_token’] = $requestTokens [‘oauth_token’];
$_SESSION [‘request_token_secret’] = $requestTokens [‘oauth_token_secret’];
//redirect to the Twitter generated registration URL, which will give us our PIN
header ( 'Location: ’ . $twitterOAuth->getAuthorizeURL ( $requestTokens ) );
?>`
这个脚本将请求令牌返回给我们,然后我们将它保存在$_SESSION
中。最后,脚本将我们重定向到 Twitter PIN 页面。参见图 4-4 。
***图 4-4。*请求令牌并被重定向后的 PIN 输出
步骤 2:验证 PIN 以接收访问令牌
要获得访问令牌,运行清单 4-5 中的,将 PIN 作为 GET 参数传入
清单 4-5。 Twitter PIN 验证:twitter_pin_validation.php
`//example usage:
//http://localhost/twitter_pin_validation.php?pin=9352006
在清单 4-5 的中,我们增加了一些安全检查来确保正确的 PIN 输入。如果成功,输出将是"PIN validation script was run"
,如果失败,输出将是错误消息。这些错误可能是由于没有传入 PIN、非数字 PIN 或生成的 PIN 超时。
该脚本加载了保存在清单 4-4 中的注册令牌。我们创建一个新的TwitterOAuth
对象,这次将请求令牌作为附加参数传入。然后我们调用getAccessToken
,将我们的 PIN 作为参数传入。这将返回访问令牌。最后,我们将访问令牌写入会话,或者在 PIN 超时时返回错误消息。
注意从清单 4-5 中生成的 PIN 确实有到期日期/时间。如果我们将
twitter_pin_validation.php
的执行延迟太久,就会得到一个错误。
尽管 OAuth 是比用户名/密码凭证更安全的系统,但您仍然需要采取预防措施。如果攻击者能够检索您的访问令牌,那么他们就能够访问您的 Twitter 帐户。
步骤 3:使用访问令牌进行身份验证,以使用 Twitter API
现在,我们的会话数据中保存了访问令牌。这些令牌使我们能够认证和使用 Twitter API。参见清单 4-6 。
***清单 4-6。*推特用法示例:twitter_usage.php
`<?php
error_reporting(E_ALL ^ E_NOTICE);
require_once(“twitteroauth/twitteroauth.php”);
require_once(“twitter_config.php”);
session_start();
//since we know our access tokens now, we will pass them into our constructor
t
w
i
t
t
e
r
O
A
u
t
h
=
n
e
w
T
w
i
t
t
e
r
O
A
u
t
h
(
C
O
N
S
U
M
E
R
K
E
Y
,
C
O
N
S
U
M
E
R
S
E
C
R
E
T
,
twitterOAuth = new TwitterOAuth( CONSUMER_KEY,CONSUMER_SECRET,
twitterOAuth=newTwitterOAuth( CONSUMERKEY,CONSUMERSECRET, _SESSION[“access_token”], $_SESSION[“access_token_secret”]);
//verify credentials through Twitter API call
$user_info = $twitterOAuth->get( “account/verify_credentials” );
if ( KaTeX parse error: Expected 'EOF', got '&' at position 11: user_info &̲& !user_info->error ) {
print “Hello “.$user_info->screen_name.”!
”;
print “Pushing out a status message.”;
// Post our new status
$twitterOAuth->post(
‘statuses/update’,
array( ‘status’ => "writing status…foobar " )
);
//other api calls
}else{
die( “error verifying credentials” );
}
?>`
如果我们通过了正确的身份验证,这将输出以下内容:
Hello bdanchilla! Pushing out a status message.
这条线
$twitterOAuth->post( 'statuses/update', array( 'status' => " writing status…foobar " ) );
应该输出一个状态信息。然而,如果我们去我们的 Twitter 账户,我们看到什么也没有发表。这是因为我们的应用不允许写访问。
Twitter REST API 方法是GET
或POST
。GET
方法读取数据。POST
方法写数据。我们大部分的函数调用都是读取数据,因此会用GET
来调用。编辑信息,比如更新我们的状态或关注新朋友,需要写权限,因此调用POST
。
要纠正这一点,我们需要回到 Twitter 开发站点,将应用的默认访问类型设置从只读更改为读写。参见图 4-5 。确保保存配置更改。
注意当切换应用类型和访问类型时,您可能需要清除您的会话数据。
***图 4-5。*修改我们应用的访问类型
重新运行前面的脚本,然后再次检查 Twitter。我们的状态现在应该已发布。
API 使用示例:朋友状态
对于我们的下一个例子,如清单 4-7 所示,我们将显示我们朋友的最新状态。在本例中,我们像前面的脚本一样连接到 Twitter。然后我们调用statuses/friends,
来检索我们朋友的最后状态。这非常类似于我们的公共搜索 API 示例,但是显示的是我们的朋友而不是普通人。
我们也调用shuffle($friends)
来随机化我们的朋友列表。
***清单 4-7。*显示好友的最新状态:friend_status.php
`<?php
error_reporting(E_ALL ^ E_NOTICE);
require_once(“twitteroauth/twitteroauth.php”);
require_once(“twitter_config.php”);
session_start();
//since we know our access tokens now, we will pass them into our constructor
t
w
i
t
t
e
r
O
A
u
t
h
=
n
e
w
T
w
i
t
t
e
r
O
A
u
t
h
(
C
O
N
S
U
M
E
R
K
E
Y
,
C
O
N
S
U
M
E
R
S
E
C
R
E
T
,
twitterOAuth = new TwitterOAuth( CONSUMER_KEY, CONSUMER_SECRET,
twitterOAuth=newTwitterOAuth( CONSUMERKEY,CONSUMERSECRET, _SESSION[“access_token”], $_SESSION[“access_token_secret”] );
//verify credentials through Twitter API call
$user_info = $twitterOAuth->get( “account/verify_credentials” );
if ( KaTeX parse error: Expected 'EOF', got '&' at position 11: user_info &̲& !user_info->error ) {
echo ‘
Connected as:
’;echo ‘’;
echo ‘’;
echo ‘’;
echo ‘’;
echo ‘’;
echo ‘
’; echo $user_info->screen_name . ‘ ’; echo ' echo ‘ | ’; echo ‘ | ’; echo ‘Last tweet: ’ . $user_info->status->text . ‘ |
echo ‘
My Friends
’;$friends = $twitterOAuth->get( “statuses/friends” );
shuffle( $friends ); //randomize which tweets are shown
echo ‘’;
foreach ( $friends as $f ) {
echo '<tr background-color: ’ . $f->profile_background_color . ‘">’;
echo ‘’;
echo ‘’;
echo ‘’;
}
echo ‘
’; echo $f->screen_name . ‘ ’; echo ' echo ‘ | ’; echo ‘Last tweet: ’ . $f->status->text . ‘ |
} else {
die( “error verifying credentials” );
}
?>`

***图 4-6。*朋友最后一条推文的输出示例
通过回调进行认证
在前面的示例中,您可能会认为,一定有比获取您的 PIN 并在单独的步骤中手动输入它更好的身份验证方法。确实有;但是,它需要一个外部可用的 web 服务器,以便 OAuth 服务提供者(Twitter)可以回调消费者(我们的应用)。
拥有回调位置比 PIN 少产生一个步骤,因为回调脚本充当对您的验证。它证明你拥有这个应用。这很像当你加入一个新的网站,他们通过给你发送确认信息来验证你提供的电子邮件。如果没有回电,那么你可以假装成其他任何人。
回到我们的 Twitter 开发帐户,我们需要将应用类型更改为 browser,并提供一个回调 URL。参见图 4-7 。
***图 4-7。*更改我们的应用类型并提供回调 URL
注意记住回调必须是外部可用的,因此本地测试服务器(即
[
localhost/](http://localhost/)
)将不起作用。
我们认证的第一部分重用了在清单 4-4 中找到的twitter_registration.php
脚本。我们将被重定向到一个像你在图 4-8 中看到的页面,要求我们输入用户名和密码。
***图 4-8。*重定向至登录屏幕
当我们点击“登录”时,我们将被重定向到我们的回调函数,这是我们在应用设置中注册的。这消除了对 PIN 验证的需要。我们应该看到这样的结果:
Welcome bdanchilla! You have logged in using Twitter.
列表 4-8 是我们的回调脚本。
***清单 4-8。*我们的回调处理程序,callback.php
`<?php
error_reporting(E_ALL ^ E_NOTICE);
require_once(“twitteroauth/twitteroauth.php”);
require_once(“twitter_config.php”);
session_start();
//verify that the oauth_token parameter of the callback URL matches the session token
if ( $_GET[“oauth_token”] == $_SESSION[“request_token”] ) {
//pass in our request tokens that have been stored in our
S
E
S
S
I
O
N
_SESSION
SESSION twitterOAuth = new TwitterOAuth(
CONSUMER_KEY, CONSUMER_SECRET,
$_SESSION[“request_token”], $_SESSION[“request_token_secret”] );
$accessToken = $twitterOAuth->getAccessToken();
//ensure that we have a numeric user_id
if ( isset(KaTeX parse error: Expected 'EOF', got '&' at position 25: …en["user_id"]) &̲& is_numeric(accessToken[“user_id”]) ) {
//save the access tokens to our session
$_SESSION[“access_token”] =
a
c
c
e
s
s
T
o
k
e
n
[
"
o
a
u
t
h
t
o
k
e
n
"
]
;
accessToken["oauth_token"];
accessToken["oauthtoken"]; _SESSION[“access_token_secret”] = $accessToken[“oauth_token_secret”];
// Success! Redirect to the welcome page
header( “location: welcome.php” );
} else {
// Failure : ( go back to the login page
header( “location: login.php” );
}
}else{
die( “Error: we have been denied access” );
}
?>`
在我们的欢迎页面或会话中的任何其他页面上,我们现在可以构造一个新的TwitterOAuth
对象并连接到 twitter。参见清单 4-9 。
***清单 4-9。*欢迎页面:welcome.php
<?php error_reporting(E_ALL ^ E_NOTICE); require_once("twitteroauth/twitteroauth.php"); require_once("twitter_config.php"); session_start();
`if( !empty( $_SESSION[“access_token”] ) &&
!empty( $_SESSION[“access_token_secret”] )
) {
t
w
i
t
t
e
r
O
A
u
t
h
=
n
e
w
T
w
i
t
t
e
r
O
A
u
t
h
(
C
O
N
S
U
M
E
R
K
E
Y
,
C
O
N
S
U
M
E
R
S
E
C
R
E
T
,
twitterOAuth = new TwitterOAuth( CONSUMER_KEY, CONSUMER_SECRET,
twitterOAuth=newTwitterOAuth( CONSUMERKEY, CONSUMERSECRET, _SESSION[“access_token”],
$_SESSION[“access_token_secret”] );
//check that we are connected
$user_info = $twitterOAuth->get( ‘account/verify_credentials’ );
if ( KaTeX parse error: Expected 'EOF', got '&' at position 11: user_info &̲& !user_info->error ) {
echo “Welcome " . $user_info->screen_name.”!";
//perform other
//API calls
} else {
die( “Error: bad credentials.” );
}
} else {
die( “Error: your access_token was not found in your $_SESSION.” );
}
?>`
使用 Twitter OAuth 登录您的网站
与使用 OpenID 类似,您可以使用 Twitter 的 OAuth 登录作为网站的登录机制。你可能已经看过下面这张图片,可以从[
dev.twitter.com/pages/sign_in_with_twitter](http://dev.twitter.com/pages/sign_in_with_twitter)
的各个网站上获得。
要在前面的例子中添加登录按钮,我们只需修改清单 4-4 中的代码,使其不自动重定向我们,而是显示一个图片链接。参见清单 4-10 。
清单 4-10。【login.php 用签到按钮注册 Twitter】
`<?php
error_reporting(E_ALL ^ E_NOTICE);
require_once(‘twitteroauth/twitteroauth.php’);
require_once(‘twitter_config.php’);
session_start(); //start a session
//the constructor takes our ‘consumer key’, ‘consumer secret’ as arguments
$twitterOAuth = new TwitterOAuth( CONSUMER_KEY,CONSUMER_SECRET );
//returns the oauth request tokens {oauth_token, oauth_token_secret}
$requestTokens = $twitterOAuth->getRequestToken();
//we write the tokens into the $_SESSION
$_SESSION[‘request_token’] = $requestTokens[‘oauth_token’];
S
E
S
S
I
O
N
[
′
r
e
q
u
e
s
t
t
o
k
e
n
s
e
c
r
e
t
′
]
=
_SESSION['request_token_secret'] =
SESSION[′requesttokensecret′]= requestTokens[‘oauth_token_secret’];
//header( "Location: ". $twitterOAuth->getAuthorizeURL( $requestTokens ) );
//Display Twitter log in button with encoded link
?>
">
`
使用一个数据库存储多个用户
我们将扩展前面的例子,在数据库中存储用户凭证,见清单 4-11 。为了简单起见,我们将使用 SQLite。在生产环境中,您可能希望确保存储的文件保存在 webroot 之外,或者使用非平面文件数据库以获得更高的安全性。有关 SQLite 的更多信息,请参考第七章。
清单 4-11。 Twitter 数据库连接类:twitter_db_connect.php
`<?php
class Twitter_DBConnect {
static $db;
private $dbh;
private function Twitter_DBConnect() {
try {
t
h
i
s
−
>
d
b
h
=
n
e
w
P
D
O
(
′
s
q
l
i
t
e
:
t
u
s
e
r
s
′
)
;
this->dbh = new PDO( 'sqlite:t_users' );
this−>dbh=newPDO(′sqlite:tusers′); this->dbh->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
} catch ( PDOException $e ) {
print "Error!: " . $e->getMessage() . “\n”;
die ();
}
}
public static function getInstance() {
if ( !isset( Twitter_DBConnect::KaTeX parse error: Expected '}', got 'EOF' at end of input: …ter_DBConnect::db = new Twitter_DBConnect();
}
return Twitter_DBConnect::$db->dbh;
}
}
?>`
这个类遵循单例设计模式来存储数据库连接的一个实例。Singleton 模式的关键特征是构造函数是私有的,我们确保返回类的同一个实例。
注意本书没有涉及设计模式,但是关于单例模式的更多内容,请参考
[
en.wikipedia.org/wiki/Singleton_pattern](http://en.wikipedia.org/wiki/Singleton_pattern)
。特别是对于 PHP 设计模式,我们建议你阅读 Matt Zandstra (Apress,2010)的 PHP 对象、模式和实践。
***清单 4-12。*数据库操作–选择、插入、更新:twitter_db_actions.php
`<?php
error_reporting(E_ALL ^ E_NOTICE);
require_once(‘twitter_db_connect.php’);
session_start();
class Twitter_DB_Actions {
private $dbh; //database handle
public function __construct() {
t
h
i
s
−
>
d
b
h
=
T
w
i
t
t
e
r
D
B
C
o
n
n
e
c
t
:
:
g
e
t
I
n
s
t
a
n
c
e
(
)
;
this->dbh = Twitter_DBConnect::getInstance();
this−>dbh=TwitterDBConnect::getInstance(); this->createTable();
}
public function createTable() {
q
u
e
r
y
=
"
C
R
E
A
T
E
T
A
B
L
E
I
F
N
O
T
E
X
I
S
T
S
o
a
u
t
h
u
s
e
r
s
(
o
a
u
t
h
u
s
e
r
i
d
I
N
T
E
G
E
R
,
o
a
u
t
h
s
c
r
e
e
n
n
a
m
e
T
E
X
T
,
o
a
u
t
h
p
r
o
v
i
d
e
r
T
E
X
T
,
o
a
u
t
h
t
o
k
e
n
T
E
X
T
,
o
a
u
t
h
t
o
k
e
n
s
e
c
r
e
t
T
E
X
T
)
"
;
query = "CREATE TABLE IF NOT EXISTS oauth_users( oauth_user_id INTEGER, oauth_screen_name TEXT, oauth_provider TEXT, oauth_token TEXT, oauth_token_secret TEXT )";
query="CREATETABLEIFNOTEXISTSoauthusers( oauthuseridINTEGER, oauthscreennameTEXT, oauthproviderTEXT, oauthtoken TEXT, oauthtokensecretTEXT )"; this->dbh->exec( $query );
}
public function saveUser( KaTeX parse error: Expected '}', got 'EOF' at end of input: …en ) { users =
t
h
i
s
−
>
g
e
t
T
w
i
t
t
e
r
U
s
e
r
B
y
U
I
D
(
i
n
t
v
a
l
(
this->getTwitterUserByUID( intval(
this−>getTwitterUserByUID(intval(accessToken[‘user_id’]) );
if ( count( KaTeX parse error: Expected '}', got 'EOF' at end of input: … { this->updateUser( KaTeX parse error: Expected 'EOF', got '}' at position 35: …er' ); }̲ else { …this->insertUser( KaTeX parse error: Expected 'EOF', got '}' at position 35: …er' ); }̲ }` ` p…query = “SELECT * from oauth_users WHERE oauth_provider = ‘twitter’”;
$stmt = $this->dbh->query(
q
u
e
r
y
)
;
query );
query); rows = $stmt->fetchAll( PDO::FETCH_OBJ );
return $rows;
}
public function getTwitterUserByUID( KaTeX parse error: Expected '}', got 'EOF' at end of input: …id ) { query = “SELECT * from oauth_users WHERE oauth_provider= ‘twitter’ AND oauth_user_id = ?”;
$stmt = $this->dbh->prepare(
q
u
e
r
y
)
;
query );
query); stmt->execute( array(
u
i
d
)
)
;
uid ) );
uid)); rows = $stmt->fetchAll( PDO::FETCH_OBJ );
return $rows;
}
public function insertUser( $user_info, KaTeX parse error: Expected '}', got 'EOF' at end of input: …'' ) { query = “INSERT INTO oauth_users (oauth_user_id, oauth_screen_name,
oauth_provider, oauth_token, oauth_token_secret) VALUES (?, ?, ?, ?, ?)”;
v
a
l
u
e
s
=
a
r
r
a
y
(
values = array(
values=array( user_info[‘user_id’], $user_info[‘screen_name’],
p
r
o
v
i
d
e
r
,
provider,
provider, user_info[‘oauth_token’],
u
s
e
r
i
n
f
o
[
′
o
a
u
t
h
t
o
k
e
n
s
e
c
r
e
t
′
]
)
;
user_info['oauth_token_secret'] );
userinfo[′oauthtokensecret′]); stmt = $this->dbh->prepare(
q
u
e
r
y
)
;
query );
query); stmt->execute( KaTeX parse error: Expected '}', got 'EOF' at end of input: …nserted user: {user_info[‘screen_name’]}";
}
public function updateUser( $user_info, KaTeX parse error: Expected '}', got 'EOF' at end of input: …'' ) { query = “UPDATE oauth_users SET oauth_token = ?, oauth_token_secret = ?,
oauth_screen_name = ?
WHERE oauth_provider = ? AND oauth_user_id = ?”;
$values = array( $user_info[‘screen_name’],
u
s
e
r
i
n
f
o
[
′
o
a
u
t
h
t
o
k
e
n
′
]
,
user_info['oauth_token'],
userinfo[′oauthtoken′], user_info[‘oauth_token_secret’], $provider,
u
s
e
r
i
n
f
o
[
′
u
s
e
r
i
d
′
]
)
;
user_info['user_id'] );
userinfo[′userid′]); stmt = $this->dbh->prepare(
q
u
e
r
y
)
;
query );
query); stmt->execute( KaTeX parse error: Expected '}', got 'EOF' at end of input: …Updated user: {user_info[‘screen_name’]}";
}
}
?>`
***清单 4-13。*我们更新了回调脚本,callback_with_db.php
<?php error_reporting(E_ALL ^ E_NOTICE); require_once("twitteroauth/twitteroauth.php"); require_once("twitter_config.php"); **require_once("twitter_db_actions.php");** session_start();
`//verify that the oauth_token parameter of the callback URL matches the session token
if ( $_GET[“oauth_token”] == $_SESSION[“request_token”] ) {
//pass in our request tokens that have been stored in our
S
E
S
S
I
O
N
_SESSION
SESSION twitterOAuth = new TwitterOAuth(
CONSUMER_KEY, CONSUMER_SECRET,
$_SESSION[“request_token”], $_SESSION[“request_token_secret”] );
$accessToken = $twitterOAuth->getAccessToken();
//ensure that we have a numeric user_id
if ( isset( $accessToken[“user_id”] ) && is_numeric( KaTeX parse error: Expected '}', got 'EOF' at end of input: …DB** ** twitter_db_actions = new Twitter_DB_Actions();**
**
t
w
i
t
t
e
r
d
b
a
c
t
i
o
n
s
−
>
s
a
v
e
U
s
e
r
(
twitter_db_actions->saveUser(
twitterdbactions−>saveUser(accessToken);**
//Success! Redirect to welcome page
//The welcome.php page will also need to be modified to read our tokens from the database and not the session
header( “location: welcome.php” );
} else {
// Failure 😦 go back to login page
header( “location: login.php” );
}
}
?>`
将 OAuth(或 OpenID)与您的站点集成的优点是,您不需要人们提交另一个注册表单并记住另一个用户名/密码集。大多数主要的内容管理系统(CMSes)都有 OAuth 和 OpenID 插件,比如 Wordpress 和 Drupal。
缓存数据
为了消除每次页面刷新时向 Twitter 请求信息的需要,一种常见的方案是缓存数据。Twitter REST API 将 OAuth 用户的请求调用限制为每小时 350 个,匿名用户为每小时 150 个。在人口密集的网站上,缓存是必要的。我们不会实现缓存,但会描述基本的技术。
数据缓存存储数据以供以后检索。要缓存 Twitter 信息,您需要定期向 Twitter 发出请求。这通常是通过所谓的 cron 作业自动完成的。每次请求后,新的结果被插入数据库,过时的记录被删除。当用户访问网站时,他们看到的信息来自数据库,而不是直接来自 Twitter。
更多 API 方法和示例
Twitter API 分为以下几类:时间线,状态,用户,列表,列表成员,列表订阅者,直接消息,友情,社交图,账号,收藏夹,通知,
涵盖整个 API 超出了本书的范围,但是我们可以尝试一些方法。详细描述可在网上获得。例如,方法friends_timeline
在[
dev.twitter.com/doc/get/statuses/friends_timeline](http://dev.twitter.com/doc/get/statuses/friends_timeline)
有文档。从文档中,我们可以看到该方法是用GET
调用的,可以以 JSON、XML、RSS 或 Atom 格式返回,需要认证,并且有各种可选参数。
当然,我们不需要在会话中存储我们的访问令牌。我们可以将访问令牌内容保存到文件中。为了安全起见,这些应该在文档根目录之外。我们可以修改清单 4-5 来将我们的访问令牌写入磁盘(参见清单 4-14 )。
***清单 4-14。*在物理文件中存储我们的访问令牌
//write our oauth access tokens to files file_put_contents( "access_token", $accessOAuthTokens['oauth_token'] ); file_put_contents( "access_token_secret", $accessOAuthTokens['oauth_token_secret'] );
将我们的应用更改为“client”,运行 twitter_registration.php,然后使用 PIN 将清单 _4-14.php 保存到磁盘上。现在我们在磁盘上有了访问令牌,我们可以通过用file_get_contents
读取它们来进行身份验证。见清单 4-15 。
***清单 4-15。*一个单独的可重复使用的文件,twitter_oauth_signin.php, to authenticate
`<?php
error_reporting(E_ALL ^ E_NOTICE);
require_once(“twitteroauth/twitteroauth.php”);
require_once(“twitter_config.php”);
//access_token and access_token_secret are
//the file names holding our access tokens
$twitterOAuth = new TwitterOAuth(
CONSUMER_KEY, CONSUMER_SECRET,
file_get_contents( “access_token” ),
file_get_contents( “access_token_secret” ) );
?>`
我们可以要求清单 4-15 中的文件来缩短我们调用 Twitter API 的脚本。
在这里,我们按顺序检索并输出多达 20 条好友更新的 Twitter 数据,包括我们自己。这与我们个人 Twitter 主页上的视图相同。参见清单 4-16 。
***清单 4-16。*按顺序获取好友更新的 Twitter 数据
`<?php
error_reporting(E_ALL ^ E_NOTICE);
require_once(“twitter_oauth_signin.php”);
$friends_timeline = $twitterOAuth->get( ‘statuses/friends_timeline’, array( ‘count’ => 20 ) );
var_dump( $friends_timeline );
?>`
清单 4-17 显示了我们最近的 tweet IDs 和状态。
***清单 4-17。*我们最近的推文及其 id
`<?php
error_reporting(E_ALL ^ E_NOTICE);
require_once(“twitter_oauth_signin.php”);
$tweets = $twitterOAuth->get( ‘statuses/user_timeline’ );
foreach ( $tweets as $t ) {
echo $t->id_str . ": " . $t->text . “
”;
}
?>`
清单 4-17 的示例输出如下:
68367749604319232: 850,000,000,000 pennies for skype 68367690535940096: Da da da da dat, da da da da Jackie Wilson said 47708149972602880: Tuesday morning and feelin' fine 43065614708899840: Devendra Banhart - At the Hop http://bit.ly/sjMaa 39877487831957505: shine on you crazy diamond 39554975369658369: Excited to listen to new Radiohead and Fleet Foxes soon : ) 39552206701072384: writing a chapter...about twitter
如果我们想通过编程删除一条推文呢?我们简单地输入 id 值并调用 Twitter API POST 方法statuses/destroy
。tweet ID 应该是一个字符串,而不是一个数字。参见清单 4-18 。
***清单 4-18。*摧毁一个状态
`<?php
error_reporting(E_ALL ^ E_NOTICE);
require_once(“twitter_oauth_signin.php”);
$tweet_id = “68367749604319232”;
$result = $twitterOAuth->post( ‘statuses/destroy’,
array( ‘id’ => $tweet_id )
);
if ( $result ) {
if ( $result->error ) {
echo “Error (ID #” . $tweet_id . “)
”;
echo $result->error;
} else {
echo “Deleting post: $tweet_id!”;
}
}
?>`
要添加和删除友谊,我们首先检查友谊是否存在,然后销毁或创建一个。参见清单 4-19 。
***清单 4-19。*建立和破坏友谊
`<?php
error_reporting(E_ALL ^ E_NOTICE);
require_once(“twitter_oauth_signin.php”);
//get our information
$user_info = $twitterOAuth->get( “account/verify_credentials” );
//check if we are friends with Snoopy, if not, then we create the friendship.
if ( !$twitterOAuth->get( ‘friendships/exists’, array(
‘user_a’ => KaTeX parse error: Expected '}', got 'EOF' at end of input: …endship!'; twitterOAuth->post( ‘friendships/create’, array( ‘screen_name’ => ‘Snoopy’ ) );
}
//check if we are friends with Garfield. If not, then we create the friendship.
if ( !$twitterOAuth->get( ‘friendships/exists’, array(
‘user_a’ => KaTeX parse error: Expected '}', got 'EOF' at end of input: …endship!'; twitterOAuth->post( ‘friendships/create’, array( ‘screen_name’ => ‘Garfield’ ) );
}
//check if we are friends with Garfield. If yes, we destroy that friendship.
if ( $twitterOAuth->get( ‘friendships/exists’, array(
‘user_a’ => KaTeX parse error: Expected '}', got 'EOF' at end of input: …endship!'; twitterOAuth->post( ‘friendships/destroy’, array( ‘screen_name’ => ‘garfield’ ) );
}
?>`
在清单 4-19 中,exists
查询是一个GET
方法,而destroy
和create
命令是POST
方法。
脸书
好消息是,使用脸书 API 开发应用与开始使用 Twitter API 非常相似,因为两者都使用 OAuth 进行身份验证。
首先,转到[www.facebook.com/developers/apps.php](http://www.facebook.com/developers/apps.php)
并点击“设置新应用”链接。系统会提示您通过手机或信用卡验证您的帐户。这和 Twitter 不一样,Twitter 是做邮件验证的。
注意如果您已经通过电话在脸书上验证了不同的功能,如移动服务,您将不会再次收到验证挑战。
***图 4-9。*脸书验证请求
电话方法只需要重新发送短信中收到的代码,不需要提供信用卡信息。因此,丹希拉建议这样做。接下来,选择一个应用名称。有趣的是,没有脸书的许可,你不能在你的申请中使用“脸”这个词。
***图 4-10。*选择应用名称并同意服务条款
就像我们的 Twitter 应用一样,我们得到一些为我们生成的 OAuth 消费者密钥。
***图 4-11。*我们的申请信息和设置
注意因为脸书使用 OAuth,我们可以在 Twitter 部分创建的
oauth_users
表中插入用户。我们唯一需要改变的是将“facebook”作为$provider
参数传入。
与 Twitter 不同,脸书有一个用 PHP 编写的官方 SDK。它在[
github.com/facebook/php-sdk/downloads](https://github.com/facebook/php-sdk/downloads)
可用,由一个文件facebook.php
组成。我们与脸书连接的方式需要一个公共可访问的回调位置,就像我们的第二个 Twitter 连接示例一样。我们需要指定我们的应用将被使用的网站 URL。默认回调位置是已执行脚本的当前 URL。
***图 4-12。*我们的网站设置
我们还可以选择为我们的应用创建一个画布页面。http://developers.facebook.com/docs/guides/canvas/的脸书官方称画布页是“…字面上的意思是在脸书运行你的应用的空白画布。您可以通过提供画布 URL 来填充画布页面,其中包含构成您的应用的 HTML、JavaScript 和 CSS。”
***图 4-13。*我们的脸书集成画布设置
虽然画布页旨在与您的脸书应用相关联,但它可以是任何网页。在图 4-14 中,我们简单地将这本书的进展页面列为画布 URL。
***图 4-14。*我们的画布页
每个脸书应用都有一个关联的个人资料页面,其中有链接、建议、设置和广告选项。
***图 4-15。*我们的申请简介页面
此外,还有一些选项可以帮助在 iPhone、Android 上设置脸书应用,并通过脸书积分实现货币化。
现在让我们看看我们的第一个 API 例子,如清单 4-20 所示。
***清单 4-20。*我们的登录脚本:login.php
`<?php
error_reporting(E_ALL ^ E_NOTICE);
require_once(“facebook.php”);
//create a new Facebook object
$facebook = new Facebook( array(
‘appId’ => ‘YOUR_APP_ID’,
‘secret’ => ‘YOUR_APP_SECRET’
) );
//the login page is also the callback page, so we check if we have been authenticated
$facebook_session = $facebook->getSession();
if ( !empty( KaTeX parse error: Expected '}', got 'EOF' at end of input: …n user user_info = $facebook->api( ‘/me’ );
if ( !empty( $user_info ) ) {
displayUserInfo( $user_info );
} else {
die( “There was an error.” );
}
} catch ( Exception $e ) {
print KaTeX parse error: Expected 'EOF', got '}' at position 22: …Message(); }̲ } else { /…login_url = $facebook->getLoginUrl();
header( "Location: " . $login_url );
}
function displayUserInfo( KaTeX parse error: Expected '}', got 'EOF' at end of input: …come <a href='{user_info[‘link’]}’ rel=‘external’/>" .
u
s
e
r
i
n
f
o
[
′
n
a
m
e
′
]
.
′
<
/
a
>
!
<
b
r
/
>
′
;
e
c
h
o
"
G
e
n
d
e
r
:
"
.
user_info['name'] . '</a>!<br/>'; echo "Gender: ".
userinfo[′name′].′</a>!<br/>′; echo"Gender:".user_info[‘gender’].“
”;
echo “Hometown: “.$user_info[‘location’][‘name’].”
”;
}
?>`
您可能会注意到,脸书认证比 Twitter 更加简化。运行清单 4-20 会把你带到一个权限请求页面,就像你在图 4-16 中看到的那样。
***图 4-16。*向用户请求基本权限
当用户点击“允许”时,你会立即收到一堆关于他们的信息。确切的信息量取决于脸书臭名昭著的隐私政策、用户设置以及我们要求的任何额外权限。获得授权后,该脚本将显示如下输出:
Welcome <ins>Brian Danchilla</ins>! Gender: male Hometown: Saskatoon, Saskatchewan
添加退出脸书的链接
在清单 4-20 中的调用之后,我们将添加一个注销链接,如清单 4-21 中的所示。
***清单 4-21。*我们修改的登录脚本login2.php
,带有注销回拨
`<?php
error_reporting(E_ALL ^ E_NOTICE);
require_once(“facebook.php”);
//create a new Facebook object
$facebook = new Facebook( array(
‘appId’ => ‘YOUR_APP_ID’,
‘secret’ => ‘YOUR_APP_SECRET’
) );
//the login page is also the callback page, so we check if we have been authenticated
$facebook_session = $facebook->getSession();
if ( !empty( KaTeX parse error: Expected '}', got 'EOF' at end of input: …n user user_info = $facebook->api( ‘/me’ );
if ( !empty( $user_info ) ) {
displayUserInfo(
u
s
e
r
i
n
f
o
)
;
/
/
a
d
j
u
s
t
t
h
e
U
R
L
t
o
m
a
t
c
h
t
h
a
t
o
f
t
h
e
a
p
p
l
i
c
a
t
i
o
n
s
e
t
t
i
n
g
s
user_info ); //adjust the URL to match that of the application settings
userinfo); //adjusttheURLtomatchthatoftheapplicationsettings logout_location = (string) html_entity_decode(
$facebook->getLogoutUrl(
array( ‘next’ => ‘http://www.foobar.com/logout.php’ ) ) );
echo “Logout”;
} else {
die( “There was an error.” );
}
} catch ( Exception $e ) {
print $e->getMessage();
}
} else {
//try generating session by redirecting back to this page
$login_url = $facebook->getLoginUrl();
header( "Location: " . $login_url );
}
function displayUserInfo( KaTeX parse error: Expected '}', got 'EOF' at end of input: …come <a href='{user_info[‘link’]}’ rel=‘external’/>" .
u
s
e
r
i
n
f
o
[
′
n
a
m
e
′
]
.
′
<
/
a
>
!
<
b
r
/
>
′
;
e
c
h
o
"
G
e
n
d
e
r
:
"
.
user_info['name'] . '</a>!<br/>'; echo "Gender: ".
userinfo[′name′].′</a>!<br/>′; echo"Gender:".user_info[‘gender’].“
”;
echo “Hometown: “.$user_info[‘location’][‘name’].”
”;
}
?>`
我们已经传入了参数'next'
,这将是脸书在执行注销后重定向的地方。参见清单 4-22 。
***清单 4-22。*我们的注销文件,Logout.php
<p>You are now logged out.<br/> <a href="http://www.foobar.com/login.php">login</a></p>
请求额外权限
使用脸书 API 还可以做许多其他事情,但是有些需要用户授予额外的权限。为了获得更多的许可,我们向我们的登录 URL 传递一个带有关键字req_perms
的数组。req_perms 的值应该是逗号分隔的权限列表。
$login_url = $facebook->getLoginUrl( array( "req_perms" => "user_photos, user_relationships" ) );
***图 4-17。*向用户请求额外权限
我们现在可以通过在displayUserInfo
函数的末尾添加下面一行来将关系状态添加到我们的显示中。
` echo
u
s
e
r
i
n
f
o
[
′
r
e
l
a
t
i
o
n
s
h
i
p
s
t
a
t
u
s
′
]
.
"
(
"
.
user_info['relationship_status'] . " (" .
userinfo[′relationshipstatus′]."(".user_info[‘significant_other’]
[‘name’].“)
”;
Welcome Brian Danchilla!
Gender: male
Hometown: Saskatoon, Saskatchewan
Engaged (Tressa Kirstein)`
getLoginUrl
函数还有一些我们应该知道的可选参数。这些是:
next: the URL to go to after a successful login cancel_url: the URL to go to after the user cancels display: can be "page" (default, full page) or "popup"
图形 API
脸书的每个对象都可以通过它的图形 API 获得。可访问的对象类型包括相册、应用、签入、评论、文档、域、事件、友好列表、组、见解、链接、消息、注释、页面、照片、帖子、评论、状态消息、订阅、主题、用户和视频。
注意更多关于脸书图形 API 的信息可在
[
developers.facebook.com/docs/reference/api/](http://developers.facebook.com/docs/reference/api/)
获得。
作为 API 的一个例子,关于用户 Brian Danchilla 的信息可以在 URL 中找到
[
graph.facebook.com/brian.danchilla](http://graph.facebook.com/brian.danchilla)
,输出 JSON:
{ "id": "869710636", "name": "Brian Danchilla", "first_name": "Brian", "last_name": "Danchilla", "link": "http://www.facebook.com/brian.danchilla", "username": "brian.danchilla", "gender": "male", "locale": "en_US" }
Graph API 的一些安全区域要求提供访问令牌。需要访问令牌的示例 URL 是[
graph.facebook.com/me](http://graph.facebook.com/me)
,否则它将输出以下内容:
{ "error": { "type": "OAuthException", "message": "An active access token must be used to query information about the current user." } }
查找相册和照片
在本章的最后一个例子中,我们将显示脸书相册,包括封面图片、相册名称和照片数量。从清单 4-21 的开始,用displayAlbums();
替换行displayUserInfo( $user_info );
。我们现在将定义displayAlbums
函数,如清单 4-23 所示。
***清单 4-23。*显示脸书相册的方法
function displayAlbums( Facebook $facebook ) { $albums = $facebook->api( '/me/albums?access_token=' . $facebook_session['access_token'] ); $i = 0; print "<table>"; foreach ( $albums["data"] as $a ) { if ( $i == 0 ) { print "<tr>"; } //get the cover photo cover $photo = $facebook->api( $a['cover_photo'] . '?access_token=' . $facebook_session['access_token']); print "<td>"; print "<img src='" . $photo["picture"] . "'/><br/>"; print $a["name"] . " (" . $a["count"] . " photos)<br/>"; print "</td>"; ++$i; if ( $i == 5 ) { print "</tr>"; $i = 0; } } print "</table>"; }
在清单 4-23 的中,我们将$facebook
对象传递给了displayAlbums
方法。我们提供我们的访问令牌,并检索我们所有的相册。然后我们遍历结果,开始在一个五列宽的表格中显示专辑信息。我们将获得的cover_photo
id 信息输入到第二个 API 调用中,以获取具有该 id 的照片细节。
***图 4-18。*清单 4-23 样本专辑输出
总结
在本章中,您学习了 OAuth 的认证流程,然后可以自由使用 Twitter 和脸书 API。了解一个新的 API 的最好方法通常是一头扎进去。作为开发人员,通常没有必要对一个 API 了如指掌。学习当前任务的相关部分,并根据需要扩展你的知识库。在开发环境中,不要害怕错误或第一次尝试就不成功的代码。
社交媒体开发是时尚的,不像其他编程领域那么重要。然而,这并不意味着它一定容易。准备学习新兴技术、库和不断变化的 API。
社交媒体火起来的一个关键原因是它具有吸引力和社会性。换句话说,人们喜欢使用它。作为社交媒体开发人员,有一些独特而有趣的开发机会,我们也应该试着从中获得乐趣。*
五、前沿
本章将介绍 PHP 5.3 的新特性。这些新特性包括名称空间、闭包、新的文本格式 nowdoc 和goto
语句。这最后的创新有点像过去的爆炸,仍然像第一个完全过程化的语言,如 Pascal,开始在程序员中流行时一样受到鄙视。常规使用的goto
语句仍然非常令人不快;有人甚至会说,在程序员中使用它是一种致命的罪恶。这场争论是由著名的论文“去认为有害的声明”(Edsger Dijkstra,1968)开始的,goto
的声明从那时起就被认为是可疑的。然而,拥有选择权从来都不是坏事。编程不是宗教;目标是简单、清晰和高效。如果goto
语句可以帮助程序员达到这些目标,那么使用它就非常有意义。
尽管有争议,但语句并不是 PHP 5.3 最重要的新特性。名称空间是迄今为止最重要的新特性。匿名函数也很重要,也称为闭包或 lambda 函数,它支持大量新的编程方法,而不会污染全局名称空间。
还有一种新的文档格式,称为 nowdoc,它类似于 heredoc,但在某些情况下更加通用。PHP 5.3 也是第一个包含标准 PHP 库(SPL)作为语言不可分割的一部分的版本。在早期版本中,SPL 是一个扩展。最后但并非最不重要的是,有 PHP 存档,称为 phar,它使用户能够创建类似于 Java JAR 存档的文件,其中包含整个应用。
名称空间
名称空间是许多编程语言的标准特性。名称空间解决的问题如下:不同子程序之间经常使用的一种通信方法是通过全局变量进行通信。许多编程库都有全局变量供大量其他例程使用。随着语言的发展和不同编程库数量的增加,变量名冲突的可能性呈指数增长。名称空间有助于划分全局名称空间,避免变量名冲突,这可能会导致奇怪和不可预测的错误。PHP 在 5.3 版之前没有名称空间。对名称空间的需求是由于语言本身的发展而产生的。名称空间是可以包含类、函数或常数的语法对象。它们按层次排序,可以包含子名称空间。
名称空间语法非常简单,易于理解。清单 5-1 由三个文件组成,展示了如何定义和使用名称空间。第一个文件 domestic.php 定义了类animal
,并将其实例初始化为值dog
。第二个文件 wild.php 也定义了同一个类animal
,这次是在名称空间wild
中,并将其实例初始化为字符串tiger
。最后,script5.1.php 展示了如何使用它。
***清单 5-1。*使用名称空间
`domestic.php :
<?php class animal { function __construct() { $this->type='dog'; } function get_type() { return($this->type); } } ?>wild.php :
<?php namespace wild; class animal { function __construct() { $this->type='tiger'; } function get_type() { return($this->type); } } ?>script5.1.php :
#!/usr/bin/env php
执行产生了预期的结果,如下所示:
./script5.1.php dog tiger tiger
名称空间wild
在文件 wild.php 中定义。如果没有定义名称空间,我们的类会产生完全不同的结果。一旦命名空间被定义,一个类只能通过使用命名空间\类约定来寻址。还可以通过使用use
语句将名称空间导入到本地名称空间中,并用一个更方便的名称为其命名。名称空间定义语句块。如果文件中有多个名称空间,必须用花括号括起来,如清单 5-2 所示。
***清单 5-2。*具有多个名称空间的文件
`animals.php:
<?php namespace animal\wild { class animal { static function whereami() { print __NAMESPACE__."\n"; } function __construct() { $this->type='tiger'; } function get_type() { return($this->type); } } } namespace animal\domestic { class animal { function __construct() { $this->type='dog'; } function get_type() { return($this->type); } } } ?>`这里我们还可以看到子名称空间,由反斜杠字符“\”分隔。还有一个常量__NAMESPACE_
,它将包含当前的名称空间名称。这与其他 PHP 特殊常量非常相似,比如__FILE__
或__CLASS__
。清单 5-3 展示了如何在脚本中使用它。
***清单 5-3。*在脚本中使用子名称空间
<?php require_once('animals.php'); use \animal\wild\animal as beast; $c=new beast();
printf("%s\n",$c->get_type()); beast::whereami(); ?>
函数whereami
是静态的,所以它只能在类上下文中调用,而不能在对象上下文中调用。在类上下文中调用函数的语法是class::function($arg)
。这种调用不依赖于特定的对象,被称为类上下文调用。
类\animal\wild\animal
被别名化到beast
中,它的名字被导入到本地名称空间中。在导入的名称空间上也允许调用类函数之类的操作。
还有预定义的全局名称空间。所有普通函数都是全局名称空间的一部分。调用函数\phpversion()
完全等同于调用不带“\”前缀的函数phpversion()
,如下所示:
php -r 'print \phpversion()."\n";' 5.3.3
尽管创建内置函数的本地版本从来都不是一个好主意,但是在函数名前面加上“\”将确保被调用的版本来自全局名称空间,而不是本地版本。
命名空间和自动加载
在前面的章节中,您学习了__autoload
函数,它用于将类加载到程序中。换句话说,__autoload
函数可以帮助自动化清单 5-3 中的require_once
指令。
基本的自动加载功能如下所示:
function __autoload($class) { require_once("$class.php"); }
当所讨论的类包含名称空间时,完整路径被传递给__autoload
函数。
让我们修改清单 5-3 中的如下所示:
`<?php
function __autoload($class) {
print “$class\n”;
exit(0);
}
use animal\wild\animal as beast;
$c=new beast();
printf(“%s\n”,$c->get_type());
beast::whereami();
?>`
执行时,结果将是完整路径:
animal\wild\animal
为了在 autoload 中使用名称空间,我们应该开发一个目录层次结构,用正斜杠替换反斜杠字符,并包含文件。使用str_replace
或preg_replace
功能可以替换字符。对于像这样简单的任务,str_replace
功能比preg_replace
便宜。
命名空间结论
名称空间是抽象容器,用于保存对象名称的逻辑分组。它们是其他语言共有的众所周知的特性,有时被称为包或模块。脚本每天都在变得更大更复杂,这使得发明新的标识符越来越难。在这一章中,我们一直在使用两个不同的类,都叫做animal
。有了这样的命名约定,几乎可以保证你会遇到引入讨厌的 bug 的冲突。有了名称空间,我们就能够总是引用正确的类,甚至可以根据需要用更方便的名称来代替类名。
名称空间非常新,至少在 PHP 中是这样;很少有软件包(额外的 PHP 库)使用它们,但是名称空间也是一个重要的非常受欢迎的语言特性。我毫不怀疑你会发现这个特别的功能非常有用。
匿名函数(闭包)
这其实不是一个新功能;它从 PHP 4.0.1 开始就可用了,但是语法变得更加优雅了。在 PHP 的早期版本中,也可以使用create_function
内置函数创建匿名函数。匿名函数通常很短,在许多其他函数中用作回调例程。下面是一个示例脚本,它将使用内置函数array_reduce()
对数组中的值求和。array_map
和array_reduce
函数是 Google map-reduce 算法的 PHP 实现。函数array_reduce()
在一个数组上被递归调用以产生一个单值输出。
array_reduce
函数的语法非常简单:array_reduce($array,callback_function)
。回调函数有两个参数:第一个由前一次迭代返回;第二个是当前数组元素。清单 5-4 展示了这个脚本。
清单 5-4。array_reduce
功能
`<?php
$y = 0;
$arr = range(1, 100);
//
s
u
m
=
c
r
e
a
t
e
f
u
n
c
t
i
o
n
(
′
sum=create_function('
sum=createfunction(′x,
y
′
,
′
r
e
t
u
r
n
(
y','return(
y′,′return(x+$y);');
s
u
m
=
f
u
n
c
t
i
o
n
(
sum = function (
sum=function(x, KaTeX parse error: Expected '}', got 'EOF' at end of input: … { return (x + $y);
};
s
i
g
m
a
=
a
r
r
a
y
r
e
d
u
c
e
(
sigma = array_reduce(
sigma=arrayreduce(arr,
s
u
m
)
;
p
r
i
n
t
"
sum); print "
sum);print"sigma\n";
?>`
匿名函数被创建并存储到变量$sum
中。注释掉是老的做事方式,使用create_function
解决方案。新的方式更优雅。两种方法功能相同,产生相同的输出。
此外,匿名函数可以作为返回值从函数返回。PHP 的可见性规则阻止外部作用域的变量在内部可见,这意味着不能在内部函数中访问外部函数的参数。为了从被返回的函数中访问封闭函数的参数,我们必须使用一个全局变量。整个事情看起来会像这样:
function func($a) { global $y; $y = $a; return function ($x) { global $y; return $y + $x; }; }
这将返回一个依赖于全局变量的匿名函数。术语“闭包”来自 Perl,它有不同的作用域规则,这些规则支持不同的用法。在 PHP 中,闭包主要用于创建简短的回调函数,防止在不必要的地方浪费全局名称。这种创建匿名函数的新方法虽然在语法上很优雅,但其实并不新鲜。
Nowdoc
Nowdoc 是一种在脚本中插入自由格式文本的新方法。它看起来几乎像一个 heredoc,但有一个本质的区别:nowdoc 没有进一步解析,这使得它非常适合插入 PHP 代码甚至 SQL 命令。例如,Oracle RDBMS 拥有以“V$
”开头的内部表。在 PHP 5.3 之前,查询中的每一个美元符号都必须用反斜杠“转义”,就像这样:
$FILE="select lower(db_name.value) || '_ora_' || v\$process.spid || nvl2(v\$process.traceid, '_' || v\$process.traceid, null ) || '.trc' from v\$parameter db_name cross join v\$process join v\$session on v\$process.addr = v\$session.paddr where db_name.name = 'instance_name' and v\$session.sid=:SID and v\$session.serial#=:SERIAL";
如果没有 nowdoc 语法,这个查询就必须完全像这样编写,这既乏味又难看。nowdoc 语法允许输入这样的查询,而不使用难看的反斜杠字符。heredoc 语法如下所示:
$FILE= = <<<EOT select lower(db_name.value) || '_ora_' || v\$process.spid || nvl2(v\$process.traceid, '_' || v\$process.traceid, null ) || '.trc' from v\$parameter db_name cross join v\$process join v\$session on v\$process.addr = v\$session.paddr where db_name.name = 'instance_name' and v\$session.sid=:SID and v\$session.serial#=:SERIAL; EOT;
新的 newdoc 语法如下所示:
$FILE = <<<'EOT' select lower(db_name.value) || '_ora_' || v$process.spid || nvl2(v$process.traceid, '_' || v$process.traceid, null ) || '.trc' from v$parameter db_name cross join v$process join v$session on v$process.addr = v$session.paddr where db_name.name = 'instance_name' and v$session.sid=:SID and v$session.serial#=:SERIAL; EOT;
唯一的区别是结束标识符周围的单引号和转义美元符号的移除的反斜杠字符。其他都完全一样。
注意对于“文本结束”标识符的规则完全相同;分号字符前面不能有任何空格,后面也不能有空格。
为了看出区别,让我们看看清单 5-5 中的代码片段。
清单 5-5。【heredoc 和 nowdoc 的区别
`<?php
class animal {
public $species;
public KaTeX parse error: Expected group after '_' at position 20: …; function _̲_construct(kind,KaTeX parse error: Expected '}', got 'EOF' at end of input: …ame) { this->species=
k
i
n
d
;
kind;
kind; this->name=KaTeX parse error: Expected 'EOF', got '}' at position 11: name; }̲ function _…this->species.‘.’.$this->name);
}
}
$pet = new animal(“dog”,“Fido”);
KaTeX parse error: Expected '}', got 'EOF' at end of input: …e world is my {pet->species}.
His name is {$pet->name}.\n
This is the short name: $pet\n
EOT;
print “NEWDOC:\n$text\n”;
KaTeX parse error: Expected '}', got 'EOF' at end of input: …e world is my {pet->species}.
His name is {$pet->name}.\n
This is the short name: KaTeX parse error: Undefined control sequence: \n at position 4: pet\̲n̲ ̲EOT; print "HER…text";`
相同的文本首先被定义为 PHP 5.3 newdoc 文本,然后被定义为通常的和众所周知的 heredoc 文本。输出阐明了脚本中字符串指定的两种方式之间的区别:
`NEWDOC:
My favorite animal in the whole world is my {KaTeX parse error: Expected 'EOF', got '}' at position 13: pet->species}̲. His name …pet->name}.\n
This is the short name: $pet\n
HEREDOC:
My favorite animal in the whole world is my dog.
His name is Fido.
This is the short name: dog.Fido`
第一个版本是 PHP 5.3 中的新版本,没有解释任何东西。嵌入的变量引用、变量本身、甚至特殊字符都完全按照它们在文本中出现的样子显示。这就是 nowdoc 格式的美妙之处。旧的 heredoc 版本解释了一切:变量引用、变量名称和特殊字符。该特性主要用于将 PHP 或其他代码插入脚本。对于 heredoc 来说,类似下面这样的事情要困难得多:
<?php $x = 10; $y = <<<'EOT' $x=$x+10; EOT; eval($y); print "$x\n"; ?>
有了新的 nowdoc 格式,插入 SQL、PHP 代码和动态执行变得容易多了。Nowdoc 并不是 heredoc 格式的替代品,它对于需要简单模板且不需要 Smarty 引擎全部功能的应用仍然非常有用。Smarty 是 PHP 使用的最流行的模板引擎。它有许多选项和可能性,比新的 nowdoc 格式复杂得多。Nowdoc 只是在需要将代码作为字符串插入 PHP 脚本时使用的辅助工具。
本地 goto 语句
PHP 5.3 引入了极具争议的 local goto
语句(其中形容词“local”表示不可能跳出例程或者进入循环)。在一些语言中,最著名的是 C,也有可能进行“非本地goto
或“长跳转”,但这在 PHP 中是不可能的。本地goto
语句的限制与其他语言相同:不能跳转到循环中,也不能跳转到当前子例程之外。
goto 语句是作为最后的选择提供的,而不是应该经常使用的。语法非常简单。清单 5-6 展示了一个while
循环被重写为使用一个goto
语句:
***清单 5-6。*一张goto
报表样本
<?php $i=10; LAB: echo "i=",$i--,"\n"; if ($i>0) goto LAB; echo "Loop exited\n"; ?>
标签以冒号结尾。这个小脚本产生了预期的输出:
i=10 i=9 i=8 i=7 i=6 i=5 i=4 i=3 i=2 i=1 Loop exited
我没有真正的goto
陈述的例子,因为我还没有需要它的时候。我认为这是 PHP 5.3 中最少使用的新特性。然而,很高兴知道它就在那里,在需要的时候。再一次,程序员不喜欢使用goto
语句。然而,如前所述,编程不是一种宗教;对于违反编程风格的罪行,没有教条或惩罚。最终目标是效率和清晰。如果goto
服务于这一目的,它是一个受欢迎的选择。
标准 PHP 库
标准 PHP 库(SPL)是一组类,类似于 C++中的标准模板库(STL)。SPL 包含了标准编程结构的类,比如堆栈、堆、双向链表和优先级队列,这非常有用。在第一章中简单提到了SplFileObject
级。SPL 的文档可以在这里找到:[
us3.php.net/manual/en/book.spl.php](http://us3.php.net/manual/en/book.spl.php)
。
第一个显示的类是SplMaxHeap
。基本上,数字以随机的顺序被插入到类SplMaxHeap
的对象中。检索数字时,它们按降序排列。有一个完全相同的类SplMinHeap
,它按照升序对其元素进行排序。清单 5-7 包含了这个脚本。
清单 5-7。 SplMaxHeap
剧本
<?php $hp = new SplMaxHeap(); for ($i = 0;$i <= 10;$i++) { $x = rand(1, 1000); print "inserting: $x\n"; $hp->insert($x); } $cnt = 1; print "Retrieving:\n"; foreach ($hp as $i) { print $cnt++ . " :" . $i . "\n"; } ?>
这些类可以针对日期、字符串或任何数据类型进行扩展和实现。执行该脚本时,结果如下所示:
./ttt.php inserting: 753 inserting: 770 inserting: 73 inserting: 760 inserting: 782 inserting: 982 inserting: 643 inserting: 924 inserting: 288 inserting: 367 Retrieving:
1 :982 2 :924 3 :782 4 :770 5 :760 6 :753 7 :643 8 :367 9 :288 10 :73
由rand
函数生成的随机数以随机顺序插入到$hp
中。检索时,它们按降序排序。这个类可以像前面的例子一样直接使用,也可以扩展。如果类被扩展,子类应该实现方法compare
。清单 5-8 给出了一个例子。
***清单 5-8。*扩展类
`<?php
class ExtHeap extends SplMaxHeap {
public function compare(array $var1,array KaTeX parse error: Expected '}', got 'EOF' at end of input: …ar2) { t1=strtotime(
v
a
r
1
[
′
h
i
r
e
d
a
t
e
′
]
)
;
var1['hiredate']);
var1[′hiredate′]); t2=strtotime(
v
a
r
2
[
′
h
i
r
e
d
a
t
e
′
]
)
;
r
e
t
u
r
n
(
var2['hiredate']); return(
var2[′hiredate′]); return(t1-$t2);
}
}
$var1=array(‘ename’=>‘Smith’,‘hiredate’=>‘2009-04-18’,‘sal’=>1000);
$var2=array(‘ename’=>‘Jones’,‘hiredate’=>‘2008-09-20’,‘sal’=>2000);
$var3=array(‘ename’=>‘Clark’,‘hiredate’=>‘2010-01-10’,‘sal’=>2000);
$var4=array(‘ename’=>‘Clark’,‘hiredate’=>‘2007-12-15’,‘sal’=>3000);
$hp=new ExtHeap();
h
p
−
>
i
n
s
e
r
t
(
hp->insert(
hp−>insert(var1);
h
p
−
>
i
n
s
e
r
t
(
hp->insert(
hp−>insert(var2);
h
p
−
>
i
n
s
e
r
t
(
hp->insert(
hp−>insert(var3);
h
p
−
>
i
n
s
e
r
t
(
hp->insert(
hp−>insert(var4);
foreach($hp as KaTeX parse error: Expected '}', got 'EOF' at end of input: …Hiredate:%s\n",emp[‘ename’],$emp[‘hiredate’]);
}
?>`
这并不像乍看起来那样微不足道。这个脚本按照日期值对数组进行排序。新的compare
函数不接受任何不是数组的参数。通过将日期转换为纪元格式来进行比较。我们对SplMaxHeap
的扩展将从最近的条目到最早的条目进行排序:
./script5.6.php Ename:Clark Hiredate:2010-01-10 Ename:Smith Hiredate:2009-04-18 Ename:Jones Hiredate:2008-09-20 Ename:Clark Hiredate:2007-12-15
除了堆、栈和队列类,还有一些非常有趣的处理文件的类。SplFileObject
类在第一章中显示,并将在数据库集成章节中再次使用。然而,还有更有趣的file
类。下一个有趣的是SplFileInfo
。它返回给定文件的文件信息:
<?php $finfo=new SplFileInfo("/home/mgogala/.bashrc"); print "Basename:".$finfo->getBasename()."\n"; print "Change Time:".strftime("%m/%d/%Y %T",$finfo->getCTime())."\n"; print "Owner UID:".$finfo->getOwner()."\n"; print "Size:".$finfo->getSize()."\n"; print "Directory:".$finfo->isDir()? "No":"Yes"; print "\n"; ?>
这个类可以从标准 C 库中获取创建时间、访问时间、名称、所有者以及所有通常由fstat
函数提供的信息。以下是该脚本的输出:
./script5.7.php Basename:.bashrc Change Time:02/18/2011 09:17:24 Owner UID:500 Size:631 No
这个类本身很有趣,但也是解释FileSystemIterator
类所需要的。这个类也是 SPL 的一部分,其功能与 Unix find
命令相同:它遍历目录树并通过结果返回一个迭代器。清单 5-9 显示了打印/usr/local
目录中所有子目录名称的脚本。
***清单 5-9。*打印/usr/local
目录下的子目录名
<?php $flags = FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS; $ul = new FileSystemIterator("/usr/local", $flags); foreach ($ul as $file) { if ($file->isDir()) { print $file->getFilename() . "\n"; } } ?>
这些标志规定了将要返回的内容。通过设置CURRENT_AS_FILEINFO
标志,我们要求每个迭代器条目都是一个文件信息对象,一个SplFileInfo
类的成员。还有一个CURRENT_AS_PATHNAME
标志,用于指示FileSystemiterator
类的对象返回路径名,而不是文件信息。当然,文件信息包含路径名和所有其他可用的信息,所以在查找目录时调用它是很自然的事情。SKIP_DOTS
标志指示迭代器跳过“.”还有“…”目录。在我的系统上,该脚本的输出如下所示:
var libexec sbin src tora bin include skype_static-2.1.0.81 man lib share etc
注意,这个脚本的输出在另一个系统上可能看起来不同。例如,Windows 安装通常没有名为“/usr/local”的目录另一个有用的迭代器是Globiterator
,它与上面显示的FileSystemiterator
略有不同。Globiterator
将经历文件系统模式,如“*”。php”。清单 5-10 给出了Globiterator
类用法的一个简单例子:
清单 5-10。 Globiterator
类用法
<?php $flags = FilesystemIterator::CURRENT_AS_PATHNAME; $ul = new Globiterator("*.php", $flags); foreach ($ul as $file) { print "$file\n"; } ?>
在这种情况下,我们只对文件名感兴趣,所以使用了CURRENT_AS_PATHNAME
标志。当然,旗帜CURRENT_AS_FILEINFO
同样有效,而旗帜SKIP_DOTS
毫无意义。
SPL 的结论
从技术上来说,SPL 在 5.3 并不新鲜;它在 5.2 中也存在,但是作为一个单独的扩展。然而,从版本 5.3 开始,它就是 PHP 不可分割的一部分,如果不卸载整个 PHP 语言,它就不能被禁用或卸载。它是一个相当大的、正在发展的扩展,有许多有用的部分。这一章展示了最有用和最实用的方法,但是关于 SPL 还有比这一章所能涵盖的更多的东西。在未来的版本中,它可能会增长、扩展,甚至变得更加有用。熟悉 SPL 当然可以节省很多写剧本的精力和麻烦。我个人认为它非常有用,因为它内置了错误检查和异常。
Phar 扩展
Java 是网络上非常流行的语言。Java 有*。jar 归档使开发人员可以将几个文件打包成一个归档文件,并作为应用执行。在 5.3 版本中,PHP 出于同样的目的创建了 phar 扩展。这个扩展的目的是允许创建和操作 PHP 存档文件。因此得名:Php 存档。
在清单 5-1 的中,我们有一个包含两个额外的类文件的脚本,wild.php 和 domestic.php。为了分发应用,我们需要分发三个文件。如果有更多的类,要分发的文件数量会变得更大。目标是只分发两个文件:可执行脚本本身和包含所有必需的类文件的 phar 文件。换句话说,来自清单 5-1 的脚本的新版本将看起来像清单 5-11 的。
***清单 5-11。*清单 5-1 修订
<?php include 'phar://animals.phar/wild.php'; include 'phar://animals.phar/domestic.php'; $a=new animal(); printf("%s\n",$a->get_type()); $b=new \wild\animal(); printf("%s\n",$b->get_type()); ?>
窍门就在 include 指令中,它包含文件 animals.phar 并引用这些文件。这样的文件是如何创建的?正如 Java 有名为 jar 的程序一样,PHP 5.3 发行版也有名为 phar 的程序。要获得帮助,需要执行“phar help”。有相当多的选择,都有很好的记录。
Phar archiver 只是一个 PHP 脚本,它使用。phar 扩展。在大多数发行版中,它仍然没有手册页,所以“phar help”是我们能得到的最好的帮助。现在是时候创建我们的第一个 phar 档案了。语法非常简单:
phar pack -f animals.phar -c gz wild.php domestic.php
“pack
”参数指示“phar”程序以“-f
”选项中给定的名称创建档案,并打包文件“wild.php
”和“domestic.php
”。为了成功,php.ini 参数phar.readonly
必须设置为off
最初,该参数设置为“on”,这将阻止创建归档。要使用的压缩算法是“zip”支持的算法有 zip、gz (gzip)和 bz2 (bzip2)。默认值是不压缩。这样,我们可以毫无问题地执行清单 5-2 中的脚本。输出看起来和预期的一样:
dog tiger
这还不是全部。PHP Archiver 能做的不止这些。如果我们想确保存档不被篡改,我们可以对其进行签名:
phar sign -f animals.phar -h sha1
这将使用非常流行的 SHA1 算法对 animals.phar 文件进行签名,并防止篡改。为了好玩,我们给文件添加一个空行,并尝试重新执行清单 5-3 中的脚本。结果如下:
./script5.3.php PHP Warning: include(phar://animals.phar/wild.php): failed to open stream: phar "/tmp/animals.phar" has a broken signature in /tmp/script5.3.php on line 3 PHP Warning: include(): Failed opening 'phar://animals.phar/wild.php' for inclusion (include_path='.:/usr/local/PEAR') in /tmp/script5.3.php on line 3 PHP Warning: include(phar://animals.phar/domestic.php): failed to open stream: phar "/tmp/animals.phar" has a broken signature in /tmp/script5.3.php on line 4
PHP Warning: include(): Failed opening 'phar://animals.phar/domestic.php' for inclusion (include_path='.:/usr/local/PEAR') in /tmp/script5.3.php on line 4 PHP Fatal error: Class 'animal' not found in /tmp/script5\. 3.php on line 5
Include 命令不起作用,我们的脚本悲惨地失败了。这意味着我们的脚本可以得到适当的保护,不会被篡改,因为修改后的脚本不会通过签名验证。Phar 还可以从档案中提取文件,添加和删除文件。所有命令都非常简单明了。下面是 list 命令的一个示例,它列出了 phar 归档文件的内容:
phar list -f animals.phar |-phar:///home/mgogala/work/book/Chapter5/animals.phar/domestic.php \-phar:///home/mgogala/work/book/Chapter5/animals.phar/wild.php
phar 不仅可以列出列表,还可以提取和删除存档成员,就像它的对应 jar、ar 和 tar 一样。该语法与 list 和 insert 命令的语法非常相似。命令“phar delete -f animals.phar -e wild.php
”将从档案中删除脚本“wild . PHP”phar add -f animals.phar wild.php
将把它添加回来,命令phar extract -f animals.phar -i wild.php
将从档案中提取它。Phar 也可以处理正则表达式。这个命令将把整个目录打包到一个名为“test.phar”的 phar 档案中::“phar pack -f test.phar -c zip *.php
”
包含文件的文件名将在标准输出中列出。Phar 还可以创建一个可执行脚本。为此,我们必须创建所谓的“存根”Stub 是在执行 phar 归档文件时控制被转移到的脚本。这个存根的格式有些特殊。下面是我们从清单 5-1 中得到的脚本,稍微修改了一下,以便用作我们 phar 存档的存根:
<?php include 'phar://animals.phar/wild.php'; include 'phar://animals.phar/domestic.php'; $a=new animal(); printf("%s\n",$a->get_type()); $b=new \wild\animal(); printf("%s\n",$b->get_type()); __HALT_COMPILER(); ?>
这个文件将被称为“stub.php
”,并作为“存根”添加到我们的档案中注意最后的__HALT_COMPILER()
功能。这个函数必须存在,以终止脚本的执行。此外,在 phar 分隔符__HALT_COMPILER()
和脚本终止符("?>”)。添加存根很简单:
phar stub-set -f animals.phar -s stub.php
请注意,当文件作为存根添加时,如果使用“phar list”列出,它将不会显示为归档的一部分但是,可以将归档文件作为 PHP 脚本执行:
php animals.phar dog tiger
在基于 Unix 的系统上,还可以在脚本的开头添加“bang ”,这样就可以从命令行执行脚本。下面是语法:
phar stub-set -f animals.phar -s stub.php -b '#!/usr/bin/env php'
在 Unix 术语中,“!/usr/bin/env php”被称为“bang ”,它使 shell 能够找到合适的解释器来执行脚本。phar 档案“animals.phar”现在可以在命令行上执行,当然,只有在操作系统使其可执行之后:
./animals.phar dog tiger
我们已经创建了一个完全包含的应用,它包含执行归档所需的所有类和存根。Phar 程序本身实际上是一个 phar 档案,可以在命令行上执行:
phar list -f /opt/php/bin/phar |-phar:///opt/php/bin/phar.phar/clicommand.inc |-phar:///opt/php/bin/phar.phar/directorygraphiterator.inc |-phar:///opt/php/bin/phar.phar/directorytreeiterator.inc |-phar:///opt/php/bin/phar.phar/invertedregexiterator.inc |-phar:///opt/php/bin/phar.phar/phar.inc \-phar:///opt/php/bin/phar.phar/pharcommand.inc
有可能把所有的套路都提取出来研究。Phar 是开源的,就像整个 PHP 语言一样,所以我们鼓励你这样做。还有一个应用编程接口(API ),它允许通过 PHP 脚本创建和操作 phar 档案。
Phar 是一个非常简单但重要的程序。它将改变 PHP 应用的分发和打包方式,并且有望节省一些空间。它是 PHP 5.3 和更高版本中一个相当低调但极其重要的扩展。这个扩展没有像名称空间或 nowdoc 格式那样吸引那么多的关注,但是它将对 PHP 应用的分发方式产生深远的影响。已经宣布了将 phar 用于 PhpMyAdmin 和 pgFouine 等脚本的意图。
Phar 不会影响脚本性能,因为归档文件只被解析一次。脚本开始时的时间是最小的,至少根据我的经验,不会影响执行时间。还有一个重要的缓存机制叫做 APC,它允许缓存变量和整个文件。替代 PHP 缓存(APC)是一个可选模块,需要单独安装。但是,它经常被使用,因为它可以提供很大的性能增益。特别是函数apc_compile_file
可以用来把一个 PHP 文件编译成字节码,缓存,就这样用,大大提高了速度。Phar 与最新的 APC 兼容,phar 文件可以使用apc_compile file
缓存。APC 不会自动安装在 PHP 上,所以我不会详细讨论它,但是感兴趣的人可以在这里找到手册页:
http://us3.php.net/manual/en/book.apc.php
总结
在本章中,您了解了 PHP 5.3 的重要新特性,比如名称空间和新的 newdoc 格式。您还了解了 SPL,它从 PHP 5.3 和 phar 扩展开始成为 PHP 不可或缺的一部分,非常低调但也非常重要。没有本章最重要的一点。这里介绍的所有工具都是众所周知的“前沿”,非常有用。名称空间将越来越频繁地出现,并支持越来越复杂的应用系统。Phar 档案可以将整个应用系统打包成一个文件,从而使应用的分发更加容易。
SPL 仍在开发中,但迄今为止开发的模块非常有前途和有用。使用 PHP 5.2 和更早版本的编程风格很容易,不需要任何努力。然而,PHP 作为一种语言发展非常迅速,自觉地将自己局限于旧版本会使我的 PHP 编程技能在很大程度上变得过时和无用。学习新东西并不难;很好玩。我真心推荐。
六、表单设计和管理
基于 Web 的表单是应用新数据的常见来源。一般来说,这些数据是非结构化的,在存储之前通常需要进行处理、格式化或其他处理。数据也可能来自潜在的不可靠来源。本章将演示从 web 表单中获取数据、使用 JavaScript 验证输入字段、通过 AJAX 请求将数据传递给 PHP 以及在数据存储服务中维护数据完整性的方法。我们还会给你一些关于操作图像、集成多种语言和使用正则表达式的建议。
数据验证
在基于 web 的表单中,有两个主要的数据验证区域,它们有两个不同的功能。第一种类型的验证发生在客户端使用 JavaScript 的表单中,第二种类型发生在 PHP 通过GET
或POST
请求在服务器端接收数据时。
JavaScript 验证的作用是双重的,两个动作都发生在客户端。它可以用来通知客户机(网站用户)关于输入数据的建议和警告,并将数据放入接收 PHP 脚本所寻找的一致模式中。PHP 验证更多地关注于维护接收到的数据的完整性,同时对其进行操作,使其与已经存储的数据保持一致和兼容。
我们将为 JavaScript 验证定义一个表单和两个表单元素——一个接受必需的姓和名,另一个接受带有可选区号的电话号码。对于此示例,表单中不会包含提交按钮;相反,它将由 JavaScript 在搜索函数中处理,并在发生onblur
事件时被激活。这将在本章后面讨论。
在清单 6-1 的中,我们使用了GET
方法,这样我们就可以在浏览器的地址栏中显示提交的值。这将允许使用其他数据进行快速测试,并绕过 JavaScript 验证。
***清单 6-1。*使用GET
方法的验证示例
`
<input type=‘text’ id=‘name’ name=‘name’ οnkeyup=‘validate(this);’ οnfοcus=‘this.select();’
οnblur=‘search();’ value=‘First and Last Name’ />
<input type=‘text’ id=‘phone’ name=‘phone’ οnkeyup=‘validate(this);’ οnfοcus=
‘this.select();’ οnblur=‘search();’ value=‘Phone Number’ />
测试完成后,表格可以切换到POST
方法。使用POST
方法可以获得干净的 URL 外观,而表单参数对客户端是不可见的(例如,[
domain.com/dir/](http://domain.com/dir/)
或[
domain.com/dir/script.php](http://domain.com/dir/script.php)
)。或者,如果提交的页面可以被客户端加入书签,那么使用GET
方法会更好,因为客户端在再次访问 web 页面时不需要再次“重新发布”表单数据。当使用GET
方法时,表单提交后浏览器的 URL 将类似于[
domain.com/?var1=exa&var2=mple](http://domain.com/?var1=exa&var2=mple)
。
注根据项目需求,您可以使用
POST
或GET
方法。重要的是要记住,没有一种方法比另一种更安全;两者都可以利用。然而,使用POST
可能会稍微安全一些,因为普通用户不能通过操作查询字符串获得不同的结果。请务必阅读关于安全的第十一章了解更多详情。
当客户端在清单 6-1 的文本框中输入时,JavaScript 验证函数被onkeyup
事件调用,并传递this
作为引用。这将允许 JavaScript 访问执行验证所需的所有元素属性。如果两个字段都通过了必要的验证,那么onblur
事件将尝试使用 AJAX 提交表单。在第十五章中,我们将研究如何执行 AJAX 请求。onfocus
事件选择已经输入到文本框中的文本,这样客户端就不必删除已经输入的数据。这是通过在 JavaScript 中调用select()
方法并使用当前元素的属性(onfocus='this.select();')
来实现的。
我们将定义接受一个参数的validate
JavaScript 函数(参见清单 6-2 )。验证将包括正则表达式模式匹配。我们将在本章的后面解释如何建立正则表达式。现在,我们将专注于基本结构。
清单 6-2。validate
JavaScript 函数
function validate(a){ if(a.value.length>3) switch(a.name){ case 'name': if(a.value.match(/^[a-zA-Z\-]+ [a-zA-Z\-]+$/)){ /* ... successful match code for name ... */ return true; }else{ /* ... no match code for name ... */ } break; case 'phone': if(a.value.match((/^((\(|\[)?\d{3}?(\]|\))?(\s|-)?)?\d{3}(\s|-)?\d{4}$/)){ /* ...successful match code for phone ... */ return true; } else{ /* ... no match code for phone ... */ } break; } return false; }//validate function
validate
函数仅在字符串长度大于三时才开始对表单输入字段进行检查。如果正在验证的数据集没有包含足够的字符,则必须调整验证阈值。如果在验证中执行的代码发出 AJAX 请求,限制字符串的长度也有助于避免对服务器资源造成不必要的压力,这些资源可能会执行数据库查找或运行算法。
注意在清单 6-2 中,我们假设收集的数据长度大于三个字符。不过,通常从表单中收集的数据包含三个或更少字符的字符串和数字。在这种情况下,
a.value.length>3
if
语句可以被移到case
语句中,并被调整以匹配被验证字段的每个模式。
接下来,我们将使用表单元素的名称来确定值需要匹配哪个表达式。通过返回true
或false
,我们也可以将这个函数用于我们的搜索函数。我们通过调用字符串的match
函数来匹配文本框值和表达式。如果表达式匹配成功,则返回匹配字符串。如果不存在匹配,则返回null
。
我们将在清单 6-3 中定义search
函数,它将验证两个表单字段,然后在清单 6-4 中,我们将启动一个 AJAX 请求将表单值传递给一个 PHP 脚本。
***清单 6-3。*定义search
功能
function search(){ if(validate(document.getElementById('name')) &&validate(document.getElementById('phone'))){ //build and execute AJAX request } }//save function
使用search
函数,首先通过调用validate
函数并传入元素属性来验证所需的参数。实际的表单提交是通过 AJAX 请求执行的。
如果姓名和电话文本框的值被验证,表单将请求 AJAX 提交一个类似于[
localhost/script7_1.php?name=john+smith&phone=(201) 443-3221](http://localhost/script7_1.php?name=john+smith&phone=(201)443-3221)
的 URL。我们现在可以开始构建 PHP 验证组件了。由于属性在 URL 中,我们可以通过手动调整 URL 中已知的异常和可接受的格式来测试不同的值。例如,我们使用以下 URL 测试 PHP 执行的名称验证:
http://localhost/script7_1.php?name=Shérri+smith&phone=(201) 443-3221 http://localhost/script7_1.php?name=john+o'neil&phone=(201) 443-3221 http://localhost/script7_1.php?name=john+(*#%_0&phone=(201) 443-3221
然后,我们可以使用下一组 URL 测试 PHP 执行的电话验证:
http://localhost/script7_1.php?name=john+smith&phone=2014433221 http://localhost/script7_1.php?name=john+smith&phone=john+smith http://localhost/script7_1.php?name=john+smith&phone=201 443-3221 ext 21
我们现在可以介绍清单 6-4 中的 PHP 验证代码。
清单 6-4。 PHP 验证
<?php $formData=array();//an array to house the submitted form data foreach($_GET as $key => $val){ $formData[$key]=htmlentities($val,ENT_QUOTES,'UTF-8'); } if(isset($formData['name'])&&isset($formData['phone'])){ $expressions=array('name'=>"/^[a-zA-Z\-]+ [a-zA-Z\-]+$/", 'phone'=>"/^((\(|\[)?\d{3}?(\]|\))?(\s|-)?)?\d{3} (\s|-)?\d{4}$/" ); if(preg_match($expressions['name'],$formData['name'],$matches['name'])===1 && preg_match($expressions['phone'],$formData['phone'],$matches['phone'])===1){ /* code, do something with name and phone */ } } ?>
函数preg_match
接受一个正则表达式,后跟一个与表达式匹配的字符串,然后是一个填充了匹配结果的数组。
有几个方便的 PHP 扩展也有助于数据验证、净化和表达式匹配。数据过滤是清理和验证数据的一种良好且一致的方式。它包括几个用于验证客户端输入数据的常用函数。清单 6-5 通过过滤器库的 PHP filter_var
函数使用 URL 和电子邮件验证过滤器。通过向函数传递一个字符串以及 sanitize 过滤器或 validate 过滤器来使用filter_var
函数。sanitize 过滤器从字符串中删除不支持的字符,而 validate 过滤器确保字符串格式正确并包含正确的数据类型。
***清单 6-5。*PHPfilter_var
函数
`<?php
//string(15) “email@example.com”
var_dump(filter_var(‘email@example.com’, FILTER_VALIDATE_EMAIL));
//string(18) “e.mail@exam.ple.ab”
var_dump(filter_var(‘e.mail@exam.ple.ab’, FILTER_VALIDATE_EMAIL));
//string(20) “em-ail@example.co.uk”
var_dump(filter_var(‘em-ail@example.co.uk’, FILTER_VALIDATE_EMAIL));
//bool(false)
var_dump(filter_var(‘www.domain@.com’, FILTER_VALIDATE_EMAIL));
//bool(false)
var_dump(filter_var(‘email@domain’, FILTER_VALIDATE_EMAIL));
//bool(false)
var_dump(filter_var(‘example.com’, FILTER_VALIDATE_URL));
//bool(false)
var_dump(filter_var(‘www.example.com’, FILTER_VALIDATE_URL));
//string(22) “http://www.example.com”
var_dump(filter_var(‘http://www.example.com’, FILTER_VALIDATE_URL));
//string(23) “http://www.e#xample.com”
var_dump(filter_var(‘http://www.e#xample.com’, FILTER_VALIDATE_URL));
//bool(false)
var_dump(filter_var(‘www example com’, FILTER_VALIDATE_URL));
//bool(false)
var_dump(filter_var(‘www.ex#ample.com’, FILTER_VALIDATE_URL));
?>`
整理过滤器对于准备一致的数据很有用。他们也使用filter_var
功能。清单 6-6 使用 URL 和电子邮件验证过滤器。
***清单 6-6。*使数据与 URL 和电子邮件一致验证过滤器
`<?php
//string(15) “email@example.com”
var_dump(filter_var(‘email@example.com’, FILTER_SANITIZE_EMAIL));
//string(17) “e.mail@exam.pl.ab”
var_dump(filter_var(‘e.mail@exam.plé.ab’, FILTER_SANITIZE_EMAIL));
//string(20) “em-ail@example.co.uk”
var_dump(filter_var(‘em-ail@examp"le.co.uk’, FILTER_SANITIZE_EMAIL));
//string(16) “www.dom!ain@.com”
var_dump(filter_var(‘www.dom!ain@.com’, FILTER_SANITIZE_EMAIL));
//string(13) “email@do^main”
var_dump(filter_var(‘email@do^main’, FILTER_SANITIZE_EMAIL));
//string(11) “example.com”
var_dump(filter_var(‘example.com’, FILTER_SANITIZE_URL));
//string(15) “www.example.com”
var_dump(filter_var(“\twww.example.com”, FILTER_SANITIZE_URL));
//string(22) “http://www.example.com”
var_dump(filter_var(‘http://www.example.com’, FILTER_SANITIZE_URL));
//string(23) “http://www.e#xample.com”
var_dump(filter_var(‘http://www.e#xample.com’, FILTER_SANITIZE_URL));
//string(13) “wwwexamplecom”
var_dump(filter_var(‘www example com’, FILTER_SANITIZE_URL));
?>`
Perl 兼容的正则表达式(PCRE)库也包括一些有用的函数,比如正则表达式find and replace
、grep
、match
和match all
,还有一个方便的find and replace using a callback
函数。
在清单 6-7 的中,我们将使用preg_match_all
函数来识别所有以大写字符开头,后面跟着小写字符的字符串。我们将使用var_export
函数将$matches
数组中匹配的结果转储到屏幕上。
***清单 6-7。*使用preg_match_all
功能
`<?php
s t r = ′ A F o r d c a r w a s s e e n a t S u p e r C l e a n c a r w a s h . ′ ; p r e g m a t c h a l l ( ′ / [ A − Z ] [ a − z ] + / ′ , str='A Ford car was seen at Super Clean car wash.'; preg_match_all('/[A-Z][a-z]+/', str=′AFordcarwasseenatSuperCleancarwash.′;pregmatchall(′/[A−Z][a−z]+/′,str, m a t c h e s ) ; v a r e x p o r t ( matches); var_export( matches);varexport(matches);
/*
array (
0 =>
array (
0 => ‘Ford’,
1 => ‘Super’,
2 => ‘Clean’,
),
)
*/
?>`
在清单 6-7 中,preg_match_all
被传递了三个参数,正则表达式,匹配的字符串,以及包含匹配的数组。我们还可以传递一个标志来调整$matches
数组,并传递一个偏移量来从字符串中的某个字符位置开始,如清单 6-8 所示。
***清单 6-8。*调整$matches
阵列
`<?php
$str=‘A Ford car was seen at Super Clean car wash.’;
preg_match_all(‘/[A-Z][a-z]+/’,
s
t
r
,
str,
str,matches,PREG_PATTERN_ORDER,5);
var_export($matches);
preg_match_all(‘/[A-Z][a-z]+/’,
s
t
r
,
str,
str,matches,PREG_SET_ORDER);
var_export(
m
a
t
c
h
e
s
)
;
‘
‘
p
r
e
g
m
a
t
c
h
a
l
l
(
′
/
[
A
−
Z
]
[
a
−
z
]
+
/
′
,
matches);` `preg_match_all('/[A-Z][a-z]+/',
matches);‘‘pregmatchall(′/[A−Z][a−z]+/′,str,
m
a
t
c
h
e
s
,
P
R
E
G
O
F
F
S
E
T
C
A
P
T
U
R
E
)
;
v
a
r
e
x
p
o
r
t
(
matches,PREG_OFFSET_CAPTURE); var_export(
matches,PREGOFFSETCAPTURE);varexport(matches);
/*
// PREG_PATTERN_ORDER,5
array (
0 =>
array (
0 => ‘Super’,
1 => ‘Clean’,
),
)
// PREG_SET_ORDER
array (
0 =>
array (
0 => ‘Ford’,
),
1 =>
array (
0 => ‘Super’,
),
2 =>
array (
0 => ‘Clean’,
),
)
// PREG_OFFSET_CAPTURE
array (
0 =>
array (
0 =>
array (
0 => ‘Ford’,
1 => 2,
),
1 =>
array (
0 => ‘Super’,
1 => 23,
),
2 =>
array (
0 => ‘Clean’,
1 => 29,
),
),
)
*/
?>`
PCRE 库中的其他函数以非常相似的方式工作,对于数据验证、提取和操作相关的任务非常有用。
上传文件/图像
向服务器添加文档是一种常见的请求。文档通常位于远程计算机或服务器上,必须移动到托管服务器。这可以使用表单元素来完成。
在通过基于 web 的表单向服务器添加文件之前,最好检查一下 PHP 配置文件。它包含许多设置,这些设置直接影响表单的工作方式、能做什么、能做多少以及持续多长时间。在对上传和表单相关问题进行故障排除时,熟悉并了解这些配置设置非常重要。另一个常见问题是拥有适当的权限来访问接收服务器上的目录。
考虑清单 6-9 中的表单,它允许通过浏览/上传过程或者通过来自服务器的 URL 添加文档。在表单中,定义了一个新的表单属性enctype
,它定义了表单数据的编码方式。当表单使用二进制数据(在本例中为输入类型文件)时,这是必需的。
***清单 6-9。*定义enctype
属性
`
如果没有在表单标签中定义,enctype
的默认值是application/x-www-form-urlencoded
,它将处理除文件上传和非 ASCII 数据之外的大多数输入类型。当网站用户提交这个表单时,两个不同的 PHP 超全局变量将包含数据:localfile
输入字段的$_FILES
和remoteurl
输入字段的$_POST
。$_FILES
将包含关于localfile
的元数据。$_FILES
包含的元数据如表 6-1 所示。
在将清单 6-9 中localfile
的文件移动到永久存储位置之前,最好确保该文件来自 HTTP POST。这可以使用函数is_uploaded_file()
来完成,它接受一个参数,临时文件名['tmp_name']
。上传后,要将文件移动到某个目录,可以使用move_uploaded_file()
函数,该函数接受两个参数,临时名称['tmp_name']
和目标名称。
对于输入远程 URL,我们可以使用几个选项从远程 URL 获取文件。通过 HTTP、HTTPS 和 FTP 下载文件的一种便捷方法是通过shell_exec
函数使用wget
(一个命令行实用程序),它执行一个 shell 命令并返回输出(如果有的话)。下载文档的其他方法包括 socket 相关的工具,如 fsocketopen 或 curl。可以使用以下语法下载文件:
shell_exec('wget '. escapeshellcmd($_POST['remoteurl']));
注意escapeshellcmd
功能的使用。这用于转义常用的恶意字符,以防止在服务器上执行任意命令。
图像转换和缩略图
在使用基于 web 的应用时,图像是一种常见的文件类型。这些可以用在照片图库、截图和幻灯片中。在上一节中,您学习了如何从浏览器上传表单或者通过命令行实用程序wget
结合shell_exec
向服务器添加文档。既然文件已经驻留在服务器上,我们就可以开始操作它们,使之适合其他应用所需的结构。
PHP 有一个叫做 GD 的图像库。它包含一个可以创建和操作图像的函数列表。我们将只触及那些调整大小和转换的一小部分。名为php_info()
的函数可以用来检查安装在服务器上的 GD 的版本。
创建缩略图时,我们将创建原始图像的 PNG 副本,宽度调整为 200 像素,高度将根据原始图像而变化。为了获得图像的大小,我们将使用函数getimagesize()
,该函数接受图像名称作为参数,并返回一个包含width
、height
和mime
的元数据数组。让我们考虑一下清单 6-10 。
***清单 6-10。*使用getimagesize()
功能
<?php $imgName='image.jpg'; $thumbName='thumb.png'; $metaData=getimagesize($imgName); $img='';
`$newWidth=200;
n
e
w
H
e
i
g
h
t
=
newHeight=
newHeight=metaData[1]/(
m
e
t
a
D
a
t
a
[
0
]
/
metaData[0]/
metaData[0]/newWidth);
switch(KaTeX parse error: Expected '}', got 'EOF' at end of input: …jpeg': img=imagecreatefromjpeg(
i
m
g
N
a
m
e
)
;
b
r
e
a
k
;
c
a
s
e
′
i
m
a
g
e
/
p
n
g
′
:
imgName); break; case 'image/png':
imgName); break; case′image/png′: img=imagecreatefrompng(
i
m
g
N
a
m
e
)
;
b
r
e
a
k
;
c
a
s
e
′
i
m
a
g
e
/
g
i
f
′
:
imgName); break; case 'image/gif':
imgName); break; case′image/gif′: img=imagecreatefromgif(
i
m
g
N
a
m
e
)
;
b
r
e
a
k
;
c
a
s
e
′
i
m
a
g
e
/
w
b
m
p
′
:
imgName); break; case 'image/wbmp':
imgName); break; case′image/wbmp′: img=imagecreatefromwbmp($imgName);
break;
}
if(KaTeX parse error: Expected '}', got 'EOF' at end of input: img){ imgThumb=imagecreatetruecolor(
n
e
w
W
i
d
t
h
,
newWidth,
newWidth,newHeight);
imagecopyresampled(
i
m
g
T
h
u
m
b
,
imgThumb,
imgThumb,img,0,0,0,0,
n
e
w
W
i
d
t
h
,
newWidth,
newWidth,newHeight,
m
e
t
a
D
a
t
a
[
0
]
,
metaData[0],
metaData[0],metaData[1]);
imagepng($imgThumb,
t
h
u
m
b
N
a
m
e
)
;
i
m
a
g
e
d
e
s
t
r
o
y
(
thumbName); imagedestroy(
thumbName); imagedestroy(imgThumb);
}
?>`
名为$metadata
的数组包含 MIME 类型以及原始图像的高度和宽度。这些信息将用于在 GD 中打开原始图像。我们现在可以为缩略图确定$newheight
的值。MIME 类型是通过 switch 语句发送的,因此我们可以处理不同类型的图像文件。imagecreatefromjpg
和类似的函数将打开原始图像并返回该图像的资源句柄。使用imagecreatetruecolor
创建具有定义宽度和高度的缩略图。当创建图像的副本时,imagecopyresampled
函数接受几个参数,缩略图的资源句柄,原始文件的资源句柄,目的点和源点的 x 和 y 值,新的宽度和高度,以及原始宽度和高度。缩略图是通过提供 thumb 资源和一个新文件名用imagepng
创建的。使用imagedestroy
函数并传递 thumb 资源,资源最终被销毁。
最终结果是我们想要的缩略图格式的 PNG 图像。在清单 6-11 中,显示了来自getimagesize()
的 image.jpg 和 thumb.png 的输出。
***清单 6-11。*image.jpg 和 thumb.png 从getimagesize()
输出
//image.jpg array ( 0 => 1600, 1 => 1200, 2 => 2, 3 => 'width="1600" height="1200"', 'bits' => 8,
'channels' => 3, 'mime' => 'image/jpeg', ) //thumb.png array ( 0 => 200, 1 => 150, 2 => 3, 3 => 'width="200" height="150"', 'bits' => 8, 'mime' => 'image/png', )
在图 6-1 中显示的缩略图是由清单 6-10 中的脚本创建的。原始图像是一个宽 1200 像素、高 900 像素的 JPEG,输出图像是一个宽 200 像素、高 150 像素的 PNG。
***图 6-1。*创建的缩略图清单 6-10
正则表达式
正则表达式对于描述文本中的模式很有用,可以用在 JavaScript、MySQL 和 PHP 中。在我们开始构建正则表达式(regex)之前,我们应该首先获得一个正则表达式编辑器。在 Windows 上,Regex Tester by antix.co.uk
是免费的,使用简单。还有许多其他可用的正则表达式编辑器,其中一些具有更多功能。然而,作为开始,Regex Tester 是一个很好的工具。
表 6-2 列出了 regex 中可用的字符及其各自的匹配。
在表 6-2 中,我们看到了常用的字符以及它们匹配的内容。在清单 6-7 和清单 6-8 中,使用了正则表达式[A-Z][a-z]+
。基于表 6-2 ,表达式现在可以读作:匹配一个大写字母字符一次[A-Z]
,后面跟着一个小写字母字符[a-z]
重复一次或多次+
。大小写字母 A 和 Z 之间使用的连字符被解释为 A 和 Z 之间的任何字符。连字符的另一个例子是[a-e]
,它匹配 A、b、c、d 和 e,但不匹配 f 或 g,或者使用数字[1-3]
,它匹配 1、2 或 3,但不匹配 4 或 5。表达式[A-Z][a-z]{4}
将匹配以大写字符开头的所有五个字母单词。
在 JavaScript 中,我们可以通过使用表 6-3 中定义的字符串方法match
、search
、replace
和split
将表达式应用于字符串。也可以使用RegExp
对象,它有compile
、exec
和test
方法,都在表 6-4 中定义。
在 PHP 中,我们可以使用 PCRE 函数将正则表达式应用于字符串,并执行各种函数(表 6-5 )…
正如这些使用 PHP 和 JavaScript 的例子所示,正则表达式非常有用,是执行简单和复杂数据操作的好工具。表达式也可以在 MySQL 中用来执行复杂的搜索。例如,查询可以是"select * from contacts where civics regexp '[0-9]{5}'"
,它将返回在 civics 列中包含有效的 5 位邮政编码(或者任何其他 5 位数字)的所有记录。
多语言集成
当查看预期以某种方式输出的数据时,总是令人惊讶,您会遇到那些看起来奇怪的 A 或问号或其他奇怪的字符嵌套在应该是干净的数据中。大多数情况下,这是某种编码错误的结果。无论是来自表单、CSV 文件还是从文档中提取的文本,输入数据的脚本都不期望某些特定字符集之外的字符,可能是 ISO-8859-1 或 UTF-8。
注ISO-8859-1(Latin-1)字符集包含 8 位单字节 ASCII 编码图形字符集,包括标准和扩展 ASCII 字符(0-255)。UTF 8 字符集是 unicode 的多字节字符编码,包含 ISO-8859-1 中的字符,包括带有音调符号的字符。
当您遇到编码问题时,有一些事情需要观察和识别。数据的来源很重要,如果是来自网页,最好确认定义了正确的内容类型。这可以在 XHTML 的 HEAD 部分找到。如果我们将内容类型设置为 UTF-8,它将如下所示:
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
数据库可能是编码问题的另一个来源。创建 MySQL 数据库时,会设置一个排序规则。表和行也是如此。如果在数据库不期望的时候提供了 UTF-8 字符,则该字符在存储时可能会被误传。如果我们接受 MySQL 的 UTF-8 字符,排序规则可能是utf8_general_ci
(不区分大小写)或者utf8_unicode_ci
。我们还可以在建立数据库连接后,通过执行查询'set names utf8'
来请求 MySQL 采用 UTF-8 字符集。
最后,PHP 代码可能无法正确识别字符串的正确编码。有几个函数和库可以帮助解决编码问题,还有一些标志可以采用特定的字符集。函数utf8_encode()
将把一个 ISO-88591 字符串编码成 UTF-8。将某种字符编码转换成另一种编码的另一个有用的函数是使用mb_convert_encoding()
函数,它接受一个字符串、to 编码类型和 from 编码类型。'mb_*'
函数来自多字节字符串函数库,包含许多不同的多字节字符函数,比如mb_substr
(获取字符串的一部分)、mb_strlen
(获取字符串的长度),或者mb_eregi_replace
(regex find replace)。某些 PHP 函数也接受charset
标志。例如,当使用htmlentities()
时,我们可以传递一个标志来指定 UTF-8 字符集htmlentities($str,ENT_QUOTES,'UTF-8')
。
除了 MB 函数,PHP 提供的人类语言和字符编码支持中还有iconv
模块。iconv
函数,特别是iconv()
函数会将字符串转换成定义的字符编码。通过使用函数iconv("UTF-8","ISO-8859-1//IGNORE//TRANSLIT",$str)
,我们可以将一个 UTF-8 字符串转换成它的 ISO-8859-1 等价物。//IGNORE
和//TRANSLIT
标志用于指定如何处理不可转换的字符。IGNORE
标志无声地删除未知字符和所有后续字符,其中TRANSLIT
将尝试猜测要转换成的正确字符。
总结
在设计和编写基于 web 的表单时,有许多问题需要记住,比如数据验证、维护数据完整性和操作文件(包括图像)。在本章中,我们向您展示了一系列解决这些问题的技术,并辅以合适的代码示例和参考信息。您可以在客户端使用 JavaScript 验证输入数据,也可以在服务器端使用 PHP 验证输入数据。这些方法是互补的,因为客户端验证侧重于确保用户输入可接受的数据,而服务器端验证更多地着眼于维护数据库的完整性和一致性。
没有一个真正的开发人员能够放弃正则表达式(regex ),所以我们在本书的上下文中提供了一个简短的介绍。Regex 可以在许多情况下用于查找、匹配、拆分和替换字符串或部分字符串;我们在 JavaScript 和 PHP 中都提供了这种类型的简单示例。最后但同样重要的是,关于多语言集成的最后一个简短部分强调了确保数据以预期和可接受的格式传输的重要性。
在下一章,我们将研究 PHP 与传统关系数据库之外的数据库的集成;例如 SQLite3、MongoDB 和 CouchDB。