Getting Started With Symfony 4

Through this article, we are going to take a look at the Symfony 4 framework made by SensioLabs.


Introduction

To create a web application, we have many tools at our disposal. Choosing is sometimes a hard task. However, some tools are some kind of reference, as Symfony is. Here, we are going to have an overview of this framework. To achieve this, we are going to use the fourth version.

What is Symfony?

Symfony is the leading PHP framework available to everyone under an Open Source license. It is built on top of a set of decoupled and reusable components named Symfony Components. Symfony use generic components to allow us to focus on other tasks.

An overview of some elements

Before we dive into the code, let’s have an overview of some elements used by Symfony to understand better what we are going to do.

Symfony Components

Symfony Components are a set of decoupled and reusable PHP libraries. Those components can even be used without Symfony.

Symfony Flex

Symfony Flex is a way to install and manage Symfony applications. It automates the most common tasks of Symfony applications.

It is a Composer plugin that modifies the behavior of the require, update, and remove commands. For example, when we execute the require command, the application will make a request to the Symfony Flex server before trying to install the package with Composer. If there is no information about that package that we want to install, the Flex server returns nothing and the package installation follows the usual procedure based on Composer. If there is information, Flex returns it in a file called a “recipe” and the application uses it to decide which package to install and which automated tasks to run after the installation.

Flex keeps tracks of the recipes it installed in a symfony.lock file, which must be committed to our code repository.

Recipes are defined in a manifest.json file and the instructions defined in this file are also used by Flex when uninstalling dependencies to undo all changes.

Security Checker

Security Checker is a command-line tool that checks if our application uses dependencies with known security vulnerabilities.

Doctrine

Symfony doesn’t provide a component to work with databases. However, it provides an integration of the Doctrine library. Doctrine is an object-relational mapper (ORM). It sits on top of a powerful database abstraction layer (DBAL).

In a few words, Doctrine allows us to insert, update, select or delete an object in a relational database. It also allows us to generate or update tables via classes.

Twig

Twig is a template engine for PHP and can be used without Symfony, although it is also made by SensioLabs.

A few terms

Through our example, we are also going to use a few terms. Let’s define them before.

Controller

A Controller is a PHP function we create. It reads information from a Request Object. It then creates and returns a Response Object. That response could be anything, like HTML, JSON, XML or a file.

Route

A Route is a map from a URL path to a Controller. It offers us clean URLs and flexibility.

Requests and Responses

Symfony provides an approach through two classes to interact with the HTTP request and response. The Request class is a representation of the HTTP request message, while the Response class, obviously, is a representation of an HTTP response message.

A way to handle what comes between the Request and the Response is to use a Front Controller. This file will handle every request coming into our application. It means it will always be executed and it will manage the routing of different URLs to different parts of our application.

In Symfony, incoming requests are interpreted by the Routing component and passed to PHP functions that return Response Objects. It means that the Front Controller will pass the Request to Symfony. This last one will create a Response Object and turns it to text headers and content that will finally be sent back.

Project structure

When we will start our project, our project directory will contain the following folders:

  • config — holds config files
  • src — where we place our PHP code
  • bin — contains executable files
  • var — where automatically-created files are stored (cache, log)
  • vendor — contains third-party libraries
  • public — contains publicly accessible files

A simple example

Now we know more about Symfony, it is time to do something with it.

Setting up our project

Let’s first start by creating our project. We can do as Symfony’s documention suggets or with, for example, use PHP Docker Boilerplate if we want to use Docker. However, we have to be sure that we have at least PHP 7.1 and our configuration allows URL rewriting. If we are a macOS user, we can encounter some trouble with our PHP version. An explanation of how update our PHP version can be found here. We also have to be sure that we have the latest version of Composer.

Following Symfony’s documention, it is something like so:

composer create-project symfony/skeleton simple-app

Setting up our project

This creates a new directory named simple-app, downloads some dependencies into it and generates the basic directories and files we need to get started.

Now, let’s move into our directory to install and run our server:

cd simple-app
composer require server --dev
php bin/console server:run

Installing and running our server

Now, if we use PHP Docker Boilerplate, it would be like so:

git clone https://github.com/webdevops/php-docker-boilerplate.git simple-app
cd simple-app
cp docker-compose.development.yml docker-compose.yml
composer create-project symfony/skeleton app
composer require server --dev
docker-compose up -d

Installing Symfony using PHP Docker Boilerplate

Webserver will be available at port 8000.

We also have to change some values in etc/environment*.yml:

DOCUMENT_ROOT=/app/public/
DOCUMENT_INDEX=index.php

etc/environment*.yml file

To run the Symfony CLI, we can do it like so:

docker-compose run --rm app php bin/console server:start
# OR
docker-compose run --rm app bash
php bin/console server:start

Running Symfony CLI using PHP Docker Boilerplate

When or project is ready, if we want to install Security Checker, we have to do it like so:

composer require sec-checker

Installing Security Checker

We also want to install the Web Debug Toolbar. It displays debugging information along the bottom of our page while developing.

composer require --dev profiler

Installing Web Debug Toolbar

