In this section of the Beginners Guide to Symfony 3 series we are going to take what we have learned about in part one and part two, and build upon this to implement a secure Members Only Support Form.

The idea behind this form is that to access it, you must be logged in (which also means being a Registered User of the site), and with the User being logged in, this means we as developers can access additional information about the user making the support request, which we can use to populate the form.

To make things more interesting, we are not going to be using FOSUserBundle. There are already videos tutorials for FOSUserBundle available on the site, so if you prefer that approach then please watch those videos instead.

The reasoning for not using FOSUserBundle is two-fold.

Firstly, not every project needs FOSUserBundle, but for many - and myself included - it almost feels like a go-to dependency. Start a new project, add FOSUserBundle and some other bundles, and away we go.

However, in adding FOSUserBundle not only do we have to work with third party code, but potentially we may not even need a lot of the functionality it provides.

There is a second benefit to not using FOSUserBundle in this part of the course. This is that in order to get a better understanding of what FOSUserBundle brings for you, it is important to understand how to add a similar setup without using FOSUserBundle.

Before proceeding any further, at this stage I would advise you that you should have at least completed the previous two sections of this course, or have equivalent knowledge.

Database Setup

Up until this point we have not yet worked with the database.

However, from this point on we are starting to work with a database. You must have access to a MySQL / PostgreSQL / SQLite database to continue at this point.

Covering the installation and setup of these database is outside the scope of this tutorial. If you are truly struggling please do let me know and I will try my best to help you out.

To begin with, please ensure you have updated your parameters.yml file with the database settings specific to your environment:

// /app/config/parameters.yml

parameters:
    database_host:     127.0.0.1
    database_port:     ~
    database_name:     some_db_name
    database_user:     your_db_username
    database_password: your_db_password

Once you have these settings in place, be sure to create your database by running the command:

php bin/console doctrine:database:create

Run this command from your project root. Further information is available in the official Symfony documentation.

Generating Your First Doctrine Entity

With your database connectivity setup and a database created, we are good to generate our first Doctrine entity.

Now, at this point a perfectly valid question might be:

What the heck is a Doctrine entity?

Good question :) It's the same one I had when first encountering this stuff. In fact, I decided that - heck, I know better - and I totally bypassed Doctrine for my first real project. That ended well.

For my next project I determined that I would use Doctrine to figure out why it was the recommended way to work with the database. I've never looked back.

A Doctrine entity is simply an object with some form of identity. To you and me, that typically means an object with an $id property.

This $id property can be a plain old integer, or a more complex ID such as a guid, which is a long string that's a real pain for humans to remember. The standard integer is the default, and that's what we're going to work with here.

However, note that whilst the entity generator will give us an auto-generated integer by default, this is technically not the recommended approach.

Now, if you're looking at Symfony then - I hope - by now I'm sure you've worked with a database in some capacity before.

Taking your typical database you would have one or more tables, and each table represents some concept:

  • Users
  • Products
  • Social Media Posts
  • Students
  • Teachers

etc, etc.

If we take Students as our example, then the table might have:

  • id
  • first name
  • last name
  • date of birth

Stuff like that.

We can find a student by their ID, or by querying against some combination of the properties we have saved for them. To do this you'd write a SQL query. Largely all SQL implementations have a very similar syntax for writing queries.

When working with Doctrine we won't directly use SQL. Instead we will have a few methods of querying data, and the syntax is slightly different to SQL. If you have SQL experience though, this will definitely help you as Doctrine's query syntax (DQL / Doctrine Query Language) is very similar.

Now we covered a little earlier that entities are objects with identity. Imagine if we had our student table that represented all the students in our class. Due to absolute sheer coincidence, we have two students in our class called "Ken Lee", and both Kens were born on the exact same day.

As humans we know one Ken from another.

But if we didn't have the concept of identity inside our code, how would we differentiate the two Kens inside the database? Mayhem would ensue, yessir. So we assign a number from an auto incrementing sequence to both Kens, and now we have Ken 1 and Ken 2, and we consider it a solved problem :)

Depending on your background as a developer, how low level you work with the database is likely to differ.

But here's the difference when working with Doctrine:

We aren't - directly - going to work with the database at all.

Instead, the database is hidden away from us. And instead, we work with objects. And these objects just so happen to be stored in whatever database we have configured.

One of the benefits of this is that we can use MySQL, or PostgreSQL, or SQLite, and still use the exact same syntax when working with Doctrine.

Again, depending on your background in development, you may typically start by creating your database first, then working 'upwards' from there. And again, with Doctrine that is generally not the case.

We will start by thinking about our entities (our objects) first.

We will plan out how they interact, and from there, we can defer the work of how these objects are saved to Doctrine. The weird part about this is that sometimes, we may use multiple tables to save off one entity.

This is a more advanced concept, but just be aware that you don't always have a 1-to-1 mapping between an entity and an underlying database table.

Though, most of the time, I seem too.

Admittedly this is likely a total paradigm shift. It's one of those things that takes a little (or a lot of) adjustment but is worthwhile in the end. At least, that's been my experience.

