D8FTW: REST in Core
Drupal 8 core offers a routing and request handling pipeline that gives developers more control over how to handle an incoming request than ever before. Developers can route an incoming request based on any HTTP property, or even derived information. Controllers can return page bodies, full responses, domain objects that can be turned into full responses, or anything else PHP supports.
That's great, but doesn't that mean we have to then, um, do all of that ourselves? We can, and many times we should, but in many cases we don't have to!
Core ships with a trio of modules that enable push-button support for offering up content as a Web service, and (you guessed it) can be enhanced via contrib.
Serialization
The first is the Serialization.module, which in turn is built on the Symfony Serializer component. The Serialization module offers a standard way to convert a classed object to a serialized string and back again. That process consists of two phases: A Normalizer, which converts between an object and a known nested-array structure (and back again), and an Encoder, which converts between that array structure and some string format (and back again). A serializer object contains a set of normalizers and encoders, and can figure out on the fly which to use.
The Serializer includes normalizers for content entities as well as JSON and XML encoders. That means core provides clean round-trip support between any content Entity and any defined output format. That is, once you pass the serializer service as a dependency to your code you can simply do this:
<span style="color: #000000"><span style="color: #0000BB"><?php<br>$json </span><span style="color: #007700">= </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">serializer</span><span style="color: #007700">-></span><span style="color: #0000BB">serialize</span><span style="color: #007700">(</span><span style="color: #0000BB">$entity</span><span style="color: #007700">, </span><span style="color: #DD0000">'json'</span><span style="color: #007700">);<br></span><span style="color: #0000BB">$xml </span><span style="color: #007700">= </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">serializer</span><span style="color: #007700">-></span><span style="color: #0000BB">serialize</span><span style="color: #007700">(</span><span style="color: #0000BB">$entity</span><span style="color: #007700">, </span><span style="color: #DD0000">'xml'</span><span style="color: #007700">);<br></span><span style="color: #0000BB">?></span></span>
Poof, we now have a JSON-ified version of an entity and an XML-ified version of the entity. And we can go the other way, too:
<span style="color: #000000"><span style="color: #0000BB"><?php<br>$entity </span><span style="color: #007700">= </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">serializer</span><span style="color: #007700">-></span><span style="color: #0000BB">deserialize</span><span style="color: #007700">(</span><span style="color: #0000BB">$json</span><span style="color: #007700">, </span><span style="color: #0000BB">Node</span><span style="color: #007700">::class, </span><span style="color: #DD0000">'json'</span><span style="color: #007700">);<br></span><span style="color: #0000BB">?></span></span>
The net result is that we now have a standard universal serialized format for all entities! Or at least for their internal structure, which is appropriate in some cases but not all.
It's also straightforward to write new Normalizers and Encoders, as they're simply tagged services with a defined interface. Another core module, HAL.module, provides serializers and encoders using the Hypertext Application Language format, a special flavor of JSON (or XML) that includes hypermedia links as well.
REST Resources
The second core module is REST.module. REST module uses the core plugin system to define "rest resource" plugins. Each resource can live at a defined path pattern, which implies one or more routes at that path, and has separate methods for handling each HTTP method it supports. Resources do not need to correspond to any other Drupal object; they can, but there's nothing inherent in them that requires them to do so. That's good, because REST resources need not, and often should not, correspond to underlying objects in the system.
A method on a REST plugin acts as a controller, and while it can return any value it generally will return a special subclass of Response
called ResourceResponse
that will handle serializing a data object provided on it as well as setting appropriate cache tags. Core provides two resource plugins, one for content entities, the most likely to be used, and one for database logs, mostly just to prove that it can be done. In fact, the database log resource is quite simple:
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br>* Provides a resource for database watchdog log entries.<br>*<br>* @RestResource(<br>* id = "dblog",<br>* label = @Translation("Watchdog database log"),<br>* uri_paths = {<br>* "canonical" = "/dblog/{id}"<br>* }<br>* )<br>*/<br></span><span style="color: #007700">class </span><span style="color: #0000BB">DBLogResource </span><span style="color: #007700">extends </span><span style="color: #0000BB">ResourceBase </span><span style="color: #007700">{<p> </p></span><span style="color: #FF8000">/**<br> * Responds to GET requests.<br> *<br> * Returns a watchdog log entry for the specified ID.<br> *<br> * @param int $id<br> * The ID of the watchdog log entry.<br> *<br> * @return \Drupal\rest\ResourceResponse<br> * The response containing the log entry.<br> *<br> * @throws \Symfony\Component\HttpKernel\Exception\HttpException<br> */<br></span><span style="color: #007700">public function </span><span style="color: #0000BB">get</span><span style="color: #007700">(</span><span style="color: #0000BB">$id </span><span style="color: #007700">= </span><span style="color: #0000BB">NULL</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">$id</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$record </span><span style="color: #007700">= </span><span style="color: #0000BB">db_query</span><span style="color: #007700">(</span><span style="color: #DD0000">"SELECT * FROM {watchdog} WHERE wid = :wid"</span><span style="color: #007700">, array(</span><span style="color: #DD0000">':wid' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$id</span><span style="color: #007700">))<br> -></span><span style="color: #0000BB">fetchAssoc</span><span style="color: #007700">();<br> if (!empty(</span><span style="color: #0000BB">$record</span><span style="color: #007700">)) {<br> return new </span><span style="color: #0000BB">ResourceResponse</span><span style="color: #007700">(</span><span style="color: #0000BB">$record</span><span style="color: #007700">);<br> }<p> throw new </p></span><span style="color: #0000BB">NotFoundHttpException</span><span style="color: #007700">(</span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Log entry with ID @id was not found'</span><span style="color: #007700">, array(</span><span style="color: #DD0000">'@id' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$id</span><span style="color: #007700">)));<br> }<p> throw new </p></span><span style="color: #0000BB">HttpException</span><span style="color: #007700">(</span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'No log entry ID was provided'</span><span style="color: #007700">));<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
In this case, all that's provided is GET support. POST or PUT requests will automatically be rejected with an HTTP 405 (Method Not Allowed) error. The resource is exposed at the URI /dblog/{id}
. And all it does is read back a single record out of the watchdog log as an array, which will get normalized to JSON or XML or whatever was requested by the serializer. (You likely shouldn't enable log Web service resources in production, but it's fun to play with on your own server to get a feel for how the system works.)
The plugin for content entities leverages the entity's already-defined path, so that the serialized version of an entity, such as a node or taxonomy term, lives at the same path as the HTML version of it. They are the same underlying object so should be exposed as just different representations of the same resource.
REST UI
For various reasons mainly related to available development time, the UI for REST module lives in contrib. The REST UI module provides a basic UI for configuring which REST resources should be enabled, which methods should be allowed, which formats are allowed, and which authentication mechanisms are allowed. (Core offers cookie-based auth and HTTP Basic Auth, the latter of which is only ever safe over SSL. Contrib also offers an OAuth module.) See the screenshots below.
So for example, we can enable GET JSON requests for Taxonomy terms, GET and PUT HAL requests for Nodes, and not expose anything else as an API; and just by checking a few boxes. (Isn't that the definition of success in Drupal, just checking a few boxes?)
Caveats
There are two limitations of the core REST support that are important to mention. A moment ago we said that we use the same path for both the HTML and JSON/XML/Whatever version of an entity. That's true, but we don't, technically, use the same URI.
HTTP, by design, allows a request to specify what formats they're willing to accept for a resource, using the Accept
header. The server will then compare that list against what it knows it can offer and send back the best option or an error that it cannot find a workable format. However, sending back different formats from the same URI creates a caching problem, as any proxy servers or browsers that try to cache the response can cache whichever format is requested first, then send that format (wrongly) in the future on other requests. Again, the spec has a simple solution here: The Vary
header on the Response, which can be used to tell clients and proxies to use both the URI and the Accept header to determine if a request matches a cached response. Problem solved, right?
Well, it would be if clients followed the spec. Unfortunately there's a number of issues in practice: