How To Develop A CRUD App with Symfony 6 & React

How To Develop A CRUD App with Symfony 6 & React

The initial installation, configuration, & implementation of CRUD operations using Symfony as the backend platform and React as the frontend platform.

Table of contents

No heading

No headings in the article.

Introduction

Symfony is an open-source PHP framework for building web-based applications. It was developed by Fabien Potencier in 2005 and is sponsored by SensioLabs.

These are the great in-built features of Symfony.

  • MVC (Model View Controller) based system
  • Easy URI Routing
  • Code reusability
  • Session handling
  • Error log handling
  • Security related to cross-site requests
  • Twig templating
  • Active community

Version 6 of Symfony was just released, and there are numerous changes to the directory structure and integration flow that make it easier to comprehend and construct a web application compared to earlier versions.

In this post, we will see the initial installation, configuration, and implementation of CRUD operations using Symfony as the backend platform and React as the frontend platform. Let’s begin with the installation.

Installation Steps of Symfony 6

Installation of Symfony can be done using either the composer OR we can use the Symfony CLI also. Now please go into the particular directory in which you want to install Symfony 6.

-> Please make sure that your composer version >= 2

Installation using Composer

composer create-project symfony/website-skeleton crud-react-app-symfony-6

Installation using CLI

symfony new crud-react-app-symfony-6 --full

Assuming you have successfully installed an application on your local system.

Database and Environment Configuration

Now, let’s configure the database and other variables to be used in our web application.

Here, we need to work with the env file for the global variables. When we install Symfony 6, we will have an env file at the root.

Make sure to set the APP_ENV and it should be a dev to enable the debug logs in case there are some errors in our programming.

Open the .env file from the root and update the values for the below parameters.

APP_ENV = dev

DATABASE_URL = Your database parameters

Example: DATABASE_URL="mysql://username:password@localhost:3306/database_name?serverVersion=5.7&charset=utf8mb4"

Now the application is in development mode and your database is configured and connected.

If you have created a database using phpMyAdmin, then it is fine, but in case the database is not created, you can create a database using a simple command for the same.

php bin/console doctrine:database:create

Now, let’s create a database table that we are going to use for our CRUD operation.

Create a Database Entity

An entity is nothing but a class that represents the database table and its columns; the entity will be created in the /src/Entity directory.

There is a command that needs to be executed to create an entity and columns for the same.

php bin/console make:entity
It/Command will ask for the name you want for the entity. For our demo, we are using “Employee.”
Class name of the entity to create or update (e.g. GentlePopsicle):
> Employee

Now it will ask to create columns along with the type, so for our demo, I am adding the columns here.

New property name (press <return> to stop adding fields):
> fullname

 Field type (enter ? to see all types) [string]:
> string

 Field length [255]:
> 255

 Can this field be null in the database (nullable) (yes/no) [no]:
> no

We need to follow these steps for all the columns that we are going to use in our CRUD demo. So here are the field lists that need to be followed.

email, password, contact, degree , designation, address

Here, contact and address are not required fields that we are going to consider in our demo, so you have to set them nullable. Yes.

Assuming entity creation is done, let’s migrate the entity so it will be used when you set up the demo on another server or reuse it in the future.

Create Migration

php bin/console make:migration

The above command creates the migration file in the migrations folder in your project root.

We need to execute one more command to migrate the entity in our database.

php bin/console doctrine:migrations:migrate

That’s all with entities and migration. You can check your database; there should be an employee table and the above-defined columns.

Now, we are going to create an API controller which will be used on the react side later in the demo.

Create Controller

The controller plays the middle man role, with each request and response being handled via the controller only.

Execute the below command to create our API controller. It will be created in the src/Controller folder.

php bin/console make:controller EmployeeController

Once the controller is created, we need to add the required API functions to the controller and it will be used in our CRUD operations.

src/Controller/EmployeeController.php
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\Persistence\ManagerRegistry;
use App\Entity\Employee;
/**
 * @Route("/api", name="api_")
 */