Anyway, to generate our first entity - our Member - we are going to run:

php bin/console doctrine:generate:entity

The follow the prompts - or watch the video, until we have the following:

<?php

// /src/AppBundle/Entity/Member.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Member
 *
 * @ORM\Table(name="member")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\MemberRepository")
 */
class Member
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="username", type="string", length=255, unique=true)
     */
    private $username;

    /**
     * @var string
     *
     * @ORM\Column(name="email", type="string", length=255, unique=true)
     */
    private $email;

    /**
     * @var string
     *
     * @ORM\Column(name="password", type="string", length=64)
     */
    private $password;


    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set username
     *
     * @param string $username
     *
     * @return Member
     */
    public function setUsername($username)
    {
        $this->username = $username;

        return $this;
    }

    /**
     * Get username
     *
     * @return string
     */
    public function getUsername()
    {
        return $this->username;
    }

    /**
     * Set email
     *
     * @param string $email
     *
     * @return Member
     */
    public function setEmail($email)
    {
        $this->email = $email;

        return $this;
    }

    /**
     * Get email
     *
     * @return string
     */
    public function getEmail()
    {
        return $this->email;
    }
}

Note that the $id property doesn't have a setId method - and don't be tempted to add one either. This is intentional.

For every other field you have added in the generator you should see a corresponding class property, and a getter / setter combo.

Each class property will be 'annotated' with various metadata that describe how this property should be saved off to the database.

In the class docblock itself there are two further annotations:

/**
 * Member
 *
 * @ORM\Table(name="member")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\MemberRepository")
 */
class Member

Doctrine will save this entity to a table named member.

And there is an associated repositoryClass configured for us which the generator also created for us. This will be empty at this stage:

<?php

// /src/AppBundle/Repository/MemberRepository.php

namespace AppBundle\Repository;

/**
 * MemberRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class MemberRepository extends \Doctrine\ORM\EntityRepository
{
}

This class is where we would write all the methods that query our stored entities of this type. We won't need this at this stage.

There's tons to cover with Doctrine so if your interest is at all piqued, I would suggest watching the Doctrine Databasics course.

Turning Our Member Entity Into A Symfony User

Whilst the generator has created us a class which we can use as a Doctrine entity representing the concept of a Member, this won't immediately / magically work as a Symfony User object.

To begin with, we will need to 'enhance' our Member class by implementing a couple of interfaces.

You needn't worry about remembering any of this, it is all documented on the official Symfony documentation.

Let's implement these two interfaces now:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Member
 *
 * @ORM\Table(name="member")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\MemberRepository")
 */
class Member implements UserInterface, \Serializable
{

The obvious parts being:

implements UserInterface, \Serializable

And the less obvious part being:

use Symfony\Component\Security\Core\User\UserInterface;

In implementing these two interfaces we will be required to add a bunch of extra methods to our Member object:

UserInterface

  • getRoles
  • getSalt
  • eraseCredentials

To quickly cover what these methods offer:

getRoles returns an array of strings. Each string is the name of a "role". A role is something that makes sense in your application. For example, if your application was a blog, you may have ROLE_EDITOR, ROLE_AUTHOR, ROLE_CONTRIBUTOR, and so on. Roles begin with the prefix ROLE_, and most commonly a logged-in User will have, at the very least, ROLE_USER.

getSalt returns the 'salt' that was originally used to encode the password. If a password was not encoded using a salt then this may return null. A salt is random data (a random string of characters) that helps ensure two identical passwords do not end up with the same hash when saved, making our system more secure. This explanation from tylerl on StackOverflow is fantastic.

eraseCredentials should remove any sensitive data from the User (Member) object. As you'll soon see, we will temporarily store the plainPassword on our Member, and we don't want this information saved anywhere. We will explicitly not declare an annotation to ensure that the plainPassword is never saved off to the database, but if we aren't careful, it could be serialized / saved to the user's session.

Serializable

  • serialize
  • unserialize

These two methods define a way to convert from (serialize) an object to something (in our case, JSON), and convert back from JSON to the original object type (unserialize). The idea here is to define a predictable / repeatable method of conversion.

Now, from the names of these two interfaces alone, UserInterface is the more obvious of the two. But why do we need to implement Serializable?

Why Do We Need To Implement Serializable?

Our user experience would be fairly shocking if we requested the user login for every request they made to our site.

We expect to log in once, and then this login session should be remembered for some predetermined length of time before expiring.

However, once we have logged in once, how can our website be sure that a future request comes from this currently logged in user?

The answer is that when a user initially visits our Symfony website, a new session is created.

This session is managed by our webserver, and will store the state of this user for some pre-determined duration.

We can change this duration by updating our Symfony configuration files, but by default it will be whatever length is set in the session.cookie_lifetime value inside your servers php.ini file, which again by default is zero / 0. Zero here means the length of the browser session - or until the user completely closes their browser, to you and me.

Now a couple of extra points here - closing the browser means fully closed. On a Mac, this would be equivalent of cmd+q. If you just close your browser, OSX will keep it open in the background leading to a confusing outcome of still being logged in when you next open a new Chrome window to the site. Same goes for tabs. To clarify - you must effectively kill the browser.

Let's walk through an example of this. I would strongly encourage you to try this out for yourselves.

We will do this as simply as possible by running a local webserver. As I will cover a little further shortly, this can get more complicated in the real world.

cd /path/to/your/project

php bin/console server:start

