Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

PoC integration with FSCHateoasBundle #327

Closed
wants to merge 1 commit into from

5 participants

@lsmith77
Owner

just opening this to allow for discussion and brain storming.

this is just a proof of concept to show that its possible to leverage the route collection data to add the hateoas link relations.

a proper implementation would add the metadata during the bootstrap phase of Symfony2. this would prevent the runtime overhead and would ensure that the metadata isnt just linked to specific routes/controllers.

this would probably also allow the removal of the HateoasCollectionInterface

@hjr3

I think we should be careful here. HATEOAS is a principle, not an actual design pattern or implementation. When a media type has a certain set of principles it can then be said it is using HATEOAS. The problem I have with the FSCHateoasBundle is that it is too broad. This is akin to wanting to write good object oriented code and creating a SeparationOfConcernsBundle. There are many ways to do separation of concerns just like there are many ways to achieve HATEOAS.

I would like to move in a direction where we write bundles that implement certain media types. There are a few major ones out there right now:
1. Atom+xml
2. Collection+json (this is Atom+xml but the specs are mutually exclusive)
3. Hal+json / Hal+xml

Atom and Collection are great for authoring APIs, such as blogs, newspapers and CMS. The number of H-factors is small and it is easier to understand. Hal is a more general media type that allows for more flexibility. It has more H-factors and can be harder to understand. I think implementing bundles for Hal or Collection/Atom are about the same amount of code though.

At HauteLook, we use Hal+json because there is no defined e-commerce media type out there. The tooling for it has been horrible. We face challenges on both the server side (symfony) and the client side (ember.js) in trying to make Hal+json easy to use. I know @baldurrensch has been working tirelessly with the existing bundles to try and make them more Hal friendly. The PR's are slow to be merged, or even rejected, because people do not understand or disagree with what we are trying to do.

HauteLook is definitely committed to solving the Hal tooling problem with symfony bundles. I just think trying to make a bundle that allows for easier implementation of all media types is just too broad and challenging at this time. Half of the problem any solution is education on how hypermedia works. Focusing on solving specific use cases, such as Hal, allows us to establish a convention. We can publish a README or series of articles that are concrete enough for people to understand and empower them to build hypermedia friendly APIs.

@mvrhov

@hradtke: You did see my implementation of hal+json on JSMSerializer bundle.. It's not up to date with the master, but it still works quite fine. Also there is another PR from another author which is supposed to be based on a master.
Either way I don't care which implementation I'm going to use in the future but It has to support at lest what my PR does.
Now regarding the json+hal it's not that hard I find it simple or at least my co-workers did. They had a C++ implementation quite quickly.

Now there is one another format called siren and at a glance it really looks way more complicated than hal+json.

@hjr3

@mvrhov Yes, @baldurrensch is the author of the other PR who works with me at HauteLook. I agree that it doesn't matter if JSMSerializer or FOSRestBundle have the actual implementation. We should try, for the communities sake, to keep it together. I know @baldurrensch has had some trouble with getting the PR merged into JSMSerializer.

I don't think hal+json is difficult to understand as a format. However, understanding what affordances Mike Kelly made (and why he made them) is a deeper dive into hypermedia design. Those of us who are students of hypermedia design can grok hal+json, but I would imagine that many people do not really understand it all. My point in mentioning that was only to stress that we establish a convention that is pointing those people who are new to hypermedia APIs in the right direction and try to prevent them from making common mistakes.

Thank you for linking Siren! That looks interesting.

@lsmith77
Owner

@hradtke i do agree that its impossible to write a tool that will turn your code into a RESTful application. This is why I have always taken the approach in this Bundle of "use at will tools". Notice that its possible to disable the automatic link relations and add new link relations in this PoC. Also like I was discussing with @ludofleury by linking the configuration to the controller methods, rather than the entity, we can also look into more context aware link relations.

@lsmith77 lsmith77 PoC integration with FSCHateoasBundle
this is just a proof of concept to show that its possible to leverage the route collection data to add the hateoas link relations.

