[原创中文翻译]symfony askeet24:第十七天,API。
October 8, 2007 – 6:01 am[欢迎转载,转载请注名出处http://symfony.net.cn。本文英文版权归symfony官方网站所有]
The API
程序(二次)开发接口
An Application Programming Interface, or API, is a developer’s interface to a particular service on your application, so that it can be included in external websites. Think about Google Maps or Flickr, which are used to extend lots of websites over the Internet thanks to their APIs.
程序(二次)开发接口,API,提供给开发者进行二次开发的程序接口,可以被外部网站使用。想一下Google地图或者Flickr,由于他们的API接口在互联网上催生了很多网站。
Askeet makes no exception, and we believe that in order to increase the service’s popularity, it has to be made available to other websites. The RSS feed developed during day 11 was a first approach to that requirement, but we can do much better.
askeet也不例外,我们相信为了提高访问量,必须和其他网站和平相处。第十一天开发的RSS订阅是第一个满足此类需要的,我们可以做得更好。
Askeet will provide an API of answers to a question asked by the user. The access to this API will be restricted to registered askeet users, through HTTP authentication. The API response format chosen is Representational State Transfer, or REST - that means that the response is a simple XML block similar to most of the output of main APIs in the web:
askeet的API接口提供显示读者回答问题的答案列表。此API仅限已注册读者使用。API回应格式使用Representational State Transfer,或者REST——这意味着回应是简单的XML block,类似很多网站使用的API接口:
<?xml version="1.0" encoding="utf-8" ?>
<rsp stat="ok" version="1.0">
<question href="http://www.askeet.com/question/what-shall-i-do-tonight-with-my-girlfriend” time=”2005-11-21T21:19:18Z” >
<title>What shall I do tonight with my girlfriend?</title>
<tags>
<tag>activities</tag>
<tag>relatives</tag>
<tag>girl</tag>
<tags>
<answers>
<answer relevancy=”50″ time=”2005-11-22T12:21:53Z”>You can try to read her poetry. Chicks love that kind of things.</answer>
<answer relevancy=”0″ time=”2005-11-22T15:45:03Z”>Don’t bring her to a doughnuts shop. Ever. Girls don’t like to be seen eating with their fingers - although it’s nice.</answer>
</answers>
</question>
</rsp>
We will implement the API in a new module of the frontend application, so use the command line to build the module skeleton:
我们在程序frontend的新模块中执行API,那么建立模块骨架:
$ symfony init-module frontend api
HTTP Authentication
HTTP验证
We choose to limit the use of the API to registered askeet users. For that, we will use the HTTP authentication process, which is a built-in authentication mechanism of the HTTP protocol. It is different from the web authentication that we have seen previously because it doesn’t even require a web page - all the exchanges take place in the HTTP headers.
我们选择限制注册读者使用API功能。所以,我们使用HTTP验证方式,基于HTTP协议的内嵌验证原理。这和我们先前看到的web验证不同,因为它不需要web页面——所有的交换都发生在HTTP头。
We will need the authentication method included in a custom validator during day six, so first of all we will do some refactoring and relocate the login code in the UserPeer model class:
在第六天我们需要把验证函数包含到自定义验证里,所以首先我们要做些修饰和重载登录代码到UserPeer模型类:
public static function getAuthenticatedUser($login, $password)
{
$c = new Criteria();
$c->add(UserPeer::NICKNAME, $login);
$user = UserPeer::doSelectOne($c);
// nickname exists?
if ($user)
{
// password is OK?
if (sha1($user->getSalt().$password) == $user->getSha1Password())
{
return $user;
}
}
return null;
}
The new class method UserPeer::getAutenticatedUser() can now be used in the myLoginValidator.class.php (we’ll leave that to you) and in the new api/index web service:
新类方法UserPeer::getAutenticateUser()现在被用在myLoginValidator.class.php(我们把这个留给你)和新api/index web服务器上:
<?php
class apiActions extends sfActions
{
public function preExecute()
{
sfConfig::set(’sf_web_debug’, false);
}
public function executeIndex()
{
$user = $this->authenticateUser();
if (!$user)
{
$this->error_code = 1;
$this->error_message = ‘login failed’;
$this->forward(’api’, ‘error’);
}
// do some stuff
}
private function authenticateUser()
{
if (isset($_SERVER[’PHP_AUTH_USER’]))
{
if ($user = UserPeer::getAuthenticatedUser($_SERVER[’PHP_AUTH_USER’], $_SERVER[’PHP_AUTH_PW’]))
{
$this->getContext()->getUser()->signIn($user);
return $user;
}
}
header(’WWW-Authenticate: Basic realm=”askeet API”‘);
header(’HTTP/1.0 401 Unauthorized’);
}
public function executeError()
{
}
}
?>
First of all, before executing any action of the API module (thus in the preExecute() method), we turn off the web debug toolbar. The view of this action being XML, the insertion of the toolbar code would produce a non-valid response.
首先,在执行API模块的任何动作前(在preExecute()方法里),我们关闭web调试工具栏。动作的视图是XML,工具栏代码无法产生回应。
The first thing that the index action will do is to check whether a login and a password are provided, and if they match an existing askeet account. If that is not the case, the authenticateUser() method sets the response HTTP header to ‘401′. It will cause an HTTP authentication window to pop-up in the user’s browser; the user will have to resubmit the request with the login and password.
首先,index动作将检查登录名和密码是否被提供了并匹配现有帐户。如果没有匹配成功,authenticateUser()方法设置HTTP header为401。会弹出提示窗口;用户必须再提交一次登录名和密码。
// first request to the API, without authentication
GET /api/index HTTP/1.1
Host: mysite.example.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8) Gecko/20051111 Firefox/1.5
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
...
// the API returns a 401 header with no content
HTTP/1.x 401 Authorization Required
Date: Thu, 15 Dec 2005 10:32:44 GMT
Server: Apache
WWW-Authenticate: Basic realm=”Order Answers Feed”
Content-Length: 401
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=iso-8859-1
// a login box will then appear on the user’s window.
// Once the user enters his login/password, a new GET is sent to the server
GET /api/index HTTP/1.1
Host: mysite.example.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8) Gecko/20051111 Firefox/1.5
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
…
Authorization: Basic ZmFicG90OnN5bWZvbnk=
An Authorization attribute is added to the HTTP request, which is sent again. It contains a base 64 encoded ‘login:password’ string. This is what the $_SERVER[’PHP_AUTH_USER’] and $_SERVER[’PHP_AUTH_PW’] look for in our authenticateUser() method.
验证属性被加到HTTP请求了,并且会再发送一次。用加密64位编码处理“登录名:密码”字符串。这就是$_SERVER[’PHP_AUTH_USER’]和$_SERVER[’PHP_AUTH_PW’]在authenticateUser()方法里查找到的。
Base64 does not output an encrypted version of its input. Decoding a base64-encoded string is very easy, and it reveals the password in clear. For instance, decoding the string ZmFicG90OnN5bWZvbnk= gives fabpot:symfony. So you have to consider that the password transits in clear in the Internet (as when entered in a web form) and can be intercepted. HTTP authentication must be restricted to non-critical content and services for this reason. Added protection could be gained by requiring the HTTPS protocol for API calls as well.
这64位编码没有强加密。解码64位编码字符串很简单,能清晰得显示密码。例如,解码字符串:ZmFicG90OnN5bWZvbnk=,结果是:fabpot:symfony。现在你必须思考得是密码在互联网上明文传送(在web表单中输入一样),会被劫取。HTTP验证必须限制non-critical内容和服务。可以靠HTTPS协议来完成此功能。
If a login and password are provided and exist in the user database, then the index action executes. Otherwise, it forwards to the error action (empty) and displays the errorSuccess.php template:
如果登录名和密码被提供并且已经存在于用户数据库里,那么index动作执行。否则,进入error动作(为空)并显示errorSuccess.php模板:
<?php echo '<?' ?>xml version="1.0" encoding="utf-8" ?>
<rsp stat="fail" version="1.0">
<err code="<?php echo $error_code ?>" msg="<?php echo $error_message ?>" />
</rsp>
Of course, you have to set all the views of the api module to a XML content-type, and to deactivate the decorator. This is done by adding a view.yml file in the askeet/apps/frontend/modules/api/config/ directory:
当然,我们必须设置所有的api模块的视图为XML格式,撤消装饰代码。在askeet/apps/frontend/modules/api/config/下填加view.yml文件:
all:
has_layout: off
http_metas:
content-type: text/xml
The reason why the index action returns a forward(’api’, ‘error’) instead of a sfView::ERROR in case of error is because all of the actions of the api module use the same view. Imagine that both our index action and another one, for instance popular, end up with sfView::ERROR: we would have to serve two identical error views (indexError.php and popularError.php) with the same content. The choice of a forward() limits the repetition of code. However, it forces the execution of another action. A similar result can be achieved in a much cheaper way by calling return array(’api’, ‘errorSuccess’); instead: This mentions the view that has to be executed, and bypasses the action completely.
为什么index动作返回用代码forward(’api’, ‘error’),而不用代码sfView::ERROR呢?原因是万一有错误,所有的api模块的动作使用同样的视图。想象我们的index动作和其他的动作,例如popular,用sfView::ERROR结束:那么我们不得不对待两个一样的错误视图(indexError.php和popularError.php)。forward()的选择限制了重复的代码。然而,它强制执行了其他动作。相似的结果可以用很简洁的方法来实现,返回数组(’api’,'errorSuccess’);作为代替:提到的视图必须执行,完全绕过了动作。
API response
API回应
Building an XML response is exactly like building an XHTML page. So none of the following should surprise you now that you have 16 days of symfony behind you.
建立一个XML回应非常像创建一个XHTML页面。你已经有16天symfony经验了,后面没什么难的了。
api/index action
api/index动作
public function executeQuestion()
{
$user = $this->authenticateUser();
if (!$user)
{
$this->error_code = 1;
$this->error_message = 'login failed';
$this->forward(’api’, ‘error’);
}
if (!$this->getRequestParameter(’stripped_title’))
{
$this->error_code = 2;
$this->error_message = ‘The API returns answers to a specific question. Please provide a stripped_title parameter’;
$this->forward(’api’, ‘error’);
}
else
{
// get the question
$question = QuestionPeer::getQuestionFromTitle($this->getRequestParameter(’stripped_title’));
if ($question->getUserId() != $user->getId())
{
$this->error_code = 3;
$this->error_message = ‘You can only use the API for the questions you asked’;
$this->forward(’api’, ‘error’);
}
else
{
// get the answers
$this->answers = $question->getAnswers();
$this->question = $question;
}
}
}
questionSuccess.php template
questionSuccess.php模板
<?php echo '<?' ?>xml version="1.0" encoding="utf-8" ?>
<rsp stat="ok" version="1.0">
<question href="<?php echo url_for(‘@question?stripped_title=’.$question->getStrippedTitle(), true) ?>” time=”<?php echo strftime(’%Y-%m-%dT%H:%M:%SZ’, $question->getCreatedAt(’U')) ?>”>
<title><?php echo $question->getTitle() ?></title>
<tags>
<?php foreach ($sf_user->getSubscriber()->getTagsFor($question) as $tag): ?>
<tag><?php echo $tag ?></tag>
<?php endforeach ?>
</tags>
<answers>
<?php foreach ($answers as $answer): ?>
<answer relevancy=”<?php echo $answer->getRelevancyUpPercent() ?>” time=”<?php echo strftime(’%Y-%m-%dT%H:%M:%SZ’, $answer->getCreatedAt(’U')) ?>”><?php echo $answer->getBody() ?></answer>
<?php endforeach ?>
</answers>
</question>
</rsp>
Add a new routing rule for this API call:
给API增加新路由规则:
api_question:
url: /api/question/:stripped_title
param: { module: api, action: question }
Test it
测试一下
As the response of a REST API is simple XML, you can test it with a simple browser by requiring:
REST API是简单的XML,你可以简单通过浏览器浏览一下:
http://askeet/api/question/what-shall-i-do-tonight-with-my-girlfriend
Integrating an external API
统一外部API
Integrating an external API is not any harder than reading XML in PHP. As there is no immediate interest to integrate an existing external API in askeet, we will describe in a few words how to integrate the askeet API in a foreign website - whether built with symfony or not.
统一外部的API比用PHP读XML简单。askeet没有immediate interest来统一已经存在的外部API,这里我们描述一下外部网站怎么使用askeet的API接口——用symfony写的或者不用symfony写的。
PHP5 comes bundled with SimpleXML, a very easy-to-use set of tools to interpret and loop through an XML document. With SimpleXML, element names are automatically mapped to properties on an object, and this happens recursively. Attributes are mapped to iterator accesses.
PHP5的发行包包含了SimpleXML,解释和循环XML文档,非常简单实用一套工具。用SimpleXML,单元名自动载入了对象,递归进行。属性是迭代访问。
To reconstitute the list of answers to a question provided by the API into a simple page, all it takes is these few lines of PHP:
API返回的问题答案列表进入simple页面,只需要几行PHP代码:
<?php $xml = simplexml_load_file(dirname(__FILE__).'/question.xml') ?>
<h1><?php echo $xml->question->title ?></h1>
<p>Published on <?php echo $xml->question[’time’] ?></p>
<h2>Tags</h2>
<ul>
<?php foreach ($xml->question->tags->tag as $tag): ?>
<li><?php echo $tag ?></li>
<?php endforeach ?>
</ul>
<h2>Answers to this question from askeet users</h2>
<ul>
<?php foreach ($xml->question->answers->answer as $answer): ?>
<li>
<?php echo $answer ?>
<br />
Relevancy: <?php echo $answer[’relevancy’] ?>% - Pulished on <?php echo $answer[’time’] ?>
</li>
<?php endforeach ?>
</ul>
Paypal donation
Paypal赠品
While we talk about external APIs, some of them are very simple to integrate and can bring a lot to your site. The Paypal donation API is a simple chunk of HTML code in which the email of the accountant must be included.
当我们谈论外部API时,有些很容易统一并且可以大量被引入网站。paypal赠品API是简单的HMTL代码,首先需要包含读者账户里的email地址。
Wouldn’t it be a good motivation for askeet users who generously answer questions to be able to receive a small donation from all the happy users who found their answer useful? The ‘Donate’ button could appear on the user profile page, and link to his/her Paypal donation page.
这难道不是个好的动机吗?askeet的某用户回答了问题,收到来另一个正好需要此答案的读者的小赠品。“Donate”这个按扭显示在用户资料页面上,链接到用户的paypal 赠品页。
First, add a has_paypal column to the User table in the schema.xml:
首先,给用户表加个has_paypal字段,修改schema.xml:
<column name=”has_paypal” type=”boolean” default=”0″ />
Rebuild the model, and add to the user/show template the following code:
重建模型,给user/show模板加下面的代码:
<?php if ($subscriber->getHasPaypal()): ?>
<p>If you appreciated this user's contributions, you can grant him a small donation.</p>
<form action="https://www.paypal.com/cgi-bin/webscr” method=”post”>
<input type=”hidden” name=”cmd” value=”_xclick”>
<input type=”hidden” name=”business” value=”<?php echo $subscriber->getEmail() ?>”>
<input type=”hidden” name=”item_name” value=”askeet”>
<input type=”hidden” name=”return” value=”http://www.askeet.com“>
<input type=”hidden” name=”no_shipping” value=”1″>
<input type=”hidden” name=”no_note” value=”1″>
<input type=”hidden” name=”tax” value=”0″>
<input type=”hidden” name=”bn” value=”PP-DonationsBF”>
<input type=”image” src=”http://images.paypal.com/images/x-click-but04.gif” border=”0″ name=”submit” alt=”Donate to this user”>
</form>
<?php endif ?>
Now a user must be given the opportunity to declare a Paypal account linked to his/her email address. It will be a good occasion to allow a user to modify his/her profile. If a logged user displays his/her own profile, an ‘edit profile’ must appear. It will link to a user/edit action, used both to display the form and to handle the form submission. The ‘edit profile’ form will allow the modification of the password and the email address. The nickname, as it is used as a key, cannot be modified. Since you are familiar with symfony by now, the code will not be described here but included in the SVN repository.
现在用户都有机会通过email获得paypal赠品了。这是个好方式,允许用户修改他们的“用户资料”。如果已经登录的用户显示他们的“用户资料页”,“修改”链接是必须显示的。要有user/edit动作,即要显示表单又要处理表单提交的数据。“edit profile”按扭将允许修改密码和电子邮件地址。用户名不可修改。现在你很熟悉symfony了,这些就不在这里写了,代码见SVN库。