Getting Setup with Symfony 4 and FOSRESTBundle [FOSRESTBundle]


In the previous section we created a Symfony 4 JSON API without any dedicated REST / JSON API bundles. We saw that whilst getting a Symfony 4 API up and running was possible, certain parts were painful, or repetitive. In particular:

  • Error processing / display (we made use of Symfony form)
  • Converting a POST / PUT / PATCH request from raw input to JSON
  • Route management - naming, correct verbs, restrictions

And that's where bundles come in to help out.

In this next section specifically we are looking at FOSRESTBundle.

The plan is to satisfy our Behat test suite without changing anything in that test suite (if at all possible). The implementation will be similar - but not identical to - the Symfony 4 JSON API implementation.

Setting Up a Symfony 4 JSON API with FOS REST Bundle

Going with the Symfony 4 approach of requiring only what we need, we're going to make use of the symfony/skeleton as our starting point. As a quick heads up, if you do want a more fully featured implementation then consider the symfony/website-skeleton, which as the name suggests is a configuration more suited to "traditional" Symfony-based websites.

We aren't building a website.

We are building an API. This isn't a true RESTful API. This is a simpler JSON API.

This Symfony 4 JSON API isn't truly RESTful as it lacks HATEOS. There's a bundle for that. It really depends on how true to the original Roy Fielding REST Dissertation you wish to go. For my needs, this setup works absolutely fine. Your needs may be different, and that's ok - please adapt accordingly.

Ok, but enough with the computer science, and back to the pragmatic, practical real world.

The first thing we need to do is install a fresh copy of Symfony 4:

composer create-project symfony/skeleton symfony-4-fos-rest-api

Installing symfony/skeleton (v4.0.5)
  - Installing symfony/skeleton (v4.0.5) Loading from cache
Created project in symfony-4-fos-rest-api
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 21 installs, 0 updates, 0 removals
  - Installing symfony/flex (v1.0.70) Loading from cache
  - Installing symfony/polyfill-mbstring (v1.7.0) Loading from cache
  - Installing symfony/console (v4.0.6) Loading from cache
  - Installing symfony/routing (v4.0.6) Loading from cache
  - Installing symfony/http-foundation (v4.0.6) Loading from cache
  - Installing symfony/yaml (v4.0.6) Loading from cache
  - Installing symfony/framework-bundle (v4.0.6) Loading from cache
  - Installing symfony/http-kernel (v4.0.6) Loading from cache
  - Installing symfony/event-dispatcher (v4.0.6) Loading from cache
  - Installing psr/log (1.0.2) Loading from cache
  - Installing symfony/debug (v4.0.6) Loading from cache
  - Installing symfony/finder (v4.0.6) Loading from cache
  - Installing symfony/filesystem (v4.0.6) Loading from cache
  - Installing psr/container (1.0.0) Loading from cache
  - Installing symfony/dependency-injection (v4.0.6) Loading from cache
  - Installing symfony/config (v4.0.6) Loading from cache
  - Installing psr/simple-cache (1.0.1) Loading from cache
  - Installing psr/cache (1.0.1) Loading from cache
  - Installing symfony/cache (v4.0.6) Loading from cache
  - Installing symfony/dotenv (v4.0.6) Loading from cache
Writing lock file
Generating autoload files
Symfony operations: 4 recipes (8c83ba97f122646fd318798f4b1c23ed)
  - Configuring symfony/flex (>=1.0): From github.com/symfony/recipes:master
  - Configuring symfony/framework-bundle (>=3.3): From github.com/symfony/recipes:master
  - Configuring symfony/console (>=3.3): From github.com/symfony/recipes:master
  - Configuring symfony/routing (>=4.0): From github.com/symfony/recipes:master
Executing script cache:clear [OK]
Executing script assets:install --symlink --relative public [OK]

Some files may have been created or updated to configure your new packages.
Please review, edit and commit them: these files are yours.

 What's next? 

  * Run your application:
    1. Change to the project directory
    2. Execute the php -S 127.0.0.1:8000 -t public command;
    3. Browse to the http://localhost:8000/ URL.

       Quit the server with CTRL-C.
       Run composer require server --dev for a better web server.

  * Read the documentation at https://symfony.com/doc

And because we've used the symfony/skeleton, there are a bunch of extra dependencies that we will need immediately. Truthfully these are the sorts of things that you would only normally notice as you start using - or trying to use - your brand new Symfony installation. Here's the extras we will use:

composer require \
  symfony/serializer-pack \
  symfony/orm-pack \
  symfony/validator \
  symfony/form \
  friendsofsymfony/rest-bundle

There are six other requirements we will use:

composer require \
  annotation \
  cors \
  monolog

composer require --dev \
  profiler \
  web-server \
  maker

Ok, let's quickly cover why we need each.

Our Dependencies

composer require symfony/serializer-pack

FOSRESTBundle needs a serializer. A serializer is responsible for turning our objects - in our case our Album entity - into a different format - again, in our case, JSON.

There are two supported options for a serializer. We can either use JMS Serializer, or the Symfony Serializer.

The JMS Serializer is the more typically used, in my experience. However, it's overkill for our needs. The Symfony Serializer is comparable in function unless you have some more advanced needs, and personally I find it easier to work with. You can switch out for the JMS Serializer, if you so desire.

composer require symfony/orm-pack

We'll be working with an entity - the Album - and we'll want to do all the usual stuff with this entity: Creating new Albums, and both updating and deleting existing Albums.

