The Context [API Platform]


In the previous video we looked at the Entry Point. This was the first of the two differences to routing than you would find with a 'typical' Symfony project. In this video we will look at the second difference, the Context.

Available Formats

Out of the box we can work with raw JSON, JSON-LD (JSON for Linking Data), and JSON-HAL.

HTML is also available for the docs.

JSON-LD is the preferred format.

Raw JSON support is only considered 'experimental'. There are no guarantees given that using raw JSON will work moving forwards. That's an interesting point, in my opinion.

However, you can implement any format you like.

If you'd like to know more, the docs here are great.

Given that JSON-LD is the preferred format, this is the format we will work in when consuming our API Platform setup. This means our Behat tests will be in need of some, ahem, -minor- major tweaking.

Awesome Documentation

Even though I spend a lot of my time writing and speaking words, I would absolutely prefer to spend my time writing code.

Feel the same?

Yeah, I think that's why most closed-source projects have fairly, shall we say, lacking documentation. Read the code! being the usual expectation. Good times.

Well, here's where working with a RESTful API (or JSON API, whatever) actually has a massive improvement over monolithic projects.

By very definition, we are creating an API - an Application Programmer Interface.

Typically when we hear the acronym API lately we tend to think immediately of the HTTP style thing we're building here.

But an API is literally any public interface to any library or program we interact with.

I'm going to speak from personal experience here. Largely when I worked with monoliths I didn't think nearly enough about the API I was building. Everything would end up tightly coupled, and whilst on the initial development cycle this was a benefit, when it came time to extend and improve, life became harder.

My business and presentation layers would be very tightly integrated.

If a deadline loomed large, bringing in extra hands was harder as everything would be intermixed, and more challenging to quickly comprehend for anyone new to the project.

Conversely an API that separates front and back end via an explicit boundary - this boundary usually / potentially even including different programming languages - there is an in-built need to document.

It's very likely you will using a setup like this when working on a larger scale project. It's highly possible that even if not right now, sooner or later at least one other person will directly interact with your system.

This could be you as a front end dev. It could be a colleague. It could be someone on the other side of the world using your API in a way you never even dreamed of.

Now, fortunately we gain advantages of documentation in two ways.

Firstly, we have our Behat tests. These are human readable living documents that serve to test and prove our system.

We're using a standalone Behat project as a means to test our API. It's worth noting that API Platform also uses Behat for testing their own code, and recommend you write Behat tests for your logic, too.

Secondly, we get automatically generated Open API (formerly Swagger) documentation created for us, for any and all entities we have in our system.

Just add a new entity, and boom, fully documented.

Docs can be consumed in JSON-LD, JSON-HAL, JSON, and HTML.

HTML is the default for docs, and is available immediately after you've completed installation of the API Platform. Head on over to /docs now to see yours.

Contexts

Context is amongst my least favourite words in Computer Science.

I find the term to be vague and confusing.

Which is a bit unusual as the entire point of the context is to be as descriptive and helpful as possible.

When two people communicate with one another, the conversation takes place in a shared environment, typically called "the context of the conversation". This shared context allows the individuals to use shortcut terms, like the first name of a mutual friend, to communicate more quickly but without losing accuracy. A context in JSON-LD works in the same way. It allows two applications to use shortcut terms to communicate with one another more efficiently, but without losing accuracy. -- [W3C]]5

Let's quickly remind ourselves of the route:

/contexts/{shortName}.{_format}

Examples we can immediately use being:

  • https://localhost:8443/contexts/Entrypoint
  • https://localhost:8443/contexts/Greeting

Note the {_format} placeholder only supports JSON-LD to the best of my knowledge. This is because the concept of a Context is specific to JSON-LD.

The shortName can be explicitly defined for any resource we create. A default is guessed for any annotated entities.

Anyway, don't worry about guessing the short names of your resources - the correct link to the related context is returned whenever you GET a valid resource. We'll see this again shortly.

Any resource we consume when using JSON-LD will come with an @context property. This is a special reserved keyword in JSON-LD.

The @context's value may contain a string, an array, or an object.

