How to make an API with Symfony

I will show you how to set up a simple API with Symfony and a tool called Postman to test your API.

These days an API is a very common use case for web applications and several other software like android apps for example.

The only thing you will need is a preinstalled version of the Symfony framework. To learn how to do this please read the tutorial for beginners.

Our plan to make an API with Symfony

We will set up an API with the FOSRest bundle, this bundle allows you to rapidly develop Restful APIs with the blink of an eye.

With this API, we will create, update, delete and list a custom entity, meaning at the end of this tutorial you have a good understanding of how to build your own API.

The setup within Symfony

Installing the bundle

composer require friendsofsymfony/rest-bundle

Since this bundle uses the Symfony recipe principle, it will ask you want to apply this recipe, please confirm that you do.

The bundle also requires a serializer, the default is JMS so let us use that one.

composer require jms/serializer-bundle

This should result in an error-free process when trying to clear the cache, please do so to confirm:

bin/console cache:clear

If anything fails, please contact me in the comments below and I’ll try to help you out.

Configure FOSRest bundle

In config/packages/fos_rest.yaml:

fos_rest:
    format_listener:
        rules:
            - { path: ^/, prefer_extension: true, fallback_format: json, priorities: [ json ] }

The endpoints of our API with Symfony

Create your first API endpoint

Endpoints are created within controllers, the easiest way to do it is with annotations.

Please installer the maker-bundle, it’s the quickest way to create controllers, entities, forms, …

composer require symfony/maker-bundle --dev

And create your first controller, called the PostController.

bin/console make:controller PostController

This should create a file within src/Controller called PostController.php.

As you can see within the annotations in that file, it creates a route /post, let’s try and visit it in our browser! Huh, doesn’t work?

That’s right, atm there’s no real routing handled by our application because the apache-pack isn’t installed, please install this first and test it afterward!

bin/console req apache-pack

Go back to your browser, works like a charm now doesn’t it? 🙂

Let’s rewrite our index function to:

use FOS\RestBundle\Controller\Annotations as Rest;
...
/**
 * @return \Symfony\Component\HttpFoundation\JsonResponse
 * @Rest\Route("/posts", methods={"GET"})
*/
public function index()
{
  return $this->json([
    'message' => 'Welcome to your new controller!',
    'path' => 'src/Controller/PostController.php',
  ]);
}

Please notice that we’re now using the rest bundle annotations, not the Symfony routes anymore.

Let’s create a few more routes:

/**
* @Rest\Route("/posts", methods={"POST"})
*/
public function create()
{
  return $this->json([
    'message' => 'Creating a post'
  ]);
}
/**
 * @return \Symfony\Component\HttpFoundation\JsonResponse
 * @Rest\Route("/posts/{id}", methods={"PUT"})
 */
public function update()
{
  return $this->json([
    'message' => 'Updating'
  ]);
}
/**
* @return \Symfony\Component\HttpFoundation\JsonResponse
* @Rest\Route("/posts/{id}", methods={"DELETE"})
*/
public function delete()
{
  return $this->json([
    'message' => 'Deleting'
  ]);
}

We created a list function (index), an update, delete and post, all of these fit perfectly in the REST principle, why?

Well, you might’ve noticed, but the index function and create function both use the same endpoints and so do update & delete. This is possible because we’re using REST, which takes care of the METHOD handling:

  • A GET request to /posts will return all posts
  • A POST request to /posts will create a post
  • A PUT request /posts/1 will update post ID 1
  • A DELETE request /posts/1 will delete post ID 1

Test your endpoints with Postman

What is Postman?

Postman is a useful tool to test your API endpoints, you can find more about it here.

Test the endpoints as described above and you should see the corresponding results.

The Post entity

To fetch, update, create and delete entries from and to the database we need to create an Entity. Read all about entities in my tutorial about Doctrine.

To support Doctrine and ORM, you’ll have to install it:

composer req orm

All you need to do now is connect your database by changing the DATABASE_URL value in the .env file, you can also find instructions in detail in the previously mentioned tutorial for Doctrine.

Now let’s create our entity!

bin/console make:entity Post

Executing this command will already add a file in src/Entity called Post.php, it even asks you if you want extra fields.

Please add extra fields:

  • title of type string
  • content of type text
  • createdAt of type datetime

Follow the instructions given by the command line, you’ll figure it out, if not you’ll find me in the comments 😉

Almost ready, please update your database accordingly using:

bin/console doctrine:schema:update -f

Check your database, it should now have a table called post.

Endpoint logic

Now that we have our endpoints and our entity, let’s combine both so that the endpoint fetches his data directly from the database.

List of posts

public function index(PostRepository $postRepository)
{
    return $this->json($postRepository->findAll());
}

Visit /posts with your browser or Postman, it should return an empty array, which is correct as atm our database is empty.

