Create a membership site with Symfony 2

In this article we'll be looking at how you can create a membership area for a new web site application using the Symfony 2 framework. This tutorial aims to glue together all the steps from setting up a Symfony project to logging in and accessing a protected page. A lot of the individual sections of this tutorial are well documented in the Symfony documentation, so this is your best point of reference if you get stuck, or don't hesitate to use the comment area at the bottom if you need help.

Goal

To create a new Symfony application with a registration and login form that allows users to access protected resources. Users will be persisted and authenticated against a MySQL database.

login-form

By the end of the tutorial you should have the core functionality for a user to login, register and access an example protected page.

Method

In this tutorial we'll be utilising the Symfony 2 Framework along with the Friends Of Symfony (FOS) user bundle (FOSUserBundle).  You'll be guided throught the following steps:

  • Project Setup
    • Install Symfony
    • Create the Symfony Project
    • Basics Introduction
    • Database Configuration
  • Membership 
    • Membership Bundles
    • Customise the Layout
    • Create Authenticated Page

Prerequisites and Requirements

So before we get started, I'm assuming the reader is a competent PHP developer and understands the concept of MVC (Model, View Controller) architecture. In this tutorial you'll be guided through the entire set up process and as such it is written for relative beginners. This will however be a practical tutorial to achieve the desired goal, rather than focussing too much on how Symfony works. If you're completely new to the framework and need a more detailed guide to using Symfony then I'd recommend starting with a hello world project using the Symfony Quick Tour

If you are familiar with Symfony - I'll be building the logic for this application into the default AppBundle, you can of course create this into your own custom bundle if you'd prefer.

I'll be referring to the project as 'CS Symfony Members', which is used in the domain and directory structure, so update any references to this with your own project title.

Finally, you'll need the following configuration requirements:

  • Local server configured (LAMP/MAMP/WAMP)
  • PHP 5.4 or higher
  • Have composer installed globally

Install Symfony

To get started we'll be installing the Symfony core framework using the Symfony installer: 

Linux / Mac

Run the following commands to add Symfony:

sudo curl -LsS https://symfony.com/installer -o /usr/local/bin/symfony
sudo chmod a+x /usr/local/bin/symfony

Test this works by typing 'symfony' at the command line.

Windows

Open a command console and run the following to download Symfony, move it to your projects file and then execute it:

c:\> php -r "readfile('https://symfony.com/installer');" > symfony
c:\> move symfony c:\projects
c:\projects\> php symfony

Create the Symfony project

Next we're going to create a Symfony project - switch to the location where you want the project created and run the Symfony 'new' command.

# Linux / Mac
symfony new cs-symfony-members

# Windows
c:\> cd projects/
c:\projects\> php symfony new cs-symfony-members

Pay attention to any warnings thrown as you will be informed if your configuration does not meet minimum requirements and any changes you need to make. If successful you should now have all the core files set up within a new directory titled as your project.

Create the project using composer

You can also achieve the above using the following composer command:

composer create-project symfony/framework-standard-edition cs-symfony-members

Create a domain vhost record

Set up a vhost record and point it to the project folder /web, which is the public directory in Symfony. In this tutorial I'll be using cs-symfony-members.dev. Note that Symfony doesn't have a single index.php file, but features two primary execution files - one for development (app_dev.php) and another for production (app.php). 

Test it works by going to your domain and clicking on the app_dev.php, you should see a welcome message. You may need to debug errors such as folder permissions.

Basics Introduction

If you're new to Symfony, here's a quick summary of the basic workings to get started...

User url requests are mapped to resources, so in the following example the request would be for a 'test' resource:

http://cs-symfony-members.dev/test

Requests are handled in PHP classes called controllers, which contain functions called actions. Actions contain the code that decides what to do, and what to return in response to that request. In a usual scenario, the action would run a view file that will contain the markup you wish to present to the user. 

Taking the practical example of the welcome screen during setup, the request is simply '/', so let's take a look at the controller and action for this request in the following file:

/src/AppBundle/Controller/DefaultController.php

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction(Request $request)
    {
        return $this->render('default/index.html.twig', [
            'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..'),
        ]);
    }
}

So this is an example of a controller (the class), and an action (indexAction function). If you look at the annotation before the action, you'll see how a route can be defined, which relates to the request. So this action will pick up the route '/' and execute the code in the function. In this case, it will render a view file, which contains the markup that you saw on the welcome page. We can view this file in:

/app/Resources/views/default/index.html.twig

{% extends 'base.html.twig' %}
{% block body %}
<h1>Welcome to Symfony</h1>
    {# ... #}
{% endblock %}

This is a template file created using a template engine called twig, which mixes some of it's own syntax for simple if and loop statements with html markup. Notice how in the main content is contained within a 'body' block - if you look inside the extended layout file 'base.html.twig' you'll see there is also a 'body' block where the contents will be dropped into.

So we've now got Symfony installed, and you've got an example route set up for a controller and action, which is loading a twig view template. We'll now move onto setting up our application. 

Database Configuration

In this tutorial we'll be configuring a MySQL database using the Doctrine ORM. There are alternatives to Doctrine that you can use such as Propel.

What is Doctrine?

Doctrine is an ORM (object relationship manager), it's job is to map objects in your application to your database (e.g. a user object for a users table). Doctrine allows you to focus on working with your objects / entities within your application, and it will take care of the hard work when fetching / persisting those objects to the database.

Connection Parameters

Start by adding your database configuration details into the following config file:

/app/config/parameters.yml

parameters:
    database_driver:    pdo_mysql
    database_host:      localhost
    database_name:      cs_symfony_members
    database_user:      root
    database_password:  password

You may find Symfony auto-populated this file for you, but no database will exist at this stage. With the details added / updated, you can now ask Docrine to create the database for you:

php bin/console doctrine:database:create

That's it for now - you should have an empty local database with the name referenced in your config file. You might wish to check the collation is set to UTF-8 (under operations in phpmyadmin), by default UTF-8 will be the setting for Doctrine when interacting with your database.

Membership

Now that we have the Symfony core configured, we can move on to setting up the members area.

Install User Bundle

One of the core concepts of symfony is bundles, which is essentially the grouping of code within the application. So for example, the default controller that we looked at in the last section belonged to the 'AppBundle', you'll notice the containing directory is titled this way, and there is an 'AppBundle.php' located within the directory if you need to do any bootstrapping.

One of the benefits of bundles is that it's easy to add third-party code into your application. In this instance, we're going to use the FOSUserBundle, which will handle the majority of the functionality that we're looking for such as processing registration and authentication.

FOSUserBundle builds on the Symfony Security Component, which provides a system for authenticating users and ACL (access control list) functionality. You don't have to worry too much about the inner workings of this as FOSUserBundle is built on top, but you will need to have the Security Component installed and to configure some options. We'll load this in with composer, so at your console type the following:

composer require symfony/security

This will download the package and add it to your require list in the composer.json file in the root of your application.

Symfony features translations so that you can use multiple languages within your application, to ensure we can use the default texts for the user bundle, you need to enable the translator in your config file located in:

/app/config/config.yml

framework:
    translator: ~

Now we can download the FOSUserBundle with composer:

composer require friendsofsymfony/user-bundle "~2.0@dev"

Once downloaded, bundles need to be enabled, you can do this by adding the bundle to the list in the registerBundles method in the following file:

app/AppKernel.php

public function registerBundles()
 {
    $bundles = [
        new FOS\UserBundle\FOSUserBundle(),
    ];
 }

Configure members application

With all the appropriate bundles installed and activated, we can now move on to configuring our membership application.

To start with we want to create a user entity that will extend another class provided by the user bundle. Create the directory structure and file as follows:

/src/AppBundle/Entity/User.php

namespace AppBundle\Entity;

use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="`user`")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    public function __construct()
    {
        parent::__construct();
    }
}

If you're wondering where the rest of the fields are, take a look inside the BaseUser file where you'll see the user bundle has already configured key fields such as username, email and password.

Security Config

Next we need to configure the security so that Symfony knows how to interact with the user bundle, and the permissions required for different areas:

/app/config/security.yml

security:
    encoders:
        FOS\UserBundle\Model\UserInterface: bcrypt

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN

    providers:
        fos_userbundle:
            id: fos_user.user_provider.username

    firewalls:
        main:
            pattern: ^/
            form_login:
                provider: fos_userbundle
                csrf_token_generator: security.csrf.token_manager
            logout:       true
            anonymous:    true

    access_control:
        - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin/, role: ROLE_ADMIN }