class EmployeeController extends AbstractController
{
    /**
     * @Route("/employee", name="app_employee", methods={"GET"})
     */
    public function index(ManagerRegistry $doctrine): Response
    {
        $employees = $doctrine
            ->getRepository(Employee::class)
            ->findAll();
        $data = [];
        foreach ($employees as $employee) {
            $data[] = [
               'id'         => $employee->getId(),
               'fullname'   => $employee->getFullname(),
               'email'      => $employee->getEmail(),
                'password'  => $employee->getPassword(),
               'degree'     => $employee->getDegree(),
               'designation'=> $employee->getDesignation(),
               'address'    => $employee->getAddress(),
               'contact'    => $employee->getContact(),
            ];
        }
        return $this->json($data);
    }
    /**
     * @Route("/employee", name="add_employee", methods={"POST"})
     */
    public function addEmployee(ManagerRegistry $doctrine, Request $request): Response
    {
        $entityManager = $doctrine->getManager();
        $employee = new Employee();
        $employee->setFullname($request->request->get('fullname'));
        $employee->setEmail($request->request->get('email'));
        $employee->setPassword($request->request->get('password'));
        $employee->setDegree($request->request->get('degree'));
        $employee->setDesignation($request->request->get('designation'));
        $employee->setAddress($request->request->get('address'));
        $employee->setContact($request->request->get('contact'));
        $entityManager->persist($employee);
        $entityManager->flush();
        return $this->json('New Employee has been added successfully with id ' . $employee->getId());
    }
    /**
     * @Route("/employee/{id}", name="employee_show", methods={"GET"})
     */
    public function showEmployee(ManagerRegistry $doctrine, int $id): Response
    {
        $employee = $doctrine->getRepository(Employee::class)->find($id);
        if (!$employee) {
            return $this->json('No Employee found for id' . $id, 404);
        }
        $data =  [
            'id'        => $employee->getId(),
            'fullname'  => $employee->getFullname(),
            'email'      => $employee->getEmail(),
            'degree'     => $employee->getDegree(),
            'designation'=> $employee->getDesignation(),
            'address'    => $employee->getAddress(),
            'contact'    => $employee->getContact(),
            'password'  => $employee->getPassword(),
        ];
        return $this->json($data);
    }
     /**
     * @Route("/employee/{id}", name="employee_edit", methods={"PUT", "PATCH"})
     */
    public function editEmployee(ManagerRegistry $doctrine, Request $request, int $id): Response
    {
        $entityManager = $doctrine->getManager();
        $employee = $entityManager->getRepository(Employee::class)->find($id);
        if(!$employee){
            return $this->json('No Employee found for id' . $id, 404);
        }
        $content = json_decode($request->getContent());
        $employee->setFullname($content->fullname);
        $employee->setEmail($content->email);
        $employee->setPassword($content->password);
        $employee->setDegree($content->degree);
        $employee->setDesignation($content->designation);
        $employee->setAddress($content->address);
        $employee->setContact($content->contact);
        $entityManager->flush();
        $data = [
            'id'        => $employee->getId(),
            'name'       => $employee->getFullname(),
            'password'   => $employee->getPassword(),
            'email'      => $employee->getEmail(),
            'degree'     => $employee->getDegree(),
            'designation'=> $employee->getDesignation(),
            'address'    => $employee->getAddress(),
            'contact'    => $employee->getContact(),
            'password'  => $employee->getPassword(),
        ];
        return $this->json($data);
    }
     /**
     * @Route("/employee/{id}", name="employee_delete", methods={"DELETE"})
     */
    public function delete(ManagerRegistry $doctrine, int $id): Response
    {
        $entityManager = $doctrine->getManager();
        $employee   = $entityManager->getRepository(Employee::class)->find($id);

        if (!$employee) {
            return $this->json('No Employee found for id' . $id, 404);
        }
        $entityManager->remove($employee);
    $entityManager->flush();
    return $this->json('Deleted a Employee successfully with id ' . $id);
   }
}

Now, let’s create a controller for the React application.

Create a React App Controller

Again, same as it is, we just need to execute a command to create a controller, but here the controller name is different.

php bin/console make:controller ReactappController
src/Controller/ReactappController.php
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class ReactappController extends AbstractController
{
    /**
     * @Route("/{reactRouting}", name="app_home", requirements={"reactRouting"="^(?!api).+"}, defaults={"reactRouting": null})
     */
    public function index(): Response
    {
        return $this->render('reactapp/index.html.twig');
    }
}

Let’s work on the template files now. We need to start with our base template and update it as per our requirements.

Work with Template Files

/templates/base.html.twig
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>{% block title %}Symfony CRUD application using React {% endblock %}</title>
    {% block stylesheets %}
        {{ encore_entry_link_tags('app') }}
    {% endblock %}
   <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
   <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}
    {{ encore_entry_script_tags('app') }}
{% endblock %}
</body>
</html>

After working on the base template, we need to update the React app template that was already created.

