Symfony REST API (without FosRestBundle) using JWT authentication: PART 1

Hicham BEN KACHOUD
7 min readJan 3, 2020

In this first article we are going to explore the simplest way to implement a REST API in symfony project without FosRestBundle. I will add the second article containing JWT authentication. Before we create this implementation, we need firstly understand what REST actually means.

1- What is Rest?

Rest as Representational State transfer is an architectural style for developing web services. It is something that can not be ignored because there is a big need for creating Restful applications in today’s ecosystem. This might be due to the continued climb of JavaScript and these frameworks.

Rest API should use HTTP protocol. This mean that when the client makes any request to this web service, it can specify any of the normal HTTP verbs of GET, POST, DELETE and PUT. Below is what would happen If the respective verbs were sent by the client.

- GET: This would be used to get the list of resources or the details of one.
- POST: this would be used to create a new resource.
- PUT: This would be used to update an existing resource.
- DELETE: This would be used to delete an existing resource.

Rest is stateless and this mean that the server side doesn’t hold any request state. The state should be kept on the client side (example of using JWT for authentication, we are going to secure our RestApi using this). So, when using authentication in our Rest Api, we need to send the authentication header in order to get a correct response in a stateless way.

2- Create a symfony project:

Firstly, we suppose you have installed php and the composer package manager to create a new symfony project. After that, create a new project using the following command in the terminal:

composer create-project symfony/skeleton demo_rest_api

create symfony project

We are using a basic symphony skeleton that is recommended for microservices and APIs. Here is what directory structure looks like:

project structure

Config: contains all bundle configuration and a list of bundle in the bundle.php.

Public: provides access to the application via index.php

Src: contains all controller, models and services.

Var: contains system logs and cache files.

Vendor: contains all external packages

Now, let’s install some necessary bundles with composer:

composer require symfony/orm-pack

composer require sensio/framework-extra-bundle

we installed the sensio/framework-extra-bundle that will help us to make code easier by using annotations for defining our routes.

We need also to install symphony/orm-pack for integration with Doctrine ORM in order to connect with a database. Bellow is the configuration of database I created which may be set in the .env file.

.env configuration file

Now, let’s create our first entity. Create a new file called Post.php inside the src/Entity folder.

<?php
namespace
App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
*
@ORM\Entity
* @ORM\Table(name="post")
*
@ORM\HasLifecycleCallbacks()
*/
class Post implements \JsonSerializable {
/**
*
@ORM\Column(type="integer")
*
@ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

/**
*
@ORM\Column(type="string", length=100
*
*/
private $name;
/**
*
@ORM\Column(type="text")
*/
private $description;

/**
*
@ORM\Column(type="datetime")
*/
private $create_date;

/**
*
@return mixed
*/
public function getId()
{
return $this->id;
}
/**
*
@param mixed $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
*
@return mixed
*/
public function getName()
{
return $this->name;
}
/**
*
@param mixed $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
*
@return mixed
*/
public function getDescription()
{
return $this->description;
}
/**
*
@param mixed $description
*/
public function setDescription($description)
{
$this->description = $description;
}

/**
*
@return mixed
*/
public function getCreateDate(): ?\DateTime
{
return $this->create_date;
}

/**
*
@param \DateTime $create_date
*
@return Post
*/
public function setCreateDate(\DateTime $create_date): self
{
$this->create_date = $create_date;
return $this;
}

/**
*
@throws \Exception
*
@ORM\PrePersist()
*/
public function beforeSave(){

$this->create_date = new \DateTime('now', new \DateTimeZone('Africa/Casablanca'));
}



/**
* Specify data which should be serialized to JSON
*
@link https://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
*
@since 5.4.0
*/
public function jsonSerialize()
{
return [
"name" => $this->getName(),
"description" => $this->getDescription()
];
}
}

And after run the command: php bin/console doctrine:schema:create to create a database table according to our Post entity.

Now, let’s create a PostController.php where we will add all methods interacting with api. It should be placed inside the folder src/Controller.

<?php
/**
* Created by PhpStorm.
* User: hicham benkachoud
* Date: 02/01/2020
* Time: 22:44
*/

namespace App\Controller;


use App\Entity\Post;
use App\Repository\PostRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