This is a minimum configuration example from the Symfony docs, which does the following:

  • encoders: defines the encryption method
  • role_hierarchy: defines available roles and their inheritence
  • providers: tells Symfony what service to use when referencing fos_userbundle
  • firewalls: set up a 'main' firewall that tells Symfony to direct users to the login form if they should be authenticated
  • access_control: define the role and authentication required for specific paths

Bundle Config

Next we configure the bundle itself in the main config file:

/app/config/config.yml

fos_user:
    db_driver: orm
    firewall_name: main
    user_class: AppBundle\Entity\User

Notice how we reference the firewall we configured, and our user entity from previous sections.

Lastly, to save ourselves some time we're going to use some default routes that come with the bundle for common requests such as login and registration. To do this we need to register the bundle's routing files in the routing config:

/app/config/routing.yml

fos_user:
    resource: "@FOSUserBundle/Resources/config/routing/all.xml"

Generate User Table

Now that we've configured everything, we can now create the table for our user entity. Doctrine will take care of this for you with the following command:

php bin/console doctrine:schema:update --force

The application functionality is now ready to go, and if you browse to your domain /app_dev.php/login you should now see the unstyled login form.

Customise the layout

Now we can pull it all together by linking into our new sections via the home page, and overriding the default templates so we can control the look and feel.

Link to the new pages

Inside our landing page view add the following code to link to our new pages:

/app/Resources/views/default

<p><a href="{{ path('fos_user_security_login') }}">Login</a> | <a href="{{ path('fos_user_registration_register') }}">Register</a></p>

The brackets and path are twig template syntax, which tells it to find the url to use for the parameter passed in (e.g. fos_user_security_login). You can define these paths in your controller annotation, the two in the example above are the user bundle defaults, which can be found in /vendor/friendsofsymfony/Resources/config/routing/...

Override the layout template

At the moment the default login page has no styling, and is using a separate template file to the one that our home page is using. If we link it into the same file then it can all be managed in one place.

Let's start with a quick tidy up, at the moment all the CSS for our homepage is directly inside the view template, if we move it into a CSS file then we can re-use it in different templates. To achieve this, go inside the /web/bundles directory and create a directory path to /app/css, then create a file called site.css within it. Open /app/Resources/views/default/index.html.twig, copy all the CSS inside the stylesheets block and paste it into the CSS file, so with the default code your CSS file should look like this:

/web/bundles/app/css/site.css

body { background: #F5F5F5; font: 18px/1.5 sans-serif; }
h1, h2 { line-height: 1.2; margin: 0 0 .5em; }
h1 { font-size: 36px; }
h2 { font-size: 21px; margin-bottom: 1em; }
p { margin: 0 0 1em 0; }
a { color: #0000F0; }
a:hover { text-decoration: none; }
code { background: #F5F5F5; max-width: 100px; padding: 2px 6px; word-wrap: break-word; }
#wrapper { background: #FFF; margin: 1em auto; max-width: 800px; width: 95%; }
#container { padding: 2em; }
#welcome, #status { margin-bottom: 2em; }
#welcome h1 span { display: block; font-size: 75%; }
#icon-status, #icon-book { float: left; height: 64px; margin-right: 1em; margin-top: -4px; width: 64px; }
#icon-book { display: none; }

@media (min-width: 768px) {
    #wrapper { width: 80%; margin: 2em auto; }
    #icon-book { display: inline-block; }
    #status a, #next a { display: block; }

    @-webkit-keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } }
    @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } }
    .sf-toolbar { opacity: 0; -webkit-animation: fade-in 1s .2s forwards; animation: fade-in 1s .2s forwards;}
}

In your base layout file you can now update it to use the css file by default:

/app/Resources/views/base.html.twig

{% block stylesheets %} <link href="{{ asset('bundles/app/css/site.css') }}" type="text/css" rel="stylesheet" /> {% endblock %}

As the main layout now has a default, you can remove the CSS block in the view template (app/Resources/views/default/index.html) if you wish to.

With the CSS reusable, let's create a template to override the user bundle pages such as login and registration. All the default templates for the user bundle are stored in the vendor folder, which you should never directly change. Instead we override the template by adding a file with the same name to the following directory: /app/Resources/FOSUserBundle/views/layout.html.twig. The bundle will essentially check for any templates of the same name in this directory to override it's originals, which you can find in the vendor folder.

