Contents
Good day fellow dev, In this blog we will be developing a CRUD app in Symfony 6 and React. We will be using react as our front-end and we will be creating an API for the CRUD operations. Before we proceed, let’s have a few discussions.
What is Symfony? Symfony is a PHP framework used to develop web applications, APIs, microservices, and web services. Symfony is one of the leading PHP frameworks for creating websites and web applications.
React or also called React.js or Reactjs is a free and open-source JavaScript library used for building user interfaces(UI). It is one of the most popular JavaScript libraries for building the front-end. React is created by Facebook and maintained by Facebook.
Step 1: Install Symfony 6
First, select a folder that you want Symfony to be installed then execute this command on Terminal or CMD to install:
Install via composer:
composer create-project symfony/website-skeleton symfony-6-react-crud
Install via Symfony CLI:
symfony new symfony-6-react-crud --full
Step 2: Set Database Configuration
We must configure our database to avoid errors. Open the .env file and set database configuration. We will be using MySQL in this tutorial. Uncomment the DATABASE_URL variable for MySQL and updates its configs. Make sure you commented out the other DATABASE_URL variables.
.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.
#
# 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=e0710317861221371d185cc932acd15b
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8"
###< doctrine/doctrine-bundle ###
After configuring the database, execute this command to create the database:
php bin/console doctrine:database:create
Step 3: Create Entity and Migration
Entity– is a class that represents a database table.
Migration – like version control for the database that allows us to modify and share database schema to your team.
Execute this command to create an Entity:
php bin/console make:entity
Class name of the entity to create or update (e.g. GentlePopsicle):
> Project
Project
created: src/Entity/Project.php
created: src/Repository/ProjectRepository.php
Entity generated! Now let's add some fields!
You can always add more fields later manually or by re-running this command.
New property name (press <return> to stop adding fields):
> name
Field type (enter ? to see all types) [string]:
> string
string
Field length [255]:
> 255
Can this field be null in the database (nullable) (yes/no) [no]:
> no
updated: src/Entity/Project.php
Add another property? Enter the property name (or press <return> to stop adding fields):
> description
Field type (enter ? to see all types) [string]:
> text
text
Can this field be null in the database (nullable) (yes/no) [no]:
> no
updated: src/Entity/Project.php
Add another property? Enter the property name (or press <return> to stop adding fields):
>
Success!
Next: When you're ready, create a migration with php bin/console make:migration
Now that we have finished creating an entity, we will then create a migration:
php bin/console make:migration
This will create a migration file, inside the migration file containing SQL. we will then run the SQL using this command:
php bin/console doctrine:migrations:migrate
Step 4: Create API Controller
A Controller is the one responsible for receiving Requests and returning Response.
Execute this command:
php bin/console make:controller ProjectController
After creating the Controller add these lines of codes:
src/Controller/ProjectController.php
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use App\Entity\Project;
/**
* @Route("/api", name="api_")
*/
class ProjectController extends AbstractController
{
/**
* @Route("/project", name="project_index", methods={"GET"})
*/
public function index(ManagerRegistry $doctrine): Response
{
$products = $doctrine
->getRepository(Project::class)
->findAll();
$data = [];
foreach ($products as $product) {
$data[] = [
'id' => $product->getId(),
'name' => $product->getName(),
'description' => $product->getDescription(),
];
}
return $this->json($data);
}
/**
* @Route("/project", name="project_new", methods={"POST"})
*/
public function new(ManagerRegistry $doctrine, Request $request): Response
{
$entityManager = $doctrine->getManager();
$project = new Project();
$project->setName($request->request->get('name'));
$project->setDescription($request->request->get('description'));
$entityManager->persist($project);
$entityManager->flush();
return $this->json('Created new project successfully with id ' . $project->getId());
}
/**
* @Route("/project/{id}", name="project_show", methods={"GET"})
*/
public function show(ManagerRegistry $doctrine, int $id): Response
{
$project = $doctrine->getRepository(Project::class)->find($id);
if (!$project) {
return $this->json('No project found for id' . $id, 404);
}
$data = [
'id' => $project->getId(),
'name' => $project->getName(),
'description' => $project->getDescription(),
];
return $this->json($data);
}
/**
* @Route("/project/{id}", name="project_edit", methods={"PUT", "PATCH"})
*/
public function edit(ManagerRegistry $doctrine, Request $request, int $id): Response
{
$entityManager = $doctrine->getManager();
$project = $entityManager->getRepository(Project::class)->find($id);
if (!$project) {
return $this->json('No project found for id' . $id, 404);
}
$content = json_decode($request->getContent());
$project->setName($content->name);
$project->setDescription($content->description);
$entityManager->flush();
$data = [
'id' => $project->getId(),
'name' => $project->getName(),
'description' => $project->getDescription(),
];
return $this->json($data);
}
/**
* @Route("/project/{id}", name="project_delete", methods={"DELETE"})
*/
public function delete(ManagerRegistry $doctrine, int $id): Response
{
$entityManager = $doctrine->getManager();
$project = $entityManager->getRepository(Project::class)->find($id);
if (!$project) {
return $this->json('No project found for id' . $id, 404);
}
$entityManager->remove($project);
$entityManager->flush();
return $this->json('Deleted a project successfully with id ' . $id);
}
}
Step 5: Create SPA Controller
We will then create a controller that will load the react app. To create a controller execute the code below:
php bin/console make:controller SpaController
After creating the controller, update the index method:
/src/Controller/SpaController.php
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SpaController extends AbstractController
{
/**
* @Route("/{reactRouting}", name="app_home", requirements={"reactRouting"="^(?!api).+"}, defaults={"reactRouting": null})
*/
public function index()
{
return $this->render('spa/index.html.twig');
}
}
Step 6: Update View Files
Let’s update now the view files. First, let’s update the /templates/base.html.twig file. Copy the code below:
/templates/base.html.twig
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Symfony React SPA!{% 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>
Then, lest update the view file that was also created when we create the spa controller:
/templates/spa/index.html.twig
{% extends 'base.html.twig' %}
{% block body %}
<div id="app"></div>
{% endblock %}
Now that we have setup the back-end lets proceed on the front-end.
Step 7: Install Encore and React Dependencies
We will now install the Symfony Webpack Encore Bundle. Run these commands to install the PHP and JavaScript Dependencies:
composer require symfony/webpack-encore-bundle
yarn install
Then install the decencies for our react:
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
We will install sweetalert to have a beautiful pop-up box:
npm install sweetalert2
And now we update the config of webpack.config.js.
webpack.config.js
const Encore = require('@symfony/webpack-encore');
// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on 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/')
/*
* ENTRY CONFIG
*
* Each entry will result in one JavaScript file (e.g. app.js)
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
*/
.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()
/*
* FEATURE CONFIG
*
* Enable & configure other features below. For a full
* list of features, see:
* https://symfony.com/doc/current/frontend.html#adding-more-features
*/
.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;
})
// enables Sass/SCSS support
//.enableSassLoader()
// uncomment if you use TypeScript
//.enableTypeScriptLoader()
// uncomment if you use React
//.enableReactPreset()
// uncomment to get integrity="..." attributes on your script & link tags
// requires WebpackEncoreBundle 1.4 or higher
//.enableIntegrityHashes(Encore.isProduction())
// uncomment if you're having problems with a jQuery plugin
//.autoProvidejQuery()
;
module.exports = Encore.getWebpackConfig();
Step 8: Create The React Files
We will now start creating our react files. But before let’s run this command first to compile the react files and watch JavaScript file changes:
yarn encore dev --watch
We will create these files inside /assets directory. These will be what the file structure looks:
You can have your own way of managing or structuring you files.
Let’s create the Main.js file – This file we be the one that handles the routing:
assets/js/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 ProjectList from "./pages/ProjectList"
import ProjectCreate from "./pages/ProjectCreate"
import ProjectEdit from "./pages/ProjectEdit"
import ProjectShow from "./pages/ProjectShow"
function Main() {
return (
<Router>
<Routes>
<Route exact path="/" element={<ProjectList/>} />
<Route path="/create" element={<ProjectCreate/>} />
<Route path="/edit/:id" element={<ProjectEdit/>} />
<Route path="/show/:id" element={<ProjectShow/>} />
</Routes>
</Router>
);
}
export default Main;
if (document.getElementById('app')) {
const rootElement = document.getElementById("app");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<Main />
</StrictMode>
);
}
After creating the Main.js file, let’s update the app.js file:
/assets/app.js
/*
* Welcome to your app's main JavaScript file!
*
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
*/
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.css';
// start the Stimulus application
import './bootstrap';
require('./Main');
Create a folder named components then inside the /assets/components folder, let’s create a file named Layout.js – this will serve as a template.
/assets/components/Layout.js
import React from 'react';
const Layout =({children}) =>{
return(
<div className="container">
{children}
</div>
)
}
export default Layout;
We will now create a folder in /assets named pages. Inside the folder let’s create these files for our pages:
- ProjectCreate.js
- ProjectEdit.js
- ProjectList.js
- ProjectShow.js
/assets/pages/ProjectCreate.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 ProjectCreate() {
const [name, setName] = useState('');
const [description, setDescription] = useState('')
const [isSaving, setIsSaving] = useState(false)
const handleSave = () => {
setIsSaving(true);
let formData = new FormData()
formData.append("name", name)
formData.append("description", description)
axios.post('/api/project', formData)
.then(function (response) {
Swal.fire({
icon: 'success',
title: 'Project saved successfully!',
showConfirmButton: false,
timer: 1500
})
setIsSaving(false);
setName('')
setDescription('')
})
.catch(function (error) {
Swal.fire({
icon: 'error',
title: 'An Error Occured!',
showConfirmButton: false,
timer: 1500
})
setIsSaving(false)
});
}
return (
<Layout>
<div className="container">
<h2 className="text-center mt-5 mb-3">Create New Project</h2>
<div className="card">
<div className="card-header">
<Link
className="btn btn-outline-info float-right"
to="/">View All Projects
</Link>
</div>
<div className="card-body">
<form>
<div className="form-group">
<label htmlFor="name">Name</label>
<input
onChange={(event)=>{setName(event.target.value)}}
value={name}
type="text"
className="form-control"
id="name"
name="name"/>
</div>
<div className="form-group">
<label htmlFor="description">Description</label>
<textarea
value={description}
onChange={(event)=>{setDescription(event.target.value)}}
className="form-control"
id="description"
rows="3"
name="description"></textarea>
</div>
<button
disabled={isSaving}
onClick={handleSave}
type="button"
className="btn btn-outline-primary mt-3">
Save Project
</button>
</form>
</div>
</div>
</div>
</Layout>
);
}
export default ProjectCreate;
/assets/pages/ProjectEdit.js
import React, { useState, useEffect } from 'react';
import { Link, useParams } from "react-router-dom";
import Layout from "../components/Layout"
import Swal from 'sweetalert2'
import axios from 'axios';
function ProjectEdit() {
const [id, setId] = useState(useParams().id)
const [name, setName] = useState('');
const [description, setDescription] = useState('')
const [isSaving, setIsSaving] = useState(false)
useEffect(() => {
axios.get(`/api/project/${id}`)
.then(function (response) {
let project = response.data
setName(project.name);
setDescription(project.description);
})
.catch(function (error) {
Swal.fire({
icon: 'error',
title: 'An Error Occured!',
showConfirmButton: false,
timer: 1500
})
})
}, [])
const handleSave = () => {
setIsSaving(true);
axios.patch(`/api/project/${id}`, {
name: name,
description: description
})
.then(function (response) {
Swal.fire({
icon: 'success',
title: 'Project updated successfully!',
showConfirmButton: false,
timer: 1500
})
setIsSaving(false);
})
.catch(function (error) {
Swal.fire({
icon: 'error',
title: 'An Error Occured!',
showConfirmButton: false,
timer: 1500
})
setIsSaving(false)
});
}
return (
<Layout>
<div className="container">
<h2 className="text-center mt-5 mb-3">Edit Project</h2>
<div className="card">
<div className="card-header">
<Link
className="btn btn-outline-info float-right"
to="/">View All Projects
</Link>
</div>
<div className="card-body">
<form>
<div className="form-group">
<label htmlFor="name">Name</label>
<input
onChange={(event)=>{setName(event.target.value)}}
value={name}
type="text"
className="form-control"
id="name"
name="name"/>
</div>
<div className="form-group">
<label htmlFor="description">Description</label>
<textarea
value={description}
onChange={(event)=>{setDescription(event.target.value)}}
className="form-control"
id="description"
rows="3"
name="description"></textarea>
</div>
<button
disabled={isSaving}
onClick={handleSave}
type="button"
className="btn btn-outline-success mt-3">
Update Project
</button>
</form>
</div>
</div>
</div>
</Layout>
);
}
export default ProjectEdit;
/assets/pages/ProjectList.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 ProjectList() {
const [projectList, setProjectList] = useState([])
useEffect(() => {
fetchProjectList()
}, [])
const fetchProjectList = () => {
axios.get('/api/project')
.then(function (response) {
setProjectList(response.data);
})
.catch(function (error) {
console.log(error);
})
}
const handleDelete = (id) => {
Swal.fire({
title: 'Are you sure?',
text: "You won't be able to revert this!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, delete it!'
}).then((result) => {
if (result.isConfirmed) {
axios.delete(`/api/project/${id}`)
.then(function (response) {
Swal.fire({
icon: 'success',
title: 'Project deleted successfully!',
showConfirmButton: false,
timer: 1500
})
fetchProjectList()
})
.catch(function (error) {
Swal.fire({
icon: 'error',
title: 'An Error Occured!',
showConfirmButton: false,
timer: 1500
})
});
}
})
}
return (
<Layout>
<div className="container">
<h2 className="text-center mt-5 mb-3">Symfony Project Manager</h2>
<div className="card">
<div className="card-header">
<Link
className="btn btn-outline-primary"
to="/create">Create New Project
</Link>
</div>
<div className="card-body">
<table className="table table-bordered">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th width="240px">Action</th>
</tr>
</thead>
<tbody>
{projectList.map((project, key)=>{
return (
<tr key={key}>
<td>{project.name}</td>
<td>{project.description}</td>
<td>
<Link
to={`/show/${project.id}`}
className="btn btn-outline-info mx-1">
Show
</Link>
<Link
className="btn btn-outline-success mx-1"
to={`/edit/${project.id}`}>
Edit
</Link>
<button
onClick={()=>handleDelete(project.id)}
className="btn btn-outline-danger mx-1">
Delete
</button>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
</div>
</Layout>
);
}
export default ProjectList;
/assets/pages/ProjectShow.js
import React, {useState, useEffect} from 'react';
import { Link, useParams } from "react-router-dom";
import Layout from "../components/Layout"
import axios from 'axios';
function ProjectShow() {
const [id, setId] = useState(useParams().id)
const [project, setProject] = useState({name:'', description:''})
useEffect(() => {
axios.get(`/api/project/${id}`)
.then(function (response) {
setProject(response.data)
})
.catch(function (error) {
console.log(error);
})
}, [])
return (
<Layout>
<div className="container">
<h2 className="text-center mt-5 mb-3">Show Project</h2>
<div className="card">
<div className="card-header">
<Link
className="btn btn-outline-info float-right"
to="/"> View All Projects
</Link>
</div>
<div className="card-body">
<b className="text-muted">Name:</b>
<p>{project.name}</p>
<b className="text-muted">Description:</b>
<p>{project.description}</p>
</div>
</div>
</div>
</Layout>
);
}
export default ProjectShow;
Step 6: Run the Application
After finishing the steps above, you can now run your application by executing the command below:
symfony server:start
Open this URL and test the app:
http://localhost:8000/
Screenshots:
Index Page
Create Page
Edit Page
Show Page