Symfony tutorial for beginners

symfony Apr 23, 2020

Hey there.

In this tutorial, you will learn how to create a basic Symfony application. When you finish this tutorial, you will know how to create a simple CRUD application. And you will get familiar with controllers, templates, entities, routes, etc.  And you will be ready to start working on more advanced topics.

Requirements


I assume that you have basic knowledge of these things:

  1. Basic PHP/MySQL knowledge
  2. HTML/CSS knowledge
  3. You should know how to use the terminal(console)

And you have installed:

  1. PHP 7+
  2. MySQL 5.6+ or MariaDB
  3. You are using macOS or Linux(Ubuntu, Mint .. whatever)
  4. If you are on Windows, you will need a XAMMP or something similar.
  5. Text editor - PhpStorm or Visual Studio Code.
  6. Also, I recommend installing Symfony plugin for PhpStorm or VSCode

Let's start.


Installation

You need to download the Symfony installer. Go to https://symfony.com/download and download it.

I am using macOS, so I will write next:

curl -sS https://get.symfony.com/cli/installer | bash

After installation you should be able to write commands like this one:

symfony help
Symfony help screenshot.

Symfony CLI application has a lot of built-in features. But we will talk about that later. Let's create a new Symfony project. I am gonna build a simple blog.

symfony new --full blog

This command will create a skeleton of the app. You should see the message like this one

Project is ready.

Now we need a web server, but instead of installing Nginx or apache, we will use a simple built-in PHP webserver.

To run a web-server, open up a new terminal window and write the next line in your project directory.

symfony server:start

Or you can use this one:

php -S 127.0.0.1:8000

Now you have a web server up and running.

web server is running

Open that link in the browser and you should see a welcome page.

Symfony 5 start page.

First page

To create a page you need to create a route and a controller.
Let's start with the controller.
I want to create the main page for my blog, where I will show a list of blog posts.
Create a first controller and index action. Create a  MainController.php under  src/Controller folder:

<?php

namespace App\Controller;

class MainController
{

    public function index()
    {
        return 'test';
    }
}

Now you need to create a route for this action. We will use Symfony Annotations to add routes.

<?php

namespace App\Controller;

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;

class MainController
{
    /**
     * @Route("/", name="index")
     */
    public function index()
    {
        return new Response('test');
    }
}

As you can see we added a new annotation directive @Route . It's a special annotation that tells the framework to use index method to process our main route.

Reload your page, you should see the word test in your browser.

Congratulations, you have created your first action.

Database Schema, Entity