Here's an example layout template, set to extend the same base that our home page is using. 

/app/Resources/FOSUserBundle/views/layout.html.twig

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

{% block title %}CS Symfony Members{% endblock %}

{% block body %}
<div id="wrapper">
    <div id="container">
    {% block fos_user_content %}{% endblock %}
    </div>
</div>
{% endblock %}

{% block stylesheets %}

    <link href="{{ asset('bundles/app/css/site.css') }}" type="text/css" rel="stylesheet" />

{% endblock %}

Notice how we can now re-use the stylesheet for this template. Browse to your domain with /app_dev.php/login and you should now see that the login form is wrapped inside a similar container to the homepage and is using the same css and base layout.

Override the view templates

Lastly, what if we want to customise some of the specific view contents of the login form? We can override view templates in a similar way to the layout by matching the same view path as it is inside the vendor folder. So for example we can override the login view by creating the following file inside /app/Resources/FOSUserBundle/views/Security/login.html.twig, which was copied from /vendor/friendsofsymfony/Resources/views/Security/login.html.twig

 /app/Resources/FOSUserBundle/views/Security/login.html.twig

{% extends "FOSUserBundle::layout.html.twig" %}

{% trans_default_domain 'FOSUserBundle' %}

{% block fos_user_content %}
    {% if error %}
        {{ error.messageKey|trans(error.messageData, 'security') }}
    {% endif %}

    <h1>Login</h1>

    <form action="{{ path("fos_user_security_check") }}" method="post">
        <input type="hidden" name="_csrf_token" value="{{ csrf_token }}" />

        <label for="username">{{ 'security.login.username'|trans }}</label>
        <input type="text" id="username" name="_username" value="{{ last_username }}" required="required" />

        <abel for="password">{{ 'security.login.password'|trans }}</label>
        <input type="password" id="password" name="_password" required="required" />

        <input type="checkbox" id="remember_me" name="_remember_me" value="on" />
        <label for="remember_me">{{ 'security.login.remember_me'|trans }}</label>

        <input type="submit" id="_submit" name="_submit" value="{{ 'security.login.submit'|trans }}" />
    </form>
{% endblock fos_user_content %}

So this is an exact copy of the template, but with the addition of a heading tag saying 'Login'. Browse to the login page now, and you should see the heading, illustrating how you can override the view files.

Create Authenticated Pages

With the core functionality configured and control over the layout, let's finish by creating a resource that is only available once the user has authenticated.

Create a controller action

Open up the DefaultController.php inside the AppBundle and add a new action:

/src/AppBundle/Controller/DefaultController.php

/**
     * @Route("/dashboard", name="dashboard")
     */
    public function dashboardAction(Request $request)
    {
        return $this->render('default/dashboard.html.twig', [
            'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..'),
        ]);
    }

This will be an authenticated only resource accessed via '/dashboard', which will render the dashboard view template.

Create a view template

Add a view template for the above action:

/app/Resources/views/default/dashboard.html.twig

{% extends 'base.html.twig' %}
{% block body %}
    <div id="wrapper">
        <div id="container">
            <h1>Dashboard
            <p>Members only area.<p/>
        </div>
    </div>
{% endblock %}

Configure the security

Lastly we need to tell Symfony that this page is for authenticated users only. To do this, add the following to the security.yml file:

/app/config/security.yml

access_control:
        - { path: ^/dashboard, role: ROLE_USER }

This states that any requests to the dashboard path must have the user role.

Try it out

If you now go back to your home page, you should be able to click the register link and complete the form to add a new user. If you try visiting the new /app_dev.php/dashboard resource when you're logged out then it will direct you back to the login form. Once you've logged in it will accept you as an authenticated user and show the dashboard view. Note, there's a default log out url you can use by going to /app_dev.php/logout.

You now have a structure in place to experiment with and develop your membership application.

Sign Up

NEXT: Show multiple property values in a form select drop down in Symfony

How to show multiple property values in a form select drop down in Symfony.

comments powered by Disqus
Sign Up

Popular Tags

350x250

Need a web developer?

If you'd like to work with code synthesis on your next project get in touch via the contact page.