Let’s create a manual entry using a SQL query:

INSERT INTO post (title, content, created_at) VALUES ('Test post', 'This is the content', NOW())

To make FOSRest work with your entities, you’ll have to change the controller, it should extend the AbstractFOSRestController instead of the default AbstractController.

And change your index action as follows:

 public function index(PostRepository $postRepository)
{
  $view = $this->view($postRepository->findAll());
  return $this->handleView($view);
}

Visit your /posts with Postman and have a look at the result!

[
    {
        "id": 1,
        "title": "Test post",
        "content": "This is the content",
        "created_at": "2019-08-16T13:22:09+00:00"
    }
]

Create a post

Remember that to create a post we use the same endpoint, but with the POST method instead of the GET method. Which in turn calls the create() method.

To create a post, we need title and content to be passed to the endpoint with a form submission or an AJAX request.

The data we send should look like:

{
  "title": "Test post 2",
  "content": "This is content of post 2"
}

We will, later on, pass this with Postman to our endpoint, but for now, you already know how the data you send is structured.

/**
* @Rest\Route("/posts", methods={"POST"})
*/
public function create(Request $request)
{
  $post = new Post();
  $post->setTitle($request->get('title'));
  $post->setContent($request->get('content'));

  $em = $this->getDoctrine()->getManager();
  $em->persist($post);
  $em->flush();

  $view = $this->view($post);
  return $this->handleView($view);
}

The code above creates a new Post object and sets the title and content with the data given within the request. After creating the object we should flush it to the database with our EntityManager using Doctrine.

Try it! You should get an error 🙂 We didn’t pass created_at to our object, which is required. But passing creation date would be kind of stupid, so we let Doctrine take care of this with Doctrine Lifecycle Events.

Add this to your Post class: @ORM\HasLifecycleCallbacks()
This makes sure lifecycle callbacks are supported.

/**
 * @ORM\Entity(repositoryClass="App\Repository\PostRepository")
 * @ORM\HasLifecycleCallbacks()
 */
class Post 
....

@ORM\PrePersist()

This annotation will make sure this is called before persisting the entity, so setting the date to the current timestamp.

/**
* @ORM\PrePersist()
*/
public function setCreatedAt(): self
{
    $this->createdAt = new \DateTime();
    return $this;
}

This your endpoint now with Postman, make sure you enter your JSON object as raw JSON (application/json) in the body.

Updating a post

To update your post, we should first find the original, since the id argument is passed to the update function we can fetch the Post by ID with the entity manager.

/**
* @Rest\Route("/posts/{id}", methods={"PUT"})
*/
public function update($id, PostRepository $postRepository)
{
  $post = $postRepository->find($id);
  $view = $this->view($post);
  return $this->handleView($view);
}

Test your function by using the endpoint /posts/1 and the PUT method using the previous or edited JSON we used in the POST example.

It should return your first post, without edited data, that’s fine! Now let’s update it!

/**
* @Rest\Route("/posts/{id}", methods={"PUT"})
*/
public function update($id, PostRepository $postRepository, Request $request)
{
  $post = $postRepository->find($id);
  $post->setTitle($request->get('title'));
  $post->setContent($request->get('content'));

  $this->getDoctrine()->getManager()->flush();
  $view = $this->view($post);
  return $this->handleView($view);
}

As you can see above, it’s pretty much the same as the POST method, but here we first fetch the existing Post object, we change the values and we flush the entity using the Doctrine manager.

Note: we don’t persist our entity anymore, because it’s already persisted ( it exists already ).

Executing the endpoint now, the data for the first post will be changed to the date submitted within your JSON. Easy!

Delete a post

Since our posts are quite useless now, let’s go and delete them! Just as we had the PUT request, we first need to fetch the existing post before we can delete it.

**
* @return \Symfony\Component\HttpFoundation\JsonResponse
* @Rest\Route("/posts/{id}", methods={"DELETE"})
*/
public function delete($id, PostRepository $postRepository)
{
  $post = $postRepository->find($id);

  $em = $this->getDoctrine()->getManager();
  $em->remove($post);
  $em->flush();

  return $this->json([
    'message' => "Post with ID $id has been deleted"
  ]);
}

You can see the remove method as the opposite of the persist method. Persist method makes Doctrine aware of a new entity while the remove method will delete the entity, but nothing actually happens until you flush it.

Give it a go! After executing the DELETE request on /posts/2 you should create another GET request on /posts, see what happened?

  • You updated your first post
  • You delete your second post

Conclusion

Well, you now created a fully working REST API with Symfony, tested it with Postman and validated your result at the end.

Good job!

If you liked this post or got stuck somewhere halfway let me know in the comments, always here to help!

Leave a Reply

avatar
  Subscribe  
Notify of