TL;DR: Symfony is a PHP framework as well as a set of reusable PHP components and libraries. It uses the Model-View-Controller design pattern and can be scaled to be used in any requirement. It aims to speed up the creation and maintenance of web applications, replacing repetitive code. In this tutorial, I'll show you how to create your very own blog, with content stored in the database and authentication through Auth0. The finished code can be found at this GitHub repository.
Symfony Tutorial: Introduction
What is Symfony?
Symfony is an Open Source PHP framework originally created by Sensiolabs, an interactive agency. The Symfony PHP framework comes with a very large and active community. This framework has become very popular, mainly due to the vast number of reusable PHP components. These components are not limited to the framework itself but can be found throughout the PHP community, such as Drupal, WordPress, phpBB, and Laravel.
Symfony Components is a set of decoupled and reusable PHP libraries. They are becoming the standard foundation on which the best PHP applications are built on. You can use any of these components in your own applications independently from the Symfony Framework. Below you will find a list of some of the components that are used in this article as an example.
- Asset
- Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files.
- Config
- Helps you find, load, combine, autofill and validate configuration values.
- Console
- Eases the creation of beautiful and testable command line interfaces.
- Dotenv
- Parses
files to make environment variables stored in them accessible via.env
,getenv()
or$_ENV
.$_SERVER
- Parses
- Form
- Provides tools to easy creating, processing and reusing HTML forms.
- HttpKernel
- Provides the building blocks to create flexible and fast HTTP-based frameworks.
- Routing
- Maps an HTTP request to a set of configuration variables.
- Security
- Provides an infrastructure for sophisticated authorization systems.
- Templating
- Provides all the tools needed to build any kind of template system.
- Yaml
- Loads and dumps YAML files.
What Will We Build
In this article, we will be looking at how to install a new version of the Symfony PHP framework, along with making use of Doctrine to create two new database tables (
Author
and BlogPosts
) in order to store our blog data in. Following this, we will be making use of Doctrine Migrations to pre-populate our newly created database tables with some mock data to allow you to see the blog at work. Once the initial set up is complete, we will cover user authentication with Auth0 allowing users to create their own author entry in the database.Bootstrapping Symfony
Installing Symfony
Install Symfony via Composer with the following command:
composer create-project symfony/skeleton:4.0.5 blog
Once Composer has finished downloading all the required third-party libraries change directory into your project with:
cd blog
Install Doctrine bundle & various libraries
In order to communicate with the database and make use of other features, you need to install several third-party libraries. So run the following commands:
composer require symfony/orm-pack composer require annotations composer require validator composer require template composer require security-bundle composer require --dev maker-bundle
Update DotEnv File
In your root directory, there is a file called
.env
, you should see something similar to the following:###> doctrine/doctrine-bundle ### DATABASE_URL=mysql://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME} ###< doctrine/doctrine-bundle ###
Then you have to replace any of the values with the correct settings for your MySQL database. For example,
{DATABASE_HOST}
could be replaced by 127.0.0.1
. Don't have MySQL installed? An easy way to bootstrap one is with this script, it just needs Docker installed on the host machine.NOTE: The default database port is 3306, so if you haven't configured your MySQL database port, change the above
{DATABASE_PORT}
to 3306
.If needed, run the following command
php bin/console doctrine:database:create
, which will create a database with the value of your database name.Create the Blog Controller
Create new
BlogController
by running the following command php bin/console make:controller
When it asks for
The class name of the controller to create
, type in: BlogController
.Once the command has finished running, you'll find a new file in
src/Controller
called BlogController.php
, there should be an action similar to the following:/** * @Route("/blog", name="blog") */ public function index() { return $this->render('blog/index.html.twig', [ 'controller_name' => 'BlogController', ]); }
Replace the above with:
/** * @Route("/", name="homepage") */ public function indexAction() { return $this->render('blog/index.html.twig', [ 'controller_name' => 'BlogController', ]); }
In order to see your blog in your web browser for the duration of this tutorial, you need to have Symfony's web server installed in your application. To do this run the following command:
composer require server --dev
Now that the basic configuration has been set up, let's run the following command:
php bin/console server:run
You will see something similar to:
[OK] Server listening on http://127.0.0.1:8000
. So paste this URL into your browser, you'll be shown a Hello BlogController!
page.Creating a New Author Entity
Create new
Author
entity by running the following command:php bin/console make:entity
When it asks for
The class name of the entity to create
, type in: Author
.Once the command has finished running, you'll find a new file in
src/Entity
called Author.php
. Open this and configure it like so:<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * Author * * @ORM\Table(name="author") * @ORM\Entity(repositoryClass="App\Repository\AuthorRepository") */ class Author { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string * * @ORM\Column(name="name", type="string", length=255, unique=true) */ private $name; /** * @var string * * @ORM\Column(name="title", type="string", length=255) */ private $title; /** * @var string * * @ORM\Column(name="username", type="string", length=255, unique=true) */ private $username; /** * @var string * * @ORM\Column(name="company", type="string", length=255) */ private $company; /** * @var string * * @ORM\Column(name="short_bio", type="string", length=500) */ private $shortBio; /** * @var string * * @ORM\Column(name="phone", type="string", length=255, nullable=true) */ private $phone; /** * @var string * * @ORM\Column(name="facebook", type="string", length=255, nullable=true) */ private $facebook; /** * @var string * * @ORM\Column(name="twitter", type="string", length=255, nullable=true) */ private $twitter; /** * @var string * * @ORM\Column(name="github", type="string", length=255, nullable=true) */ private $github; /** * Get id * * @return int */ public function getId() { return $this->id; } /** * Set name * * @param string $name * * @return Author */ public function setName($name) { $this->name = $name; return $this; } /** * Get name * * @return string */ public function getName() { return $this->name; } /** * Set title * * @param string $title * * @return Author */ public function setTitle($title) { $this->title = $title; return $this; } /** * Get title * * @return string */ public function getTitle() { return $this->title; } /** * Set username * * @param string $username * * @return Author */ public function setUsername($username) { $this->username = $username; return $this; } /** * Get username * * @return string */ public function getUsername() { return $this->username; } /** * Set company * * @param string $company * * @return Author */ public function setCompany($company) { $this->company = $company; return $this; } /** * Get company * * @return string */ public function getCompany() { return $this->company; } /** * Set shortBio * * @param string $shortBio * * @return Author */ public function setShortBio($shortBio) { $this->shortBio = $shortBio; return $this; } /** * Get shortBio * * @return string */ public function getShortBio() { return $this->shortBio; } /** * Set phone * * @param string $phone * * @return Author */ public function setPhone($phone) { $this->phone = $phone; return $this; } /** * Get phone * * @return string */ public function getPhone() { return $this->phone; } /** * Set facebook * * @param string $facebook * * @return Author */ public function setFacebook($facebook) { $this->facebook = $facebook; return $this; } /** * Get facebook * * @return string */ public function getFacebook() { return $this->facebook; } /** * Set twitter * * @param string $twitter * * @return Author */ public function setTwitter($twitter) { $this->twitter = $twitter; return $this; } /** * Get twitter * * @return string */ public function getTwitter() { return $this->twitter; } /** * Set github * * @param string $github * * @return Author */ public function setGithub($github) { $this->github = $github; return $this; } /** * Get github * * @return string */ public function getGithub() { return $this->github; } }
“Doctrine is a great tool to have around when developing with PHP and Symfony.”
Tweet This
Creating a New BlogPost Entity
Create new
BlogPost
entity by running the following command php bin/console make:entity
When it asks for
The class name of the entity to create
, type in: BlogPost
.Once the command has finished running, you'll find a new file in
src/Entity
called BlogPost.php
. Open this and configure it like so:<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * BlogPost * * @ORM\Table(name="blog_post") * @ORM\Entity(repositoryClass="App\Repository\BlogPostRepository") * @ORM\HasLifecycleCallbacks */ class BlogPost { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string * * @ORM\Column(name="title", type="string", length=255) */ private $title; /** * @var string * * @ORM\Column(name="slug", type="string", length=255, unique=true) */ private $slug; /** * @var string * * @ORM\Column(name="description", type="string", length=2000) */ private $description; /** * @var string * * @ORM\Column(name="body", type="text") */ private $body; /** * @var Author * * @ORM\ManyToOne(targetEntity="Author") * @ORM\JoinColumn(name="author_id", referencedColumnName="id") */ private $author; /** * @var \DateTime * * @ORM\Column(name="created_at", type="datetimetz") */ private $createdAt; /** * @var \DateTime * * @ORM\Column(name="updated_at", type="datetime") */ private $updatedAt; /** * Get id * * @return int */ public function getId() { return $this->id; } /** * Set title * * @param string $title * * @return BlogPost */ public function setTitle($title) { $this->title = $title; return $this; } /** * Get title * * @return string */ public function getTitle() { return $this->title; } /** * Set slug * * @param string $slug * * @return BlogPost */ public function setSlug($slug) { $this->slug = $slug; return $this; } /** * Get slug * * @return string */ public function getSlug() { return $this->slug; } /** * Set description * * @param string $description * * @return BlogPost */ public function setDescription($description) { $this->description = $description; return $this; } /** * Get description * * @return string */ public function getDescription() { return $this->description; } /** * Set body * * @param string $body * * @return BlogPost */ public function setBody($body) { $this->body = $body; return $this; } /** * Get body * * @return string */ public function getBody() { return $this->body; } /** * Set author * * @param Author $author * * @return BlogPost */ public function setAuthor(Author $author) { $this->author = $author; return $this; } /** * Get author * * @return Author */ public function getAuthor() { return $this->author; } /** * Set createdAt * * @param \DateTime $createdAt * * @return BlogPost */ public function setCreatedAt($createdAt) { $this->createdAt = $createdAt; return $this; } /** * Get createdAt * * @return \DateTime */ public function getCreatedAt() { return $this->createdAt; } /** * Set updatedAt * * @param \DateTime $updatedAt * * @return BlogPost */ public function setUpdatedAt($updatedAt) { $this->updatedAt = $updatedAt; return $this; } /** * Get updatedAt * * @return \DateTime */ public function getUpdatedAt() { return $this->updatedAt; } /** * @ORM\PrePersist */ public function prePersist() { if (!$this->getCreatedAt()) { $this->setCreatedAt(new \DateTime()); } if (!$this->getUpdatedAt()) { $this->setUpdatedAt(new \DateTime()); } } /** * @ORM\PreUpdate */ public function preUpdate() { $this->setUpdatedAt(new \DateTime()); } }
You may have noticed a
private $author;
property that links to the previous Entity we created. All this does is make sure the entity knows that Author
has a ManyToOne
relationship with BlogPost
.Although you have created these entities, your database still has no tables in there. Based off these entities, Doctrine can create the tables we've specified. In order to do this, all you have to do is run:
php bin/console doctrine:schema:update --force
Install Doctrine-Fixtures
We want to just populate some data into the newly created tables as examples during the creation of the blog. So install doctrine-fixtures.
composer require --dev doctrine/doctrine-fixtures-bundle
Create Author and BlogPost Fixtures
Create a new file (and the directories the file is stored in) under
src/DataFixtures/ORM/Fixtures.php
and insert the following into the file:<?php namespace App\DataFixtures\ORM; use App\Entity\Author; use App\Entity\BlogPost; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Common\Persistence\ObjectManager; class Fixtures extends Fixture { public function load(ObjectManager $manager) { $author = new Author(); $author ->setName('Joe Bloggs') ->setTitle('Developer') ->setUsername('auth0-username') ->setCompany('The Writing Company') ->setShortBio('Lorem Ipsum is simply placeholder text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard placeholder text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages.') ->setPhone('070000000') ->setFacebook('joebloggs') ->setTwitter('joe.bloggs') ->setGithub('joe-bloggs'); $manager->persist($author); $blogPost = new BlogPost(); $blogPost ->setTitle('Your first blog post example') ->setSlug('first-post') ->setDescription('Lorem Ipsum is simply placeholder text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard placeholder text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.') ->setBody('Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32. The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.') ->setAuthor($author); $manager->persist($blogPost); $manager->flush(); } }
This is basically just a file that, when loaded, will create an author and a blog post, and will link the author to the blog post.
Running Fixtures
Let's run the fixtures!
php bin/console doctrine:fixtures:load
Now we have our entities, database, database tables, and some sample data in the tables.
Managing Identity with Symfony and Auth0
Installing HWIOAuth Bundle
To make Symfony integrate with Auth0, we are going to use HWIOAuth Bundle, an OAuth client that supports OAuth2.
First, we will set up all the configurations needed for this bundle.
So to begin we need to add the routes to your routing file:
config/routes.yaml
:# ... hwi_oauth_redirect: resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml" prefix: /connect hwi_oauth_login: resource: "@HWIOAuthBundle/Resources/config/routing/login.xml" prefix: /login auth0_login: path: /auth0/callback auth0_logout: path: /auth0/logout
In order to get the
HWIOAuthBundle
to connect to Auth0, we need to create an Auth0 resource owner. Create a new file src/Auth0ResourceOwner.php
:<?php namespace App; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; use HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\GenericOAuth2ResourceOwner; class Auth0ResourceOwner extends GenericOAuth2ResourceOwner { /** * {@inheritdoc} */ protected $paths = array( 'identifier' => 'user_id', 'nickname' => 'nickname', 'realname' => 'name', 'email' => 'email', 'profilepicture' => 'picture', ); /** * {@inheritdoc} */ public function getAuthorizationUrl($redirectUri, array $extraParameters = array()) { return parent::getAuthorizationUrl($redirectUri, array_merge(array( 'audience' => $this->options['audience'], ), $extraParameters)); } /** * {@inheritdoc} */ protected function configureOptions(OptionsResolver $resolver) { parent::configureOptions($resolver); $resolver->setDefaults(array( 'authorization_url' => '{base_url}/authorize', 'access_token_url' => '{base_url}/oauth/token', 'infos_url' => '{base_url}/userinfo', 'audience' => '{base_url}/userinfo', )); $resolver->setRequired(array( 'base_url', )); $normalizer = function (Options $options, $value) { return str_replace('{base_url}', $options['base_url'], $value); }; $resolver->setNormalizer('authorization_url', $normalizer); $resolver->setNormalizer('access_token_url', $normalizer); $resolver->setNormalizer('infos_url', $normalizer); $resolver->setNormalizer('audience', $normalizer); } }
We're going to need API keys for Auth0, so create your free account. Once signed up:
- In the dashboard, click
on the left:Applications
- Create Application
- Add a name
- Choose Regular Web Applications type
- Configure callback URL:
- In the new Auth0
, go to the settings tab.Application
- Find the text box labeled
.Allowed Callback URLs
- Paste the following in:
.http://127.0.0.1:8000/auth0/callback
- In the new Auth0
- Configure Auth0 Application to require usernames:
- In the navigation bar find and click
Connections
- Then click
Database
- Click on
Username-Password-Authentication
- Toggle
to on.Requires Username
- In the navigation bar find and click
- Go to the Applications section again and pick your
.Application
Then in your
.env
file paste the following, but replace the brackets and their contents with the details found in your application:AUTH0_CLIENT_ID=(Client ID on Auth0) AUTH0_CLIENT_SECRET=(Client Secret on Auth0) AUTH0_DOMAIN=(Domain on Auth0)
To use all of this, we need to create a new file in
config/packages
called hwi_oauth.yaml
then populate this file with:hwi_oauth: firewall_names: [secured_area] resource_owners: auth0: type: oauth2 class: 'App\Auth0ResourceOwner' client_id: "%env(AUTH0_CLIENT_ID)%" client_secret: "%env(AUTH0_CLIENT_SECRET)%" base_url: "https://%env(AUTH0_DOMAIN)%" scope: "openid profile"
Finally, replace the contents of your
config/packages/security.yaml
file with:security: providers: hwi: id: hwi_oauth.user.provider firewalls: secured_area: anonymous: ~ oauth: resource_owners: auth0: "/auth0/callback" login_path: /login use_forward: false failure_path: /login oauth_user_provider: service: hwi_oauth.user.provider logout: path: /auth0/logout target: / main: anonymous: ~ access_control: - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/secured, roles: ROLE_OAUTH_USER }
The config above is setting up URLs/sections in your blog that require the user to be authenticated.
In order to install HWIOAuth Bundle, which uses a virtual package
php-http/client-implementation
, we need to install several third-party libraries. However, this can be easily done with this command:composer require hwi/oauth-bundle php-http/guzzle6-adapter php-http/httplug-bundle
NOTE: You may have an issue if your version of PHP is too high. If you encounter an error similar to
overridden by "config.platform.php" version (5.5.9) does not satisfy that requirement
, remove the PHP requirement in config in composer.json
so find and remove:"platform": { "php": "5.5.9" },
Now run
composer update
.Create the Author Page
The first thing we will want to do is create a new form. This is a class that allows us to validate the user's input, such as creating an author. So, in the
src/
path, create a new directory called Form
.
Within that directory create a new file named AuthorFormType.php
. Add the code below to your new file:<?php namespace App\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\NotBlank; class AuthorFormType extends AbstractType { /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { // Below is all of the inputs that the author will see when creating their new author access. Each one contains its own attributes where applicable, such as the field `name` being required. // Please note the naming of these form elements is the same naming as the entity. We will be passing in the Author entity so the form knows what the data is for. $builder ->add( 'name', TextType::class, [ 'constraints' => [new NotBlank()], 'attr' => ['class' => 'form-control'] ] ) ->add( 'title', TextType::class, [ 'constraints' => [new NotBlank()], 'attr' => ['class' => 'form-control'] ] ) ->add( 'company', TextType::class, [ 'constraints' => [new NotBlank()], 'attr' => ['class' => 'form-control'] ] ) ->add( 'shortBio', TextareaType::class, [ 'constraints' => [new NotBlank()], 'attr' => ['class' => 'form-control'] ] ) ->add( 'phone', TextType::class, [ 'attr' => ['class' => 'form-control'], 'required' => false ] ) ->add( 'facebook', TextType::class, [ 'attr' => ['class' => 'form-control'], 'required' => false ] ) ->add( 'twitter', TextType::class, [ 'attr' => ['class' => 'form-control'], 'required' => false ] ) ->add( 'github', TextType::class, [ 'attr' => ['class' => 'form-control'], 'required' => false ] ) ->add( 'submit', SubmitType::class, [ 'attr' => ['class' => 'form-control btn-primary pull-right'], 'label' => 'Become an author!' ] ); } /** * {@inheritdoc} */ public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'data_class' => 'App\Entity\Author' ]); } /** * {@inheritdoc} */ public function getName() { return 'author_form'; } }
Create new
AdminController
by running the following command php bin/console make:controller
When it asks for
The class name of the controller to create
, type in: AdminController
.Once the command has finished running, you'll find a new file in
src/Controller
called AdminController.php
.Open your new AdminController (
src/Controller/AdminController.php
), we need to make use the entity manager and the repositories for the entities in order to retrieve database data. At the top of this class, inject these services in the construct:/** @var EntityManagerInterface */ private $entityManager; /** @var \Doctrine\Common\Persistence\ObjectRepository */ private $authorRepository; /** @var \Doctrine\Common\Persistence\ObjectRepository */ private $blogPostRepository; /** * @param EntityManagerInterface $entityManager */ public function __construct(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager; $this->blogPostRepository = $entityManager->getRepository('App:BlogPost'); $this->authorRepository = $entityManager->getRepository('App:Author'); }
As you can see, there is a class declared here so we need to add it to the namespaces at the top of the file. Where it says:
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
Add the following four below them:
use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; use App\Entity\Author; use App\Form\AuthorFormType;
Great, in the entire controller we can call
blogPostRepository
, authorRepository
or entityManager
when needed. The first two are used for retrieving data from the database, whereas the third will be used for inserting, updating, or deleting data.The three extra
use
classes on that list are:- the
class, this allows you to gather data, for example, if there are any parameters in a POST or GET request,Request
- the
entity that maps objects to the database,Author
- the
that will specify the input values users can provide when creating a new Author;AuthorFormType
In the same class, you should find a method called
index()
similar to the example below:/** * @Route("/admin", name="admin") */ public function index() { return $this->render('admin/index.html.twig', [ 'controller_name' => 'AdminController', ]); }
Replace the above method with the one shown below:
/** * @Route("/admin/author/create", name="author_create") */ public function createAuthorAction(Request $request) { if ($this->authorRepository->findOneByUsername($this->getUser()->getUserName())) { // Redirect to dashboard. $this->addFlash('error', 'Unable to create author, author already exists!'); return $this->redirectToRoute('homepage'); } $author = new Author(); $author->setUsername($this->getUser()->getUserName()); $form = $this->createForm(AuthorFormType::class, $author); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->entityManager->persist($author); $this->entityManager->flush($author); $request->getSession()->set('user_is_author', true); $this->addFlash('success', 'Congratulations! You are now an author.'); return $this->redirectToRoute('homepage'); } return $this->render('admin/create_author.html.twig', [ 'form' => $form->createView() ]); }
The above code checks whether an author already exists for this user, whether the form has been submitted and whether it passes all validations, if it does it will then redirect the user to the index page.
NOTE: You may have noticed that it sets a session to
true
for user_is_author
this will make sense when we reach the part that discusses and implements event listeners (next).Finally, we want to pass the form into a template that the user will see. So, lets create a new template in
templates/admin/
called create_author.html.twig
. In this template, paste the following:{% extends 'base.html.twig' %} {% block title %}{% endblock %} {% block body %} <div class="container"> <div class="blog-header"> <h2 class="blog-title">Creating your author</h2> </div> <div class="row"> <div class="col-md-12 col-lg-12 col-xl-12"> {% for label, messages in app.flashes %} {% for message in messages %} <div class="alert alert-{{ label }}" role="alert"> <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> {{ message }} </div> {% endfor %} {% endfor %} </div> <div class="col-sm-12 blog-main"> {{ form_start(form) }} <div class="col-md-12"> <div class="form-group col-md-4"> {{ form_row(form.name) }} </div> <div class="form-group col-md-4"> {{ form_row(form.title) }} </div> <div class="form-group col-md-4"> {{ form_row(form.company) }} </div> <div class="form-group col-md-12"> {{ form_row(form.shortBio) }} </div> <div class="form-group col-md-4"> {{ form_row(form.phone) }} </div> <div class="form-group col-md-4"> {{ form_row(form.facebook) }} </div> <div class="form-group col-md-4"> {{ form_row(form.twitter) }} </div> <div class="form-group col-md-4"> {{ form_row(form.github) }} </div> <div class="form-group col-md-4"> {{ form_row(form.shortBio) }} </div> </div> <div class="form-group col-md-4 pull-right"> {{ form_row(form.submit) }} </div> {{ form_end(form) }} </div> </div> </div> {% endblock %}
You can clear the cache in your Symfony application to see if everything is ok and there are no errors. Note that there is nothing to see yet, we just want to guarantee that the configuration is correct.
php bin/console cache:clear
Including Bootstrap and Styles
As it is, this template will look very ugly in your browser. So let's create a new CSS file.
public/css/style.css
Copy the contents of the file found here into your new
style.css
file.Now, it's time to include this file into the base template alongside with Bootstrap's CSS and Javascript files.
Open
templates/base.html.twig
and replace its contents with:<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>{% block title %}Welcome!{% endblock %}</title> {% block stylesheets %}{% endblock %} <link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" /> <!-- Latest compiled and minified CSS --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> <!-- jQuery library --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <!-- Latest compiled JavaScript --> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> <!-- Custom CSS File --> <script src="css/style.css"></script> </head> <body> {% block body %}{% endblock %} {% block javascripts %}{% endblock %} </body> </html>
This just includes your custom CSS file as well as Bootstrap's CSS and Javascript files into your page.
Asking for "About Me" Information
Although a user is authenticated through Auth0, we don't really want them to access the rest of the author section until they've filled some basic "About me" details. So, what we want is to ensure that any
/author/
route they try to access, if they don't have an "Author" entry, redirects them to the form to write about themselves.An event listener is, as the name states, a listener to specific programmed events which then run the predefined code once the conditions are met.
Let's create the
CheckIsAuthorListener.php
file in the src/EventListener/Author
directory. In this file, let's place the following code:<?php namespace App\EventListener\Author; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpKernel\Event\FilterControllerEvent; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; class CheckIsAuthorListener { /** @var RouterInterface */ protected $router; /** @var SessionInterface */ protected $session; /** @var TokenStorageInterface */ private $tokenStorage; /** @var EntityManagerInterface */ private $entityManager; /** * @param RouterInterface $router * @param SessionInterface $session * @param TokenStorageInterface $tokenStorage * @param EntityManagerInterface $entityManager */ public function __construct( RouterInterface $router, SessionInterface $session, TokenStorageInterface $tokenStorage, EntityManagerInterface $entityManager ) { $this->router = $router; $this->session = $session; $this->tokenStorage = $tokenStorage; $this->entityManager = $entityManager; } /** * On kernel.controller * * @param FilterControllerEvent $event * * @return void */ public function onKernelController(FilterControllerEvent $event) { // Don't add to the flasher if the current path does not begin with /admin if (!preg_match('/^\/admin/i', $event->getRequest()->getPathInfo())) { return; } if (null === $user = $this->tokenStorage->getToken()->getUser()) { return; } // Use the session to exit this listener early, if the relevant checks have already been made if (true === $this->session->get('user_is_author')) { return; } $route = $this->router->generate('author_create'); // Check we are not already attempting to create an author! if (0 === strpos($event->getRequest()->getPathInfo(), $route)) { return; } // Check if authenticated user has an author associated with them. if ($author = $this->entityManager ->getRepository('App:Author') ->findOneByUsername($user->getUsername()) ) { $this->session->set('user_is_author', true); } if (!$author && $this->session->get('pending_user_is_author')) { $this->session->getFlashBag()->add( 'warning', 'Your author access is being set up, this may take up to 30 seconds. Please try again shortly.' ); $route = $this->router->generate('homepage'); } else { $this->session->getFlashBag()->add( 'warning', 'You cannot access the author section until you become an author. Please complete the form below to proceed.' ); } $event->setController(function () use ($route) { return new RedirectResponse($route); }); } }
We now need to add this class as a service so that Symfony runs it on each controller request. In
config/services.yaml
add the following as the last item of services
:App\EventListener\Author\CheckIsAuthorListener: tags: - { name: kernel.event_listener, event: kernel.controller }
It's great setting up HWIOAuth Bundle and configuring Auth0 to allow users to log in, but we don't yet have anywhere in the Symfony installation to actually log in. So, for the time being, we're going to change the homepage action in the Blog Controller.
Open your
./templates/blog/index.html.twig
template defined in your BlogController indexAction and replace the contents with the following in:{% extends 'base.html.twig' %} {% block body %} <nav class="navbar navbar-default navbar-static-top"> <div id="navbar" class="collapse navbar-collapse pull-right"> <ul class="nav navbar-nav"> <AmpContent> <li><a href="{{ path("author_create") }}">Admin</a></li> <li><a href="{{ logout_url("secured_area") }}">Logout</a></li> </AmpContent> <NonAmpContent> <li class="active"><a href="/connect/auth0">Login</a></li> </NonAmpContent> </ul> </div> </nav> {% endblock %}
As you can see, now we provide a link so that unauthenticated users can log in. If they are logged in, two links are shown: one to log out and one to access the admin section.
“Using Symfony and OAuth together is quite easy.”
Tweet This
Next Steps
In the next article, we will be covering installing Bootstrap, a framework, to make the blog nicer visually. We will also enhance our blog engine to allow visitors to:
- see a list of blog posts;
- read a specific blog post;
- and find out more about the author.
Authenticated authors will be able to:
- create a new blog post;
- see all of their own blog posts;
- delete their own blog posts from the system.
About the author
Greg Holmes
Guest Author