Apparently, I'm the first person ever to need an HTTP PUT endpoint29 Sep 2014
It never ceases to amaze me, how seemingly common problems start to look like they must be fringe cases once you start working on them. This week, me and my colleague Seth Miller were tasked with putting together a RESTful HTTP PUT endpoint, to accept partial updates to a data model.
A quick refresher: PUT is one of four core HTTP methods used to interact with a RESTful API. For the sake of this blog post, let’s say we’re working with a Beer and Breweries API. Our beer endpoints would look like this:
- GET /beers[/:id]
- Responds with a list of beers, or a single beer with the matching id.
- POST /beers
- Creates a new beer, with data the provided by the client
- PUT /beers/:id
- Makes changes to an an existing beer, with data provided by the client
- DELETE /beers/:id
- Deletes the beer with the provided :id
For this project, we’re using Zend Framework 2 (v2.3). We’re also using a couple other nifty tools to make our lives much easier:
- Doctrine ORM: An Object Relational Mapper, which is a fancy way of saying that it handles saving and retrieving models from our MySQL database. We use annotations to tell Doctrine which models are associated with which database tables, the types of each field, and relationships between models.
- JMS Serializer: Allows you to configure exactly how you want your models serialized into json. You can use your same serialization rules to convert json back into model objects.
Between these two tools, am I able to configure-and-forget the details of my incoming and outgoing transactions.
I really like this workflow, because it keeps my controllers skinny, and focused on the task at hand. It also keeps my models clean and stupid, as they should be.
Time to update a model (this should be easy, right?)
So I’m super happy with this setup, and ready to start banging out some quick endpoints. Here are some easy ones:
Besides a couple small quirks between ZF2 and the JMS Serializer, this is all pretty slick, and works just as you would expect.
Cool, so let’s update a beer.
That looks like it should work. But in fact, we get a MySQL error for trying to insert null into a NOT NULL field. So what’s really happening here? Let’s take another look at the controller:
So we’ve found ourselves in a doozy of a pickle here. We have these awesome tools, but we somehow can’t figure out how to perform this common task.
Doctrine can’t really be to blame here. We’re giving it an object with null values, and it’s saving that object to the db, exactly as you’d suspect. The problem is that the serializer creates an entirely new model with our data, when we really want it update an existing model. In other words, I’d like to do something like this:
Alas, this is not how the serializer works.
My colleague, Seth, and I spent hours looking for a work-around. We started with the Serializer’s (pathetically incomplete) documentation – nothing. Then Stack Overflow – nothing. Then the serializer source code – nothing. Really, are we the only developers who have ever tried to update a model with the JMS Serializer?
Finally, Seth happened on a issue in the serializer’s github repo, “Allow Constructed Object to be Passed to Deserialize”. That sounds promising! And what’s this? A merged pull request? There is light at the end of the tunnel!
But did the pull request allow deserialize to accept an object? No. Did it explain (in English) how to do so? Definately not.
I’ll spare you all of the frustration and tears that followed, but it was eventually Seth who found it: a line of code in the pull request, deep inside some test code, of all places:
What the heck’s an
InitializedObjectConstructor? We tried pulling it into our project, and the file couldn’t even be loaded. Turns out it’s a test fixture, created solely for that one test, then hidden away from the world.
But for you, dear reader, I will explain (in English) how to use this thing. First we copy/pasted the
InitializedObjectConstructor into our project. We then used it in creating our Serializer service:
We then add a few magic lines to our
update action (don’t ask what they do, just be happy they work);
Someday, I imagine all of this kind of standard blueprint code will be much easier to implement. I mean really, this kind of simple API endpoint should be like falling off a log. Until then, we’ll have to take some time to really get to know our tools, even if it means searching through tests to figure how to use them.