/**
* Class PostController
*
@package App\Controller
*
@Route("/api", name="post_api")
*/
class PostController extends AbstractController
{
/**
*
@param PostRepository $postRepository
*
@return JsonResponse
*
@Route("/posts", name="posts", methods={"GET"})
*/
public function getPosts(PostRepository $postRepository){
$data = $postRepository->findAll();
return $this->response($data);
}

/**
*
@param Request $request
*
@param EntityManagerInterface $entityManager
*
@param PostRepository $postRepository
*
@return JsonResponse
*
@throws \Exception
*
@Route("/posts", name="posts_add", methods={"POST"})
*/
public function addPost(Request $request, EntityManagerInterface $entityManager, PostRepository $postRepository){

try{
$request = $this->transformJsonBody($request);

if (!$request || !$request->get('name') || !$request->request->get('description')){
throw new \Exception();
}

$post = new Post();
$post->setName($request->get('name'));
$post->setDescription($request->get('description'));
$entityManager->persist($post);
$entityManager->flush();

$data = [
'status' => 200,
'success' => "Post added successfully",
];
return $this->response($data);

}catch (\Exception $e){
$data = [
'status' => 422,
'errors' => "Data no valid",
];
return $this->response($data, 422);
}

}


/**
*
@param PostRepository $postRepository
*
@param $id
*
@return JsonResponse
*
@Route("/posts/{id}", name="posts_get", methods={"GET"})
*/
public function getPost(PostRepository $postRepository, $id){
$post = $postRepository->find($id);

if (!$post){
$data = [
'status' => 404,
'errors' => "Post not found",
];
return $this->response($data, 404);
}
return $this->response($post);
}

/**
*
@param Request $request
*
@param EntityManagerInterface $entityManager
*
@param PostRepository $postRepository
*
@param $id
*
@return JsonResponse
*
@Route("/posts/{id}", name="posts_put", methods={"PUT"})
*/
public function updatePost(Request $request, EntityManagerInterface $entityManager, PostRepository $postRepository, $id){

try{
$post = $postRepository->find($id);

if (!$post){
$data = [
'status' => 404,
'errors' => "Post not found",
];
return $this->response($data, 404);
}

$request = $this->transformJsonBody($request);

if (!$request || !$request->get('name') || !$request->request->get('description')){
throw new \Exception();
}

$post->setName($request->get('name'));
$post->setDescription($request->get('description'));
$entityManager->flush();

$data = [
'status' => 200,
'errors' => "Post updated successfully",
];
return $this->response($data);

}catch (\Exception $e){
$data = [
'status' => 422,
'errors' => "Data no valid",
];
return $this->response($data, 422);
}

}


/**
*
@param PostRepository $postRepository
*
@param $id
*
@return JsonResponse
*
@Route("/posts/{id}", name="posts_delete", methods={"DELETE"})
*/
public function deletePost(EntityManagerInterface $entityManager, PostRepository $postRepository, $id){
$post = $postRepository->find($id);

if (!$post){
$data = [
'status' => 404,
'errors' => "Post not found",
];
return $this->response($data, 404);
}

$entityManager->remove($post);
$entityManager->flush();
$data = [
'status' => 200,
'errors' => "Post deleted successfully",
];
return $this->response($data);
}





/**
* Returns a JSON response
*
*
@param array $data
*
@param $status
*
@param array $headers
*
@return JsonResponse
*/
public function response($data, $status = 200, $headers = [])
{
return new JsonResponse($data, $status, $headers);
}

protected function transformJsonBody(\Symfony\Component\HttpFoundation\Request $request)
{
$data = json_decode($request->getContent(), true);

if ($data === null) {
return $request;
}

$request->request->replace($data);

return $request;
}

}

Here we defined the five routes:

  • GET /api/posts: will return a list of the Posts.
get all posts api
  • POST /api/posts: will create a new Post.
add new post api
  • GET /api/posts/id: will return a Post matching the specific identifer
get single post
  • PUT /api/posts/id : will update the Post.
update post

This is the result after update:

post after updated
  • DELETE /api/posts/id: will delete the Post.
delete post

This is the result of get all posts after delete the post with the ID 3:

all posts after delete

The code source can be found: Here

Conclusion:

So now we understand what REST and Restful is. A restful API should be stateless. We know how to create a Restful application using HTTP verbs. All in all we now have a good understanding of REST and are ready to create Restful applications in a professional way.

Next Article we will take a look at how to secure the restful API using JWT authentication.

--

--