Skip to content

Instantly share code, notes, and snippets.

@luismisanchez
Last active January 16, 2022 13:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save luismisanchez/db06a6b245e9045b84bb1005c6261135 to your computer and use it in GitHub Desktop.
Save luismisanchez/db06a6b245e9045b84bb1005c6261135 to your computer and use it in GitHub Desktop.
Symfony fundamentals notes

Installation

curl -sS https://get.symfony.com/cli/installer | bash
mv /Users/Username/.symfony/bin/symfony /usr/local/bin/symfony
symfony

Check requirements

symfony check:req

Create a project

symfony new project_name

Where project_name will be the directory that the new app will live in. This also happens to be the name of the site we're building. The symfony new command already initialized a git repository for us and made the first commit.

Start a web server

php -S 127.0.0.1:8000 -t public/

or

symfony serve -d <- Daemon mode

Server status and logs

  • Check status:
symfony server:status
  • Start/Stop:
symfony server:start
symfony server:stop

Recommended phpStorm plugins

Symfony, PHP Annotations, PHP toolbox. Then make sure the "Synchronize IDE settings with composer.json" box is checked in "Languages and Frameworks", "PHP", "Composer" section.

Security checker

The Local PHP Security Checker is a command line tool that checks if your PHP application depends on PHP packages with known security vulnerabilities. It uses the Security Advisories Database behind the scenes.

Check https://github.com/fabpot/local-php-security-checker for installation and usages.

