Federico Zacayan

Software Devloper.

Symfony 4

Unfortunately, Symfony is a framework flexible and it allows you to extend its functionality.

That is way we can create a RESFul API almost without writing code through CLI using API-PLATFORM.

But, let's go to a completely different history.

There was upon a time a group of people who used to live developing software WRITING code.

And that history starts like this...

Assuming you have an environment ready to deploy symfony 4, pointing your server at public/index.php, in the root folder of the project run the following composer commands.

#to create a basic symfony 
composer create-project symfony/skeleton .
#open the borwser at http://localhost/ it has to show you the welcome page
#to check the commands available
bin/console
#to add more functionality in the CLI
composer require symfony/maker-bundle --dev
#now you see more commands available
#to check the commands available (again)
bin/console
#to (try) create an entity
bin/console make:entity 
#to install orm (required)
composer require orm 
#to (finally) create an entity
bin/console make:entity 

Following the instructions you can create an entity named Product with `name` and `description`.

At this point, two files were created in /src/Entity folder and in /src/Repository folder.

Let's go to configure the database.

In the .env file just tweak the configuration you need.

DATABASE_URL=mysql://user:password@127.0.0.1:3306/db_name

Probably you can have not the database fresh.

Let's clean and generate the database and create the tables.

#to try to drop the database
bin/console doctrine:database:drop 
#to drop the database (again)
bin/console doctrine:database:drop --force
#to create the database
bin/console doctrine:database:create
#to create the table
bin/console doctrine:schema:create

Now you are able to create a controller

bin/console make:controller --no-template

You can named it ProductApiController. We think it is a good practice isolate the API controllers and URL from the rest of potential web project.

If everythin was good you can test the application in:

http://localhost/product/api

The resutl is:

{"message":"Welcome to your new controller!","path":"src\/Controller\/ProductApiController.php"}

We propose make some validations to the data to be triggered when we update or create a product.

So, we need to install validator.

composer require validator

Then we need to generate a folder named `validator` into the `config` fodler. (yes! it is weird. Why in `config` folder?)

Then, write the following validations in /config/validator/validation.yaml file.

#/config/validator/validation.yaml
App\Entity\Product:
    properties:
        name:
            - NotBlank: ~
        description:
            - NotBlank: ~
            - Email:
                message: The email "{{ value }}" is not a valid email.

As we will receive json into the request body, we will need to serialize it.

So, let't install serializer

#to install serializer
composer require serializer

As RESTFul API we need the same url to create, read, update and delete a resource.

So, we need to install rest-bundle.

#to install resbundle 
composer require friendsofsymfony/rest-bundle

This bundle include no official bundles, install them anyway.

Now, we can configure the routes mapping `product` with our controller and adding a prefix `api`:

#/config/routes.yaml
product:
    type      : rest
    resource  : App\Controller\ProductApiController
    prefix    : api

And we have to set some variables in /config/packages/fos_rest.yaml bundle.

# /config/packages/fos_rest.yaml
fos_rest:
    routing_loader:
        default_format: json
        include_format: false
    format_listener:
        rules:
            - { path: '^/api', priorities: ['json'], fallback_format: json}
            - { path: '^/', priorities: ['text/html', '*/*'], fallback_format: html, prefer_extension: true }
    
    body_converter:
        enabled: true
        validate: true
        validation_errors_argument: validationErrors

    exception:
        enabled: true
        exception_controller: 'fos_rest.exception.controller:showAction'
    
    view:
        view_response_listener: 'force'
        formats:
            json: true

If you test, at `http://localhost/api/products` you will see options-resolver is needed.

'body_converter.validate: true' requires OptionsResolver component installation ( composer require symfony/options-resolver )

So, we have to install it.

# to install options-resolver
composer require symfony/options-resolver
# to install framework-extra-bundle (required)
composer require sensio/framework-extra-bundle
# to install twig (required)
composer require twig

Probably, you will find errors related with twig and your /config/bundle.yaml looks like this.

FOS\RestBundle\FOSRestBundle::class => ['all' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],

But they have to look like this, in diferent order, placing twig before it is needed.

Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
FOS\RestBundle\FOSRestBundle::class => ['all' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],

Now, you can write php in /src/Controller/ProductApiController.php

<?php # /src/Controller/ProductApiController.php
namespace App\Controller;

use FOS\RestBundle\Controller\AbstractFOSRestController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use App\Entity\Product;
use App\Repository\ProductRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;

class ProductApiController extends AbstractFOSRestController
{
    
    public function __construct(
        ProductRepository $productRepository,
        EntityManagerInterface $entityManagerInterface
    ){
            $this->productRepository = $productRepository;
            $this->entityManager = $entityManagerInterface;
    }
    
    //Get all
    public function productsAction(){
        $data = $this->productRepository->findAll();
        return $this->view($data, Response::HTTP_OK);
    }
    
    //Get by ID
    public function getProductsAction(Product $product){
        return $this->view($product, Response::HTTP_OK);
    }
    
    //Create
    /**
     * @ParamConverter("product", converter="fos_rest.request_body")
     */
    public function postProductsAction(
    	Product $product, 
    	ConstraintViolationListInterface $validationErrors
    ){
        
        if (count($validationErrors) > 0) {
            return $this->view($validationErrors , Response::HTTP_BAD_REQUEST);
        }
        $this->entityManager->persist($product);
        $this->entityManager->flush();
        return $this->view($product , Response::HTTP_CREATED);
    }
    
    //Update some fields
    public function patchProductsAction(
    	Product $product, 
    	Request $request, 
    	ValidatorInterface $validator
    ){
        return $this->update($product, $request, $validator);
    }
    
    //Update all fields
    public function putProductsAction(
    	Product $product, 
    	Request $request, 
    	ValidatorInterface $validator
    ){
        return $this->update($product, $request, $validator);
    }
    
    //update
    private function update($product, $request, $validator){
        $json = $request->getContent();
        $newData = json_decode($json, true);
        
        foreach($newData as $propertyName => $value){
            $method = 'set'.ucfirst($propertyName);
            if(method_exists($product,$method)){
                $product->$method($value);
            }
        }
        
        $validationErrors = $validator->validate($product);
        if (count($validationErrors) > 0) {
            return $this->view($validationErrors , Response::HTTP_BAD_REQUEST);
        }
        
        $this->entityManager->persist($product);
        $this->entityManager->flush();
        return $this->view($product , Response::HTTP_OK);
    }
    
    //Delete
    public function deleteProductsAction(Product $product){
        $this->entityManager->remove($product);
        $this->entityManager->flush();
        return $this->view($product , Response::HTTP_OK);
    }
}

And that is all. The code is simple.

The complex part could be the update when the entity is populated generating the setters dinamically.

This was the way developers used create rest api applications before api-platform appear.

You can test the application. But check the routes availables.

bin/console debug:router

You can see:

 ------------------ -------- -------- ------ -------------------------- 
  Name               Method   Scheme   Host   Path                      
 ------------------ -------- -------- ------ -------------------------- 
  _twig_error_test   ANY      ANY      ANY    /_error/{code}.{_format}  
  products           GET      ANY      ANY    /api/products             
  get_products       GET      ANY      ANY    /api/products/{product}   
  post_products      POST     ANY      ANY    /api/products             
  patch_products     PATCH    ANY      ANY    /api/products/{product}   
  put_products       PUT      ANY      ANY    /api/products/{product}   
  delete_products    DELETE   ANY      ANY    /api/products/{product}   
 ------------------ -------- -------- ------ -------------------------- 

So, you can test on:

http://localhost/api/products