Symfony 6 CRUD App Easy Tutorial

Symfony 6 CRUD App Easy Tutorial

Avatar photoPosted by

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:

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 5 crud index image Binaryboxtuts

Symfony 6 CRUD App Create Page

symfony 5 crud create image Binaryboxtuts

Symfony 6 CRUD App Update Page

symfony 5 crud update image Binaryboxtuts

Symfony 6 CRUD App Show Page

symfony 5 crud show image Binaryboxtuts