REST APIã®äœæã¯ç°¡åãªäœæ¥ã§ã¯ãããŸããã ããããçå£ã«ïŒ APIãæ£ããèšè¿°ãããå Žåã¯ãããèãããã©ã°ããã£ã¹ããAPIããã¢ããå€æããå¿ èŠããããŸãã RESTã¯ãGETãPOSTãPUTãããã³åé€ã ãã§ã¯ãããŸããã å®éã«ã¯ããªãœãŒã¹éã§çžäºäœçšãããå ŽåããªãœãŒã¹ãå¥ã®å ŽæïŒããªãŒå ãªã©ïŒã«ç§»åããå¿ èŠãããå ŽåããŸãã¯ç¹å®ã®ãªãœãŒã¹å€ãååŸããå ŽåããããŸãã
ãã®èšäºã«ã¯ããã®ç®çã§Symfony2 ã FOSRestBundle ã NelmioApiDocBundleããã³Propelã䜿çšããŠããŸããŸãªAPIãµãŒãã¹ãå®è£ ããããšã§åŠãã ãã¹ãŠãå«ãŸããŠããŸãã ããšãã°ããŠãŒã¶ãŒãšé£æºããããã®APIãäœæããŸãã
話ããŸãã...ïŒ
APIã¯ãã¯ã©ã€ã¢ã³ãã¢ããªã±ãŒã·ã§ã³ã«ãã£ãŠäœ¿çšãããŸãã 圌ãã¯ããªãã®APIãµãŒãã¹ã«ã¢ã¯ã»ã¹ããæ¹æ³ãç¥ã£ãŠããå¿ èŠããããé«å質ã®ããã¥ã¡ã³ãã¯ãããéæããããã®è¯ãæ¹æ³ã§ãããããã«ã€ããŠã¯èšäºã®æåŸã§è©³ãã説æããŸãã
ããã«ãã¯ã©ã€ã¢ã³ããšã®éä¿¡æ¹æ³ãç¥ã£ãŠããå¿ èŠããããŸããHTTPãããã³ã«ãã€ãŸãAcceptããããŒã¯ããã«åœ¹ç«ã¡ãŸãã æ¬è³ªçã«ãã¯ã©ã€ã¢ã³ãã¯åä¿¡ãããããŒã¿åœ¢åŒã®ã¿ã€ãã®ããããŒãéä¿¡ããŸãã
ãã ããFOSRestBundleã§ã¯ããã¹ãŠããã§ã«è¡ãããŠããŸãã ãã®éšåã远跡ããå¿ èŠããããŸãããèšå®ã§ãµããŒããã圢åŒã決å®ããå¿ èŠããããŸãã ã»ãšãã©ã®å ŽåãéåžžJSONã䜿çšããŸãããã»ãã³ãã£ã¯ã¹ã®åé¡ãçºçããå Žåã¯ãXMLãéä¿¡ããŸãã ãã®éšåãåŸã§åŒ·èª¿è¡šç€ºãããŸãã
äœãåŸãïŒ
HTTP GETã¡ãœããã¯ã¹ãçã§ãã ã€ãŸãããã®ã¡ãœããã䜿çšããŠããŒã¿ãäœåºŠèŠæ±ããŠããåãããŒã¿ãåä¿¡ããå¿ èŠããããŸãã å€æŽããŠã¯ãããŸããã GETã䜿çšããŠããªãœãŒã¹ïŒã³ã¬ã¯ã·ã§ã³ãŸãã¯åå¥ã®ãªãœãŒã¹ïŒãååŸããŸãã Symfony2ã§ã¯ãã«ãŒãã£ã³ã°ã«ãŒã«ã®èª¬æã¯æ¬¡ã®ããã«ãªããŸãã
# src/Acme/DemoBundle/Resources/config/routing.yml acme_demo_user_all: pattern: /users defaults: { _controller: AcmeDemoBundle:User:all, _format: ~ } requirements: _method: GET acme_demo_user_get: pattern: /users/{id} defaults: { _controller: AcmeDemoBundle:User:get, _format: ~ } requirements: _method: GET id: "\d+"
UserControllerã¯ã©ã¹ã«ã¯æ¬¡ã®ã³ãŒããå«ãŸããŸãã
<?php namespace Acme\DemoBundle\Controller; use Acme\DemoBundle\Model\User; use Acme\DemoBundle\Model\UserQuery; use FOS\RestBundle\Controller\Annotations as Rest; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class UserController { /** * @Rest\View */ public function allAction() { $users = UserQuery::create()->find(); return array('users' => $users); } /** * @Rest\View */ public function getAction($id) { $user = UserQuery::create()->findPk($id); if (!$user instanceof User) { throw new NotFoundHttpException('User not found'); } return array('user' => $user); } }
get *ïŒïŒã¡ãœããã§ãã©ã¡ãŒã¿ãŒã³ã³ããŒã¿ãŒã䜿çšãã代ããã«ãåžžã«èªåã§ãªããžã§ã¯ããååŸããå¿ èŠããããŸãã åŸã§èª¬æããŸãããä»ã®ãšããã¯ãç§ãä¿¡ããŠãã ãããæ¬åœã«è¯ãã§ãã
ã¹ããŒã¿ã¹ã³ãŒãã¯ã¯ã©ã€ã¢ã³ãã«ãšã£ãŠéèŠã§ãããããã£ãŠããŠãŒã¶ãŒãååšããªãå Žåã¯ã NotFoundHttpExceptionäŸå€ã䜿çšããŸããããã«ãããã¹ããŒã¿ã¹ã³ãŒã404ã®å¿çãè¿ãããŸãã
View泚éã䜿çšããŠããŠãŒã¶ãŒãªããžã§ã¯ããç®çã®åœ¢åŒã§è¡šç€ºããŸãããŠãŒã¶ãŒã¯ãããAcceptããããŒã§æå®ããŸãã 泚éã«ãšã€ãªã¢ã¹ïŒRestïŒã䜿çšããããšã¯ãåŸã§èª¬æããViewãªããžã§ã¯ããšã®ç«¶åãåé¿ããã®ã«åœ¹ç«ã€ããªãã¯ã§ãã ç°¡åã«èšãã°ã泚éã¯ãã®ã¯ã©ã¹ãåç §ããŸãã 泚éã䜿çšãããã©ããã«ããããããããã¯å¥œã¿ã®åé¡ã§ãã
ãããŠæåŸã«ãallActionïŒïŒã¡ãœããã getActionãšåãåäœã§ãããŠãŒã¶ãŒã®éžæãååŸããŠè¿ãã ãã§ãã
ãŠãŒã¶ãŒãªããžã§ã¯ãã«ã¯ãidãã¡ãŒã«ããŠãŒã¶ãŒåããã¹ã¯ãŒãã®4ã€ã®ããããã£ããããŸãã ããããåžžèã§ã¯ãAPIãä»ããŠç¡æã§ã¢ã¯ã»ã¹ããããã®ãã¹ã¯ãŒãããŠãŒã¶ãŒã«äžããããšã¯ã§ããŸããã ãªããžã§ã¯ããã·ãªã¢ã«åãããšãã«ãã®ããããã£ãé€å€ããæãç°¡åãªæ¹æ³ã¯ãã·ãªã¢ã©ã€ã¶ãŒãèšå®ããããšã§ãã YAML圢åŒã®ã»ããã¢ããäŸïŒ
# In Propel, the most part of the code is located in base classes # src/Acme/DemoBundle/Resources/config/serializer/Model.om.BaseUser.yml Acme\DemoBundle\Model\om\BaseUser: exclusion_policy: ALL properties: id: expose: true username: expose: true email: expose: true
ããã©ã«ãã§ã¯ããªããžã§ã¯ãã®ãã¹ãŠã®ããããã£ãé€å€ããããšããå§ãããŸããå¿ èŠãªããããã£ã¯æ瀺çã«è¿œå ããå¿ èŠããããŸãã ããã«ããã倧ããªãªããžã§ã¯ãã®æè»æ§ãåäžããŸãã ããã¯4ã€ã®ããããã£ã§ã¯ããŸãæå³ããããŸããããããã§ããã®æŠç¥ãå®ãããšããŸãããããæçµçã«ãã¡ã€ã¢ãŠã©ãŒã«ãæ§æããæ¹æ³ã§ãã
ãã®çµæã次ã®JSONå¿çãååŸããŸãã
{ "user": { "id": 999, "username": "xxxx", "email": "xxxx@example.org" } }
ã·ã³ãã«ã§ããïŒ ãã ãããããããŠãŒã¶ãŒãäœæãå€æŽããŸãã¯åé€ããå¿ èŠããããããã¯æ¬¡ã®ç« ã®ãããã¯ã«ãªããŸãã
æçš¿ãã
ãŠãŒã¶ãŒã®äœæã«ã¯ãHTTP POSTã¡ãœããã®äœ¿çšãå«ãŸããŸãã ããããã©ã®ããã«ããŒã¿ãåãåããŸããïŒ ããããã©ã®ããã«ç¢ºèªããŸããïŒ ãããŠãæ°ãããªããžã§ã¯ããã©ã®ããã«äœæããŸããïŒ ãããã®3ã€ã®è³ªåã«ã¯ãè€æ°ã®åçãŸãã¯æŠç¥ããããŸãã
éã·ãªã¢ã«åã¡ã«ããºã ã䜿çšããŠãã·ãªã¢ã«åãããå ¥åãããã©ãŒã ãªããžã§ã¯ããäœæã§ããŸãã ãã³ãžã£ãã³ãšããååã®ç·ã¯ã ãã©ãŒã è±å¡©è£ 眮ã«åãçµãã§ããŸãã ãã®æ¹æ³ã¯ãSerializerã³ã³ããŒãã³ãã䜿çšããå Žåãšãããã«ç°ãªããŸãã ãããç°¡åã«æããŸãã
Symfonyã®ã¯ãŒã«ãªãã©ãŒã ã³ã³ããŒãã³ãã䜿çšããŠãäžåºŠã«ãã¹ãŠãå®è¡ããŸãã æ°ãããŠãŒã¶ãŒãäœæãããã©ãŒã ã¯ã©ã¹ãäœæããŸãããã PropelBundleã䜿çšãããšã propelïŒformïŒgenerateã³ãã³ãcommandã䜿çšã§ããŸãïŒ
php app/console propel:form:generate @AcmeDemoBundle User
ãã®ã³ãã³ãã¯ã次ã®ãã©ãŒã ã¯ã©ã¹ãäœæããŸãã
<?php namespace Acme\DemoBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class UserType extends AbstractType { /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('username'); $builder->add('email', 'email'); $builder->add('password', 'password'); } /** * {@inheritdoc} */ public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Acme\DemoBundle\Model\User', 'csrf_protection' => false, )); } /** * {@inheritdoc} */ public function getName() { return 'user'; } }
ç§ã¯èªåã®æã§åŸ®èª¿æŽããå¿ èŠããããŸããïŒé»åã¡ãŒã«ãšãã¹ã¯ãŒãã®çš®é¡ããããŠCSRFä¿è·ããªãã«ããŸããã REST APIã§ã¯ãã»ãšãã©ã®å ŽåãOAuthãªã©ã®ã»ãã¥ãªãã£ã¬ã€ã€ãŒã䜿çšããŸãã RESTã³ã³ããã¹ãã§CSRFãä¿è·ããŠãæå³ããããŸããã
ããã§ãæ€èšŒã«ãŒã«ãè¿œå ããå¿ èŠããããŸãã é©åãªã³ã³ããŒãã³ãã®ãããã§ãããã¯ç°¡åã«ãªããŸãã ãã¹ãŠã®çä¿¡ããŒã¿ãå®å šãªæ¹æ³ã§ç°¡åã«ãã§ãã¯ã§ããã®ã§ããã®ã³ã³ããŒãã³ããæ¬åœã«æ°ã«å ¥ã£ãŠããŸãã
ç§ãã¡ã®ã±ãŒã¹ã«æ»ã£ãŠãYAMLã§æ€èšŒã«ãŒã«ã説æããã®ã«æ £ããŠããŸããã誰ãããªãã®éžæãå¶éããŸããã 以äžã«äŸã瀺ããŸãã
# src/Acme/DemoBundle/Resources/config/validation.yml Acme\DemoBundle\Model\User: getters: username: - NotBlank: email: - NotBlank: - Email: password: - NotBlank:
ã³ã³ãããŒã©ãŒã§ã¡ãœãããæžããŸãããïŒ
<?php // ... public function newAction() { return $this->processForm(new User()); }
ããäžã€ã®ãã³ãã ãã©ãŒã ã®åŠçã«ã¯ãåžžã«å¥ã®æ¹æ³ã䜿çšããŠãã ããã ããããããªãã¯èªåã«æè¬ããŸãã processFormïŒïŒã¡ãœããã¯æ¬¡ã®ããã«ãªããŸãã
// ... private function processForm(User $user) { $statusCode = $user->isNew() ? 201 : 204; $form = $this->createForm(new UserType(), $user); $form->bind($this->getRequest()); if ($form->isValid()) { $user->save(); $response = new Response(); $response->setStatusCode($statusCode); $response->headers->set('Location', $this->generateUrl( 'acme_demo_user_get', array('id' => $user->getId()), true // absolute ) ); return $response; } return View::create($form, 400); }
èŠããã«ããã©ãŒã ãäœæããåä¿¡ããŒã¿ãããã«ãã€ã³ããããã¹ãŠã®ããŒã¿ãæå¹ã§ããã°ããŠãŒã¶ãŒãä¿åããŠå¿çãè¿ããŸãã åé¡ãçºçããå Žåã¯ããã©ãŒã ãšãšãã«400ã³ãŒããè¿ãããšãã§ããŸãã ãã©ãŒã ã¯ã©ã¹ã®ã€ã³ã¹ã¿ã³ã¹ã¯ããšã©ãŒã¡ãã»ãŒãžã衚瀺ããããã«ã·ãªã¢ã«åãããŸãã ããšãã°ã次ã®ãããªãšã©ãŒã¬ã¹ãã³ã¹ã衚瀺ãããå ŽåããããŸãã
{ "children": { "username": { "errors": [ "This value should not be blank." ] } } }
泚ïŒããã§è¡šç€ºãããViewã¯ã©ã¹ã¯ãã¢ãããŒã·ã§ã³ã§äœ¿çšãããã®ãšåãã§ã¯ãªãããããšã€ãªã¢ã¹ã䜿çšããŸããã ãã®ã¯ã©ã¹ã®è©³çŽ°ã«ã€ããŠã¯ãFOSRestBundleããã¥ã¡ã³ãã®ãView Layerãã®ç« ãåç §ããŠãã ããã
ããã§ã¯ããã©ãŒã ã®ååãæž¡ãããšãéèŠã§ãã éåžžã顧客ã¯æ¬¡ã®ãããªãã®ãéä¿¡ããŸãã
{ "user": { "username": "foo", "email": "foo@example.org", "password": "hahaha" } }
curlã§ãã®ã¡ãœãããåŒã³åºãããšãã§ããŸãïŒ
curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"user":{"username":"foo", "email": "foo@example.org", "password": "hahaha"}}' http://example.com/users
FOSRestBundleèšå®ã®body_listenerãã©ã¡ãŒã¿ãŒã«trueãèšå®ããŠãã ããã ãã®ãã©ã¡ãŒã¿ãŒã䜿çšãããšãJSONãXMLãªã©ã®åœ¢åŒã§ããŒã¿ãåä¿¡ã§ããŸãã ç¹°ãè¿ããŸããããã¹ãŠãããã«äœ¿çšã§ããŸãã
åã«èšã£ãããã«ããã¹ãŠãããŸãããã°ããŠãŒã¶ãŒãä¿åãïŒ$ user-> Propelã§ä¿åïŒããããŠçããè¿ããŸãã
ãªãœãŒã¹ãäœæãããããšã瀺ãã¹ããŒã¿ã¹ã³ãŒã201ãéä¿¡ããå¿ èŠããããŸãã ãã®ã¡ãœããã«ã¯Viewã¢ãããŒã·ã§ã³ã䜿çšããŠããªãããšã«æ³šæããŠãã ããã
ããããã³ãŒããããèŠããšãç§ãå¥åŠãªããšãããããšã«æ°ä»ããããããŸããã å®éããªãœãŒã¹ãäœæãããšããç¹å®ã®æ å ±ãã€ãŸããã®ãªãœãŒã¹ãžã®ã¢ã¯ã»ã¹æ¹æ³ãè¿ãå¿ èŠããããŸãã ã€ãŸããURIãè¿ãå¿ èŠããããŸãã HTTPä»æ§ã«ã¯ã LocationããããŒã䜿çšããå¿ èŠããããšæžãããŠããŸãã ãã ãããŠãŒã¶ãŒIDãªã©ã®æ å ±ãååŸããããã«å¥ã®èŠæ±ãè¡ããããšã¯æããªãã§ãããïŒãšã«ããæ®ãã®æ å ±ã¯æ¢ã«ãããŸãïŒã ãããŠãç§ã®äž»ãªæŠå¿µã衚瀺ãããŸãïŒãã©ã°ããã£ã¹ããŸãã¯ããã¢ãã¯ïŒ
ç¥ã£ãŠãïŒ ç§ã¯ããã¢ãã¯ãªã¢ãããŒãã奜ã¿ãç§ã¯ä»æ§ã«åŸãã LocationããããŒã®ã¿ãè¿ããŸãïŒ
Location: http://example.com/users/999
ã¯ã©ã€ã¢ã³ããšããŠJavaScriptãã¬ãŒã ã¯ãŒã¯Backbone.jsã䜿çšããå Žåãæ£ããAPIããµããŒãããŠããªãããããã®äžéšãæžãæããããªãããããã¹ãŠã«å ããŠIdãè¿ããŸãã å®çšäž»çŸ©è ã§ããããšã¯ãŸã ããã»ã©æªãã¯ãããŸããã
ãã®ã¢ã¯ã·ã§ã³ã®ã«ãŒãã£ã³ã°ã«ãŒã«ãå¿ããã«è¿œå ããŠãã ããã ãªãœãŒã¹ã®äœæã¯ã³ã¬ã¯ã·ã§ã³ãžã®POSTèŠæ±ãªã®ã§ãæ°ããã«ãŒã«ãè¿œå ããŸãã
acme_demo_user_new: pattern: /users defaults: { _controller: AcmeDemoBundle:User:new, _format: ~ } requirements: _method: POST
æ°ãããªãœãŒã¹ã®äœææ¹æ³ãç¥ã£ãŠããã°ããããå€æŽããã®ã¯éåžžã«ç°¡åã§ãã
PUT vs PATCHããã¡ã€ãïŒ
ç¹ã«HTTP PUTã¡ãœããã䜿çšããŠããå ŽåããªãœãŒã¹ãå€æŽãããšããªãœãŒã¹ã眮ãæããããŸãã ãŸãããªãœãŒã¹éã®å·®ç°ãååŸããŠå ã®ãªãœãŒã¹ã«ããããé©çšããPATCHã¡ãœããããããŸããã€ãŸãã éšåæŽæ°ãå®è¡ããŸãã
åã«è¡ã£ãäœæ¥ã®ãããã§ããªãœãŒã¹ã®å€æŽã¯ããªãç°¡åã«å®è£ ã§ããŸãã ã³ã³ãããŒã©ã«æ°ããã¡ãœãããèšè¿°ããæ°ããã«ãŒãã£ã³ã°ã«ãŒã«ãè¿œå ããå¿ èŠããããŸãã ããã§ã¯ããŠãŒã¶ãŒã®ãªããžã§ã¯ããååŸããããã«ãã©ã¡ãŒã¿ãŒã³ã³ããŒã¿ãŒã«äŸåã§ããŸãã ãã®ãããªãŠãŒã¶ãŒãååšããªãå Žåããã©ã¡ãŒã¿ãŒã³ã³ããŒã¿ãŒã¯äŸå€ãã¹ããŒãããã®äŸå€ã¯ã¹ããŒã¿ã¹ã³ãŒã404ã®å¿çã«å€æãããŸãã
<?php // ... public function editAction(User $user) { return $this->processForm($user); }
ãªãœãŒã¹ãå€æŽãããšããããšã¯ããã®ãªãœãŒã¹ã«é¢ãããã¹ãŠããã§ã«ç¥ã£ãŠãããããPUTãªã¯ãšã¹ãã§ãã®ãªãœãŒã¹ã®URIã®ã¿ãè¿ãããšãã§ãããšããããšã§ãã
acme_demo_user_edit: pattern: /users/{id} defaults: { _controller: AcmeDemoBundle:User:edit, _format: ~ } requirements: _method: PUT
ãããŠããã ãã§ãïŒ ãªãœãŒã¹ã®åé€ã¯ã©ãã§ããïŒ
åé€
ãªãœãŒã¹ã®åé€ã¯éåžžã«ç°¡åã§ãã ã«ãŒãã£ã³ã°ã«ãŒã«ãè¿œå ããŸãã
acme_demo_user_delete: pattern: /users/{id} defaults: { _controller: AcmeDemoBundle:User:remove, _format: ~ } requirements: _method: DELETE
ãããŠãçãã¡ãœãããæžããŸãïŒ
<?php // ... /** * @Rest\View(statusCode=204) */ public function removeAction(User $user) { $user->delete(); }
æ°åè¡ã®ã³ãŒããæžããã ãã§ãCRUDæäœãå®å šã«å®è£ ããå®å šã«æ©èœããAPIãå®è£ ããŸããã ã§ã¯ããŠãŒã¶ãŒã€ã³ã¿ã©ã¯ã·ã§ã³ã®è¿œå ã«ã€ããŠã¯ã©ãã§ããããïŒ äŸãã°ãåæ ïŒ
RESTãä»ããŠç¹å®ã®ãŠãŒã¶ãŒã®ãã¬ã³ããªã¹ããååŸããæ¹æ³ å人ãç¹å®ã®ãŠãŒã¶ãŒã«å±ãããŠãŒã¶ãŒã®ã³ã¬ã¯ã·ã§ã³ãšèããå¿ èŠããããŸãã ãã®çžäºäœçšãå®è£ ããŸãããã
åæ ã¢ã«ãŽãªãºã
æåã«ãæ°ããã«ãŒãã£ã³ã°ã«ãŒã«ãäœæããå¿ èŠããããŸãã ç§ãã¡ã¯åéããŠãŒã¶ãŒã®ã³ã¬ã¯ã·ã§ã³ãšèããŠããã®ã§ããªãœãŒã¹ããçŽæ¥ååŸããŸãã
acme_demo_user_get_friends: pattern: /users/{id}/friends defaults: { _controller: AcmeDemoBundle:User:getFriends, _format: ~ } requirements: _method: GET
ãã®ã¢ã¯ã·ã§ã³ã¯ãã³ã³ãããŒã©ãŒã§ã¯æ¬¡ã®ããã«ãªããŸãã
<?php // ... public function getFriendsAction(User $user) { return array('friends' => $user->getFriends()); }
以äžã§ãã 次ã«ãå¥ã®ãŠãŒã¶ãŒã®åéã«ãªãããã»ã¹ã説æããæ¹æ³ã«ã€ããŠèããŠã¿ãŸãããã ãããRESTã§ã©ã®ããã«ç®¡çããŸããïŒ äœãäœæããªããããåéã®ã³ã¬ã¯ã·ã§ã³ã«POSTã䜿çšããããšã¯ã§ããŸããã äž¡æ¹ã®ãŠãŒã¶ãŒããã§ã«ååšããŸãã ã³ã¬ã¯ã·ã§ã³å šäœãæ¬åœã«çœ®ãæããããªããããPUTã¡ãœããã䜿çšããããšã¯ã§ããŸããã ããã¯æ¬åœã«ç§ãã¡ãå°æãããããšãã§ããŸã...
ãã ããHTTPãããã³ã«ä»æ§ã«ã¯ãåé¡ã解決ããLINKã¡ãœãããèšè¿°ãããŠããŸãã ããã¯èšããŸãïŒ
LINKã¡ãœããã¯ãRequest-URIã§æå®ãããæ¢åã®ãªãœãŒã¹ãšä»ã®æ¢åã®ãªãœãŒã¹ãšã®éã«1ã€ä»¥äžã®é¢ä¿ã確ç«ããŸãã
ããããŸãã«ç§ãã¡ãå¿ èŠãšãããã®ã§ãã 2ã€ã®ãªãœãŒã¹ããªã³ã¯ãããã®ã§ãAPIãµãŒãã¹ãäœæãããšãã«ãªãœãŒã¹ãå¿ããŠã¯ãªããŸããã ã§ã¯ãsymfony2ã§ãããè¡ãæ¹æ³ã¯ïŒ
ç§ã®æ¹æ³ã¯ãèŠæ±ãªã¹ããŒã䜿çšããããšã«åºã¥ããŠããŸãã ã¯ã©ã€ã¢ã³ãã¯ããªãœãŒã¹ã®LINKèŠæ±ãéä¿¡ããå°ãªããšã1ã€ã®ãªã³ã¯ããããŒãéä¿¡ããŸãã
LINK /users/1 Link: <http://example.com/users/2>; rel="friend" Link: <http://example.com/users/3>; rel="friend"
ã¯ãšãªãªã¹ãã䜿çšãããšãã¡ãœããã§ã¯ãªãŒã³ãªå ¥åãååŸã§ãããããéåžžã«äŸ¿å©ãªãªãã·ã§ã³ã§ãã æçµçã«ããã®ã¡ãœããã®ç®çã¯ãªããžã§ã¯ãããªã³ã¯ããããšã§ãããã³ã³ãããŒã©ãŒã§URIãæäœããããããŸããã ãã®åã«ãã¹ãŠã®å€æãå®è¡ããå¿ èŠããããŸãã
ãªã¯ãšã¹ããªã¹ããŒã¯ããããã¹ãŠã®ãªã³ã¯ããããŒãååŸããããããSymfony2 RouterMatcherã³ã³ããŒãã³ãã«äœ¿çšããŠã³ã³ãããŒã©ãŒãšã¡ãœããã®ååãååŸããŸãã ãŸãããã©ã¡ãŒã¿ãæºåããŸãã
ã€ãŸããã³ã³ãããŒã©ãŒãäœæããå¿ èŠãªãã©ã¡ãŒã¿ãŒã䜿çšããŠé©åãªã¡ãœãããåŒã³åºãããã«å¿ èŠãªãã¹ãŠã®æ å ±ãå«ãŸããŠããŸãã ãã®äŸã§ã¯ãUserControllerã®getUserïŒïŒã¡ãœãããåLinkããããŒã«å¯ŸããŠåŒã³åºãããŸãã ãããããã©ã¡ãŒã¿ãŒã³ã³ããŒã¿ãŒã䜿çšããªãã£ãçç±ã§ããããã«ãããåŒæ°ã®å€ãšããŠidã䜿çšã§ããããã«ãªãããã®ãããªãœãŒã¹ãååŸã§ããŸããã ç§ã¯ããã€ãã®ä»®å®ãããŸããïŒ
- ãŠãŒã¶ãŒãååšããªãå ŽåãäŸå€ãè¿ãããŸãã
- View泚éã䜿çšãããããæ»ãå€ãšããŠé åãååŸããŸãã
ãªãœãŒã¹ãªããžã§ã¯ããååŸããããããããèŠæ±å±æ§ãšããŠé 眮ãããªã¹ããŒã®å Žåãäœæ¥ã¯å®äºããŸãã ã³ãŒãã¯æ¬¡ã®ãšããã§ãã
<?php namespace Acme\DemoBundle\EventListener; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\HttpFoundation\Request; class LinkRequestListener { /** * @var ControllerResolverInterface */ private $resolver; private $urlMatcher; /** * @param ControllerResolverInterface $controllerResolver The 'controller_resolver' service * @param UrlMatcherInterface $urlMatcher The 'router' service */ public function __construct(ControllerResolverInterface $controllerResolver, UrlMatcherInterface $urlMatcher) { $this->resolver = $controllerResolver; $this->urlMatcher = $urlMatcher; } public function onKernelRequest(GetResponseEvent $event) { if (!$event->getRequest()->headers->has('link')) { return; } $links = array(); $header = $event->getRequest()->headers->get('link'); /* * , * . * * Link * http://tools.ietf.org/html/rfc2068#section-19.6.2.4 */ while (preg_match('/^((?:[^"]|"[^"]*")*?),/', $header, $matches)) { $header = trim(substr($header, strlen($matches[0]))); $links[] = $matches[1]; } if ($header) { $links[] = $header; } $requestMethod = $this->urlMatcher->getContext()->getMethod(); // GET // , (LINK/UNLINK) $this->urlMatcher->getContext()->setMethod('GET'); // $stubRequest = new Request(); foreach ($links as $idx => $link) { $linkParams = explode(';', trim($link)); $resource = array_shift($linkParams); $resource = preg_replace('/<|>/', '', $resource); try { $route = $this->urlMatcher->match($resource); } catch (\Exception $e) { // // Link continue; } $stubRequest->attributes->replace($route); if (false === $controller = $this->resolver->getController($stubRequest)) { continue; } $arguments = $this->resolver->getArguments($stubRequest, $controller); try { $result = call_user_func_array($controller, $arguments); // if (!is_array($result)) { continue; } // $links[$idx] = current($result); } catch (\Exception $e) { continue; } } $event->getRequest()->attributes->set('link', $links); $this->urlMatcher->getContext()->setMethod($requestMethod); } }
ããã§ãã«ãŒãã£ã³ã°ã«ãŒã«ãäœæã§ããŸãã
acme_demo_user_link: pattern: /users/{id} defaults: { _controller: AcmeDemoBundle:User:link, _format: ~ } requirements: _method: LINK
ãããŠãã¢ã¯ã·ã§ã³ã®ã³ãŒãã¯æ¬¡ã®ããã«ãªããŸãã
<?php // ... /** * @Rest\View(statusCode=204) */ public function linkAction(User $user, Request $request) { if (!$request->attributes->has('link')) { throw new HttpException(400); } foreach ($request->headers->get('Link') as $u) { if (!$u instanceof User) { throw new NotFoundHttpException('Invalid resource'); } if ($user->hasFriend($u)) { throw new HttpException(409, 'Users are already friends'); } $user->addFriend($u); } $user->save(); }
ãŠãŒã¶ãŒããã§ã«å人ã§ããå Žåãã¹ããŒã¿ã¹ã³ãŒã409ã®å¿çãåãåããŸããããã¯ã競åãçºçããããšãæå³ããŸãã èŠæ±ã«ãªã³ã¯ããããŒãå«ãŸããŠããªãå Žåãããã¯äžé©åãªèŠæ±ïŒ400ïŒã§ãã
å人ããåé€ããå Žåãåæ§ã§ãã ããã§ã®ã¿ã UNLINKã¡ãœããã䜿çšããŸãã
ãããŠæåŸã«ã PATCHã¡ãœããã«ã€ããŠã¯èª¬æããŸããã§ããã ã€ãŸãããã®æ¹æ³ã®ã·ããªãªã¯ã©ããªãã®ã§ããããïŒ çãã¯ãéšåçãªæŽæ°ããŸãã¯å®å šã§ãªããä¿¡é Œã§ããªãããŸãã¯ã¹ãçã§ãªãã¡ãœããã§ãã éæšæºã®ã¡ãœããããããã©ã®ã¡ãœããã䜿çšãããããããªãå Žåã¯ãPATCHãæé©ã§ãã
ãŠãŒã¶ãŒããµãŒãããŒãã£ã®ã¯ã©ã€ã¢ã³ããä»ããŠã¡ãŒã«ãå€æŽã§ãããšä»®å®ããŸãã ãã®ã¯ã©ã€ã¢ã³ãã¯2段éã®ããã»ã¹ã䜿çšããŸãã ãŠãŒã¶ãŒã¯èªåã®é»åã¡ãŒã«ã¢ãã¬ã¹ãå€æŽããèš±å¯ãèŠæ±ãããªã³ã¯ãèšèŒãããé»åã¡ãŒã«ãåä¿¡ãããããã¯ãªãã¯ããŠãå€æŽã®èš±å¯ãååŸããŸãã æåã®ã¹ããããã¹ãããããŠã2çªç®ã«çŠç¹ãåãããŸãã ãŠãŒã¶ãŒã¯æ°ããã¡ãŒã«ãã¯ã©ã€ã¢ã³ãã«éä¿¡ããã¯ã©ã€ã¢ã³ãã¯APIã¡ãœãããåŒã³åºãå¿ èŠããããŸãã ã¯ã©ã€ã¢ã³ãããªãœãŒã¹ãåãåããããã眮ãæããããè³¢æã§PATCHã¡ãœãããæäŸããŸãã
PATCHã®äžçã«èª²ã
ãŸããæ°ããã«ãŒãã£ã³ã°ã«ãŒã«ãå®çŸ©ããŸãã
acme_demo_user_patch: pattern: /users/{id} defaults: { _controller: AcmeDemoBundle:User:patch, _format: ~ } requirements: _method: PATCH
ãããŠãã³ã³ãããŒã©ã«å®å šãªpatchActionïŒïŒã¡ãœãããæžãããã«ãæ³ååãæå¹ã«ããŸãã ãŠãŒã¹ã±ãŒã¹ãèŠãŠã¿ãŸãããã ã¯ã©ã€ã¢ã³ãã¯ããªãœãŒã¹ã®1ã€ä»¥äžã®å€ãéä¿¡ã§ããŸãã ãã¹ãŠã®åªããã«ãŒãã¹ããè¡ãããã«ããã¯ã€ããªã¹ãã«äŸåããŠå€§éã®å²ãåœãŠãé²ãããšã¯çŽ æŽãããããšã§ã...
å ¥åãã©ã¡ãŒã¿ããã£ã«ã¿ãªã³ã°ããŸãããïŒ
<?php $parameters = array(); foreach ($request->request->all() as $k => $v) { // if (in_array($k, array('email'))) { $parameters[$k] = $v; } }
çä¿¡ãã©ã¡ãŒã¿ãŒããã£ã«ã¿ãŒåŠçãããšããã«ãå¿ èŠãªãã©ã¡ãŒã¿ãŒãæ£ç¢ºã«ååŸããŸããã äœãåä¿¡ããªãã£ãå Žåãããã¯äžé©åãªãªã¯ãšã¹ãã§ãããã¹ããŒã¿ã¹ã³ãŒã400ã®ã¬ã¹ãã³ã¹ãè¿ãå¿ èŠããããŸãã
ãã¹ãŠãé 調ã§ããã°ãæ°ããå€ããªãœãŒã¹ã«å²ãåœãŠãããšãã§ããŸãã ããåŸ ã£ãŠ...ãããïŒ æåã«ãšã³ãã£ãã£ã確èªãããã¹ãŠã®ããŒã¿ãæå¹ãªå Žåã«ã®ã¿ä¿åããå¿ èŠããããŸãã
ãã®ã¢ã¯ã·ã§ã³ã®ã³ãŒãã¯æ¬¡ã®ããã«ãªããŸãã
<?php // .... public function patchAction(User $user, Request $request) { $parameters = array(); foreach ($request->request->all() as $k => $v) { // whitelist if (in_array($k, array('email'))) { $parameters[$k] = $v; } } if (0 === count($parameters)) { return View::create( array('errors' => array('Invalid parameters.')), 400 ); } $user->fromArray($parameters); $errors = $this->get('validator')->validate($user); if (0 < count($errors)) { return View::create(array('errors' => $errors), 400); } $user->save(); $response = new Response(); $response->setStatusCode(204); $response->headers->set('Location', $this->generateUrl( 'acme_demo_user_get', array('id' => $user->getId()), true // absolute ) ); return $response; }
ã³ãŒãã¯ãšãŠãç°¡åã§ãããïŒ ãã€ãã®ããã«ããªãœãŒã¹ãäœæãŸãã¯æŽæ°ãããšãã¯ã2xxã¹ããŒã¿ã¹ã³ãŒããšLocationããããŒãå«ãå¿çãéä¿¡ããå¿ èŠããããŸãã ããã«ã¯ã³ã³ãã³ãããªããäœãäœæããŠããªããããã³ãŒã204ãéä¿¡ããŸãã
ãããŠä»ãèšç»ã¯äœã§ããïŒ GETãPOSTãPUTãDELETEãPATCHãLINK ãããã³UNLINKã¡ãœããããã§ã«äœ¿çšããŠããŸã ã ãŠãŒã¶ãŒãäœæãåä¿¡ãå€æŽãåé€ãããã«ã¯éšåçã«æŽæ°ããããšãã§ããŸãã ãã¹ãŠã®ãŠãŒã¶ãŒã®ãªã¹ããååŸãããŠãŒã¶ãŒéã®åæ ã確ç«ã§ããŸãã ãŠãŒã¶ãŒããŒã¿ãå€æŽããå¿ èŠãããå Žåã PATCHã¡ãœãããå®å šã«äœ¿çšã§ããããšãããã£ãŠããŸãã
å®éã Richardson Maturityã¢ãã«ã«é¢ããŠã¯ ã第2ã¬ãã«ã®ã¿ãåãäžããŸããã ããã§ã¯ã HATEOASãèŠãŠã第3ã¬ãã«ã®ããã¯ã解é€ããŸãããïŒ
å«ããªäººã¯ïŒ
HATEOASã¯æãã¿ãšã¯äœã®é¢ä¿ããããŸããããããªããå®éçãªããã°ã©ããŒã§ãããšèãããªãããã®ã¢ãããŒããå«ãããšãã§ããŸãã ãã®é åèªã¯ãHypermedia As The Engine Of Application Stateã®ç¥ã§ãã ç§ã«ãšã£ãŠãããã¯ã»ãã³ãã£ã¯ã¹ãAPIãµãŒãã¹ã«è¿œå ããããšãšèŠãªãããŸãã
ãã®èšäºã®ååã§ãã¯ã©ã€ã¢ã³ããšAPIã®éã§æ å ±ã亀æããããã«äœ¿çšããã圢åŒã«ã€ããŠèª¬æããŸããã HATEOASã®ååã«åŸãããšã決ããå ŽåãJSONã¯æè¯ã®éžæè¢ã§ã¯ãããŸãããã ãã®åé¡ã®è§£æ±ºçãæäŸãã人ãããŸã ã
ãŠãŒã¶ãŒã®è¡šçŸãXMLã«å€æããŸãã
<user> <id>999</id> <username>xxxx</username> <email>xxxx@example.org</email> </user>
ããã¯ãã¯ã©ã€ã¢ã³ããXMLãèŠæ±ããå Žåã®getã¡ãœããã®åºåã§ãã HATEOASããã¯äœããããŸããã æåã®ã¹ãããã¯ããªã³ã¯ãè¿œå ããããšã§ãã
<user> <id>999</id> <username>xxxx</username> <email>xxxx@example.org</email> <link href="http://example.com/users/999" rel="self" /> </user>
ããã¯ç°¡åã§ãããŒã¿ãåãåã£ããŠãŒã¶ãŒãåç §ãããªã³ã¯ãè¿œå ããã ãã§ãã ãã ãããŠãŒã¶ãŒã³ã¬ã¯ã·ã§ã³ãããŒãžåå²ãããŠããå Žåã¯ã次ãååŸã§ããŸãã
<users> <user> <id>999</id> <username>xxxx</username> <email>xxxx@example.org</email> <link href="http://example.com/users/999" rel="self" /> <link href="http://example.com/users/999/friends" rel="friends" /> </user> <user> <id>123</id> <username>foobar</username> <email>foobar@example.org</email> <link href="http://example.com/users/123" rel="self" /> <link href="http://example.com/users/123/friends" rel="friends" /> </user> <link href="http://example.com/users?page=1" rel="prev" /> <link href="http://example.com/users?page=2" rel="self" /> <link href="http://example.com/users?page=3" rel="next" /> </users>
ããã§ãã¯ã©ã€ã¢ã³ãã¯ã³ã¬ã¯ã·ã§ã³ã衚瀺ããæ¹æ³ãããŒãžãããã²ãŒãããæ¹æ³ããŠãŒã¶ãŒãå人ãååŸããæ¹æ³ãç¥ã£ãŠããŸãã
次ã®ã¹ãããã§ã¯ã質åãžã®åçãšããŠã¡ãã£ã¢ã¿ã€ããè¿œå ããŸãããªãœãŒã¹ãšã¯äœã§ããïŒããã«ã¯äœãå«ãŸããŸããããã®ãããªãªãœãŒã¹ãäœæããã«ã¯äœãå¿ èŠã§ããïŒ
ãã®ããŒãã§ã¯ãç¬èªã®ã³ã³ãã³ãã¿ã€ãã玹ä»ããŸãã
Content-Type: application/vnd.yourname.something+xml
ãŠãŒã¶ãŒã¯ã次ã®ã¿ã€ãã®ã³ã³ãã³ããåç §ããããã«ãªããŸããïŒapplication / vnd.acme.user + xmlã
<user> <id>999</id> <username>xxxx</username> <email>xxxx@example.org</email> <link href="http://example.com/users/999" rel="self" /> <link rel="friends" type="application/vnd.acme.user+xml" href="http://example.com/users/999/friends" /> </user>
æåŸã«ãªããŸãããã3ã€ã®ç°ãªãæ¹æ³ã§APIãµãŒãã¹ã«ããŒãžã§ã³ç®¡çãè¿œå ã§ããŸããç°¡åãªæ¹æ³ã¯ãURIã«ããŒãžã§ã³çªå·ãè¿œå ããããšã§ãã
/api/v1/users
å¥ã®æ¹æ³ã¯ãæ°ããã³ã³ãã³ãã¿ã€ãã宣èšããããšã§ãã
application/vnd.acme.user-v1+xml
ãŸãã¯ãAcceptããããŒã§ããŒãžã§ã³ãã€ã³ã¿ãŒã䜿çšã§ããŸãããã®å Žåãã³ã³ãã³ãã®ã¿ã€ããå€æŽããå¿ èŠã¯ãããŸããã
application/vnd.acme.user+xml;v=1
䜿çšããæ¹æ³ãéžæããŸããæåã®æ¹æ³ãæãç°¡åã§ãããä»ã®2ã€ãããRESTfulã§ããããŸããã確ãã«ãä»ã®2ã€ã«ã¯ããã¹ããŒããªã¯ã©ã€ã¢ã³ããå¿ èŠã§ãã
ãã¹ãäž
æ£çŽã«èšããšãAPIãµãŒãã¹ã顧客ã«æäŸããããšã«ããå Žåããã®ã»ã¯ã·ã§ã³ãæãéèŠã§ããRESTã€ããªãã®ãŒã«å®å šã«åŸããã©ãããéžæã§ããŸãããAPIãµãŒãã¹ã¯å®å šã«æ©èœããå¿ èŠããããŸããã€ãŸããååã«ãã¹ããããŠããå¿ èŠããããŸãã
APIãµãŒãã¹ãæ©èœãã¹ãã§ãã¹ããããã®ã§ãã·ã¹ãã ããã©ãã¯ããã¯ã¹ãšèŠãªããŸããSymfony2ã«ã¯ããã¹ãã¯ã©ã¹ã§APIã¡ãœãããçŽæ¥åŒã³åºãããšãã§ããåªããã¯ã©ã€ã¢ã³ããå«ãŸããŠããŸãã
$client = static::createClient(); $crawler = $client->request('GET', '/users'); $response = $this->client->getResponse(); $this->assertJsonResponse($response, 200);
assertJsonResponseïŒïŒã¡ãœãããå®è£ ããWebTestCaseã¯ã©ã¹ã䜿çšããŸãã
<?php // ... protected function assertJsonResponse($response, $statusCode = 200) { $this->assertEquals( $statusCode, $response->getStatusCode(), $response->getContent() ); $this->assertTrue( $response->headers->contains('Content-Type', 'application/json'), $response->headers ); }
ããã¯ãAPIåŒã³åºãã®çŽåŸã«å®è¡ãããæåã®æ€èšŒæé ã§ãããã¹ãŠãããŸãããã°ãåãåã£ãããŒã¿ã«æ¢ã«é¢é£ããŠããä»ã®ãã§ãã¯ãå®è¡ããŸãã
APIãµãŒãã¹ã®ãã¹ããäœæãããšãã¯ãæãã€ããããšã確èªããŠãã ãããäžæ£ãªèŠæ±ã確èªããå¯èœæ§ã®ããã¹ããŒã¿ã¹ã³ãŒãããšã«å°ãªããšã1ã€ã®ã¡ãœãããšã¯ã©ã€ã¢ã³ããžã®æ£ããã¡ãã»ãŒãžããã¹ãã«å«ããããšãå¿ããªãã§ãã ãããã³ãŒã500ã¯400ãšåãæå³ã§ã¯ãããŸããã
ããã¥ã¡ã³ã
é©åãªAPIããã¥ã¡ã³ããäœæããããšã¯ãã¯ã©ã€ã¢ã³ããã¢ã¯ã»ã¹ã§ããå¯äžã®ãã®ã§ãããããéåžžã«éèŠãªãã©ã¡ãŒã¿ãŒã§ãããã¹ãŠã®ã³ãŒããæäŸããŸãããïŒ
HATEOASã¢ãããŒãã䜿çšããå ŽåãAPIã«ã¯ãã§ã«ããã¥ã¡ã³ããå«ãŸããŠããããã顧客èªèº«ãå©çšå¯èœãªãã¹ãŠã®ãªãã·ã§ã³ã«ã€ããŠåŠç¿ãããããèªåã§äœæããå¿ èŠã¯ãããŸããã
ããããHATEOAS APIã®å®è£ ã¯éåžžã«å°é£ã§ãããçŸæç¹ã§ã¯ããŸãäžè¬çã§ã¯ãããŸããã APIãµãŒãã¹ãRichardsonã¢ãã«ã®ç¬¬2ã¬ãã«ã«åŸãå Žåãããã¯ãã§ã«é©åã§ããããã¹ãŠã®ããã¥ã¡ã³ããèªåã§äœæããå¿ èŠããããŸãã
ããããNelmioApiDocBundleãå©ãã«ãªããŸãããã®ãã³ãã«ã¯Nelmioã§æžããAPIãµãŒãã¹ã®ããã¥ã¡ã³ããèªåçã«çæããŸããã³ãŒãã®ã€ã³ããã¹ãã¯ã·ã§ã³ã«åºã¥ããŠããã³ãã«ã¯å€ãã®ç°ãªãæ å ±ãåãåããé©åã«èšèšãããããŒãžã«è¡šç€ºããŸãã
ããã§ãçŽ æŽãããAPIãµãŒãã¹ãæ§ç¯ããããã®ãã¹ãŠãæã«å ¥ããŸããã
䟿å©ãªãªã³ã¯
- RFC 2616-ã»ã¯ã·ã§ã³10-ã¹ããŒã¿ã¹ã³ãŒãã®èª¬æã
- RFC 2068-ã»ã¯ã·ã§ã³19.6-è¿œå æ©èœ
- RFC 5988-Webãªã³ã¯ã
翻蚳ã®èè ããïŒå ã®èšäºã®èè ã¯ãããã«æŽæ°ããäºå®ã§ãããŸãã翻蚳ã§ãšã©ãŒãäžæ£ç¢ºããŸãã¯èŠèŠãããã¬ãŒãºãèŠã€ããå Žåã¯ãå人ã¡ãã»ãŒãžã§å ±åããŠãã ãããèšäºã®éãšç¡ç äžè¶³ãèãããšãããããäžæ£ç¢ºãããããŸãã