API Platform returns an object - denoted by the use of { and }. This is standard syntax for representing an object in JavaScript / JSON. Anything within this object is intended to teach us - as API consumers - about the intent of this resource.

Let's see a working example. Send in a request to: https://localhost:8443/contexts/Greeting

{
    "@context": {
        "@vocab": "https://localhost:8443/docs.jsonld#",
        "hydra": "http://www.w3.org/ns/hydra/core#",
        "name": "Greeting/name"
    }
}

We're told that the vocabulary (list of words and terms that our API uses) can be found locally at https://localhost:8443/docs.jsonld#.

hydra is provided with a link to the hydra spec in JSON-LD.

Finally, the name is given, which has its type set to Greeting/name, which is itself a link - which will result in a 404.

You can fix this 404 by defining exactly what the concept of a Greeting represents.

This gets a little abstract - what is a Greeting?

Well, there's a full list of things a Greeting might be over on the Schema.org hierarchy.

At the most abstract, the Greeting may just be a 'thing'.

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * This is a dummy entity. Remove it!
 *
-* @ApiResource
+* @ApiResource(iri="http://schema.org/Thing")
 * @ORM\Entity
 */
class Greeting
{
    /**
     * @var int The entity Id
     *
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @var string A nice person
     *
     * @ORM\Column
     * @Assert\NotBlank
+    * @ApiProperty(
+    *     iri="http://schema.org/name"
+    * )
     */
    public $name = '';

    public function getId(): int
    {
        return $this->id;
    }
}

And now the same request to: https://localhost:8443/contexts/Greeting

{
    "@context": {
        "@vocab": "https://localhost:8443/docs.jsonld#",
        "hydra": "http://www.w3.org/ns/hydra/core#",
        "name": "http://schema.org/name",
    }
}

Does this even matter?

Well, the aim of JSON-LD is to make your API consumable to both humans and computers alike.

A search engine - Google for instance - is a big computer. Chris, you're telling me lies, I hear you say. Surely Google is just an army of mechanical turks manually indexing the web? /s

The thing is, 404's will hurt your SEO. If you care about such things, fix this. If you don't, but you care about usability, fix this. If you really don't, then who cares, right?

luke-skywalker-i-care-meme

API Platform cares, too.

There is a full component that helps with the management and maintenance of your Schema.

We'll get back to this, shortly.

Resource Routes

There's a bunch of routes that are automatically created for any given entity-based resource:

docker-compose exec php php bin/console debug:router
 ------------------------------- -------- -------- ------ --------------------------------- 
  Name                            Method   Scheme   Host   Path                             
 ------------------------------- -------- -------- ------ --------------------------------- 
  api_greetings_get_collection    GET      ANY      ANY    /greetings.{_format}             
  api_greetings_post_collection   POST     ANY      ANY    /greetings.{_format}             
  api_greetings_get_item          GET      ANY      ANY    /greetings/{id}.{_format}        
  api_greetings_delete_item       DELETE   ANY      ANY    /greetings/{id}.{_format}        
  api_greetings_put_item          PUT      ANY      ANY    /greetings/{id}.{_format}        
 ------------------------------- -------- -------- ------ --------------------------------- 

We're going to get more interested in these in the next few videos. For now, just be aware they exist, and are automatically created for us.

Error Route

This leaves one last default route:

docker-compose exec php php bin/console debug:router
 ------------------------------- -------- -------- ------ --------------------------------- 
  Name                            Method   Scheme   Host   Path                             
 ------------------------------- -------- -------- ------ --------------------------------- 
  api_entrypoint                  ANY      ANY      ANY    /{index}.{_format}               
  api_doc                         ANY      ANY      ANY    /docs.{_format}                  
  api_jsonld_context              ANY      ANY      ANY    /contexts/{shortName}.{_format}  
  api_greetings_get_collection    GET      ANY      ANY    /greetings.{_format}             
  api_greetings_post_collection   POST     ANY      ANY    /greetings.{_format}             
  api_greetings_get_item          GET      ANY      ANY    /greetings/{id}.{_format}        
  api_greetings_delete_item       DELETE   ANY      ANY    /greetings/{id}.{_format}        
  api_greetings_put_item          PUT      ANY      ANY    /greetings/{id}.{_format}        
  _twig_error_test                ANY      ANY      ANY    /_error/{code}.{_format}         
 ------------------------------- -------- -------- ------ --------------------------------- 