Maybe changing the permissions for the debugger will be necessary.

chmod -R 1777 /app/var

Changing permissions

Creating our first page

Let’s now make our first page. But, first, let’s install what we are going to use to define our Routes:

composer require annotations

Installing Framework Extra Bundle

This allows us to use Annotation Routes instead of defining them into a YAML file. We also need to install Twig:

composer require twig

Installing Twig

We can now create our first template:

<h1>Hello World!</h1>

templates/hello.html.twig file

Now, let’s create our first Controller:

namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SimpleController extends Controller
{
/**
* @Route("/")
*/
public function index()
{
return $this->render('hello.html.twig');
}
}

src/Controller/SimpleController.php file

Now, let’s try our newly created page by visiting http://localhost:8000.

Connecting to the database

Now, it is time to try to connect our application to a database. Here, we are going to use MySQL. First, we have to install Doctrine and the MakerBundle.

composer require doctrine maker

Installing Doctrine and MakerBundle

Now, we can edit the .env file like so:

DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name"
# If we are using PHP Docker Boilerplate, it will be something like that:
# DATABASE_URL=mysql://dev:dev@mysql:3306/database

.env file

We can now use Doctrine to create the database:

php bin/console doctrine:database:create

Creating the database

Entity Class and Migrations

We are now ready to create our first Entity Class. Let’s do it like so:

php bin/console make:entity Post

Creating a Post Entity

Each property of an Entity can be mapped to a column in a corresponding table in our database. Using mapping will allow Doctrine to save an Entity Object to the corresponding table. It will also be able to query from that same table and turn the returned data into objects.

Let’s now add more fields to our Post Entity:

namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\PostRepository")
*/
class Post
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
    /**
* @ORM\Column(type="string", length=100)
*/
private $title;
    /**
* @ORM\Column(type="text")
*/
private $content;
    /**
* @ORM\Column(type="text")
*/
private $content;
}

Entity/Post.php file

Now we are ready to update our database. First, let’s create a migration:

php bin/console doctrine:migrations:diff

Creating a migration

And now, we can execute our newly generated migration:

php bin/console doctrine:migrations:migrate

Running our migration

We now need to create public setters and getters for our properties:

...
public function getId()
{
return $this->id;
}
public function getTitle()
{
return $this->title;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getContent()
{
return $this->content;
}
public function setContent($content)
{
$this->content = $content;
}
public function getExcerpt()
{
return $this->excerpt;
}
public function setExcerpt($excerpt)
{
$this->excerpt = $excerpt;
}
...

Entity/Post.php file edited

We can now create a corresponding Controller like so:

php bin/console make:controller PostController

Creating a Controller

Let’s edit our Controller to have something like so:

namespace App\Controller;
use App\Entity\Post;
use App\Repository\PostRepository;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class PostController extends Controller
{
/**
* @Route("/posts", name="post")
*/
public function index(): Response
{
$posts = $this->getDoctrine()
->getRepository(Post::class)
->findAll();
return $this->render('posts/list.html.twig', ['posts' => $posts]);
}
}

Controller/PostController.php file edited

As we can see in the above code, we query our posts before we pass the result to a view. To get our items, we use what is called a Repository. This last one is a PHP class that helps us to fetch entities of a certain class. We can edit this Repository class if we want, so we can add methods for more complex queries into it.

We can now edit the base.html.twig template and create a new named list.html.twig in a new subdirectory called posts.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Simple App{% endblock %}</title>
{% block stylesheets %}{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>

templates/base.html.twig file

{% extends 'base.html.twig' %}
{% block body %}
<h1>Posts</h1>
    <table>
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for post in posts %}
<tr>
<td>{{ post.title }}</td>
<td>
<div class="item-actions">
<a href="">
See
</a>
<a href="">
Edit
</a>
<a href="">
Delete
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="4" align="center">No posts found</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

templates/posts/list.html.twig file

Now, if we go to localhost:8000/posts, we will see a pretty rough interface and our empty posts list.

To fill our posts list, we are going to create a form. Let’s install a new component:

composer require form

Installing Form component

And of course, we need to validate that form. We can make it with Validator:

composer require validator

Installing Validatior

We can now create a template for our form:

{% extends 'base.html.twig' %}
{% block body %}
<h1>New post</h1>
    {{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
    <a href="{{ path('posts') }}">Back</a>
{% endblock %}

templates/posts/new.html.twig

Here, we create the template that is used to render the form. The form start(form) renders the start tag of the form while the form end(form) renders the end tag of the form. form widget(form) renders all the fields, which includes the field element itself, a label and any validation error messages for the field. It is also possible to render each field manually as described in the Symfony documentation.

We also need to edit our Post Entity:

namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\PostRepository")
*/
class Post
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
    /**
* @ORM\Column(type="string", length=100)
* @Assert\NotBlank()
*/
private $title;
    /**
* @ORM\Column(type="text")
* @Assert\NotBlank()
*/
private $content;
    /**
* @ORM\Column(type="text")
* @Assert\NotBlank()
*/
private $excerpt;
...
}

Entity/Post.php file edited

Validation is done by adding a set of rules, or constraints, to a class. A completed documentation about those different rules can be found here. In Symfony, validation is applied to the underlying object, it means it is checked if the object, here Post, is valid after the form has applied the submitted data to it.

Now, edit our PostController like so:

namespace App\Controller;
use App\Entity\Post;
use App\Repository\PostRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class PostController extends Controller
{
/**
* @Route("/posts", name="posts")
*/
public function index(PostRepository $repository): Response
{
$posts = $this->getDoctrine()
->getRepository(Post::class)
->findAll();
return $this->render('posts/list.html.twig', ['posts' => $posts]);
}
    /**
* @Route("/posts/new", name="new")
* @Method({"GET", "POST"})
*/
public function new(Request $request)
{
$post = new Post();
        $form = $this->createFormBuilder($post)
->add('title', TextType::class)
->add('content', TextareaType::class)
->add('excerpt', TextareaType::class)
->add('create', SubmitType::class)
->getForm();
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->getDoctrine()->getManager();
$em->persist($post);
$em->flush();
            $this->addFlash('success', 'post created');
            return $this->redirectToRoute('posts');
}
        return $this->render('posts/new.html.twig', [
'form' => $form->createView(),
]);
}
}

Controller/PostController.php file edited

In the first part of our new method, we use the Form Builder. We add three fields, corresponding to the properties of the Post class and a submit button.

We then call handleRequest to see if the form was submitted or not when the page is loading. If the form was submitted and if it is valid, we can perform some actions using the Post Object.

As we can see, here we use the persist method that tells Doctrine to “manage” the Post Object. We then call the flush method that tells Doctrine to look through all of the objects that it’s managing to see if they need to be persisted to the database.

We then render the view. It is important that the createView method is placed after the handleRequest method. Otherwise, changes done in the * SUBMIT events aren’t applied to the view.

Now, with what know, we can go a little further and add some features to our application. First, let’s edit our PostController like so:

namespace App\Controller;
use App\Entity\Post;
use App\Repository\PostRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class PostController extends Controller
{
/**
* @Route("/posts", name="posts")
*/
public function index(PostRepository $repository): Response
{
$posts = $this->getDoctrine()
->getRepository(Post::class)
->findAll();
return $this->render('posts/list.html.twig', ['posts' => $posts]);
}
    /**
* @Route("/posts/new", name="new")
* @Method({"GET", "POST"})
*/
public function new(Request $request)
{
$post = new Post();
        $form = $this->createFormBuilder($post)
->add('title', TextType::class)
->add('content', TextareaType::class)
->add('excerpt', TextareaType::class)
->add('create', SubmitType::class)
->getForm();
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->getDoctrine()->getManager();

$em->persist($post);
$em->flush();
            $this->addFlash('success', 'post created');
            return $this->redirectToRoute('posts');
}
        return $this->render('posts/new.html.twig', [
'form' => $form->createView(),
]);
}
    /**
* @Route("/{id}/show", requirements={"id": "\d+"}, name="show")
* @Method("GET")
*/
public function show(Post $post): Response
{
return $this->render('posts/show.html.twig', [
'post' => $post,
]);
}
    /**
* @Route("/{id}/edit", requirements={"id": "\d+"}, name="edit")
* @Method({"GET", "POST"})
*/
public function edit(Request $request, Post $post): Response
{
$form = $this->createFormBuilder($post)
->add('title', TextType::class)
->add('content', TextareaType::class)
->add('excerpt', TextareaType::class)
->add('edit', SubmitType::class)
->getForm();
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->getDoctrine()->getManager();

$em->flush();
            $this->addFlash('success', 'post edited');
            return $this->redirectToRoute('posts');
}
        return $this->render('posts/edit.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
    /**
* @Route("/{id}/delete", requirements={"id": "\d+"}, name="delete")
* @Method({"GET"})
*/
public function delete(Request $request, Post $post): Response
{
$em = $this->getDoctrine()->getManager();
        $em ->remove($post);
$em ->flush();
        $this->addFlash('success', 'post deleted');
        return $this->redirectToRoute('posts');
}
}

src/Controller/SimpleController.php file edited

We can now create two additional templates:

{% extends 'base.html.twig' %}
{% block body %}
<h1>{{ post.title }}</h1>
    {{ post.content }}
    <a href="{{ path('posts') }}">Back</a>
{% endblock %}

templates/posts/show.html.twig file

{% extends 'base.html.twig' %}
{% block body %}
<h1>Edit post</h1>
    {{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
    <a href="{{ path('posts') }}">Back</a>
{% endblock %}

templates/posts/edit.html.twig file

Now, we can read, edit our delete the posted we have created with our application.

Conclusion

Through this article we took a look at Symfony 4. We had an overview of the different concepts it is based on. We set up an installation of Symfony 4 and created a very simple application that let us interact with a MySQL database.

Now, we still have many things to see, like Security Annotations our custom Twig Filters that allow us to build a better application.

One last word

If you like this article, you can consider supporting and helping me on Patreon! It would be awesome! Otherwise, you can find my other posts on Medium and Tumblr. You will also know more about myself on my personal website. Until next time, happy headache!