templates/reactapp/index.html.twig
{% extends 'base.html.twig' %}
 {% block body %}
    <div id="app"></div>
 {% endblock %}

We are done with the backend configuration and have set up all the necessary assets, so let’s move to the frontend.

Here, we need to install the required dependencies for the react before we start with the programming and there are some sets of commands we need to execute.

Install React Dependencies

Here we need to install the Encore, which is a Symfony bundle, and it will install the PHP and JavaScript dependencies.

composer require symfony/webpack-encore-bundle
yarn install

Now we are going to install React dependencies.

yarn add @babel/preset-react --dev
yarn add react-router-dom
yarn add --dev react react-dom prop-types axios
yarn add @babel/plugin-proposal-class-properties @babel/plugin-transform-runtime

Additionally, as we are using React as our frontend technology, we are going to use SweetAlert instead of the core dialog box to make the alert message more beautiful.

npm install sweetalert2

Once dependencies are installed, we need to update the webpack configuration file as per our demo. The webpack.config.js file is already in the root of the project directory.

Webpack.config.js
const Encore = require('@symfony/webpack-encore');
// Manually configure the runtime environment if it is not already configured by the "encore" command.
// It's useful when you use tools that rely on the webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
    Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}  
Encore
    // directory where compiled assets will be stored
    .setOutputPath('public/build/')
    // public path used by the web server to access the output path
    .setPublicPath('/build')
    .enableReactPreset()
    // only needed for CDN's or sub-directory deploy
    //.setManifestKeyPrefix('build/')

    .addEntry('app', './assets/app.js')
    // enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
    .enableStimulusBridge('./assets/controllers.json')

    // When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
    .splitEntryChunks()
    // will require an extra script tag for runtime.js
    // but, you probably want this, unless you're building a single-page app
    .enableSingleRuntimeChunk()

    .cleanupOutputBeforeBuild()
    .enableBuildNotifications()
    .enableSourceMaps(!Encore.isProduction())
    // enables hashed filenames (e.g. app.abc123.css)
    .enableVersioning(Encore.isProduction())

    .configureBabel((config) => {
        config.plugins.push('@babel/plugin-proposal-class-properties');
    })

    // enables @babel/preset-env polyfills
    .configureBabelPresetEnv((config) => {
        config.useBuiltIns = 'usage';
        config.corejs = 3;
    })  
;  
module.exports = Encore.getWebpackConfig();

Now, let’s create the react application necessary files along with the javascript and templates in our root /assets directory, but before that, we start the watcher so it will show the errors in case we missed anything.

We need to run this command to start development watchers.

yarn encore dev --watch

Here is the screenshot of the files that we need in our react application.

0_1XWbMUCc41ITVuC4.png

These directories and files are based on the demo, you can adjust in your way as per your requirement for your application.

Now, let’s start with the Main.js file as you see in your screenshot.

assets/Main.js
import React from 'react';
import {StrictMode } from "react";
import {createRoot } from "react-dom/client";
import {BrowserRouter as Router, Routes, Route } from "react-router-dom";
import ListEmployee from "./pages/ListEmployee"
import AddEmployee from "./pages/AddEmployee"
import EditEmployee from "./pages/EditEmployee"
import ViewEmployee from "./pages/ViewEmployee"

function Main() {
    return (
        <Router>
            <Routes>
                <Route path="/"  element={<ListEmployee/>} />
                <Route path="/addEmployee"  element={<AddEmployee/>} />
                <Route path="/editEmployee/:id"  element={<EditEmployee/>} />
                <Route path="/showEmployee/:id"  element={<ViewEmployee/>} />
            </Routes>
        </Router>
    );
}
export default Main;  
if (document.getElementById('app')) {
    const rootElement = document.getElementById("app");
    const root = createRoot(rootElement);
    root.render(
        <StrictMode>
            <Main />
        </StrictMode>
    );
}

Let’s create one common file which is a react template to load the layout of the content area.

/assets/components/Layout.js

import React from 'react'; const Layout =({children}) =>{ return(

{children}
) } export default Layout; let’s modify assets/app.js the file to include the main.js file require('./Main');

Now we need to work on CRUD operation pages for the React application, so let’s create a pages folder inside the /assets folder.

So here we will create the add, edit, view, and listing pages for our CRUD operation demo as shown above in the screenshot.

