Contents
Introduction:
The four basic operations in any type of programming are CRUD. CRUD is an acronym for CREATE, READ, UPDATE, DELETE. Today we will be creating a simple CRUD application using Symfony 6.
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.
Prerequisite:
- Composer
- Symfony CLI
- MySQL
- PHP >= 8.0.2
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-crud
Install via Symfony CLI:
symfony new symfony-6-crud --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– 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
After executing the command above, it will ask a 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 values for the created_at and updated_at properties of the newly created entity.
composer require stof/doctrine-extensions-bundle
During the installation process it will ask for 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:"update")]
#[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. In this tutorial we will be generating a Controller with its entire CRUD of an Doctrine entity.
Execute this command:
php bin/console make:crud Project
It will then ask for controller name, just follow this:
php bin/console make:crud Project
Choose a name for your controller class (e.g. ProjectController) [ProjectController]:
> ProjectController
Do you want to generate tests for the controller?. [Experimental] (yes/no) [no]:
> no
created: src/Controller/ProjectController.php
created: src/Form/ProjectType.php
created: templates/project/_delete_form.html.twig
created: templates/project/_form.html.twig
created: templates/project/edit.html.twig
created: templates/project/index.html.twig
created: templates/project/new.html.twig
created: templates/project/show.html.twig
Success!
Next: Check your new CRUD by going to /project/
src/Controller/ProjectController.php
<?php
namespace App\Controller;
use App\Entity\Project;
use App\Form\ProjectType;
use App\Repository\ProjectRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/project')]
class ProjectController extends AbstractController
{
#[Route('/', name: 'app_project_index', methods: ['GET'])]
public function index(ProjectRepository $projectRepository): Response
{
return $this->render('project/index.html.twig', [
'projects' => $projectRepository->findAll(),
]);
}
#[Route('/new', name: 'app_project_new', methods: ['GET', 'POST'])]
public function new(Request $request, ProjectRepository $projectRepository): Response
{
$project = new Project();
$form = $this->createForm(ProjectType::class, $project);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$projectRepository->add($project, true);
return $this->redirectToRoute('app_project_index', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('project/new.html.twig', [
'project' => $project,
'form' => $form,
]);
}
#[Route('/{id}', name: 'app_project_show', methods: ['GET'])]
public function show(Project $project): Response
{
return $this->render('project/show.html.twig', [
'project' => $project,
]);
}
#[Route('/{id}/edit', name: 'app_project_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Project $project, ProjectRepository $projectRepository): Response
{
$form = $this->createForm(ProjectType::class, $project);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$projectRepository->add($project, true);
return $this->redirectToRoute('app_project_index', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('project/edit.html.twig', [
'project' => $project,
'form' => $form,
]);
}
#[Route('/{id}', name: 'app_project_delete', methods: ['POST'])]
public function delete(Request $request, Project $project, ProjectRepository $projectRepository): Response
{
if ($this->isCsrfTokenValid('delete'.$project->getId(), $request->request->get('_token'))) {
$projectRepository->remove($project, true);
}
return $this->redirectToRoute('app_project_index', [], Response::HTTP_SEE_OTHER);
}
}
Before we proceed on updating the twig templates we will updated first the Form of Project Entity. Open this file src/Form/ProjectType.php and remove the created_at and updated_at since we already set its values.
src/Form/ProjectType.php
<?php
namespace App\Form;
use App\Entity\Project;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('description')
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Project::class,
]);
}
}
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>
{# Run `composer require symfony/webpack-encore-bundle`
and uncomment the following Encore helpers to start using Symfony UX #}
{% block stylesheets %}
{#{{ encore_entry_link_tags('app') }}#}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
{% endblock %}
{% block javascripts %}
{#{{ encore_entry_script_tags('app') }}#}
<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>
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
We will now update the twig templates that was generated when we are creating a controller. We will add styles on it since it is a already working code for CRUD operations and we will create some tweaks.
We will be updating the files in this folder templates/project:
- _form.html.twig
- edit.html.twig
- index.html.twig
- new.html.twig
- show.html.twig
You can delete the templates/project /_delete_form.html.twig since we will be moving its code on the templates/project/index.html.twig.
Now update the files:
templates/project /_form.html.twig
{{ form_start(form) }}
<div class="form-group">
{{ form_row(form.name, {'attr': {'class': 'form-control'}}) }}
</div>
<div class="form-group">
{{ form_row(form.description, {'attr': {'class': 'form-control'}}) }}
</div>
<button class="btn {{ button_color|default('btn-outline-primary') }} mt-3">{{ button_label|default('Save Project') }}</button>
{{ form_end(form) }}
templates/project /edit.html.twig
{% extends 'base.html.twig' %}
{% block title %}Edit Project{% endblock %}
{% block body %}
<div class="container">
<h2 class="text-center mt-5 mb-3">Edit Project</h2>
<div class="card">
<div class="card-header">
<a class="btn btn-outline-info float-right" href="{{ path('app_project_index') }}">
View All Projects
</a>
</div>
<div class="card-body">
{% include 'project/_form.html.twig' with {'button_label': 'Update Project', 'button_color': 'btn-outline-success'} %}
</div>
</div>
</div>
{% endblock %}
templates/project /index.html.twig
{% extends 'base.html.twig' %}
{% block title %}Project index{% endblock %}
{% block body %}
<div class="container">
<h2 class="text-center mt-5 mb-3">Symfony Project Manager</h2>
<div class="card">
<div class="card-header">
<a class="btn btn-outline-primary" href="{{ path('app_project_new') }}">
Create New Project
</a>
</div>
<div class="card-body">
<table class="table table-bordered">
<tr>
<th>Name</th>
<th>Description</th>
<th>Date Created</th>
<th>Date Updated</th>
<th width="240px">Action</th>
</tr>
{% for project in projects %}
<tr>
<td>{{ project.name }}</td>
<td>{{ project.description }}</td>
<td>{{ project.createdAt ? project.createdAt|date('Y-m-d H:i:s') : '' }}</td>
<td>{{ project.updatedAt ? project.updatedAt|date('Y-m-d H:i:s') : '' }}</td>
<td>
<form method="post" action="{{ path('app_project_delete', {'id': project.id}) }}" onsubmit="return confirm('Are you sure you want to delete this item?');">
<a
class="btn btn-outline-info"
href="{{ path('app_project_show', {'id': project.id}) }}">
Show
</a>
<a
class="btn btn-outline-success"
href="{{ path('app_project_edit', {'id': project.id}) }}">
Edit
</a>
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ project.id) }}">
<button class="btn btn-outline-danger">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
{% endblock %}
templates/project /new.html.twig
{% extends 'base.html.twig' %}
{% block title %}New Project{% endblock %}
{% block body %}
<div class="container">
<h2 class="text-center mt-5 mb-3">Create Project</h2>
<div class="card">
<div class="card-header">
<a class="btn btn-outline-info float-right" href="{{ path('app_project_index') }}">
View All Projects
</a>
</div>
<div class="card-body">
{{ include('project/_form.html.twig') }}
</div>
</div>
</div>
{% endblock %}
templates/project /show.html.twig
{% extends 'base.html.twig' %}
{% block title %}Project{% endblock %}
{% block body %}
<div class="container">
<h2 class="text-center mt-5 mb-3">Show Project</h2>
<div class="card">
<div class="card-header">
<a class="btn btn-outline-info float-right" href="{{ path('app_project_index') }}">
View All Projects
</a>
</div>
<div class="card-body">
<b class="text-muted">Name:</b>
<p>{{ project.name }}</p>
<b class="text-muted">Description:</b>
<p>{{ project.description }}</p>
<b class="text-muted">Date Created:</b>
<p>{{ project.createdAt ? project.createdAt|date('Y-m-d H:i:s') : '' }}</p>
<b class="text-muted">Date Updated:</b>
<p>{{ project.updatedAt ? project.updatedAt|date('Y-m-d H:i:s') : '' }}</p>
</div>
</div>
</div>
{% 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 6 CRUD App Index Page
Symfony 6 CRUD App Create Page
Symfony 6 CRUD App Update Page
Symfony 6 CRUD App Show Page