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!