(Symfony own packages are deprecated at the time of writing this notes: https://github.com/sensiolabs/security-checker)

Installing Annotations Support to our project

composer require annotations

Route + Controller = Page

The Front Controller: Working Behind-the-Scenes

No matter what URL we go to the PHP file that our web server is executing is public/index.php. The only reason you don't need to have index.php in the URL is because our local web server is configured to execute index.php automatically. On production, your HTTP server config will do the same. Check the Symfony docs to learn how.

Initial routes are in config/routes.yaml:

#index:
#    path: /
#    controller: App\Controller\DefaultController::index

This route will exists under the root path of the application (/) and execute the index() method of the src/Controller/DefaultController class.

If we add annotations support, routes are defined directly on the controller class methods.

Every class we create in the src/ directory will need a namespace. This namespace must be App\ followed whatever directory the file lives in.

src/Controller/DefaultController.php:

namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;

class DefaultController
{
        public function index()
        {
            return new Response('Hello World!');
        }
}

With annotations support, the controller should be:

namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;

class DefaultController
{
     /**
     * @Route("/")
     */
     public function index()
     {
         return new Response('Hello World!');
     }

     /**
     * Wildcard route
     * 
     * @Route("/show/{slug}")
     */
     public function show($slug)
     {
        return new Response(sprintf(
            'Page to show "%s"!',
            $slug
        ));
     }

}

Controllers are called actions too. A controller method must return a Symfony Response object. Almost always. For HTTP responses we will use Symfony\Component\HttpFoundation\Response.

The Symfony console

php bin/console --help

php bin/console debug:router

...

Symfony Flex

This is a composer plugin that adds two special features to Composer itself.

Aliases

At your browser, go to http://flex.symfony.com to find and big page full of packages. Search for security. Better, search for sec-checker. Boom! This says that there is a package called sensiolabs/security-checker and it has aliases of sec-check, sec-checker, security-checker and some more.

The alias system is simple: because Symfony Flex is in our app, we can say composer require security-checker, and it will really download sensiolabs/security-checker.

Recipes

Each package you install may have a Flex "recipe". The idea is beautifully simple. Instead of telling people to install a package and then create this file, and update this other file in order to get things working, Flex executes a recipe which just does that stuff for you.

Thanks to the recipe system, whenever you install a package, Flex will check to see if that package has a recipe and, if it does, will install it. A recipe can do many things, like add files, create directories or even modify a few files, like adding new lines to your .gitignore file.

You can click to view the Recipe for any of the packages on http://flex.symfony.com. This goes to a GitHub repository called symfony/recipes. Contributed ones are on symfony/recipes-contrib.

  • Checking recipes:
composer recipes

composer recipes sensiolabs/security-checker

If we remove a package with composer remove package-name the recipe will be uninstalled too.

Twig (template library)

Unless you're building a pure API you're going to need to write some HTML. We can install it and all dependencies recipes from https://packagist.org/packages/symfony/twig-pack or using Flex:

composer require template

This will install all needed Bundles

A bundle is similar to a plugin in other software, but even better. The core features of Symfony framework are implemented with bundles (FrameworkBundle, SecurityBundle, DebugBundle, etc.) They are also used to add new features in your application via third-party bundles.

config/bundles.php

return [
    Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
    Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
    Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
    Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
];

The recipe also added a templates/ directory and the base.html.twig file:

templates/base.html.twig

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{% endblock %}
    </body>
</html>

We can change the project templates path in config/packages/twig.yaml:

twig:
    default_path: '%kernel.project_dir%/templates'

Rendering a template

As soon as you want to render a template, you need to make your controller extend Symfony\Bundle\FrameworkBundle\Controller\AbstractController. Now, obviously, a controller doesn't need to extend this base class. But, you usually will extend AbstractController for its shortcut methods.

Our controller would be:

namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class DefaultController
{
     /**
     * @Route("/")
     */
     public function index()
     {
         return new Response('Hello World!');
     }

     /**
     * Wildcard route
     * 
     * @Route("/show/{slug}")
     */
     public function show($slug)
     {
        
        $answers = [
            'Make sure your cat is sitting purrrfectly still ?',
            'Honestly, I like furry shoes better than MY cat',
            'Maybe... try saying the spell backwards?',
        ];

        return $this->render('show/question.html.twig', [
            'question' => ucwords(str_replace('-', ' ', $slug)),
            'answers' => $answers,
        ]);
     }

}

The render() method will return a Response object.

Now our template file could be:

templates/show/question.html.twig

<h1>{{ question }}</h1>
<div>
    Eventually, we'll print the full question here!
</div>

Twig syntaxes

https://twig.symfony.com/doc/

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

{% block body %}

    <h1>{{ question }}</h1>
    <h2>Answers {{ answers|length }}</h2>
    {# oh, I'm just a comment hiding here #}
    <div>
        <ul>
            {% for index, answer in answers %}
                <li>{{index}} - {{ answer }}</li>
            {% endfor %}
        </ul>
    </div>

{% endblock %}

Assets: CSS, Images, JS...

Symfony provides a component to manage assets (https://symfony.com/doc/current/components/asset.html):

composer require symfony/asset

With this package we add the assets this way instead of hardcoding paths:

templates/base.html.twig

<html>
    <head>
        {% block stylesheets %}
            <link rel="stylesheet" href="{{ asset('css/app.css') }}">
        {% endblock %}
    </head>
</html>

The Asset component manages assets through packages. A package groups all the assets which share the same properties: versioning strategy, base path, CDN hosts, etc.

Extending a block

In the base (or whatever) twig template we could have a block for javascript (or whatever):

templates/base.html.twig

<html>
    <body>
        {% block javascripts %}
            <script
              src="https://code.jquery.com/jquery-3.4.1.min.js"
              integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
              crossorigin="anonymous"></script>
        {% endblock %}
    </body>
</html>

Now if we want to extend this block on other template we could:

templates/question_show.html.twig

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

{% block javascripts %}
    {{ parent() }}
    <script src="{{ asset('js/question_show.js') }}"></script>
{% endblock %}

This parent() will add the jQuery script (and all the code inherited from the base javascript block) and then our custom script for this question_show template.

Webpack Encore

This Symfony Library allows integration of Webpack node library which is the industry-standard tool for managing your frontend assets. It combines and minifies your CSS and JavaScript files, etc.

We must have node and (recommended) yarn installed:

node -v

yarn -v

Then install webpack encore (recipe) and tell node to install dependencies with yarn:

composer require encore

yarn install

The recipe adds these 2 files:

  • assets/app.js
  • assets/styles/app.css

Webpack Encore is entirely configured by one file: webpack.config.js

To execute encore (run file watchers):

yarn watch

yarn watch is just a shortcut for running yarn run encore dev --watch. You can see it defined in your package.json. It's the same as running node node_modules/.bin/encore dev --watch.

It reads those 2 files in assets/, does some processing on them, and outputs a built version of each inside a new public/build/ directory. This way we code in the assets/ directory, but point to the built files in our templates.

Now in our Twig templates we can

        {% block stylesheets %}
            <!-- <link rel="stylesheet" href="{{ asset('css/app.css') }}"> -->
            {{ encore_entry_link_tags('app') }}
        {% endblock %}

        {% block javascripts %}
            <!-- <script src="{{ asset('js/question_show.js') }}"></script> -->
            {{ encore_entry_script_tags('app') }}
        {% endblock %}

We can add external libraries with yarn too, e.g.:

yarn add jquery --dev

yarn add bootstrap --dev

The in our app.js file:

assets/app.js

// Need jQuery? Install it with "yarn add jquery", then uncomment to import it.
import $ from 'jquery';

/**
 * Simple (ugly) code to handle the comment vote up/down
 */
var $container = $('.js-vote-arrows');
...

And in our app.css file:

assets/styles/app.css

@import "~bootstrap";

Webpack Encore can also minimize your files for production, supports Sass or LESS compiling, comes with React and Vue.js support, has automatic filename versioning and more: https://symfony.com/doc/current/frontend.html

Profiler (debug)

composer require profiler --dev

I'm using --dev because the profiler is a tool that we'll only need while we're developing: it won't be used on production. This means Composer adds it to the require-dev section of composer.json.

The profiler will add the debug bar to our dev environment and the dump() -inline- and dd() -dump and die- functions.

The debug package

composer require debug

This time I'm not using --dev because this will install something that I do want on production. It installs DebugBundle - that's not something we need on production - but also Monolog, which is a logging library. And we probably do want to log things on production. All of this is defined in symfony/debug-pack.

Installing the DebugBundle will move dd() and dump() to the debug bar.

It will also add a debugger command for the server:

php bin/console server:dump

This command will help debugging ajax calls for example.

Since Flex 1.9 composer will add each package of a pack to the composer.json file. If we are using a prior version it won't, so if we need to change the version of one of the installed packages in the pack, we need to unpack it:

composer unpack symfony/debug-pack

This will remove debug-pack from composer.json and adds its underlying packages.

Generating URLs

We have to know the name of our routes:

php bin/console debug:router

the name is under the Name column of the command output. To change the name of our routes we have to add it to our Controller annotation:

namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;

class DefaultController
{
     /**
     * @Route("/", name="app_homepage")
     */
     public function index()
     {
         return new Response('Hello World!');
     }

     /**
     * Wildcard route
     * 
     * @Route("/show/{slug}", name="app_show_question")
     */
     public function show($slug)
     {
        return new Response(sprintf(
            'Page to show "%s"!',
            $slug
        ));
     }

}

Now in our Twig template we could do:

<a class="navbar-brand" href="{{ path('app_homepage') }}"></a>

<a class="q-title" href="{{ path('app_show_question', { slug: 'reversing-a-spell' }) }}">
   <h2>Reversing a Spell</h2>
</a>

Returning a JSON response

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;

class CommentController extends AbstractController
{
    /**
    * @Route("/comments/{id}/vote/{direction}")
    */
    public function commentVote($id, $direction)
    {
        // todo - use id to query the database
        // use real logic here to save this to the database
        if ($direction === 'up') {
            $currentVoteCount = rand(7, 100);
        } else {
            $currentVoteCount = rand(0, 5);
        }
        // return new JsonResponse(['votes' => $currentVoteCount]);
        // shortcut to the above response:
        return $this->json(['votes' => $currentVoteCount]); 
    }
}

One of the components in Symfony is called the Serializer, and it's really good at converting objects into JSON or XML. We don't have it installed yet, but if we did, the $this->json() would start using it to serialize whatever we pass. More info: https://symfony.com/doc/current/components/serializer.html

We could have more control over the {id} and {direction} arguments, and the method allowed for this response using regular expressions on our annotations:

class CommentController extends AbstractController
{
    /**
     * @Route("/comments/{id<\d+>}/vote/{direction<up|down>}", methods="POST")
     */
    public function commentVote($id, $direction)
    {
    }
}

To get more information about a route we have a console command too:

php bin/console router:match /comments/10/vote/up --method=POST

Services

In OOP we call Services to objects that do some work.

To find services in our app we can:

php bin/console debug:autowiring // all services

php bin/console debug:autowiring log // matching log services

This command will show us relevant classes namespaces.

Autowiring

The above command tells us that there is a logger service object and its class implements some Psr\Log\LoggerInterface. Now we know it we could use it this way:

namespace App\Controller;

use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class CommentController extends AbstractController
{
    public function commentVote($id, $direction, LoggerInterface $logger)
    {
        if ($direction === 'up') {
            $logger->info('Voting up!');
        } else {
            $logger->info('Voting down!');
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment