Symfony 7 CRUD Operations: A Step-by-Step

Alaattin Dagli
7 min readApr 25, 2024

--

Introduction

Symfony, a renowned PHP framework, has solidified its position as a cornerstone in web development. Its versatility extends beyond traditional web applications to encompass the creation of robust APIs, microservices, and more. Leveraging Symfony’s rich ecosystem empowers developers to streamline the development process, ensuring scalability, maintainability, and security.

When it comes to API development, Symfony shines brightly. Its comprehensive toolset, including robust routing, powerful dependency injection, and flexible configuration options, simplifies the creation of RESTful APIs. Symfony’s adherence to industry best practices and standards ensures interoperability, making it an ideal choice for building APIs that seamlessly integrate with various systems and platforms.

In essence, Symfony serves as a reliable ally in the quest for efficient and scalable API development. Its well-crafted architecture, coupled with extensive documentation and a vibrant community, makes it the framework of choice for developers seeking to deliver high-performance APIs.

Let’s jump right in and explore how to implement CRUD operations in Symfony 7. Get ready to discover the ins and outs of Symfony 7 development and create powerful APIs. Let’s get started!

Prerequisite:

  • Composer
  • Symfony CLI
  • PHP >= 8.2
  • MySQL

Step 1: Install Symfony 7

Begin by choosing the directory where you’d like Symfony to be installed. Next, execute the following command in your Terminal or CMD to start the installation process.

Install via composer:

composer create-project symfony/skeleton:"7.0.*" symfony-7-rest-api

Install via Symfony CLI:

symfony new symfony-7-rest-api --version="7.0.*"

Step 2: Create a New Project

symfony new myproject

Step 3: Install Packages

Once Symfony is installed, the next step is to install the essential packages for our REST API. As you proceed with the package installation, you’ll be prompted to execute the commands. Simply type ‘y’ to confirm and continue.

composer require symfony/orm-pack
composer require symfony/maker-bundle --dev
composer require doctrine/doctrine-migrations-bundle

Step 4: Configure Database

Next, navigate to the project directory and update the .env file with your database connection details:

.env

# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration

###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=YOUR_SECRET_KEY_LIKE_ba5212ed327ed12
###< symfony/framework-bundle ###

###> doctrine/doctrine-bundle ###
DATABASE_URL=mysql://db_user:db_password@localhost/db_name
###< doctrine/doctrine-bundle ###

Step 5: Create an Entity

php bin/console make:entity Product
<?php


namespace App\Entity;


use App\Repository\ProductRepository;
use Doctrine\ORM\Mapping as ORM;


#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;


#[ORM\Column(length: 255, nullable: true)]
private ?string $name;


#[ORM\Column(nullable: true)]
private ?string $description;


#[ORM\Column(nullable: true)]
private ?float $price;


public function getId(): ?int
{
return $this->id;
}


public function getName(): ?string
{
return $this->name;
}


public function setName(string $name): self
{
$this->name = $name;

return $this;
}


public function getDescription(): ?string
{
return $this->description;
}


public function setDescription(string $description): self
{
$this->description = $description;

return $this;
}


public function getPrice(): ?float
{
return $this->price;
}


public function setPrice(float $price): self
{
$this->price = $price;

return $this;
}
}

ProductRepository will be created automatically src/Repository location. You can add functions if you need like:

<?php


namespace App\Repository;


use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;


/**
* @extends ServiceEntityRepository<Product>
*
* @method Product|null find($id, $lockMode = null, $lockVersion = null)
* @method Product|null findOneBy(array $criteria, array $orderBy = null)
* @method Product[] findAll()
* @method Product[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ProductRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Product::class);
}


public function findByPriceGreaterThan($price)
{
return $this->createQueryBuilder('p')
->andWhere('p.price > :price')
->setParameter('price', $price)
->getQuery()
->getResult();
}
}

Step 6: Migrate entity to database

php bin/console make:migration
php bin/console doctrine:migrations:migrate

Step 7: Update Migration (Optional)

If you need to make changes to your entity later, update the migration file located in the migrations/ directory.

<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240423070224 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE product (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255), description VARCHAR(255), price DOUBLE PRECISION, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE product');
}
}

Step 8: Implement Controller Actions

symfony console make:controller ProductController

Now, implement the controller actions for each route. Here’s a simplified example:

<?php


// src/Controller/ProductController.php
namespace App\Controller;


use App\Entity\Product;
use App\Repository\ProductRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;


/**
* @Route("/api/products", name="product_api")
*/
class ProductController extends AbstractController
{
/**
* @Route("/", name="index", methods={"GET"})
*/
public function index(ProductRepository $productRepository, SerializerInterface $serializer): Response
{
$products = $productRepository->findAll();
$data = $serializer->serialize($products, 'json');

return new Response($data, 200, ['Content-Type' => 'application/json']);
}


/**
* @Route("/{id}", name="show", methods={"GET"})
*/
public function show(Product $product, SerializerInterface $serializer): Response
{
$data = $serializer->serialize($product, 'json');
return new Response($data, 200, ['Content-Type' => 'application/json']);
}


/**
* @Route("/", name="create", methods={"POST"})
*/
public function create(Request $request, EntityManagerInterface $entityManager, SerializerInterface $serializer): Response
{
$requestData = $request->getContent();

$product = $serializer->deserialize($requestData, Product::class, 'json');

if (!$product->getName() || !$product->getDescription() || !$product->getPrice()) {
return new JsonResponse(['error' => 'Missing required fields'], 400);
}

$entityManager->persist($product);
$entityManager->flush();

$data = $serializer->serialize($product, 'json');

return new JsonResponse(['message' => 'Product created!', 'product' => json_decode($data)], 201);
}


/**
* @Route("/{id}", name="update", methods={"PUT"})
*/
public function update(Product $product, Request $request, EntityManagerInterface $entityManager, SerializerInterface $serializer): Response
{
$requestData = $request->getContent();
$updatedProduct = $serializer->deserialize($requestData, Product::class, 'json');

$product->setName($updatedProduct->getName());
$product->setDescription($updatedProduct->getDescription());
$product->setPrice($updatedProduct->getPrice());

$entityManager->flush();

return new Response('Product updated!', 200);
}


/**
* @Route("/{id}", name="delete", methods={"DELETE"})
*/
public function delete(Product $product, EntityManagerInterface $entityManager): Response
{
$entityManager->remove($product);
$entityManager->flush();

return new Response('Product deleted!', 200);
}


/**
* @Route("/search/{id}", name="search_by_id", methods={"GET"})
*/
public function findById(ProductRepository $productRepository, int $id, SerializerInterface $serializer): Response
{
$product = $productRepository->find($id);

if (!$product) {
return new JsonResponse(['error' => 'Product not found'], 404);
}

$data = $serializer->serialize($product, 'json');

return new Response($data, 200, ['Content-Type' => 'application/json']);
}


/**
* @Route("/search/price/{price}", name="search_by_price", methods={"GET"})
*/
public function findByPrice(ProductRepository $productRepository, float $price, SerializerInterface $serializer): Response
{
$products = $productRepository->findBy(['price' => $price]);

if (!$products) {
return new JsonResponse(['error' => 'Products not found'], 404);
}

$data = $serializer->serialize($products, 'json');

return new Response($data, 200, ['Content-Type' => 'application/json']);
}


/**
* @Route("/search/description/{description}", name="search_by_description", methods={"GET"})
*/
public function findByDescription(ProductRepository $productRepository, string $description, SerializerInterface $serializer): Response
{
$products = $productRepository->findBy(['description' => $description]);

if (!$products) {
return new JsonResponse(['error' => 'Products not found'], 404);
}

$data = $serializer->serialize($products, 'json');

return new Response($data, 200, ['Content-Type' => 'application/json']);
}


/**
* @Route("/search/price-greater-than/{price}", name="search_by_price_greater_than", methods={"GET"})
*/
public function findByPriceGreaterThan(ProductRepository $productRepository, float $price, SerializerInterface $serializer): Response
{
$products = $productRepository->findByPriceGreaterThan($price);

if (!$products) {
return new JsonResponse(['error' => 'Products not found'], 404);
}

$data = $serializer->serialize($products, 'json');

return new Response($data, 200, ['Content-Type' => 'application/json']);
}
}

Step 9: Define API Routes

Add API routes for CRUD operations in config/routes.yaml:

controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute
product_api_index:
path: /api/products
controller: 'App\Controller\ProductController::index'
methods: ['GET']


product_api_show:
path: /api/products/{id}
controller: 'App\Controller\ProductController::show'
methods: ['GET']


product_api_create:
path: /api/products
controller: 'App\Controller\ProductController::create'
methods: ['POST']


product_api_update:
path: /api/products/{id}
controller: 'App\Controller\ProductController::update'
methods: ['PUT']


product_api_delete:
path: /api/products/{id}
controller: 'App\Controller\ProductController::delete'
methods: ['DELETE']


product_api_search_by_id:
path: /api/products/search/{id}
controller: 'App\Controller\ProductController::findById'
methods: ['GET']


product_api_search_by_price:
path: /api/products/search/price/{price}
controller: 'App\Controller\ProductController::findByPrice'
methods: ['GET']


product_api_search_by_description:
path: /api/products/search/description/{description}
controller: 'App\Controller\ProductController::findByDescription'
methods: ['GET']
product_api_search_price_greater_than:
path: /api/products/search/price-greater-than/{price}
controller: 'App\Controller\ProductController::findByPriceGreaterThan'
methods: [ 'GET' ]

Step 10: Run the Application

After finishing the steps above, you can now run your application by executing the command below:

symfony server:start

Step 11: Test The API

Finally, start the Symfony server and test your API using tools like Postman or curl:

API Endpoints

You can add endpoints after the url and test it. Url: http://localhost:8000

GET /api/products: Returns all products.
POST /api/products: Creates a new product.
PUT /api/products/{id}: Updates the product with the specified ID.
DELETE /api/products/{id}: Deletes the product with the specified ID.
GET /api/products/search/{id}: Retrieves a product by ID.
GET /api/products/search/price-greater-than/{price}: Retrieves products with a price greater than the specified value.

Get All Products (GET)

Create a new product (POST)

Update product (PUT)

DeleteProduct (DELETE)

Find Product by id (GET)

Find Products price greater than {price} (GET)

--

--