The Best Practices Book: Version: Master

Download as pdf or txt
Download as pdf or txt
You are on page 1of 40

The Best Practices Book

Version: master
generated on August 3, 2019
The Best Practices Book (master)
This work is licensed under the “Attribution-Share Alike 3.0 Unported” license (http://creativecommons.org/
licenses/by-sa/3.0/).
You are free to share (to copy, distribute and transmit the work), and to remix (to adapt the work) under the
following conditions:

• Attribution: You must attribute the work in the manner specified by the author or licensor (but not in
any way that suggests that they endorse you or your use of the work).
• Share Alike: If you alter, transform, or build upon this work, you may distribute the resulting work only
under the same, similar or a compatible license. For any reuse or distribution, you must make clear to
others the license terms of this work.

The information in this book is distributed on an “as is” basis, without warranty. Although every precaution
has been taken in the preparation of this work, neither the author(s) nor SensioLabs shall have any liability to
any person or entity with respect to any loss or damage caused or alleged to be caused directly or indirectly by
the information contained in this work.
If you find typos or errors, feel free to report them by creating a ticket on the Symfony ticketing system
(https://github.com/symfony/symfony-docs/issues). Based on tickets and users feedback, this book is
continuously updated.
Contents at a Glance

The Symfony Framework Best Practices...............................................................................................4


Creating the Project ............................................................................................................................6
Configuration .....................................................................................................................................8
Organizing Your Business Logic ........................................................................................................11
Controllers .......................................................................................................................................16
Templates.........................................................................................................................................20
Forms ...............................................................................................................................................23
Internationalization...........................................................................................................................27
Security ............................................................................................................................................29
Web Assets .......................................................................................................................................35
Tests.................................................................................................................................................36

PDF brought to you by Contents at a Glance | iii


generated on August 3, 2019
Chapter 1
The Symfony Framework Best Practices

The Symfony Framework is well-known for being really flexible and is used to build micro-sites,
enterprise applications that handle billions of connections and even as the basis for other frameworks.
Since its release in July 2011, the community has learned a lot about what's possible and how to do things
best.
These community resources - like blog posts or presentations - have created an unofficial set of
recommendations for developing Symfony applications. Unfortunately, a lot of these recommendations
are unneeded for web applications. Much of the time, they unnecessarily overcomplicate things and don't
follow the original pragmatic philosophy of Symfony.

What is this Guide About?


This guide aims to fix that by describing the best practices for developing web applications with the
Symfony full-stack Framework. These are best practices that fit the philosophy of the framework as
envisioned by its original creator Fabien Potencier1.

Best practice is a noun that means "a well-defined procedure that is known to produce near-optimum
results". And that's exactly what this guide aims to provide. Even if you don't agree with every
recommendation, we believe these will help you build great applications with less complexity.

This guide is specially suited for:

• Websites and web applications developed with the full-stack Symfony Framework.

For other situations, this guide might be a good starting point that you can then extend and fit to your
specific needs:

• Bundles shared publicly to the Symfony community;


• Advanced developers or teams who have created their own standards;
• Some complex applications that have highly customized requirements;
• Bundles that may be shared internally within a company.

1. https://connect.symfony.com/profile/fabpot

PDF brought to you by Chapter 1: The Symfony Framework Best Practices | 4


generated on August 3, 2019
We know that old habits die hard and some of you will be shocked by some of these best practices. But
by following these, you'll be able to develop applications faster, with less complexity and with the same
or even higher quality. It's also a moving target that will continue to improve.
Keep in mind that these are optional recommendations that you and your team may or may not
follow to develop Symfony applications. If you want to continue using your own best practices and
methodologies, you can still do that. Symfony is flexible enough to adapt to your needs. That will never
change.

Who this Book Is for (Hint: It's not a Tutorial)


Any Symfony developer, whether you are an expert or a newcomer, can read this guide. But since this
isn't a tutorial, you'll need some basic knowledge of Symfony to follow everything. If you are totally new
to Symfony, welcome! and read the Getting Started guides first.
We've deliberately kept this guide short. We won't repeat explanations that you can find in the vast
Symfony documentation, like discussions about Dependency Injection or front controllers. We'll solely
focus on explaining how to do what you already know.

The Application
In addition to this guide, a sample application called Symfony Demo2 has been developed with all these
best practices in mind. Execute this command to download the demo application:

Listing 1-1 1 $ composer create-project symfony/symfony-demo

The demo application is a simple blog engine, because that will allow us to focus on the Symfony
concepts and features without getting buried in difficult implementation details. Instead of developing
the application step by step in this guide, you'll find selected snippets of code through the chapters.

Don't Update Your Existing Applications


After reading this handbook, some of you may be considering refactoring your existing Symfony
applications. Our recommendation is sound and clear: you may use these best practices for new
applications but you should not refactor your existing applications to comply with these best
practices. The reasons for not doing it are various:

• Your existing applications are not wrong, they just follow another set of guidelines;
• A full codebase refactorization is prone to introduce errors in your applications;
• The amount of work spent on this could be better dedicated to improving your tests or adding
features that provide real value to the end users.

Next: Creating the Project

2. https://github.com/symfony/demo

PDF brought to you by Chapter 1: The Symfony Framework Best Practices | 5


generated on August 3, 2019
Chapter 2
Creating the Project

Installing Symfony
Use Composer and Symfony Flex to create and manage Symfony applications.

Composer1 is the package manager used by modern PHP applications to manage their dependencies.
Symfony Flex2 is a Composer plugin designed to automate some of the most common tasks performed
in Symfony applications. Using Flex is optional but recommended because it improves your productivity
significantly.

Use the Symfony Skeleton to create new Symfony-based projects.

The Symfony Skeleton3 is a minimal and empty Symfony project which you can base your new projects
on. Unlike past Symfony versions, this skeleton installs the absolute bare minimum amount of
dependencies to make a fully working Symfony project. Read the Installing & Setting up the Symfony
Framework article to learn more about installing Symfony.

Creating the Blog Application


In your command console, browse to a directory where you have permission to create files and execute
the following commands:

Listing 2-1 1 $ cd projects/


2 $ composer create-project symfony/skeleton blog

This command creates a new directory called blog that contains a fresh new project based on the most
recent stable Symfony version available.

1. https://getcomposer.org/
2. https://github.com/symfony/flex
3. https://github.com/symfony/skeleton

PDF brought to you by Chapter 2: Creating the Project | 6


generated on August 3, 2019
The technical requirements to run Symfony are simple. If you want to check if your system meets
those requirements, read Requirements for Running Symfony.

Structuring the Application


After creating the application, enter the blog/ directory and you'll see a number of files and directories
generated automatically. These are the most important ones:

Listing 2-2 1 blog/


2 ├─ bin/
3 │ └─ console
4 ├─ config/
5 └─ public/
6 │ └─ index.php
7 ├─ src/
8 │ └─ Kernel.php
9 ├─ var/
10 │ ├─ cache/
11 │ └─ log/
12 └─ vendor/

This file and directory hierarchy is the convention proposed by Symfony to structure your applications.
It's recommended to keep this structure because it's easy to navigate and most directory names are self-
explanatory, but you can override the location of any Symfony directory:

Application Bundles
When Symfony 2.0 was released, most developers naturally adopted the symfony 1.x way of dividing
applications into logical modules. That's why many Symfony applications used bundles to divide their
code into logical features: UserBundle, ProductBundle, InvoiceBundle, etc.
But a bundle is meant to be something that can be reused as a stand-alone piece of software. If
UserBundle cannot be used "as is" in other Symfony applications, then it shouldn't be its own bundle.
Moreover, if InvoiceBundle depends on ProductBundle, then there's no advantage to having two separate
bundles.

Don't create any bundle to organize your application logic.

Symfony applications can still use third-party bundles (installed in vendor/) to add features, but you
should use PHP namespaces instead of bundles to organize your own code.
Next: Configuration

PDF brought to you by Chapter 2: Creating the Project | 7


generated on August 3, 2019
Chapter 3
Configuration

Configuration usually involves different application parts (such as infrastructure and security credentials)
and different environments (development, production). That's why Symfony recommends that you split
the application configuration into three parts.

Infrastructure-Related Configuration
These are the options that change from one machine to another (e.g. from your development machine to
the production server) but which don't change the application behavior.

Define the infrastructure-related configuration options as environment variables. During development, use the
.env and .env.local files at the root of your project to set these.

By default, Symfony adds these types of options to the .env file when installing new dependencies in the
app:

Listing 3-1 1 # .env


2 ###> doctrine/doctrine-bundle ###
3 DATABASE_URL=sqlite:///%kernel.project_dir%/var/data/blog.sqlite
4 ###< doctrine/doctrine-bundle ###
5
6 ###> symfony/swiftmailer-bundle ###
7 MAILER_URL=smtp://localhost?encryption=ssl&auth_mode=login&username=&password=
8 ###< symfony/swiftmailer-bundle ###
9
10 # ...

These options aren't defined inside the config/services.yaml file because they have nothing to do
with the application's behavior. In other words, your application doesn't care about the location of your
database or the credentials to access to it, as long as the database is correctly configured.
To override these variables with machine-specific or sensitive values, create a .env.local file. This file
should not be added to version control.

PDF brought to you by Chapter 3: Configuration | 8


generated on August 3, 2019
Beware that dumping the contents of the $_SERVER and $_ENV variables or outputting the
phpinfo() contents will display the values of the environment variables, exposing sensitive
information such as the database credentials.

Canonical Parameters

Define all your application's env vars in the .env file.

Symfony includes a configuration file called .env at the project root, which stores the canonical list of
environment variables for the application. This file should be stored in version control and so should only
contain non-sensitive default values.

Applications created before November 2018 had a slightly different system, involving a .env.dist
file. For information about upgrading, see: Nov 2018 Changes to .env & How to Update.

Application-Related Configuration
Define the application behavior related configuration options in the config/services.yaml file.

The services.yaml file contains the options used by the application to modify its behavior, such as
the sender of email notifications, or the enabled feature toggles1. Defining these values in .env file would
add an extra layer of configuration that's not needed because you don't need or want these configuration
values to change on each server.
The configuration options defined in the services.yaml may vary from one environment to another.
That's why Symfony supports defining config/services_dev.yaml and config/
services_prod.yaml files so that you can override specific values for each environment.

Constants vs Configuration Options


One of the most common errors when defining application configuration is to create new options for
values that never change, such as the number of items for paginated results.

Use constants to define configuration options that rarely change.

The traditional approach for defining configuration options has caused many Symfony applications to
include an option like the following, which would be used to control the number of posts to display on
the blog homepage:

Listing 3-2 1 # config/services.yaml


2 parameters:
3 homepage.number_of_items: 10

If you've done something like this in the past, it's likely that you've in fact never actually needed to change
that value. Creating a configuration option for a value that you are never going to configure just isn't
necessary. Our recommendation is to define these values as constants in your application. You could, for
example, define a NUMBER_OF_ITEMS constant in the Post entity:

1. https://en.wikipedia.org/wiki/Feature_toggle

PDF brought to you by Chapter 3: Configuration | 9


generated on August 3, 2019
Listing 3-3 1 // src/Entity/Post.php
2 namespace App\Entity;
3
4 class Post
5 {
6 const NUMBER_OF_ITEMS = 10;
7
8 // ...
9 }

The main advantage of defining constants is that you can use their values everywhere in your application.
When using parameters, they are only available from places with access to the Symfony container.
Constants can be used for example in your Twig templates thanks to the constant() function2:

Listing 3-4 1 <p>


2 Displaying the {{ constant('NUMBER_OF_ITEMS', post) }} most recent results.
3 </p>

And Doctrine entities and repositories can access these values too, whereas they cannot access the
container parameters:

Listing 3-5 1 namespace App\Repository;


2
3 use App\Entity\Post;
4 use Doctrine\ORM\EntityRepository;
5
6 class PostRepository extends EntityRepository
7 {
8 public function findLatest($limit = Post::NUMBER_OF_ITEMS)
9 {
10 // ...
11 }
12 }

The only notable disadvantage of using constants for this kind of configuration values is that it's
complicated to redefine their values in your tests.

Parameter Naming
The name of your configuration parameters should be as short as possible and should include a common prefix
for the entire application.

Using app. as the prefix of your parameters is a common practice to avoid collisions with Symfony and
third-party bundles/libraries parameters. Then, use just one or two words to describe the purpose of the
parameter:

Listing 3-6 1 # config/services.yaml


2 parameters:
3 # don't do this: 'dir' is too generic and it doesn't convey any meaning
4 app.dir: '...'
5 # do this: short but easy to understand names
6 app.contents_dir: '...'
7 # it's OK to use dots, underscores, dashes or nothing, but always
8 # be consistent and use the same format for all the parameters
9 app.dir.contents: '...'
10 app.contents-dir: '...'

Next: Organizing Your Business Logic

2. https://twig.symfony.com/doc/2.x/functions/constant.html

PDF brought to you by Chapter 3: Configuration | 10


generated on August 3, 2019
Chapter 4
Organizing Your Business Logic

In computer software, business logic or domain logic is "the part of the program that encodes the real-
world business rules that determine how data can be created, displayed, stored, and changed" (read full
definition1).
In Symfony applications, business logic is all the custom code you write for your app that's not specific to
the framework (e.g. routing and controllers). Domain classes, Doctrine entities and regular PHP classes
that are used as services are good examples of business logic.
For most projects, you should store all your code inside the src/ directory. Inside here, you can create
whatever directories you want to organize things:

Listing 4-1 1 symfony-project/


2 ├─ config/
3 ├─ public/
4 ├─ src/
5 │ └─ Utils/
6 │ └─ MyClass.php
7 ├─ tests/
8 ├─ var/
9 └─ vendor/

Services: Naming and Configuration


Use autowiring to automate the configuration of application services.

Service autowiring is a feature provided by Symfony's Service Container to manage services with minimal
configuration. It reads the type-hints on your constructor (or other methods) and automatically passes
the correct services to each method. It can also add service tags to the services needing them, such as Twig
extensions, event subscribers, etc.
The blog application needs a utility that can transform a post title (e.g. "Hello World") into a slug (e.g.
"hello-world") to include it as part of the post URL. Let's create a new Slugger class inside src/
Utils/:

1. https://en.wikipedia.org/wiki/Business_logic

PDF brought to you by Chapter 4: Organizing Your Business Logic | 11


generated on August 3, 2019
Listing 4-2 1 // src/Utils/Slugger.php
2 namespace App\Utils;
3
4 class Slugger
5 {
6 public function slugify(string $value): string
7 {
8 // ...
9 }
10 }

If you're using the default services.yaml configuration, this class is auto-registered as a service with the ID
App\Utils\Slugger (to prevent against typos, import the class and write Slugger::class in your
code).

The id of your application's services should be equal to their class name, except when you have multiple
services configured for the same class (in that case, use a snake case id).

Now you can use the custom slugger in any other service or controller class, such as the
AdminController:
Listing 4-3 1 use App\Utils\Slugger;
2
3 public function create(Request $request, Slugger $slugger)
4 {
5 // ...
6
7 if ($form->isSubmitted() && $form->isValid()) {
8 $slug = $slugger->slugify($post->getTitle());
9 $post->setSlug($slug);
10
11 // ...
12 }
13 }

Services can also be public or private. If you use the default services.yaml configuration, all services are
private by default.

private whenever possible. This will prevent you from accessing that service via
Services should be
$container->get(). Instead, you will need to use dependency injection.

Service Format: YAML


If you use the default services.yaml configuration, most services will be configured automatically.
However, in some edge cases you'll need to configure services (or parts of them) manually.

Use the YAML format to configure your own services.

This is controversial, and in our experience, YAML and XML usage is evenly distributed among
developers, with a slight preference towards YAML. Both formats have the same performance, so this is
ultimately a matter of personal taste.
We recommend YAML because it's friendly to newcomers and concise, but you can use whatever format
you like.

PDF brought to you by Chapter 4: Organizing Your Business Logic | 12


generated on August 3, 2019
Using a Persistence Layer
Symfony is an HTTP framework that only cares about generating an HTTP response for each HTTP
request. That's why Symfony doesn't provide a way to talk to a persistence layer (e.g. database, external
API). You can choose whatever library or strategy you want for this.
In practice, many Symfony applications rely on the independent Doctrine project2 to define their model
using entities and repositories.
Doctrine support is not enabled by default in Symfony. So to use Doctrine as shown in the examples
below you will need to install Doctrine ORM support by executing the following command:

Listing 4-4 1 $ composer require symfony/orm-pack

Just like with business logic, we recommend storing Doctrine entities in the src/Entity/ directory.
The three entities defined by our sample blog application are a good example:

Listing 4-5 1 symfony-project/


2 ├─ ...
3 └─ src/
4 └─ Entity/
5 ├─ Comment.php
6 ├─ Post.php
7 └─ User.php

Doctrine Mapping Information


Doctrine entities are plain PHP objects that you store in some "database". Doctrine only knows about
your entities through the mapping metadata configured for your model classes. Doctrine supports four
metadata formats: YAML, XML, PHP and annotations.

Use annotations to define the mapping information of the Doctrine entities.

Annotations are by far the most convenient and agile way of setting up and looking for mapping
information:

Listing 4-6 1 namespace App\Entity;


2
3 use Doctrine\Common\Collections\ArrayCollection;
4 use Doctrine\ORM\Mapping as ORM;
5
6 /**
7 * @ORM\Entity
8 */
9 class Post
10 {
11 const NUMBER_OF_ITEMS = 10;
12
13 /**
14 * @ORM\Id
15 * @ORM\GeneratedValue
16 * @ORM\Column(type="integer")
17 */
18 private $id;
19
20 /**
21 * @ORM\Column(type="string")
22 */
23 private $title;
24

2. http://www.doctrine-project.org/

PDF brought to you by Chapter 4: Organizing Your Business Logic | 13


generated on August 3, 2019
25 /**
26 * @ORM\Column(type="string")
27 */
28 private $slug;
29
30 /**
31 * @ORM\Column(type="text")
32 */
33 private $content;
34
35 /**
36 * @ORM\Column(type="string")
37 */
38 private $authorEmail;
39
40 /**
41 * @ORM\Column(type="datetime")
42 */
43 private $publishedAt;
44
45 /**
46 * @ORM\OneToMany(
47 * targetEntity="App\Entity\Comment",
48 * mappedBy="post",
49 * orphanRemoval=true
50 * )
51 * @ORM\OrderBy({"publishedAt"="ASC"})
52 */
53 private $comments;
54
55 public function __construct()
56 {
57 $this->publishedAt = new \DateTime();
58 $this->comments = new ArrayCollection();
59 }
60
61 // getters and setters ...
62 }

All formats have the same performance, so this is once again ultimately a matter of taste.

Data Fixtures
As fixtures support is not enabled by default in Symfony, you should execute the following command to
install the Doctrine fixtures bundle:

Listing 4-7 1 $ composer require --dev doctrine/doctrine-fixtures-bundle

Then, this bundle is enabled automatically, but only for the dev and test environments:

Listing 4-8 1 // config/bundles.php


2 return [
3 // ...
4 Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
5 ];

We recommend creating just one fixture class3 for simplicity, though you're welcome to have more if that
class gets quite large.
Assuming you have at least one fixtures class and that the database access is configured properly, you can
load your fixtures by executing the following command:

Listing 4-9

3. https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html#writing-simple-fixtures

PDF brought to you by Chapter 4: Organizing Your Business Logic | 14


generated on August 3, 2019
1 $ php bin/console doctrine:fixtures:load
2
3 Careful, database will be purged. Do you want to continue Y/N ? Y
4 > purging database
5 > loading App\DataFixtures\ORM\LoadFixtures

Coding Standards
The Symfony source code follows the PSR-14 and PSR-25 coding standards that were defined by the PHP
community. You can learn more about the Symfony Coding standards and even use the PHP-CS-Fixer6,
which is a command-line utility that can fix the coding standards of an entire codebase in a matter of
seconds.
Next: Controllers

4. https://www.php-fig.org/psr/psr-1/
5. https://www.php-fig.org/psr/psr-2/
6. https://github.com/FriendsOfPHP/PHP-CS-Fixer

PDF brought to you by Chapter 4: Organizing Your Business Logic | 15


generated on August 3, 2019
Chapter 5
Controllers

Symfony follows the philosophy of "thin controllers and fat models". This means that controllers should
hold just the thin layer of glue-code needed to coordinate the different parts of the application.
Your controller methods should just call to other services, trigger some events if needed and then return a
response, but they should not contain any actual business logic. If they do, refactor it out of the controller
and into a service.

Make your controller extend the AbstractController base controller provided by Symfony and use
annotations to configure routing, caching and security whenever possible.

Coupling the controllers to the underlying framework allows you to leverage all of its features and
increases your productivity.
And since your controllers should be thin and contain nothing more than a few lines of glue-code,
spending hours trying to decouple them from your framework doesn't benefit you in the long run. The
amount of time wasted isn't worth the benefit.
In addition, using annotations for routing, caching and security simplifies configuration. You don't need
to browse tens of files created with different formats (YAML, XML, PHP): all the configuration is just
where you need it and it only uses one format.
Overall, this means you should aggressively decouple your business logic from the framework while, at
the same time, aggressively coupling your controllers and routing to the framework in order to get the
most out of it.

Controller Action Naming


Don't add the Action suffix to the methods of the controller actions.

The first Symfony versions required that controller method names ended in Action (e.g. newAction(),
showAction()). This suffix became optional when annotations were introduced for controllers. In
modern Symfony applications this suffix is neither required nor recommended, so you can safely remove
it.

PDF brought to you by Chapter 5: Controllers | 16


generated on August 3, 2019
Routing Configuration
To load routes defined as annotations in your controllers, add the following configuration to the main
routing configuration file:

Listing 5-1 1 # config/routes.yaml


2 controllers:
3 resource: '../src/Controller/'
4 type: annotation

This configuration will load annotations from any controller stored inside the src/Controller/
directory and even from its subdirectories. So if your application defines lots of controllers, it's perfectly
ok to reorganize them into subdirectories:

Listing 5-2 1 <your-project>/


2 ├─ ...
3 └─ src/
4 ├─ ...
5 └─ Controller/
6 ├─ DefaultController.php
7 ├─ ...
8 ├─ Api/
9 │ ├─ ...
10 │ └─ ...
11 └─ Backend/
12 ├─ ...
13 └─ ...

Template Configuration
Don't use the @Template annotation to configure the template used by the controller.

The @Template annotation is useful, but also involves some magic. We don't think its benefit is worth
the magic, and so recommend against using it.
Most of the time, @Template is used without any parameters, which makes it more difficult to know
which template is being rendered. It also hides the fact that a controller should always return a Response
object (unless you're using a view layer).

What does the Controller look like


Considering all this, here is an example of what the controller should look like for the homepage of our
app:

Listing 5-3 1 namespace App\Controller;


2
3 use App\Entity\Post;
4 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
5 use Symfony\Component\Routing\Annotation\Route;
6
7 class DefaultController extends AbstractController
8 {
9 /**
10 * @Route("/", name="homepage")
11 */
12 public function index()
13 {

PDF brought to you by Chapter 5: Controllers | 17


generated on August 3, 2019
14 $posts = $this->getDoctrine()
15 ->getRepository(Post::class)
16 ->findLatest();
17
18 return $this->render('default/index.html.twig', [
19 'posts' => $posts,
20 ]);
21 }
22 }

Fetching Services
If you extend the base AbstractController class, you can't access services directly from the container
via $this->container->get() or $this->get(). Instead, you must use dependency injection to
fetch services by type-hinting action method arguments:

Don't use $this->get() or $this->container->get() to fetch services from the container. Instead,
use dependency injection.

By not fetching services directly from the container, you can make your services private, which has several
advantages.

Using the ParamConverter


If you're using Doctrine, then you can optionally use the ParamConverter1 to automatically query for an
entity and pass it as an argument to your controller.

Use the ParamConverter trick to automatically query for Doctrine entities when it's simple and convenient.

For example:

Listing 5-4 1 use App\Entity\Post;


2 use Symfony\Component\Routing\Annotation\Route;
3
4 /**
5 * @Route("/{id}", name="admin_post_show")
6 */
7 public function show(Post $post)
8 {
9 $deleteForm = $this->createDeleteForm($post);
10
11 return $this->render('admin/post/show.html.twig', [
12 'post' => $post,
13 'delete_form' => $deleteForm->createView(),
14 ]);
15 }

Normally, you'd expect a $id argument to show(). Instead, by creating a new argument ($post)
and type-hinting it with the Post class (which is a Doctrine entity), the ParamConverter automatically
queries for an object whose $id property matches the {id} value. It will also show a 404 page if no
Post can be found.

1. https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html

PDF brought to you by Chapter 5: Controllers | 18


generated on August 3, 2019
When Things Get More Advanced
The above example works without any configuration because the wildcard name {id} matches the name
of the property on the entity. If this isn't true, or if you have even more complex logic, your best choice is
to query for the entity manually. In our application, we have this situation in CommentController:

Listing 5-5 1 /**


2 * @Route("/comment/{postSlug}/new", name="comment_new")
3 */
4 public function new(Request $request, $postSlug)
5 {
6 $post = $this->getDoctrine()
7 ->getRepository(Post::class)
8 ->findOneBy(['slug' => $postSlug]);
9
10 if (!$post) {
11 throw $this->createNotFoundException();
12 }
13
14 // ...
15 }

You can also use the @ParamConverter configuration, which is infinitely flexible:

Listing 5-6 1 use App\Entity\Post;


2 use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
3 use Symfony\Component\HttpFoundation\Request;
4 use Symfony\Component\Routing\Annotation\Route;
5
6 /**
7 * @Route("/comment/{postSlug}/new", name="comment_new")
8 * @ParamConverter("post", options={"mapping"={"postSlug"="slug"}})
9 */
10 public function new(Request $request, Post $post)
11 {
12 // ...
13 }

The point is this: the ParamConverter shortcut is great for most situations. However, there is nothing
wrong with querying for entities directly if the ParamConverter would get complicated.

Pre and Post Hooks


If you need to execute some code before or after the execution of your controllers, you can use the
EventDispatcher component to set up before and after filters.
Next: Templates

PDF brought to you by Chapter 5: Controllers | 19


generated on August 3, 2019
Chapter 6
Templates

When PHP was created 20 years ago, developers loved its simplicity and how well it blended HTML
and dynamic code. But as time passed, other template languages - like Twig1 - were created to make
templating even better.

Use Twig templating format for your templates.

Generally speaking, PHP templates are more verbose than Twig templates because they lack native
support for lots of modern features needed by templates, like inheritance, automatic escaping and named
arguments for filters and functions.
Twig is the default templating format in Symfony and has the largest community support of all non-PHP
template engines (it's used in high profile projects such as Drupal 8).

Template Locations
Store the application templates in the templates/ directory at the root of your project.

Centralizing your templates in a single location simplifies the work of your designers. In addition, using
this directory simplifies the notation used when referring to templates (e.g. $this->render('admin/
post/show.html.twig') instead of $this->render('@SomeTwigNamespace/Admin/Posts/
show.html.twig')).

Use lowercased snake_case for directory and template names.

This recommendation aligns with Twig best practices, where variables and template names use
lowercased snake_case too (e.g. user_profile instead of userProfile and
edit_form.html.twig instead of EditForm.html.twig).

Use a prefixed underscore for partial templates in template names.

1. https://twig.symfony.com/

PDF brought to you by Chapter 6: Templates | 20


generated on August 3, 2019
You often want to reuse template code using the include function to avoid redundant code. To
determine those partials in the filesystem you should prefix partials and any other template without
HTML body or extends tag with a single underscore.

Twig Extensions
Define your Twig extensions in the src/Twig/ directory. Your application will automatically detect them
and configure them.

Our application needs a custom md2html Twig filter so that we can transform the Markdown contents
of each post into HTML. To do this, create a new Markdown class that will be used later by the Twig
extension. It needs to define one single method to transform Markdown content into HTML:

Listing 6-1 1 namespace App\Utils;


2
3 class Markdown
4 {
5 // ...
6
7 public function toHtml(string $text): string
8 {
9 return $this->parser->text($text);
10 }
11 }

Next, create a new Twig extension and define a filter called md2html using the Twig\TwigFilter
class. Inject the newly defined Markdown class in the constructor of the Twig extension:

Listing 6-2 1 namespace App\Twig;


2
3 use App\Utils\Markdown;
4 use Twig\Extension\AbstractExtension;
5 use Twig\TwigFilter;
6
7 class AppExtension extends AbstractExtension
8 {
9 private $parser;
10
11 public function __construct(Markdown $parser)
12 {
13 $this->parser = $parser;
14 }
15
16 public function getFilters()
17 {
18 return [
19 new TwigFilter('md2html', [$this, 'markdownToHtml'], [
20 'is_safe' => ['html'],
21 'pre_escape' => 'html',
22 ]),
23 ];
24 }
25
26 public function markdownToHtml($content)
27 {
28 return $this->parser->toHtml($content);
29 }
30 }

And that's it!


If you're using the default services.yaml configuration, you're done! Symfony will automatically know
about your new service and tag it to be used as a Twig extension.

PDF brought to you by Chapter 6: Templates | 21


generated on August 3, 2019
Next: Forms

PDF brought to you by Chapter 6: Templates | 22


generated on August 3, 2019
Chapter 7
Forms

Forms are one of the most misused Symfony components due to its vast scope and endless list of features.
In this chapter we'll show you some of the best practices so you can leverage forms but get work done
quickly.

Building Forms
Define your forms as PHP classes.

The Form component allows you to build forms right inside your controller code. This is perfectly fine
if you don't need to reuse the form somewhere else. But for organization and reuse, we recommend that
you define each form in its own PHP class:

Listing 7-1 1 namespace App\Form;


2
3 use App\Entity\Post;
4 use Symfony\Component\Form\AbstractType;
5 use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
6 use Symfony\Component\Form\Extension\Core\Type\EmailType;
7 use Symfony\Component\Form\Extension\Core\Type\TextareaType;
8 use Symfony\Component\Form\FormBuilderInterface;
9 use Symfony\Component\OptionsResolver\OptionsResolver;
10
11 class PostType extends AbstractType
12 {
13 public function buildForm(FormBuilderInterface $builder, array $options)
14 {
15 $builder
16 ->add('title')
17 ->add('summary', TextareaType::class)
18 ->add('content', TextareaType::class)
19 ->add('authorEmail', EmailType::class)
20 ->add('publishedAt', DateTimeType::class)
21 ;
22 }
23
24 public function configureOptions(OptionsResolver $resolver)
25 {

PDF brought to you by Chapter 7: Forms | 23


generated on August 3, 2019
26 $resolver->setDefaults([
27 'data_class' => Post::class,
28 ]);
29 }
30 }

Put the form type classes in the App\Form namespace, unless you use other custom form classes like data
transformers.

To use the class, use createForm() and pass the fully qualified class name:

Listing 7-2 1 // ...


2 use App\Form\PostType;
3
4 // ...
5 public function new(Request $request)
6 {
7 $post = new Post();
8 $form = $this->createForm(PostType::class, $post);
9
10 // ...
11 }

Form Button Configuration


Form classes should try to be agnostic to where they will be used. This makes them easier to re-use later.

Add buttons in the templates, not in the form classes or the controllers.

The Symfony Form component allows you to add buttons as fields on your form. This is a nice way to
simplify the template that renders your form. But if you add the buttons directly in your form class, this
would effectively limit the scope of that form:

Listing 7-3 1 class PostType extends AbstractType


2 {
3 public function buildForm(FormBuilderInterface $builder, array $options)
4 {
5 $builder
6 // ...
7 ->add('save', SubmitType::class, ['label' => 'Create Post'])
8 ;
9 }
10
11 // ...
12 }

This form may have been designed for creating posts, but if you wanted to reuse it for editing posts, the
button label would be wrong. Instead, some developers configure form buttons in the controller:

Listing 7-4 1 namespace App\Controller\Admin;


2
3 use App\Entity\Post;
4 use App\Form\PostType;
5 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
6 use Symfony\Component\Form\Extension\Core\Type\SubmitType;
7 use Symfony\Component\HttpFoundation\Request;
8
9 class PostController extends AbstractController
10 {
11 // ...
12

PDF brought to you by Chapter 7: Forms | 24


generated on August 3, 2019
13 public function new(Request $request)
14 {
15 $post = new Post();
16 $form = $this->createForm(PostType::class, $post);
17 $form->add('submit', SubmitType::class, [
18 'label' => 'Create',
19 'attr' => ['class' => 'btn btn-default pull-right'],
20 ]);
21
22 // ...
23 }
24 }

This is also an important error, because you are mixing presentation markup (labels, CSS classes, etc.)
with pure PHP code. Separation of concerns is always a good practice to follow, so put all the view-related
things in the view layer:

Listing 7-5 1 {{ form_start(form) }}


2 {{ form_widget(form) }}
3
4 <input type="submit" class="btn" value="Create"/>
5 {{ form_end(form) }}

Validation
The constraints option allows you to attach validation constraints to any form field. However, doing that
prevents the validation from being reused in other forms or other places where the mapped object is used.

Do not define your validation constraints in the form but on the object the form is mapped to.

For example, to validate that the title of the post edited with a form is not blank, add the following in the
Post object:
Listing 7-6 1 // src/Entity/Post.php
2
3 // ...
4 use Symfony\Component\Validator\Constraints as Assert;
5
6 class Post
7 {
8 /**
9 * @Assert\NotBlank
10 */
11 public $title;
12 }

Rendering the Form


There are a lot of ways to render your form, ranging from rendering the entire thing in one line to
rendering each part of each field independently. The best way depends on how much customization you
need.
One of the simplest ways - which is especially useful during development - is to render the form tags and
use the form_widget() function to render all of the fields:

Listing 7-7 1 {{ form_start(form, {attr: {class: 'my-form-class'} }) }}


2 {{ form_widget(form) }}
3 {{ form_end(form) }}

PDF brought to you by Chapter 7: Forms | 25


generated on August 3, 2019
If you need more control over how your fields are rendered, then you should remove the
form_widget(form) function and render your fields individually. See How to Customize Form
Rendering for more information on this and how you can control how the form renders at a global level
using form theming.

Handling Form Submits


Handling a form submit usually follows a similar template:

Listing 7-8 1 public function new(Request $request)


2 {
3 // build the form ...
4
5 $form->handleRequest($request);
6
7 if ($form->isSubmitted() && $form->isValid()) {
8 $entityManager = $this->getDoctrine()->getManager();
9 $entityManager->persist($post);
10 $entityManager->flush();
11
12 return $this->redirectToRoute('admin_post_show', [
13 'id' => $post->getId()
14 ]);
15 }
16
17 // render the template
18 }

We recommend that you use a single action for both rendering the form and handling the form submit.
For example, you could have a new() action that only renders the form and a create() action that only
processes the form submit. Both those actions will be almost identical. So it's much simpler to let new()
handle everything.
Next: Internationalization

PDF brought to you by Chapter 7: Forms | 26


generated on August 3, 2019
Chapter 8
Internationalization

Internationalization and localization adapt the applications and their contents to the specific region or
language of the users. In Symfony this is an opt-in feature that needs to be installed before using it
(composer require symfony/translation).

Translation Source File Location


Store the translation files in the translations/ directory at the root of your project.

Your translators' lives will be much easier if all the application translations are in one central location.

Translation Source File Format


The Symfony Translation component supports lots of different translation formats: PHP, Qt, .po, .mo,
JSON, CSV, INI, etc.

Use the XLIFF format for your translation files.

Of all the available translation formats, only XLIFF and gettext have broad support in the tools used by
professional translators. And since it's based on XML, you can validate XLIFF file contents as you write
them.
Symfony supports notes in XLIFF files, making them more user-friendly for translators. At the end, good
translations are all about context, and these XLIFF notes allow you to define that context.

The PHP Translation Bundle1 includes advanced extractors that can read your project and
automatically update the XLIFF files.

1. https://github.com/php-translation/symfony-bundle

PDF brought to you by Chapter 8: Internationalization | 27


generated on August 3, 2019
Translation Keys
Always use keys for translations instead of content strings.

Using keys simplifies the management of the translation files because you can change the original
contents without having to update all of the translation files.
Keys should always describe their purpose and not their location. For example, if a form has a field
with the label "Username", then a nice key would be label.username, not
edit_form.label.username.

Example Translation File


Applying all the previous best practices, the sample translation file for English in the application would
be:

Listing 8-1 1 <!-- translations/messages.en.xlf -->


2 <?xml version="1.0"?>
3 <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
4 <file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
5 <body>
6 <trans-unit id="title_post_list">
7 <source>title.post_list</source>
8 <target>Post List</target>
9 </trans-unit>
10 </body>
11 </file>
12 </xliff>

Next: Security

PDF brought to you by Chapter 8: Internationalization | 28


generated on August 3, 2019
Chapter 9
Security

Authentication and Firewalls (i.e. Getting the User's Credentials)


You can configure Symfony to authenticate your users using any method you want and to load user
information from any source. This is a complex topic, but the Security guide has a lot of information
about this.
Regardless of your needs, authentication is configured in security.yaml, primarily under the
firewalls key.

Unless you have two legitimately different authentication systems and users (e.g. form login for the main site
and a token system for your API only), we recommend having only one firewall entry with the anonymous
key enabled.

Most applications only have one authentication system and one set of users. For this reason, you only
need one firewall entry. If you have separated web and API sections on your site, you will need more
firewall entries. But the point is to keep things simple.
Additionally, you should use the anonymous key under your firewall. If you need to require users to
be logged in for different sections of your site (or maybe nearly all sections), use the access_control
area.

Use the bcrypt encoder for hashing your users' passwords.

If your users have a password, then we recommend hashing it using the bcrypt encoder, instead of the
traditional SHA-512 hashing encoder. The main advantages of bcrypt are the inclusion of a salt value to
protect against rainbow table attacks, and its adaptive nature, which allows to make it slower to remain
resistant to brute-force search attacks.

Sodium is the hashing algorithm as recommended by industry standards, but this won't be available
to you unless you are using PHP 7.2+ or have the libsodium1 extension installed. bcrypt is sufficient
for most applications.

PDF brought to you by Chapter 9: Security | 29


generated on August 3, 2019
With this in mind, here is the authentication setup from our application, which uses a login form to load
users from the database:

Listing 9-1 1 # config/packages/security.yaml


2 security:
3 encoders:
4 App\Entity\User: bcrypt
5
6 providers:
7 database_users:
8 entity: { class: App\Entity\User, property: username }
9
10 firewalls:
11 secured_area:
12 pattern: ^/
13 anonymous: true
14 form_login:
15 check_path: login
16 login_path: login
17
18 logout:
19 path: security_logout
20 target: homepage
21
22 # ... access_control exists, but is not shown here

The source code for our project contains comments that explain each part.

Authorization (i.e. Denying Access)


Symfony gives you several ways to enforce authorization, including the access_control configuration
in security.yaml, the @Security annotation and using isGranted on the
security.authorization_checker service directly.

• For protecting broad URL patterns, use access_control;


• Whenever possible, use the @Security annotation;
• Check security directly on the security.authorization_checker service whenever you have a more complex
situation.

There are also different ways to centralize your authorization logic, like with a custom security voter:

Define a custom security voter to implement fine-grained restrictions.

The @Security Annotation


For controlling access on a controller-by-controller basis, use the @Security annotation whenever
possible. Placing it above each action makes it consistent and readable.
In our application, you need the ROLE_ADMIN in order to create a new post. Using @Security, this
looks like:

Listing 9-2

1. https://pecl.php.net/package/libsodium

PDF brought to you by Chapter 9: Security | 30


generated on August 3, 2019
1 use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
2 use Symfony\Component\Routing\Annotation\Route;
3 // ...
4
5 /**
6 * Displays a form to create a new Post entity.
7 *
8 * @Route("/new", name="admin_post_new")
9 * @Security("is_granted('ROLE_ADMIN')")
10 */
11 public function new()
12 {
13 // ...
14 }

Using Expressions for Complex Security Restrictions


If your security logic is a little bit more complex, you can use an expression inside @Security. In the
following example, a user can only access the controller if their email matches the value returned by the
getAuthorEmail() method on the Post object:
Listing 9-3 1 use App\Entity\Post;
2 use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
3 use Symfony\Component\Routing\Annotation\Route;
4
5 /**
6 * @Route("/{id}/edit", name="admin_post_edit")
7 * @Security("user.getEmail() == post.getAuthorEmail()")
8 */
9 public function edit(Post $post)
10 {
11 // ...
12 }

Notice that this requires the use of the ParamConverter2, which automatically queries for the Post object
and puts it on the $post argument. This is what makes it possible to use the post variable in the
expression.
This has one major drawback: an expression in an annotation cannot be reused in other parts of the
application. Imagine that you want to add a link in a template that will only be seen by authors. Right
now you'll need to repeat the expression code using Twig syntax:

Listing 9-4 1 {% if app.user and app.user.email == post.authorEmail %}


2 <a href=""> ... </a>
3 {% endif %}

A good solution - if your logic is simple enough - can be to add a new method to the Post entity that
checks if a given user is its author:

Listing 9-5 1 // src/Entity/Post.php


2 // ...
3
4 class Post
5 {
6 // ...
7
8 /**
9 * Is the given User the author of this Post?
10 *
11 * @return bool
12 */

2. https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html

PDF brought to you by Chapter 9: Security | 31


generated on August 3, 2019
13 public function isAuthor(User $user = null)
14 {
15 return $user && $user->getEmail() === $this->getAuthorEmail();
16 }
17 }

Now you can reuse this method both in the template and in the security expression:

Listing 9-6 1 use App\Entity\Post;


2 use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
3 use Symfony\Component\Routing\Annotation\Route;
4
5 /**
6 * @Route("/{id}/edit", name="admin_post_edit")
7 * @Security("post.isAuthor(user)")
8 */
9 public function edit(Post $post)
10 {
11 // ...
12 }

Listing 9-7 1 {% if post.isAuthor(app.user) %}


2 <a href=""> ... </a>
3 {% endif %}

Checking Permissions without @Security


The above example with @Security only works because we're using the ParamConverter, which gives
the expression access to the post variable. If you don't use this, or have some other more advanced use-
case, you can always do the same security check in PHP:

Listing 9-8 1 /**


2 * @Route("/{id}/edit", name="admin_post_edit")
3 */
4 public function edit($id)
5 {
6 $post = $this->getDoctrine()
7 ->getRepository(Post::class)
8 ->find($id);
9
10 if (!$post) {
11 throw $this->createNotFoundException();
12 }
13
14 if (!$post->isAuthor($this->getUser())) {
15 $this->denyAccessUnlessGranted('edit', $post);
16 }
17 // equivalent code without using the "denyAccessUnlessGranted()" shortcut:
18 //
19 // use Symfony\Component\Security\Core\Exception\AccessDeniedException;
20 // use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface
21 //
22 // ...
23 //
24 // public function __construct(AuthorizationCheckerInterface $authorizationChecker) {
25 // $this->authorizationChecker = $authorizationChecker;
26 // }
27 //
28 // ...
29 //
30 // if (!$this->authorizationChecker->isGranted('edit', $post)) {
31 // throw $this->createAccessDeniedException();
32 // }

PDF brought to you by Chapter 9: Security | 32


generated on August 3, 2019
33 //
34 // ...
35 }

Security Voters
If your security logic is complex and can't be centralized into a method like isAuthor(), you should
leverage custom voters. These are much easier than ACLs and will give you the flexibility you need in
almost all cases.
First, create a voter class. The following example shows a voter that implements the same
getAuthorEmail() logic you used above:
Listing 9-9 1 namespace App\Security;
2
3 use App\Entity\Post;
4 use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
5 use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
6 use Symfony\Component\Security\Core\Authorization\Voter\Voter;
7 use Symfony\Component\Security\Core\User\UserInterface;
8
9 class PostVoter extends Voter
10 {
11 const CREATE = 'create';
12 const EDIT = 'edit';
13
14 private $decisionManager;
15
16 public function __construct(AccessDecisionManagerInterface $decisionManager)
17 {
18 $this->decisionManager = $decisionManager;
19 }
20
21 protected function supports($attribute, $subject)
22 {
23 if (!in_array($attribute, [self::CREATE, self::EDIT])) {
24 return false;
25 }
26
27 if (!$subject instanceof Post) {
28 return false;
29 }
30
31 return true;
32 }
33
34 protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
35 {
36 $user = $token->getUser();
37 /** @var Post */
38 $post = $subject; // $subject must be a Post instance, thanks to the supports method
39
40 if (!$user instanceof UserInterface) {
41 return false;
42 }
43
44 switch ($attribute) {
45 // if the user is an admin, allow them to create new posts
46 case self::CREATE:
47 if ($this->decisionManager->decide($token, ['ROLE_ADMIN'])) {
48 return true;
49 }
50
51 break;
52
53 // if the user is the author of the post, allow them to edit the posts

PDF brought to you by Chapter 9: Security | 33


generated on August 3, 2019
54 case self::EDIT:
55 if ($user->getEmail() === $post->getAuthorEmail()) {
56 return true;
57 }
58
59 break;
60 }
61
62 return false;
63 }
64 }

If you're using the default services.yaml configuration, your application will autoconfigure your security
voter and inject an AccessDecisionManagerInterface instance into it thanks to autowiring.
Now, you can use the voter with the @Security annotation:

Listing 9-10 1 /**


2 * @Route("/{id}/edit", name="admin_post_edit")
3 * @Security("is_granted('edit', post)")
4 */
5 public function edit(Post $post)
6 {
7 // ...
8 }

You can also use this directly with the security.authorization_checker service or via the even
easier shortcut in a controller:

Listing 9-11 1 /**


2 * @Route("/{id}/edit", name="admin_post_edit")
3 */
4 public function edit($id)
5 {
6 $post = ...; // query for the post
7
8 $this->denyAccessUnlessGranted('edit', $post);
9
10 // use Symfony\Component\Security\Core\Exception\AccessDeniedException;
11 // use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
12 //
13 // ...
14 //
15 // public function __construct(AuthorizationCheckerInterface $authorizationChecker) {
16 // $this->authorizationChecker = $authorizationChecker;
17 // }
18 //
19 // ...
20 //
21 // if (!$this->authorizationChecker->isGranted('edit', $post)) {
22 // throw $this->createAccessDeniedException();
23 // }
24 //
25 // ...
26 }

Next: Web Assets

PDF brought to you by Chapter 9: Security | 34


generated on August 3, 2019
Chapter 10
Web Assets

Web assets are things like CSS, JavaScript and image files that make the frontend of your site look and
work great.

Store your assets in the assets/ directory at the root of your project.

Your designers' and front-end developers' lives will be much easier if all the application assets are in one
central location.

Use Webpack Encore1 to compile, combine and minimize web assets.

Webpack2 is the leading JavaScript module bundler that compiles, transforms and packages assets for
usage in a browser. Webpack Encore is a JavaScript library that gets rid of most of Webpack complexity
without hiding any of its features or distorting its usage and philosophy.
Webpack Encore was designed to bridge the gap between Symfony applications and the JavaScript-based
tools used in modern web applications. Check out the official Webpack Encore documentation3 to learn
more about all the available features.
Next: Tests

1. https://github.com/symfony/webpack-encore
2. https://webpack.js.org/
3. https://symfony.com/doc/current/frontend.html

PDF brought to you by Chapter 10: Web Assets | 35


generated on August 3, 2019
Chapter 11
Tests

Of all the different types of test available, these best practices focus solely on unit and functional tests.
Unit testing allows you to test the input and output of specific functions. Functional testing allows you to
command a "browser" where you browse to pages on your site, click links, fill out forms and assert that
you see certain things on the page.

Unit Tests
Unit tests are used to test your "business logic", which should live in classes that are independent of
Symfony. For that reason, Symfony doesn't really have an opinion on what tools you use for unit testing.
However, the most popular tools are PHPUnit1 and PHPSpec2.

Functional Tests
Creating really good functional tests can be tough so some developers skip these completely. Don't skip
the functional tests! By defining some simple functional tests, you can quickly spot any big errors before
you deploy them:

Define a functional test that at least checks if your application pages are successfully loading.

PHPUnit data providers help you implement functional tests:

Listing 11-1 1 // tests/ApplicationAvailabilityFunctionalTest.php


2 namespace App\Tests;
3
4 use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
5
6 class ApplicationAvailabilityFunctionalTest extends WebTestCase
7 {
8 /**
9 * @dataProvider urlProvider

1. https://phpunit.de/
2. https://www.phpspec.net/

PDF brought to you by Chapter 11: Tests | 36


generated on August 3, 2019
10 */
11 public function testPageIsSuccessful($url)
12 {
13 $client = self::createClient();
14 $client->request('GET', $url);
15
16 $this->assertTrue($client->getResponse()->isSuccessful());
17 }
18
19 public function urlProvider()
20 {
21 yield ['/'];
22 yield ['/posts'];
23 yield ['/post/fixture-post-1'];
24 yield ['/blog/category/fixture-category'];
25 yield ['/archives'];
26 // ...
27 }
28 }

This code checks that all the given URLs load successfully, which means that their HTTP response status
code is between 200 and 299. This may not look that useful, but given how little effort this took, it's
worth having it in your application.
In computer software, this kind of test is called smoke testing3 and consists of "preliminary testing to reveal
simple failures severe enough to reject a prospective software release".

Hardcode URLs in a Functional Test


Some of you may be asking why the previous functional test doesn't use the URL generator service:

Hardcode the URLs used in the functional tests instead of using the URL generator.

Consider the following functional test that uses the router service to generate the URL of the tested
page:

Listing 11-2 1 // ...


2 private $router; // consider that this holds the Symfony router service
3
4 public function testBlogArchives()
5 {
6 $client = self::createClient();
7 $url = $this->router->generate('blog_archives');
8 $client->request('GET', $url);
9
10 // ...
11 }

This will work, but it has one huge drawback. If a developer mistakenly changes the path of the
blog_archives route, the test will still pass, but the original (old) URL won't work! This means that
any bookmarks for that URL will be broken and you'll lose any search engine page ranking.

Testing JavaScript Functionality


The built-in functional testing client is great, but it can't be used to test any JavaScript behavior on your
pages. If you need to test this, consider using the Mink4 library from within PHPUnit.
If you have a heavy JavaScript frontend, you should consider using pure JavaScript-based testing tools.

3. https://en.wikipedia.org/wiki/Smoke_testing_(software)
4. http://mink.behat.org

PDF brought to you by Chapter 11: Tests | 37


generated on August 3, 2019
Learn More about Functional Tests
Consider using the HautelookAliceBundle5 to generate real-looking data for your test fixtures using Faker6
and Alice7.

5. https://github.com/hautelook/AliceBundle
6. https://github.com/fzaninotto/Faker
7. https://github.com/nelmio/alice

PDF brought to you by Chapter 11: Tests | 38


generated on August 3, 2019

You might also like