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