 [OK] Web server listening on http://127.0.0.1:8000

Ok, we're in our project's root directory and we've started up the local webserver. Cool.

We haven't yet browsed to the given URL, so before we do, let's look at the sessions directory:

ls -la var/sessions

total: 0

Nothing there.

Let's now browse to http://127.0.0.1:8000.

symfony-session-cookie-example

As soon as we do, a session cookie is dropped into our browser.

Note here the value of that cookie: ut1tlqieq37sbddqbsjotc8d2l

This is client side - i.e. in the browser.

Let's take a look on the server side:

ls -la var/sessions

total 0

drwxr-xr-x 3 codereview SharedData 102 Apr 24 10:37 dev

Ok, interesting. A new directory - dev - has been created. The username (codereview) and group (SharedData) will likely be different on yours, but that doesn't matter.

Let's look inside that directory:

ls -la var/sessions/dev

total 4.0K

-rw------- 1 codereview SharedData 179 Apr 24 10:37 sess_ut1tlqieq37sbddqbsjotc8d2l

Interesting. This filename contains the value we saw in our browser. Let's inspect its contents:

cat var/sessions/dev/sess_ut1tlqieq37sbddqbsjotc8d2l

_sf2_attributes|a:1:{s:26:"_security.main.target_path";s:22:"http://127.0.0.1:8000/";}_sf2_flashes|a:0:{}_sf2_meta|a:3:{s:1:"u";i:1493026648;s:1:"c";i:1493026644;s:1:"l";s:1:"0";}

_sf2 - aren't we using Symfony 3? :) We are, but the default $storageKey is set to _sf2_attributes, at a guess because of backwards compatibility - though I don't know this for sure.

Now let's assume we've implemented all of those 5 methods from above (implements UserInterface, \Serializable), and we log in. What happens to the contents of that session file?

cat var/sessions/dev/sess_ut1tlqieq37sbddqbsjotc8d2l

_sf2_attributes|a:2:{s:14:"_security_main";s:428:"C:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":340:{a:3:{i:0;N;i:1;s:4:"main";i:2;s:300:"a:4:{i:0;C:23:"AppBundle\Entity\Member":98:{a:3:{i:0;i:5;i:1;s:1:"w";i:2;s:60:"$2y$13$PIVgti1Mh7Rw3ruY6o134.Gs8PIydBRVFdGlrwgHznczjh33llysG";}}i:1;b:1;i:2;a:1:{i:0;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"Symfony\Component\Security\Core\Role\Rolerole";s:9:"ROLE_USER";}}i:3;a:0:{}}";}}";s:26:"_csrf/support_request_form";s:43:"GuO_0dzg7DScOuB40qhbNaFmEae91gg_CljOZMfrPwk";}_sf2_flashes|a:0:{}_sf2_meta|a:3:{s:1:"u";i:1493027627;s:1:"c";i:1493027626;s:1:"l";s:1:"0";}

Serialization therefore is about converting the User object (or Member in our case) from an object that can be used in PHP into a representation of that object that can be stored in this file. Unserialize is the process of converting back from this string to a PHP object.

We control these two methods because we control exactly what information we want to save.

Every time you make a subsequent request, your browser will send along the cookie that uniquely identifies your session. This is how the server knows who you are without you having to log in every time.

If you delete your cookie in the browser - you're logged out.

If you delete the corresponding file on the server - you're logged out.

I mentioned earlier that this gets a little more complex in the real world. For example, what happens when we outgrow a single server? What if we have two web servers, and both save the session data to their local disk? You might find yourself only logged in for 50% of your requests - that is, you're logged in on the server you originally connected too, but the other server has no idea who you are as they don't share the same hard disk... yikes.

Of course, there are solutions to these problems (save your session data in Redis for example), but this is outside the scope of this tutorial. But you should be aware of it, all the same.


Code For This Course

Get the code for this course.

Share This Episode

If you have found this video helpful, please consider sharing. I really appreciate it.


Episodes in this series

# Title Duration
1 Walking Through The Initial App 01:01
2 First Steps 04:17
3 Adding The Contact Form 04:25
4 Submitting Our Form and Sending An Email 05:10
5 Learning A Little More About Forms 05:34
6 A Different Way To Handle POST 03:55
7 One Quick Way To Style A Form 02:48
8 Creating A Members Only Support Form 04:29
9 Creating The Login Form 04:01
10 Configuration In Security.yml 04:30
11 Adding Logout 03:02
12 Registration Form - Part 1 06:16
13 Registration Form - Part 2 07:02
14 Loading Users From The Database 05:10
15 Automatically Logging In When Registering 04:21
16 Restricting Routes To Only Logged In Users 02:56
17 The Fat Controller 07:00