Before we move forward, let`s create a database schema for our blog. I will go with something simple. Our blog will have next fields:

  1. title
  2. short_description
  3. body
  4. image

I assume that  you have MySQL client installed on your local machine.

The Symfony framework uses Doctrine ORM to talk to the database. It’s an abstraction layer between application and database client. I will not go into details here. But I will explain a bit about what we will do here.

Basically you create entities(models). You save them, and doctrine does all the rest for you - saving into a real database(MySQL in our case). You don't have to write native SQL queries, instead, you will be writing queries in DQL(Doctrine Query Language). Doctrine gives you a predefined set of methods from out of the box(it's a big plus).
So let's create our first entity. To do so, write the next command in your terminal:

php bin/console make:entity

You will see an interactive window. Follow the steps I described below.

  1. Name it Blog.
  2. Now you need to add properties:
    2.1 title, type string, length 40, nullable - no
    2.2 short_description, type string, length 40, nullable - no
    2.3 body, type text, nullable - no
    2.4 image, type string, length 100, nullable yes

Now you should see two files that were created:

  1. src/Entity/Blog.php
  2. src/Repository/BlogRepository.php


If you messed up with something, don't worry. You can delete those files and do it again. If you still struggle, check this link. It's a full project, so you can find those files.

Ok great, we have our entity in place, but they before we move on we need to create an actual database schema.

Create a new MySQL database:

mysql -u {USER} -p{PASSWORD}

create database blog;

exit;

Replace {USER} with your MySQL user name, and {PASSWORD} with your user password.

Change your .env file(located in the root directory of the project).

DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7

Change db_user to your mysql userand db_password to your database password, db_name to blog

After that, populate schema by running the next command:

php bin/console doctrine:schema:create

Ok, we have created schema and entity. Now it's time to populate database with fake data(fake blog information).

For this purpose, there is a Symfony library called - FixtureBundle

Open up a terminal window and run next command:

composer require --dev orm-fixtures

This command will install the extra library and will create a file App\DataFixtures\AppFixtures

Open that file - src/DataFixtures/AppFixtures.php and let's create a few blog posts.

<?php

namespace App\DataFixtures;

use App\Entity\Blog;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;

class AppFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {
        for ($i = 0; $i < 4; ++$i) {
            $blog = new Blog();
            $blog->setTitle('Lorem ipsum');
            $blog->setBody('Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
              Proin sodales, arcu non commodo vulputate, neque lectus luctus metus, 
              ac hendrerit mi erat eu ante. Nullam blandit arcu erat,
              vitae pretium neque suscipit vitae. 
              Pellentesque sit amet lacus in metus placerat posuere. Aliquam hendrerit risus elit, non commodo nulla cursus id. 
              Vivamus tristique felis leo, vitae laoreet sapien eleifend vitae. Etiam varius sollicitudin tincidunt');
            $blog->setShortDescription('Lorem ipsum description');
        }

        $manager->flush();
    }
}

As you can see, we created four blog posts. ObjectManager is a doctrine persistence layer. It's an object that we use to manipulate with our entities.

In the example above we used persist and flush methods. Persist tells to ObjectManager that we can store this entity state to the database.
And flush flushes all changes to objects and saves them into a database(MySQL in our case).

Run this command to load these new entities into a database:

php bin/console doctrine:fixtures:load

Cool, now let's create a base view with a list of our blogs.

Index action extended

Symfony uses a template engine called Twig.

I strongly recommend reading more about twig in the documentation.

To display an HTML page from a controller you need to create a template.

There is a base.html.twig file that you can use as a base template.

Go to a root directory of the project and find a template folder. Create a new file called list.html.twig and copy/paste the next code.

{% extends 'base.html.twig' %}

{% block body %}
<h1>Blogs</h1>
{% endblock %}

This line means that we extend a base twig template and I overwrote a body block with a Blogs heading.

So let's change our controller's index action. Go to your MainController and extend it:

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;

class MainController extends AbstractController

We need to extend AbstractController because we want to use additional methods from that controller.

Now, modify your index function:

return $this->render('list.html.twig');

Reload your page:

You should see this text on your screen.

Before moving on to the next section and actually display all blogs. Let's add bootstrap CSS to our code. Add this code to the <head> section of base.html.twig file.

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

Blog list

It's time to show the list of blogs on your index page.

To do so we need to fetch these blogs from the database. So, we need to inject a BlogRepository to our method.

    /**
     * @Route("/")
     *
     * @param BlogRepository $blogRepository
     *
     * @return Response
     */
    public function index(BlogRepository $blogRepository)
    {
        return $this->render('list.html.twig');
    }

What is BlogRepository?  It's a class where you will be writing all the methods needed to fetch data from the database. And also this class has a predefined set of method which we can use.

How this thing(injections) works. This is a bit out of the scope of this tutorial. But in short, we use autowire dependency injection. The framework does all the dirty work for us.

In order to fetch all the blogs that we have, use a fetchAll method. Let's modify our index action.

    public function index(BlogRepository $blogRepository)
    {
        return $this->render('list.html.twig', ['blogs' => $blogRepository->findAll()]);
    }

And change list.html.twig file, add this line under h1 tag:

{{ dump(blogs) }}

This command is useful when you want to dump data into a readable format to debug it. If you run it, you will see something like this:

Let's bring it to a normal view. I will create a table to display a blog list.

{% extends 'base.html.twig' %}
{% block stylesheets %}
    <style>
        .align-center {
            margin: 0 auto;
        }
    </style>
{% endblock %}
{% block body %}
    <div class="row">
        <div class="col-lg-8 align-center">
            <h1>Blogs</h1>
            <table class="table table-striped ">
                <thead class="thead-dark">

                <tr>
                    <th>Title</th>
                    <th>Short description</th>
                    <th>Actions</th>
                </tr>
                </thead>
                <tbody>
                {% for blog in blogs %}
                    <tr>
                        <td>{{ blog.title }}</td>
                        <td>{{ blog.shortDescription }}</td>
                        <td>
                            <button class="btn btn-primary">Edit</button>
                        </td>
                    </tr>
                {% endfor %}
                </tbody>
            </table>

        </div>
    </div>


{% endblock %}

As you can see I used a FOR loop and displayed titles and short descriptions. and displayed titles and short descriptions.

You can reload your page and you should see the list of blogs with edit action like shown on the image below.

List

Try to add an extra button, DELETE action for each blog post.

Ok, we have a list of blogs in place, now we need a way to create a single blog post.

Create action

To create a new blog, we need to create a new action in our controller. Let's name it createBlog.

 use Symfony\Component\HttpFoundation\Request;
 
 ........
 
    /**
     * @param Request $request
     *
     * @return Response
     */
    public function createBlog(Request $request)
    {
        
        return $this->render('create.html.twig');
    }

Create a new template create.html.twig

{% extends 'base.html.twig' %}

{% block body %}
<div class="row">
    <div class="col-lg-8 align-center">
        <h1>Create Blog</h1>

    </div>
</div>
{% block %}

To create a form in Symfony you need to create a FormType. Symfony has a powerful Form features. You don't have to worry about things like validation, form fields rendering, and more. I recommend to read official documentation and get familiar with forms.

So let's create our form type, create a file called BlogFormType.php inside ./src/Form/Type folder:

<?php

namespace App\Form\Type;

use App\Entity\Blog;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class BlogFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('title', TextType::class, ['attr' => ['class' => 'form-control']]);
        $builder->add('shortDescription', TextType::class, ['attr' => ['class' => 'form-control']]);
        $builder->add('body', TextType::class, ['attr' => ['class' => 'form-control']]);
        $builder->add('imageFile', FileType::class, [
            'attr'     => ['class' => 'form-control',],
            'mapped' => false,
        ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'data_class' => Blog::class,
            ]
        );
    }
}


buildForm - method is used to describe form fields, and form types. We added one unmapped field, this means that we do not save this imageFile in the database, instead, we gonna save imageName .

We set a Blog class as a data class of our form. Thus, Symfony will know that it works with a Blog entity.

Now, let's render it, update the code in create.html.twig file:

    <div class="row">
        <div class="col-lg-8 align-center">
            <h1>Create Blog</h1>
            {{ form_start(form) }}

            {{ form_rest(form) }}
            <input type="submit" class="btn btn-primary"/>
        
            {{ form_end(form) }}
        </div>
    </div>

and createBlog action inside MainController.php:

    /**
     * @Route("/create")
     *
     * @param Request $request
     *
     * @return Response
     */
    public function createBlog(Request $request)
    {
        $form = $this->createForm(BlogFormType::class, new Blog());

        return $this->render('create.html.twig', [
            'form' => $form->createView()
        ]);
    }

As you can see, we created a form. We passed a new Blog entity object to this form, and created a form view and passed it our twig template.

Reload your page, you should see your form:

Let's update our form style. If you want to know how to customize forms and how to render them consider reading this documentation.

Update create.html.twig file.

        <div class="col-lg-8 align-center">
            <h1>Create new blog entry.</h1>

            {{ form_start(form) }}

            <div class="form-group">
                {{ form_widget(form.title) }}
            </div>
            <div class="form-group">
                {{ form_widget(form.shortDescription) }}
            </div>
            <div class="form-group">
                {{ form_widget(form.imageFile) }}
            </div>
            <div class="form-group">
                {{ form_widget(form.body) }}
            </div>

            <input type="submit" class="btn btn-primary"/>

            {{ form_end(form) }}
        </div>

Now it looks much prettier. Let's update our create action method to process our form submit:


        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $blog = $form->getData();
            $entityManager->persist($blog);
            $entityManager->flush();
            $this->addFlash('success', 'Blog was created!');
        }

And add this code to create.html.twig under <h1> tag:

            {% for label, messages in app.flashes %}
                {% for message in messages %}
                    <div class="flash-{{ label }}">
                        {{ message }}
                    </div>
                {% endfor %}
            {% endfor %}

So what have we done? In our controller, we added form validation and persisted our Blog post into a database.
In our template, we displayed a success message.

At this point, you can try to create a blog.

But what about the image? Yes, right we need to save the image somewhere on our computer. To do this we need to add more code. Let's update createBlog action.

        if ($form->isSubmitted() && $form->isValid()) {
            $blog = $form->getData();
            $imageFile = $form->get('image')->getData();
            if ($imageFile) {
                $originalFilename = pathinfo($imageFile->getClientOriginalName(), PATHINFO_FILENAME);
                $safeFilename = $slugger->slug($originalFilename);
                $newFilename = $safeFilename.'-'.uniqid().'.'.$imageFile->guessExtension();

                try {
                    $imageFile->move(
                        $this->getParameter('image_directory'),
                        $newFilename
                    );
                } catch (FileException $e) {
                    $this->addFlash('error', 'Image cannot be saved.');
                }
                $blog->setImage($newFilename);
            }

            $entityManager->persist($blog);
            $entityManager->flush();
            $this->addFlash('success', 'Blog was created!');
        }

We added image processing. We check if an image exists in the request. If so, we change its name and move it to an image_directory, and then we save the file name of the image.

I forgot to mention that you have to add image_directory parameter in

/config/services.yaml under parameters section:

parameters:
    image_directory: '%kernel.project_dir%/public'

Try it out, you should be able to upload the image. You should see a newly created image under the public directory.

Validation

Before we move on to edit action, we need to add validation to our form.

Why?

Ask yourself these questions:

  • What if a user will upload a 100 megabytes pdf file?
  • What if I inject HTML code into a title?

We have to handle such cases. For this purpose, Symfony has in-built Validation Constraints. I will show you how to use them.

We can add validation constraints with annotations. Let's edit our /Entity/Blog.php class.

use Symfony\Component\Validator\Constraints as Assert;

.......

    /**
     * @ORM\Column(type="string", length=40)
     * @Assert\NotBlank(message="Title cannot be empty.")
     * @Assert\Length(max=40)
     */
    private $title;

    /**
     * @ORM\Column(type="string", length=40)
     * @Assert\NotBlank()
     * @Assert\Length(max=40)
     */
    private $short_description;

    /**
     * @ORM\Column(type="text")
     * @Assert\NotBlank()
     */
    private $body;

    /**
     * @ORM\Column(type="string", length=100, nullable=true)
     * @Assert\Image()
     */
    private $image;

We added assertations here. Our fields cannot be blank, and we asserted that the image should be an actual image. There are many more options for Image validation, but this is out of the scope of this tutorial. You can read about options and try them here.

Now you can try to create a blog. And try to upload some file(not image), you should see the error.

Improvements

So far, we created a list and createBlog actions. We need to add one more thing. We need a create button on this page. Find a list.html.twig file and copy-paste this code(at the bottom of the body block):


    <div class="row">
        <div class="col-lg-8  align-center">
            <a href="{{ path('app_main_createblog') }}" class="btn btn-primary">Create blog</a>
        </div>
    </div>

Path - is a special function, that you can use in a twig to generate routes. To find a route, use this command:

php /bin/console debug:route

It will show every route you have in your project and its name. I recommend installing Symfony plugin in your IDE of choice. I use PHPStorm, but if you are using VisualStudioCode or something else, search for Symfony plugin. With a plugin, you don’t need to debug the route. Route autocomplete will be available for you.

You can add a name to your route explicitly, like this(in this case you don't have to debug routes - recommended):

@Route("/", name="app_index")

Lastly, I want to redirect the user to the list page after creating the blog. Change this line in createBlog action.

            $this->addFlash('success', 'Blog was created!');

            return $this->redirectToRoute('app_main_index');

And update list.html.twig, insert this code under <h1> tag:

            {% for label, messages in app.flashes %}
                {% for message in messages %}
                    <div class="flash-{{ label }}">
                        {{ message }}
                    </div>
                {% endfor %}
            {% endfor %}

Ok, we're good.

Edit action

Edit blog is pretty similar to create action, but it's a bit different. We need to fetch the blog by id and populate the form with existing data from the database.

Create a new method in the controller called editBlog

    /**
     * @Route("/edit/{id}")
     *
     * @ParamConverter("blog", class="App:Blog")
     *
     * @return Response
     */
    public function editBlog(Blog $blog, Request $request, EntityManagerInterface $entityManager, SluggerInterface $slugger)
    {
        $blog->setImage(new File(sprintf('%s/%s', $this->getParameter('image_directory'), $blog->getImage())));
        $form = $this->createForm(BlogFormType::class, $blog);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $blog      = $form->getData();
            $imageFile = $form->get('imageFile')->getData();
            if ($imageFile) {
                $originalFilename = pathinfo($imageFile->getClientOriginalName(), PATHINFO_FILENAME);

                $safeFilename = $slugger->slug($originalFilename);
                $newFilename  = $safeFilename.'-'.uniqid().'.'.$imageFile->guessExtension();

                try {
                    $imageFile->move(
                            $this->getParameter('image_directory'),
                            $newFilename
                        );
                } catch (FileException $e) {
                    $this->addFlash('error', 'Image cannot be saved.');
                }
                $blog->setImage($newFilename);
            }

            $entityManager->persist($blog);
            $entityManager->flush();
            $this->addFlash('success', 'Blog was edited!');
        }

        return $this->render('create.html.twig', [
            'form' => $form->createView(),
        ]);
    }

As you can see this method is almost the same as createAction.

     * @Route("/edit/{id}")
     *
     * @ParamConverter("blog", class="App:Blog")

{id} is a wildcard, it's a blog id, @ParamConverter annotation converts id into an Object, in our case, it's a Blog. Basically you can do it on your own using the repository method - find.

Additionally, we need to update our BlogFormType to avoid file processing(in case if there is no file present). Add a required => false here:

        $builder->add('imageFile', FileType::class, [
            'attr'     => ['class' => 'form-control',],
            'mapped' => false,
            'required' => false
        ]);

And one finishing touch, change edit button URL to existing edit action. You have to update list.html.twig file:

<td>
    <a href="{{ path('app_main_editblog', {id: blog.id}) }}" class="btn btn-primary">Edit</a>
</td>

Try it out, you should be able to click on the edit button and it will redirect you to the edit form.

Let's move on and build a delete action.

Delete blog

We need to add one more action to our MainController:

    /**
     * @Route("/delete/{id}", name="app_blog_delete")
     *
     * @param Blog                   $blog
     * @param EntityManagerInterface $em
     *
     * @return RedirectResponse
     */
    public function deleteBlog(Blog $blog, EntityManagerInterface $em): RedirectResponse
    {
        $em->remove($blog);
        $em->flush();
        $this->addFlash('success', 'Blog was edited!');

        return $this->redirectToRoute('app_main_index');
    }

It's pretty simple, we remove blog with entityManager and then we add a success message and redirect the user to the main page.


If you have not added the delete button yet, let's do it now. Go to list.html.twig and add delete button, under the edit button.

<a href="{{ path('app_blog_delete', {id: blog.id}) }}" onclick="return confirm('Are you sure?');" class="btn btn-danger">DELETE</a>

That's it.

Conclusion

In this tutorial, you have learned how to create a simple CRUD application. From this point, I suggest learning more about doctrine, Symfony forms.

If you want to see the full project, you can find it here on GitHub.

If you have any questions, comment down below, I'll try to answer.

Thank you.

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.