a proper implementation would add the metadata during the bootstrap phase of Symfony2. this would prevent the runtime overhead and would ensure that the metadata isnt just linked to specific routes/controllers.
4c94fe2
@lsmith77
Owner

looks like we should instead focus on https://github.com/willdurand/BazingaHateoasBundle now.

@willdurand
Owner

The BazingaHateoasBundle leverages the JMSSerializerBundle and the Serializer lib. By simply requiring it, it is able to add links on collections/resources without having to configure anything else (except the metadata for the links, but it is obvious). That means:

  1. add the bundle;
  2. add Hateoas specific annotations to your classes (the ones that are already configured for the Serializer) or use YAML files;
  3. enjoy!
@lsmith77
Owner

that was the case with FSCHateoasBundle more or less as well. the goal of this PR is to integrate the information we have about the routes via the route generation.

@adrienbrault

I think that the FOSRestBundle would need to implement a MetadataDriver like https://github.com/willdurand/Hateoas/blob/master/src/Hateoas/Configuration/Metadata/Driver/AnnotationDriver.php . The HateoasBundle could have a DI tag to wire it up.

@lsmith77 lsmith77 closed this
@willdurand
Owner

@lsmith77 we could even delete the branch I'd say.

@lsmith77
Owner

yeah, i guess its not very useful anymore.

@lsmith77 lsmith77 deleted the branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 4, 2013
  1. @lsmith77

    PoC integration with FSCHateoasBundle

    lsmith77 authored
    this is just a proof of concept to show that its possible to leverage the route collection data to add the hateoas link relations.
    
    a proper implementation would add the metadata during the bootstrap phase of Symfony2. this would prevent the runtime overhead and would ensure that the metadata isnt just linked to specific routes/controllers.