The ORM Pack brings in the Doctrine Bundle and Doctrine Migrations Bundle. We won't truly need Migrations here as our setup is really simple, so much so that we will largely be copy / pasting from the previous implementation. However, in real projects, Doctrine Migrations are very useful.

As part of the Doctrine Bundle Symfony Flex recipe an extra DATABASE_URL entry will have been added to our .env file. We need to make sure this new environment variable matches whatever config you have for your database. I'm using Docker for my database, as described in this video:

###> doctrine/doctrine-bundle ###
DATABASE_URL=mysql://dbuser:dbpassword@127.0.0.1:3306/fos_rest_api
###< doctrine/doctrine-bundle ###

And that should be good enough. More on this in the video, if at all unsure.

composer require symfony/validator

As we covered in the previous section, we do have some validation requirements on our Album entity. We won't accept a blank / "" title, for example. And we also need a trackCount that is one or greater.

Validation will be triggered when our data passes through Symfony's Form component. With that in mind, we better...

composer require symfony/form

Much like in many a typical web application, we will be handling incoming user data. Even though we won't have a HTML representation for our form (just yet), the process is the same as if we did.

The form component doesn't care where our data originally comes from. It could be hardcoded. It could be a typical HTML form submission (application/x-www-url-encoded), or it could be JSON (like in our case). By the time we pass the submitted data into the form, it will have been converted to an array.

In the standalone Symfony 4 JSON API implementation we had to take care of this transformation process ourselves:

    /**
     * @Route("/album", name="post_album", methods={"POST"})
     */
    public function post(
        Request $request
    ) {
        $data = json_decode(
            $request->getContent(),
            true
        );

        // ...

This is such a common occurrence (we had to do this for POST, PUT, and PATCH) that FOSRESTBundle offers a built-in, and implicitly enabled listener to do this process for us.

We'll cover this again in the POST implementation. For now, all we need to do is:

composer require friendsofsymfony/rest-bundle

FOSRESTBundle comes with a Symfony Flex recipe to add in some (very) basic configuration for us. After running this command we will have a new, interesting bit of configuration available at config/packages/fos_rest.yaml. We'll come back to this shortly.

Optional Dependencies

For the optional dependencies I'm showing the 'alternative' syntax. If you browse the Symfony Recipes Server you'll find the packages also have aliases.

An alias is just a short form name for another package.

composer require annotation
# aka
composer require sensio/framework-extra-bundle

In this particular example the alias gives away more than the full package name. We will use annotations to describe our Healthcheck endpoint. You don't need too. We're just using this as an opportunity to explore FOSRESTBundle's useful features a bit further.

composer require cors
# aka
composer require nelmio/cors-bundle

We already covered why we need to handle CORS in a previous video.

The gist being if our API is on a different domain - even a subdomain, or different port - to our front end, then we will encounter cross origin resource sharing (CORS) issues, and this bundle takes care of this problem for us.

The key change is to update the CORS_ALLOW_ORIGIN environment variable newly added to your .env file.

If you only want to allow a specific domain, or set of domains to be able to access your JSON API then fill in these domains here:

###> nelmio/cors-bundle ###
CORS_ALLOW_ORIGIN=^http://different.com:8000$
###< nelmio/cors-bundle ###

To allow anyone to access your Symfony 4 API then set a regex that matches on any URL:

###> nelmio/cors-bundle ###
CORS_ALLOW_ORIGIN=^https?://.*?$
###< nelmio/cors-bundle ###

Our API will be 'open', so I will use the second option.

composer require monolog
# aka
composer require symfony/monolog-bundle

Logs are awesome. They are super useful. Sticking with the streamlined, super minimal approach, the symfony/skeleton does not include a fully featured logger. Monolog is the default goto for Symfony projects.

Installing Monolog means important and useful debug information will be available in your var/log directory.

composer require profiler --dev
# aka
composer require symfony/profiler-pack --dev

The profiler pack is very useful in development. This adds in the /_profiler route, which can be very useful to figure out what the heck just went wrong with your request. I use it all the time.

You don't need the profiler. It's a nice visual tool. You can get by with just using the log files.

composer require web-server --dev
# aka
composer require symfony/web-server-bundle --dev

The Symfony web server bundle gives us a nicer UX over the built-in PHP web server. It's optional as you can just use the built in PHP web server if you're comfortable with its syntax.

If you don't care about CORS, feel free to bin/console server:start, and use the http://127.0.0.1:8000 default.

For our needs we'll go a little more custom:

bin/console server:start api.oursite.com:8000

 [OK] Server listening on http://api.oursite.com:8000  

If you haven't already done so, add in a host entry for this URL.

Lastly, let's add in the Maker Bundle:

composer require maker --dev
# aka
composer require symfony/maker-bundle --dev

The Maker Bundle simplifies creating new controllers, entities, and so on. It's useful if you're lazy, like me, and don't like typing lots of boilerplate.

Other Dependencies

If you want to add in any other dependencies then don't hold back.

A paginator could be useful if you have a lot of data. JWT authentication is really common on an API. You might even want FOS User Bundle.

All of these things add complexity, and our aim is to get hands-on with the basics.

As a heads up, I only provide support for the project with the dependencies listed in this tutorial.

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
37 GET'ting Multiple Resources [API Platform] 02:59
38 PUT to Update Existing Data [API Platform] 02:19
39 DELETE to Remove Data [API Platform] 01:15
40 No One Likes Errors [API Platform] 03:28