runaway

RUN..

Hello and welcome back to the Part 3 of the  “Escape the Symfony Form jail” series.

(Please refer to Part 1  & Part 2 to get into the context.)

In this part, we’re gonna deal with some templating: creating the User login interface and the voting view

Set-up the Login interface

As our post is about The Symfony Form Component, we won’t  explain with much details the templating task.

First off, we have to override the FOSUserBundle Security (Login) views.

We will also use SemanticUI as a Front-End framework.

So straight to the goal, here are the templates used for our Login interface:

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

{# app/Resources/FOSUserBundle/views/layout.html.twig #}
{% extends 'layouts/main.html.twig' %}
{% block layout_main_content %}
  {% for type, messages in app.session.flashbag.all() %}
      {% for message in messages %}
          <div class="flash-{{ type }}">
              {{ message }}
          </div>
      {% endfor %}
  {% endfor %}
  {% block fos_user_content %}
  {% endblock fos_user_content %}
{% endblock layout_main_content %}

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

{# app/Resources/FOSUserBundle/views/Security/login.html.twig #}
{% extends "@FOSUser/layout.html.twig" %}

{% block fos_user_content %}
    {{ include('@FOSUser/Security/login_content.html.twig') }}
{% endblock fos_user_content %}

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

{# app/Resources/FOSUserBundle/views/Security/login_content.html.twig #}
{% trans_default_domain 'FOSUserBundle' %}
{% block main_content %}
<h1 class="ui blue header center">Top Soccer Clubs</h1>
  <form id="login-form" action="{{ path("fos_user_security_check") }}" method = "post" class="ui form {{error.messageKey is defined?'error':''}}">
    <h3 class="ui dividing header">Login</h4>
 		{% if csrf_token %}
    <input  type="hidden" name="_csrf_token" value="{{ csrf_token }}" />
    {% endif %}
    <div class="field">
      <label> {{ 'security.login.username'|trans }}</label>
      <input  type="text" id="username" name="_username" value="{{ last_username }}" required="required" />
    </div>
    <div class="field">
      <label>{{ 'security.login.password'|trans }}</label>
      <input type="password" id="password" name="_password" required="required" />
    </div>
    <button class="ui button" type="submit" id="_submit" name="_submit"> {{ 'security.login.submit'|trans }}</button>
    <div class="field">
      {% if error %}
      <div class="ui error message" style="margin-top:32px">
        <div class="header">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
      </div>
      {% endif %}
    </div>
  </form>
{% endblock main_content %}

Set-up the application views

We also need some corner-stone views acting as a main template:

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

{# app/Resources/views/layouts/base.html.twig #}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>
        {% block title %}
            Welcome to Top Soccer Clubs 
        {% endblock %}
        </title>
        {% block stylesheets %}

        <!-- - - - - - - -->
        <!-- STYLESHEETS -->
        <!-- - - - - - - -->
        
        <!-- Google Fonts -->
        <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
       
        <!-- SEMANTIC UI -->
        <link rel="stylesheet" type="text/css" href="{{asset('bundles/vendor/semantic/semantic.min.css')}}" />

        <!-- CUSTOM STYLESHEET -->
        <link rel="stylesheet" type="text/css"  href="{{asset('bundles/css/custom.css')}}" />             

        {% endblock stylesheets %}

    </head>
    
    <body class="site">
    {% block body %}
    {% endblock body %}
    {% block javascripts %}

    <!-- - - - - - - -->
    <!-- SCRIPTS - - -->
    <!-- - - - - - - -->

    <!-- JQUERY -->
    <script type="text/javascript" src="{{asset('bundles/vendor/jquery.min.js')}}"></script>

    <!-- SEMANTIC UI -->
    <script type="text/javascript" src="{{asset('bundles/vendor/semantic/semantic.min.js')}}"></script>

    <!-- CUSTOM JS -->
    <script type="text/javascript" src="{{asset('bundles/js/custom.js')}}"></script>


    {% endblock javascripts %}

    </body>
</html>

app/Resources/views/layouts/main.html.twig

{# app/Resources/views/layouts/main.html.twig #}
{% extends 'layouts/base.html.twig' %}
{% block body %}
{% if user is defined and user is not empty %}
    {% block navbar %}
        {% include('layouts/navbar.html.twig') %}
    {% endblock navbar %}
{% endif %}
    <div class="ui main container site-content" style = "margin-top: 7em;" >
        {% block layout_main_content %}
        
        {% endblock layout_main_content %}
    </div>
    {% block footer %}
        {% include('layouts/footer.html.twig') %}
    {% endblock footer %}
{% endblock body %}

app/Resources/views/layouts/navbar.html.twig

{# app/Resources/views/layouts/navbar.html.twig #}
<div class="ui fixed inverted menu">
  <a href="{{path('homepage')}}" class="menu active item blue">Top Soccer Clubs </a>
  <a class="item" href="{{path("homepage")}}"><i class="ol list icon"></i> Top 10 </a>
  	<a class="item" href="#"><i class="thumbs up icon"></i> Upvote</a>
  <div class="right menu">
    <div class="ui simple dropdown item">
      <i class="user circle outline large icon yellow"></i>
      {{user}}
      <i class="dropdown icon"></i>
      <div class="menu">
        <a  href="#" class="item"><i class="user icon"></i>Profile</a>
        <a href="{{path('fos_user_security_logout')}}" class="item"><i class="sign out icon"></i>Logout</a>
        <a  href="#" class="item">
          <i class="github icon"></i>Github Repo
        </a>
      </div>
    </div>
  </div>
</div>

app/Resources/views/layouts/footer.html.twig

{# app/Resources/views/layouts/footer.html.twig #}
{% block footer %}
<div class="ui inverted vertical footer segment">
    <div class="ui center aligned container">
      <div class="ui horizontal inverted small divided link list">
        Top Soccer Clubs &copy 2018 by <a class="item" href="https://medunes.net" target="_blank">Medunes</a>
      </div>
    </div>
</div>
{% endblock footer %}

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

{# app/Resources/views/index.html.twig #}
{% extends 'layouts/main.html.twig' %}
{% block layout_main_content %}
    <h1>Top 10 Soccer Clubs!</h1>
    <table class="ui red table">
        <thead>
            <tr>
                <th>Team</th>
                <th>Country</th>
                <th>Ranking</th>
                <th>Upvotes</th>
            </tr>
        </thead>
        <tbody>
            {% set ranking = 1 %}
            {% for team in teams %}
            <tr>
                <td>{{team.name}}</td>
                <td>{{team.country}}</td>
                <td>{{ranking}}</td>
                <td>{{team.upvotes}}</td>
            </tr>
            {% set ranking = ranking +1 %}
            {% endfor %}
        </tbody>
    </table>
{% endblock layout_main_content %}

We will also need a JavaScript  file and a Stylesheet file:

web/bundles/js/custom.js

$("document").ready(function(){
    $("#login-form").on("submit",function(e){
        $("#login-form").addClass("loading");
    });
});
web/bundles/css/custom.css
body *
{
    font-family: 'Open Sans', sans-serif;
}
.site{
    display: flex;
  min-height: 100vh;
  flex-direction: column;
}
.site-content{
    flex: 1;
}
.ui.footer.segment {
    margin: 5em 0em 0em;
    padding: 3em 0em;
  }

That’s all for templating!

Set-up the Controller and Repositories:

As a final step, we will redirect successful logins to the “index.html.twig” view. The following controller should provide us with the needed behavior:

src/AppBundle/Controller/DefaultController.php

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction(Request $request)
    {
        $user = $this->getUser();
        $teams = $this->getDoctrine()->getManager()->getRepository("AppBundle:Team")->findTopTenTeams();
        return $this->render('index.html.twig', [
            "user"=>$user,
            "teams"=>$teams
            ]);
    }
}

It is also very important to remind that unauthorized users must be redirected to the login form. The following security configuration file will manage this.

# 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 }
        - { path: ^/, role: IS_AUTHENTICATED_FULLY }
       

As you should have noticed, there is a $teams variable we are getting inside the DefaultController and sending to the index view. We should obviously build a repository function that will query the database and grab the Top 10 teams:

src/AppBundle/Repository/TeamRepository.php

<?php
// src/AppBundle/Repository/TeamRepository.php
namespace AppBundle\Repository;
use Doctrine\ORM\EntityRepository;

/**
 * TeamRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class TeamRepository extends \Doctrine\ORM\EntityRepository
{
    public function findTopTenTeams()
    {
        $teams = $this->getEntityManager()
                      ->createQuery("
                        SELECT t.text name, c.text country, COUNT(u) upvotes
                        FROM AppBundle:Team t
                        JOIN t.country c
                        JOIN t.upvoters u
                        GROUP BY t
                        ORDER BY upvotes DESC
                      ")
                      ->setMaxResults(10)
                      ->getResult();
        return $teams;
  
    }
}

We will also  pump some automated up-votes using the following SQL query, just to have sample data for testing:

INSERT INTO `team_user`(`user_id`,`team_id`) values
(1,1),
(1,2),
(1,3),
(1,4),
(1,5),
(1,6),
(1,7),
(1,8),
(1,9),
(1,10),
(2,1),
(2,2),
(2,3),
(2,4),
(2,5),
(2,16),
(2,17),
(2,18),
(2,19),
(2,20);

DADA!

This is what our Home page should look like right now:

homepage

homepage

In the next & last Part, we will set up the up-voting form and we will actually escape the Symfony Form Jail!

Stay connected!