/assets/pages/AddEmployee.js
import React, {useState} from 'react';
import { Link } from "react-router-dom";
import Layout from "../components/Layout"
import Swal from 'sweetalert2'
import axios from 'axios';
function AddEmployee() {
    const [fullname, setFullName] = useState('');
    const [email, setEmail] = useState('')
    const [degree, setDegree] = useState('')
    const [password, setPassword] = useState('')
    const [contact, setContact] = useState('')
    const [designation, setDesignation] = useState('')
    const [address, setAddress] = useState('')
    const [isSaving, setIsSaving] = useState(false)
const saveRecord = () => {
        setIsSaving(true);
        let formData = new FormData()
        formData.append("fullname", fullname)
        formData.append("email", email)
        formData.append("password", password)
        formData.append("contact", contact)
        formData.append("degree", degree)
        formData.append("designation", designation)
        formData.append("address", address)
        if(fullname == "" || email == "" || password==""){
            Swal.fire({
                icon: 'error',
                title: 'Name, Email, Password are required fields.',
                showConfirmButton: true,
                showCloseButton: true,
            })
            setIsSaving(false)
        }else{
            axios.post('/api/employee', formData)
              .then(function (response) {
                Swal.fire({
                    icon: 'success',
                    title: 'Employee has been added successfully!',
                    showConfirmButton: true,
                })
                setIsSaving(false);
                setFullName('')
                setPassword('')
                setEmail('')
                setDegree('')
                setDesignation('')
                setContact('')
                setAddress('')
              })
              .catch(function (error) {
                Swal.fire({
                    icon: 'error',
                    title: 'Oops, Something went wrong!',
                    showConfirmButton: true,

                })
                setIsSaving(false)
              });
        }
    }
    return (
        <Layout>
            <div className="container">
                <h2 className="text-center mt-5 mb-3">Add Employee</h2>
                <div className="card">
                    <div className="card-header">
                        <Link 
                            className="btn btn-info float-left"
                            to="/">Back To Employee List
                        </Link>
                    </div>
                    <div className="card-body">
                        <form>
                            <div className="form-group">
                                <label htmlFor="name">Name</label>
                                <input 
                                    onChange={(event)=>{setFullName(event.target.value)}}
                                    value={fullname}
                                    type="text"
                                    className="form-control"
                                    id="fullname"
                                    name="fullname" required/>
                            </div>
                            <div className="form-group">
                                <label htmlFor="name">Email</label>
                                <input 
                                    onChange={(event)=>{setEmail(event.target.value)}}
                                    value={email}
                                    type="email"
                                    className="form-control"
                                    id="email"
                                    name="email" required/>
                            </div>
                            <div className="form-group">
                                <label htmlFor="password">Password</label>
                                <input 
                                    onChange={(event)=>{setPassword(event.target.value)}}
                                    value={password}
                                    type="password"
                                    className="form-control"
                                    id="password"
                                    name="password" required/>
                            </div>
                            <div className="form-group">
                                <label htmlFor="degree">Degree</label>
                                <input 
                                    onChange={(event)=>{setDegree(event.target.value)}}
                                    value={degree}
                                    type="text"
                                    className="form-control"
                                    id="degree"
                                    name="degree" required/>
                            </div>
                            <div className="form-group">
                                <label htmlFor="designation">Designation</label>
                                <input 
                                    onChange={(event)=>{setDesignation(event.target.value)}}
                                    value={designation}
                                    type="text"
                                    className="form-control"
                                    id="designation"
                                    name="designation" required/>
                            </div>
                            <div className="form-group">
                                <label htmlFor="contact">Contact</label>
                                <input 
                                    onChange={(event)=>{setContact(event.target.value)}}
                                    value={contact}
                                    type="text"
                                    className="form-control"
                                    id="contact"
                                    name="contact" required/>
                            </div>
                            <div className="form-group">
                                <label htmlFor="address">Address</label>
                                <input 
                                    onChange={(event)=>{setAddress(event.target.value)}}
                                    value={address}
                                    type="text"
                                    className="form-control"
                                    id="address"
                                    name="address" required/>
                            </div>
                            <button 
                                disabled={isSaving}
                                onClick={saveRecord} 
                                type="button"
                                className="btn btn-primary mt-3">
                                Save
                            </button>
                        </form>
                    </div>
                </div>
            </div>
        </Layout>
    );
}
export default AddEmployee;
/assets/pages/ListEmployee.js
import React,{ useState, useEffect} from 'react';
import { Link } from "react-router-dom";
import Layout from "../components/Layout"
import Swal from 'sweetalert2'
import axios from 'axios';
function ListEmployee() {
    const  [listEmployee, setEmployeeList] = useState([])
    useEffect(() => {
        fetchEmployeeList()
    }, [])
    const fetchEmployeeList = () => {
        axios.get('/api/employee')
        .then(function (response) {
          setEmployeeList(response.data);
        })
        .catch(function (error) {
          console.log(error);
        })
    }
    const deleteRecord = (id) => {
        Swal.fire({
            title: 'Are you sure you want to delete this employee?',
            icon: 'warning',
            showCancelButton: true,
            confirmButtonColor: '#3085d6',
            cancelButtonColor: '#d33',
            confirmButtonText: 'Yes'
          }).then((result) => {
            if (result.isConfirmed) {
                axios.delete(`/api/employee/${id}`)
                .then(function (response) {
                    Swal.fire({
                        icon: 'success',
                        title: 'Employee has been deleted successfully!',
                        showConfirmButton: false,
                        timer: 1000
                    })
                    fetchEmployeeList()
                })
                .catch(function (error) {
                    Swal.fire({
                        icon: 'error',
                        title: 'Oops, Something went wrong!',
                        showConfirmButton: false,
                        timer: 1000
                    })
                });
            }
          })
    }
    return (
        <Layout>
           <div className="container">
            <h2 className="text-center mt-5 mb-3">Employees</h2>
                <div className="card">
                    <div className="card-header">
                        <Link 
                            className="btn btn-primary"
                            to="/addEmployee">Add Employee
                        </Link>
                    </div>
                    <div className="card-body">
                        <table className="table table-striped table-hover table-bordered border-primary">
                            <thead>
                                <tr>
                                    <th>Name</th>
                                    <th>Email</th>
                                    <th>Degree</th>
                                    <th>Designation</th>
                                    <th>Contact</th>
                                    <th>Address</th>
                                    <th width="250px">Action</th>
                                </tr>
                            </thead>
                            <tbody>
                                {listEmployee.map((employee, key)=>{
                                    return (
                                        <tr key={key}>
                                            <td>{employee.fullname}</td>
                                            <td>{employee.email}</td>
                                            <td>{employee.degree}</td>
                                            <td>{employee.designation}</td>
                                            <td>{employee.contact}</td>
                                            <td>{employee.address}</td>
                                            <td>
                                                <Link
                                                    to={`/showEmployee/${employee.id}`}
                                                    className="btn btn-info mx-1">
                                                    View
                                                </Link>
                                                <Link
                                                    className="btn btn-success mx-1"
                                                    to={`/editEmployee/${employee.id}`}>
                                                    Edit
                                                </Link>
                                                <button 
                                                    onClick={()=>deleteRecord(employee.id)}
                                                    className="btn btn-danger mx-1">
                                                    Delete
                                                </button>
                                            </td>
                                        </tr>
                                    )
                                })}
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </Layout>
    );
}
export default ListEmployee;
/assets/pages/ViewEmployee.js
import React, {useState, useEffect} from 'react';
import { Link, useParams } from "react-router-dom";
import Layout from "../components/Layout"
import axios from 'axios';
function ViewEmployee() {
    const [id, setId] = useState(useParams().id)
    const [employee, setEmployee] = useState({fullname:"", email:"", contact:"",degree:"",designation:"",address:""})
    useEffect(() => {
        axios.get(`/api/employee/${id}`)
        .then(function (response) {
          setEmployee(response.data)
        })
        .catch(function (error) {
          console.log(error);
        })
    }, [])
    return (
        <Layout>
           <div className="container">
            <h2 className="text-center mt-5 mb-3">View Employee</h2>
                <div className="card">
                    <div className="card-header">
                        <Link 
                            className="btn btn-info float-left"
                            to="/"> Back To Employee List
                        </Link>
                    </div>
                    <div className="card-body">
                        <b className="text-muted">Name:</b>
                        <p>{employee.fullname}</p>
                        <b className="text-muted">Email:</b>
                        <p>{employee.email}</p>
                        <b className="text-muted">Contact:</b>
                        <p>{employee.contact}</p>
                        <b className="text-muted">Degree:</b>
                        <p>{employee.degree}</p>
                        <b className="text-muted">Designation:</b>
                        <p>{employee.designation}</p>
                        <b className="text-muted">Address:</b>
                        <p>{employee.address}</p>
                    </div>
                </div>
            </div>
        </Layout>
    );
}
export default ViewEmployee;

We are set to move forward with the real output now for our CRUD Symfony + React application.

We need to start the server now by executing the command.

Run the Application

Read more about running the application on this: How To Develop A CRUD App with Symfony 6 and React