Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

More Formats: HAL & CSV

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

API Platform supports multiple input and output formats. We know that we can go to /api/treasures.json to see JSON, .jsonld to see JSON-LD or even .html to see the HTML output format.

Accept Header & Content Negotiation

But adding this extension to the end of the URL is just a hack that API Platform allows. To choose the format we want the API to return, we're supposed to send an Accept header. And we can see this when we use the interactive docs. This makes a request with an Accept header set to application/ld+json. Setting this header is easy to do in JavaScript, and if you don't set it, JSON-LD is the default format.

API Platform uses three formats out of the box. You can see them down here on the bottom of the docs page. But, what in our app says that we want to use these three formats specifically? To answer that, head over to your terminal and run:

./bin/console debug:config api_platform

Inside the config, check out this formats key... which, by default, is set to those three formats. This basically says that if the Accept header is application/ld+json, use the JSON-LD format. Internally, it means that when Symfony is serializing our data, it will serialize to JSON-LD or JSON.

Adding a New Format

As a challenge, let's add a fourth format. To do that, we just need to add a new item to this config... but without completely replacing the existing formats. Copy these, then open the /config/packages/ directory. We don't have an api_platform.yaml file yet, so let's create one. Inside that, say api_platform and paste those below. And while we don't have to, I'm going to change this to use a shorter, more attractive version of this config:

api_platform:
formats:
jsonld: [ 'application/ld+json' ]
json: [ 'application/json' ]
html: [ 'text/html' ]

Done!

If we go and refresh right now, everything works the same. We have the same formats below... because we simply repeated the default config.

The new format we're going to add is another type of JSON called HAL. Here's what's going on. We all understand the JSON format. But then, to add more meaning to JSON - like certain keys that your JSON should have and their meaning - some people come out with standards that extend JSON. JSON-LD is one example and HAL is a competing standard. I don't often use HAL... so we're mostly doing this to see an example of what adding a format looks like.

Oh, and the Content-Type for HAL is supposed to be application/hal+json:

api_platform:
formats:
... lines 3 - 5
jsonhal: [ 'application/hal+json' ]

As soon as we do that, when we refresh... it shows nothing? I'm pretty sure Symfony didn't see my new config file. Hop over here and clear the cache with:

./bin/console cache:clear

Refresh again and... there we go! We now see jsonhal! And if we click, it takes us to the jsonhal version of our API homepage!

Let's try an endpoint with this format. Click on the GET request, "Try it out", and, down here, we can select which "media type" to request. Select application/hal+json, hit "Execute", and... there it is!

You can see that it's JSON... and it has the same results, but it looks a bit different. It has things like _embedded and _links... which are part of the HAL standard... and not worth talking about right now.

By the way, the reason this new format worked simply by adding a tiny bit of config is that the serializer already understands the jsonhal format. So when we request with this Accept header, API Platform asks the serializer to serialize into the jsonhal format... and it knows how to do that.

Adding a CSV Format

Okay, let's do something that's a bit more practical. What if our dragon users need to return the treasures in CSV format... like so they can import them into Quickbooks for tax purposes.

Well, CSV is a format that Symfony's Serializer understands out of the box. We know that we could add CSV right into this config file. But as an added challenge, instead of enabling the CSV for every API resource in our system, let's just add it to DragonTreasure.

Find the ApiResource attribute and, at the bottom, add formats. Just like with the configuration, if we simple put csv here, that will remove the other formats. To do this right, we need to list all of them: jsonld, json, html, and jsonhal. Each of these will read the configuration to know which content type to use. At the end, add csv. But because csv doesn't exist in the config, we need to tell it which content type will activate this. So set it to text/csv.

... lines 1 - 24
#[ApiResource(
... lines 26 - 41
formats: [
'jsonld',
'json',
'html',
'jsonhal',
'csv' => 'text/csv',
],
)]
... line 50
class DragonTreasure
{
... lines 53 - 185
}

Oh, but my editor is mad! It says:

Named arguments order does not match parameters order

We know that each PHP attribute is a class... and when we pass arguments to the attribute, we're actually passing named arguments to that class's constructor. And, with named args, the order of the args doesn't matter. I actually don't think PhpStorm should be highlighting this as a problem... but if you're annoyed like I am, you can hit "Sort arguments" and... there. It moved formats up a little higher, it's happy, and we won't have to stare at that yellow underline.

All right, head over, refresh, open up our collection endpoint and hit "Try it out". This time, down here, select text/csv then... "Engage"! Hello CSV. Too easy!

Once again, this works because Symfony's Serializer understands the CSV format. So it does all the work.

In fact, open the profiler for that request... and go down to the serializer section. Yep! We can see that it's using the csv format... which activates a CsvEncoder. That's why we get our nice results. If you needed to return your results in a custom format that's not supported by the serializer, you could add your own encoder to the system to handle that. It's super flexible

Next: Let's talk about validation!

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.0.8
        "doctrine/annotations": "^1.0", // 1.14.2
        "doctrine/doctrine-bundle": "^2.8", // 2.8.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.0
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.64.1
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.15.3
        "symfony/asset": "6.2.*", // v6.2.0
        "symfony/console": "6.2.*", // v6.2.3
        "symfony/dotenv": "6.2.*", // v6.2.0
        "symfony/expression-language": "6.2.*", // v6.2.2
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.3
        "symfony/property-access": "6.2.*", // v6.2.3
        "symfony/property-info": "6.2.*", // v6.2.3
        "symfony/runtime": "6.2.*", // v6.2.0
        "symfony/security-bundle": "6.2.*", // v6.2.3
        "symfony/serializer": "6.2.*", // v6.2.3
        "symfony/twig-bundle": "6.2.*", // v6.2.3
        "symfony/ux-react": "^2.6", // v2.6.1
        "symfony/validator": "6.2.*", // v6.2.3
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.0
        "symfony/yaml": "6.2.*" // v6.2.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.2.*", // v6.2.1
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/stopwatch": "6.2.*", // v6.2.0
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.4
        "zenstruck/foundry": "^1.26" // v1.26.0
    }
}
userVoice