As the name suggests this is a for testing various error status codes in Twig.

This route will be brought into any Symfony 4 project that uses Twig.

Nothing specific to API Platform here.

And don't worry, this route isn't available in production:

docker-compose exec php php bin/console debug:router --env=prod
 ------------------------------- -------- -------- ------ --------------------------------- 
  Name                            Method   Scheme   Host   Path                             
 ------------------------------- -------- -------- ------ --------------------------------- 
  api_entrypoint                  ANY      ANY      ANY    /{index}.{_format}               
  api_doc                         ANY      ANY      ANY    /docs.{_format}                  
  api_jsonld_context              ANY      ANY      ANY    /contexts/{shortName}.{_format}  
  api_greetings_get_collection    GET      ANY      ANY    /greetings.{_format}             
  api_greetings_post_collection   POST     ANY      ANY    /greetings.{_format}             
  api_greetings_get_item          GET      ANY      ANY    /greetings/{id}.{_format}        
  api_greetings_delete_item       DELETE   ANY      ANY    /greetings/{id}.{_format}        
  api_greetings_put_item          PUT      ANY      ANY    /greetings/{id}.{_format}        
 ------------------------------- -------- -------- ------ --------------------------------- 

Ok, thats the routing setup out of the way. Let's get our Behat test suite up and running, starting with the Healtcheck.

Episodes

# Title Duration
1 What will our JSON API actually do? 08:46
2 What needs to be in our Database for our Tests to work? 12:32
3 Cleaning up after each Test Run 02:40
4 Docker makes for Easy Databases 09:01
5 Healthcheck [Raw Symfony 4] 07:53
6 Send in JSON data using POST [Raw Symfony 4] 05:33
7 Keep your data nice and tidy using Symfony's Form [Raw Symfony 4] 10:48
8 Validating incoming JSON [Raw Symfony 4] 08:26
9 Nicer error messages [Raw Symfony 4] 06:23
10 GET'ting data from our Symfony 4 API [Raw Symfony 4] 08:11
11 GET'ting a collection of Albums [Raw Symfony 4] 01:50
12 Update existing Albums with PUT [Raw Symfony 4] 05:00
13 Upsetting Purists with PATCH [Raw Symfony 4] 02:39
14 Hitting DELETE [Raw Symfony 4] 02:11
15 How to open your API to the outside world with CORS [Raw Symfony 4] 07:48
16 Getting Setup with Symfony 4 and FOSRESTBundle [FOSRESTBundle] 09:11
17 Healthcheck [FOSRESTBundle] 06:14
18 Handling POST requests [FOSRESTBundle] 08:31
19 Saving POST data to the database [FOSRESTBundle] 09:44
20 Work with XML, or JSON, or Both [FOSRESTBundle] 04:31
21 Going far, then Too Far with the ViewResponseListener [FOSRESTBundle] 03:19
22 GET'ting data from your Symfony 4 API [FOSRESTBundle] 05:58
23 GET'ting a Collection of data from your Symfony 4 API [FOSRESTBundle] 01:27
24 Updating with PUT [FOSRESTBundle] 02:58
25 Partially Updating with PATCH [FOSRESTBundle] 02:15
26 DELETE'ing Albums [FOSRESTBundle] 01:27
27 Handling Errors [FOSRESTBundle] 08:58
28 Introducing the API Platform [API Platform] 08:19
29 The Entry Point [API Platform] 04:30
30 The Context [API Platform] 05:52
31 Healthcheck - Custom Endpoint [API Platform] 05:17
32 Starting with POST [API Platform] 07:08
33 Creating Entities with the Schema Generator [API Platform] 07:38
34 Defining A Custom POST Route [API Platform] 07:31
35 Finishing POST [API Platform] 06:29
36 GET'ting One Resource [API Platform] 02:50