Contents
Introduction:
In this blog, I will showing you how to make a Symfony 5 AJAX CRUD app. The four basic operation in any type of programming is CRUD. CRUD is an acronym for CREATE, READ, UPDATE, DELETE. In this blog, You will learn how to develop a Symfony 5 AJAX CRUD App by following an easy step-by-step tutorial. But before that let us have an introduction:
What is Symfony? Symfony is a PHP framework used to develop web application, APIs, microservices and web services. Symfony is one of the leading PHP framework for creating websites and web application.
AJAX(Asynchronous JavaScript and XML) is a set of web development techniques that use many web technologies which allow web applications work in asynchronously.
CRUD is an acronym for CREATE, READ, UPDATE, DELETE. The four basic functions in any type of programming. CRUD typically refers to operations performed in a database.
- Create -Generate new record/s.
- Read – Reads or retrieve record/s.
- Update – Modify record/s.
- Delete – Destroy or remove record/s.
Prerequisite:
- Composer
- Symfony CLI
- MySQL
- PHP >= 7.2.5
Step 1: Install Symfony 5
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-5-crud-ajax
Install via Symfony CLI:
symfony new symfony-5-crud-ajax --full
Step 2: Set Database Configuration
After installing, open the .env file and set database configuration. We will be using MySQL on 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=37febf852b869d38be2030babb187e25
###< symfony/framework-bundle ###
###> symfony/mailer ###
# MAILER_DSN=smtp://localhost
###< symfony/mailer ###
###> 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 configurating the database, execute this command to create database:
php bin/console doctrine:database:create
Step 3: Create Entity and Migration
Entity– it 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
After executing the command above, it will ask question – follow the steps below:
Class name of the entity to create or update (e.g. BraveElephant):
> 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):
> created_at
Field type (enter ? to see all types) [datetime_immutable]:
> datetime
datetime
Can this field be null in the database (nullable) (yes/no) [no]:
> yes
updated: src/Entity/Project.php
Add another property? Enter the property name (or press <return> to stop adding fields):
> updated_at
Field type (enter ? to see all types) [datetime_immutable]:
> datetime
datetime
Can this field be null in the database (nullable) (yes/no) [no]:
> yes
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 contains SQL. we will then run the SQL using this command:
php bin/console doctrine:migrations:migrate
Before we proceed on creating the controller, we will install a bundle StofDoctrineExtensionsBundle, we will be using some of its functionality to automatically set value for the created_at and updated_at property of the newly created entity.
composer require stof/doctrine-extensions-bundle
During the installation process it will ask confirmation to execute the recipe, choose yes.
Open this file config/packages/stof_doctrine_extensions.yaml and add these lines:.
config/packages/stof_doctrine_extensions.yaml
# Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html
# See the official DoctrineExtensions documentation for more details: https://github.com/Atlantic18/DoctrineExtensions/tree/master/doc/
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
timestampable: true
And then update the Project Entity, use the Timestampable to automatically set value on created_at and updated_at property of Project Entity.
src/Entity/Project.php
<?php
namespace App\Entity;
use App\Repository\ProjectRepository;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* @ORM\Entity(repositoryClass=ProjectRepository::class)
*/
class Project
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @ORM\Column(type="text")
*/
private $description;
/**
* @Gedmo\Timestampable(on="create")
* @ORM\Column(type="datetime", nullable=true)
*/
private $created_at;
/**
* @Gedmo\Timestampable(on="update")
* @ORM\Column(type="datetime", nullable=true)
*/
private $updated_at;
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 getCreatedAt(): ?\DateTimeInterface
{
return $this->created_at;
}
public function setCreatedAt(?\DateTimeInterface $created_at): self
{
$this->created_at = $created_at;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updated_at;
}
public function setUpdatedAt(?\DateTimeInterface $updated_at): self
{
$this->updated_at = $updated_at;
return $this;
}
}
Step 4: Generate Controller
A Controller is the one responsible for receiving Request and returning Response.
Execute this command to create a controller:
php bin/console make:controller ProjectController
Then add these line of codes to the newly generated controller:
src/Controller/ProjectController.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 App\Entity\Project;
class ProjectController extends AbstractController
{
/**
* @Route("/project", name="project")
*/
public function index(): Response
{
return $this->render('project/index.html.twig', [
'controller_name' => 'ProjectController',
]);
}
/**
* @Route("/project/show-all", name="project_show_all", methods={"GET"})
*/
public function showAll(): Response
{
$products = $this->getDoctrine()
->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/show/{id}", name="project_show", methods={"GET"})
*/
public function show(int $id): Response
{
$project = $this->getDoctrine()
->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/new", name="project_new", methods={"POST"})
*/
public function new(Request $request): Response
{
$entityManager = $this->getDoctrine()->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/edit/{id}", name="project_edit", methods={"PUT"})
*/
public function edit(Request $request, int $id): Response
{
$entityManager = $this->getDoctrine()->getManager();
$project = $entityManager->getRepository(Project::class)->find($id);
if (!$project) {
return $this->json('No project found for id' . $id, 404);
}
$project->setName($request->request->get('name'));
$project->setDescription($request->request->get('description'));
$entityManager->flush();
$data = [
'id' => $project->getId(),
'name' => $project->getName(),
'description' => $project->getDescription(),
];
return $this->json($data);
}
/**
* @Route("/project/delete/{id}", name="project_delete", methods={"DELETE"})
*/
public function delete(int $id): Response
{
$entityManager = $this->getDoctrine()->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: Update the Twig Templates
Twig is the templating language used in Symfony that make you create concise and readable templates, and it is more powerful on several ways than PHP templates.
We will be using Bootstrap 5 on adding styles on our template.
Open templates/base.html.twig and add the css and js of Bootstrap:
templates/base.html.twig
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
<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>
{% block stylesheets %}{% endblock %}
{% block javascripts %}{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
And then let’s update the file templates/project/index.html.twig – this file is auto generated when we generate the Controller. Now lets add these lines of codes:
templates/project/index.html.twig
{% extends 'base.html.twig' %}
{% block title %}Symfony 5 Project Manager{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="https://cdn.datatables.net/1.10.25/css/dataTables.bootstrap5.min.css">
{% endblock %}
{% block javascripts %}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
{% endblock %}
{% block body %}
<div class="container">
<h2 class="text-center mt-5 mb-3">Symfony 5 Project Manager</h2>
<div class="card">
<div class="card-header">
<button class="btn btn-outline-primary" onclick="createProject()">
Create New Project
</button>
</div>
<div class="card-body">
<div id="alert-div">
</div>
<table class="table table-bordered">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th width="240px">Action</th>
</tr>
</thead>
<tbody id="projects-table-body">
</tbody>
</table>
</div>
</div>
</div>
<!-- modal for creating and editing function -->
<div class="modal" tabindex="-1" id="form-modal">
<div class="modal-dialog" >
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Project Form</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="error-div"></div>
<form>
<input type="hidden" name="update_id" id="update_id">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name" name="name">
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea class="form-control" id="description" rows="3" name="description"></textarea>
</div>
<button type="submit" class="btn btn-outline-primary mt-3" id="save-project-btn">Save Project</button>
</form>
</div>
</div>
</div>
</div>
<!-- view record modal -->
<div class="modal" tabindex="-1" id="view-modal">
<div class="modal-dialog" >
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Project Information</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<b>Name:</b>
<p id="name-info"></p>
<b>Description:</b>
<p id="description-info"></p>
</div>
</div>
</div>
</div>
<script type="text/javascript">
showAllProjects();
/*
This function will get all the project records
*/
function showAllProjects()
{
$.ajax({
url: "/project/show-all",
method: "GET",
success: function(response) {
$("#projects-table-body").html("");
let projects = response;
for (var i = 0; i < projects.length; i++)
{
let showBtn = '<button ' +
' class="btn btn-outline-info" ' +
' onclick="showProject(' + projects[i].id + ')">Show' +
'</button> ';
let editBtn = '<button ' +
' class="btn btn-outline-success" ' +
' onclick="editProject(' + projects[i].id + ')">Edit' +
'</button> ';
let deleteBtn = '<button ' +
' class="btn btn-outline-danger" ' +
' onclick="destroyProject(' + projects[i].id + ')">Delete' +
'</button>';
let projectRow = '<tr>' +
'<td>' + projects[i].name + '</td>' +
'<td>' + projects[i].description + '</td>' +
'<td>' + showBtn + editBtn + deleteBtn + '</td>' +
'</tr>';
$("#projects-table-body").append(projectRow);
}
},
error: function(response) {
console.log(response.responseJSON)
}
});
}
/*
check if form submitted is for creating or updating
*/
$("#save-project-btn").click(function(event ){
event.preventDefault();
if($("#update_id").val() == null || $("#update_id").val() == "")
{
storeProject();
} else {
updateProject();
}
})
/*
show modal for creating a record and
empty the values of form and remove existing alerts
*/
function createProject()
{
$("#alert-div").html("");
$("#error-div").html("");
$("#update_id").val("");
$("#name").val("");
$("#description").val("");
$("#form-modal").modal('show');
}
/*
submit the form and will be stored to the database
*/
function storeProject()
{
$("#save-project-btn").prop('disabled', true);
let data = {
name: $("#name").val(),
description: $("#description").val(),
};
$.ajax({
url: "/project/new",
method: "POST",
data: data,
success: function(response) {
$("#save-project-btn").prop('disabled', false);
let successHtml = '<div class="alert alert-success" role="alert"><b>Project Created Successfully</b></div>';
$("#alert-div").html(successHtml);
$("#name").val("");
$("#description").val("");
showAllProjects();
$("#form-modal").modal('hide');
},
error: function(response) {
/*
show validation error
*/
console.log(response)
$("#save-project-btn").prop('disabled', false);
if (typeof response.responseJSON.messages.errors !== 'undefined')
{
let errors = response.responseJSON.messages.errors;
let descriptionValidation = "";
if (typeof errors.description !== 'undefined')
{
descriptionValidation = '<li>' + errors.description + '</li>';
}
let nameValidation = "";
if (typeof errors.name !== 'undefined')
{
nameValidation = '<li>' + errors.name + '</li>';
}
let errorHtml = '<div class="alert alert-danger" role="alert">' +
'<b>Validation Error!</b>' +
'<ul>' + nameValidation + descriptionValidation + '</ul>' +
'</div>';
$("#error-div").html(errorHtml);
}
}
});
}
/*
edit record function
it will get the existing value and show the project form
*/
function editProject(id)
{
$.ajax({
url: "project/show/" + id,
method: "GET",
success: function(response) {
let project = response
$("#alert-div").html("");
$("#error-div").html("");
$("#update_id").val(project.id);
$("#name").val(project.name);
$("#description").val(project.description);
$("#form-modal").modal('show');
},
error: function(response) {
console.log(response.responseJSON)
}
});
}
/*
sumbit the form and will update a record
*/
function updateProject()
{
$("#save-project-btn").prop('disabled', true);
let data = {
name: $("#name").val(),
description: $("#description").val(),
};
$.ajax({
url: "/project/edit/" + $("#update_id").val(),
method: "PUT",
data: data,
success: function(response) {
$("#save-project-btn").prop('disabled', false);
let successHtml = '<div class="alert alert-success" role="alert"><b>Project Updated Successfully</b></div>';
$("#alert-div").html(successHtml);
$("#name").val("");
$("#description").val("");
showAllProjects();
$("#form-modal").modal('hide');
},
error: function(response) {
/*
show validation error
*/
console.log(response)
$("#save-project-btn").prop('disabled', false);
if (typeof response.responseJSON.messages.errors !== 'undefined')
{
let errors = response.responseJSON.messages.errors;
let descriptionValidation = "";
if (typeof errors.description !== 'undefined')
{
descriptionValidation = '<li>' + errors.description + '</li>';
}
let nameValidation = "";
if (typeof errors.name !== 'undefined')
{
nameValidation = '<li>' + errors.name + '</li>';
}
let errorHtml = '<div class="alert alert-danger" role="alert">' +
'<b>Validation Error!</b>' +
'<ul>' + nameValidation + descriptionValidation + '</ul>' +
'</div>';
$("#error-div").html(errorHtml);
}
}
});
}
/*
get and display the record info on modal
*/
function showProject(id)
{
$("#name-info").html("");
$("#description-info").html("");
$.ajax({
url: "project/show/" + id,
method: "GET",
success: function(response) {
let project = response
$("#name-info").html(project.name);
$("#description-info").html(project.description);
$("#view-modal").modal('show');
},
error: function(response) {
console.log(response.responseJSON)
}
});
}
/*
delete record function
*/
function destroyProject(id)
{
$.ajax({
url: "/project/delete/" + id,
method: "DELETE",
success: function(response) {
let successHtml = '<div class="alert alert-success" role="alert"><b>Project Deleted Successfully</b></div>';
$("#alert-div").html(successHtml);
showAllProjects();
},
error: function(response) {
console.log(response.responseJSON)
}
});
}
</script>
{% endblock %}
Step 6: Run the Application
After finishing the steps above, you can now run your application by executing the command below:
symfony server:start
After successfully running your app, open this URL in your browser:
http://localhost:8000/project
Screenshots:
Symfony 5 AJAX CRUD App Index Page
Symfony 5 AJAX CRUD App Create Modal
Symfony 5 AJAX CRUD App Update Modal
Symfony 5 AJAX CRUD App Show Modal