This page is out of date. Refresh to see the latest.
View
28 Controller/Annotations/Hateoas.php
@@ -0,0 +1,28 @@
+<?php
+
+/*
+ * This file is part of the FOSRestBundle package.
+ *
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace FOS\RestBundle\Controller\Annotations;
+
+use Doctrine\Common\Annotations\Annotation;
+
+/**
+ * Hateoas annotation class.
+ * @Annotation
+ */
+class Hateoas extends Annotation
+{
+ /** @var string */
+ public $subject;
+ /** @var string */
+ public $identifier = 'id';
+ /** @var string */
+ public $relName;
+}
View
48 EventListener/ViewResponseListener.php
@@ -12,6 +12,7 @@
namespace FOS\RestBundle\EventListener;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
+use FOS\RestBundle\Routing\HateoasCollectionInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
@@ -125,6 +126,53 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
$view->setTemplate($template);
}
+ } elseif ($this->container->has('fsc_hateoas.metadata.factory')) {
+ $data = $view->getData();
+ if (is_object($data)) {
+ $class = $data instanceof HateoasCollectionInterface
+ ? $data->getSubject() : get_class($data);
+
+ $cacheDir = $this->container->getParameter('kernel.cache_dir');
+ $file = $cacheDir.'/fos_rest/hateoas/'.str_replace('\\', '', $class);
+ if (file_exists($file)) {
+ $collection = file_get_contents($file);
+ $collection = unserialize($collection);
+
+ $relationsBuilder = $this->container->get('fsc_hateoas.metadata.relation_builder.factory')->create();
+ $subject = strtolower($collection->getSingularName());
+ $baseParameters = array();
+ if ($collection->isFormatInRoute()
+ && null !== $request->attributes->get('_format')
+ ) {
+ $baseParameters['_format'] = $view->getFormat();
+ }
+ foreach ($collection as $routeName => $route) {
+ $relName = $route->getRelName();
+ if (!$relName) {
+ continue;
+ }
+
+ $relName = $routeName === $request->attributes->get('_route') ? 'self' : $relName;
+ $parameters = array(
+ 'route' => $routeName,
+ 'parameters' => $baseParameters,
+ );
+ foreach ($route->getPlaceholders() as $placeholder) {
+ if ($placeholder === $subject) {
+ $value = $data instanceof HateoasCollectionInterface
+ ? '{'.$placeholder.'}' : $data->{$collection->getIdentifier()}()
+ ;
+ } else {
+ $value = $request->attributes->get($placeholder);
+ }
+ $parameters['parameters'][$placeholder] = $value;
+ }
+ $relationsBuilder->add($relName, $parameters);
+ }
+
+ $this->container->get('fsc_hateoas.metadata.factory')->addObjectRelations($data, $relationsBuilder->build());
+ }
+ }
}
$response = $viewHandler->handle($view, $request);
View
27 Routing/HateoasCollectionInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of the FOSRestBundle package.
+ *
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace FOS\RestBundle\Routing;
+
+/**
+ * Hateoas
+ *
+ * @author Lukas Kahwe Smith <smith@pooteeweet.org>
+ */
+interface HateoasCollectionInterface
+{
+ /**
+ * Get subject class
+ *
+ * @return string
+ */
+ public function getSubject();
+}
View
25 Routing/Loader/Reader/RestActionReader.php
@@ -17,6 +17,7 @@
use FOS\RestBundle\Util\Inflector\InflectorInterface;
use FOS\RestBundle\Routing\RestRouteCollection;
+use FOS\RestBundle\Routing\RestRoute;
use FOS\RestBundle\Request\ParamReader;
/**
@@ -167,8 +168,18 @@ public function read(RestRouteCollection $collection, \ReflectionMethod $method,
$resources[] = null;
}
+ $hateoas = $this->readMethodAnnotation($method, 'Hateoas');
+ if ($hateoas && $hateoas->relName) {
+ $relName = $hateoas->relName;
+ } elseif ('get' === $httpMethod) {
+ $relName = empty($arguments) ? 'collection' : 'entity';
+ } else {
+ $relName = false;
+ }
+
$routeName = $httpMethod.$this->generateRouteName($resources);
- $urlParts = $this->generateUrlParts($resources, $arguments, $httpMethod);
+ $placeholders = array();
+ $urlParts = $this->generateUrlParts($resources, $arguments, $httpMethod, $placeholders, $relName);
// if passed method is not valid HTTP method then it's either
// a hypertext driver, a custom object (PUT) or collection (GET)
@@ -208,9 +219,12 @@ public function read(RestRouteCollection $collection, \ReflectionMethod $method,
}
// add route to collection
- $collection->add($routeName, new Route(
- $pattern, $defaults, $requirements, $options
- ));
+ $route = new RestRoute($pattern, $defaults, $requirements, $options);
+ $route->setPlaceholders($placeholders);
+ if ($relName) {
+ $route->setRelName($relName);
+ }
+ $collection->add($routeName, $route);
}
/**
@@ -338,7 +352,7 @@ private function generateRouteName(array $resources)
*
* @return array
*/
- private function generateUrlParts(array $resources, array $arguments, $httpMethod)
+ private function generateUrlParts(array $resources, array $arguments, $httpMethod, array &$placeholders)
{
$urlParts = array();
foreach ($resources as $i => $resource) {
@@ -358,6 +372,7 @@ private function generateUrlParts(array $resources, array $arguments, $httpMetho
} else {
$urlParts[] = '{'.$arguments[$i]->getName().'}';
}
+ $placeholders[] = $arguments[$i]->getName();
} elseif (null !== $resource) {
if ((0 === count($arguments) && !in_array($httpMethod, $this->availableHTTPMethods))
|| 'new' === $httpMethod
View
6 Routing/Loader/Reader/RestControllerReader.php
@@ -69,6 +69,12 @@ public function read(\ReflectionClass $reflection)
$this->actionReader->setNamePrefix($annotation->value);
}
+ // read hateoas annotation
+ if ($annotation = $this->readClassAnnotation($reflection, 'Hateoas')) {
+ $collection->setSubject($annotation->subject);
+ $collection->setIdentifier($annotation->identifier);
+ }
+
$resource = array();
// read route-resource annotation
if ($annotation = $this->readClassAnnotation($reflection, 'RouteResource')) {
View
11 Routing/Loader/RestRouteLoader.php
@@ -80,6 +80,17 @@ public function load($controller, $type = null)
$collection->prependRouteControllersWithPrefix($prefix);
$collection->setDefaultFormat($this->defaultFormat);
+ if ($collection->getSubject()) {
+ $cacheDir = $this->container->getParameter('kernel.cache_dir');
+ $dir = $cacheDir.'/fos_rest/hateoas';
+ if (!is_dir($dir) && false === $this->container->get('filesystem')->mkdir($dir)) {
+ throw new \RuntimeException(sprintf(
+ 'Could not create hateoas cache directory %s', $dir
+ ));
+ }
+ file_put_contents($dir.'/'.str_replace('\\', '', $collection->getSubject()), serialize($collection));
+ }
+
return $collection;
}
View
82 Routing/RestRoute.php
@@ -0,0 +1,82 @@
+<?php
+
+/*
+ * This file is part of the FOSRestBundle package.
+ *
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace FOS\RestBundle\Routing;
+
+use Symfony\Component\Routing\Route;
+
+/**
+ * Restful route.
+ *
+ * @author Lukas Kahwe Smith <smith@pooteeweet.org>
+ */
+class RestRoute extends Route
+{
+ private $placeholders;
+ private $relName;
+
+ /**
+ * Set argument names of the route.
+ *
+ * @param array $placeholders argument names
+ */
+ public function setPlaceholders($placeholders)
+ {
+ $this->placeholders = $placeholders;
+ }
+
+ /**
+ * Returns argument names of the route.
+ *
+ * @return array
+ */
+ public function getPlaceholders()
+ {
+ return $this->placeholders;
+ }
+
+ /**
+ * Returns rel name of the route.
+ *
+ * @return string
+ */
+ public function getRelName()
+ {
+ return $this->relName;
+ }
+
+ /**
+ * Set rel name of the route.
+ *
+ * @param string $relName rel name
+ */
+ public function setRelName($relName)
+ {
+ $this->relName = $relName;
+ }
+
+ public function serialize()
+ {
+ $data = parent::serialize();
+ $data = unserialize($data);
+ $data['placeholders'] = $this->placeholders;
+ $data['relName'] = $this->relName;
+ return serialize($data);
+ }
+
+ public function unserialize($data)
+ {
+ parent::unserialize($data);
+ $data = unserialize($data);
+ $this->placeholders = $data['placeholders'];
+ $this->relName = $data['relName'];
+ }
+}
View
54 Routing/RestRouteCollection.php
@@ -21,6 +21,9 @@
class RestRouteCollection extends RouteCollection
{
private $singularName;
+ private $subject;
+ private $identifier;
+ private $isFormatInRoute = false;
/**
* Set collection singular name.
@@ -43,6 +46,56 @@ public function getSingularName()
}
/**
+ * Set route subject class.
+ *
+ * @param string $subject route subject class
+ */
+ public function setSubject($subject)
+ {
+ $this->subject = $subject;
+ }
+
+ /**
+ * Set route subject class.
+ *
+ * @return string
+ */
+ public function getSubject()
+ {
+ return $this->subject;
+ }
+
+ /**
+ * Set subject identifier.
+ *
+ * @param string $identifier route identifier class
+ */
+ public function setIdentifier($identifier)
+ {
+ $this->identifier = $identifier;
+ }
+
+ /**
+ * Set subject identifier.
+ *
+ * @return string
+ */
+ public function getIdentifier()
+ {
+ return $this->identifier;
+ }
+
+ /**
+ * Check if the format should be set in the route
+ *
+ * @return Boolean
+ */
+ public function isFormatInRoute()
+ {
+ return $this->isFormatInRoute;
+ }
+
+ /**
* Adds controller prefix to all collection routes.
*
* @param string $prefix
@@ -61,6 +114,7 @@ public function prependRouteControllersWithPrefix($prefix)
*/
public function setDefaultFormat($format)
{
+ $this->isFormatInRoute = true;
foreach (parent::all() as $route) {
$route->setDefault('_format', $format);
}
Something went wrong with that request. Please try again.