curl -sS https://get.symfony.com/cli/installer | bash
mv /Users/Username/.symfony/bin/symfony /usr/local/bin/symfony
symfony
symfony check:req
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.
php -S 127.0.0.1:8000 -t public/
or
symfony serve -d <- Daemon mode
- Check status:
symfony server:status
- Start/Stop:
symfony server:start
symfony server:stop
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.
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)
composer require annotations
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 beApp\
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
.
php bin/console --help
php bin/console debug:router
...
This is a composer plugin that adds two special features to Composer itself.
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
.
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.
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'
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>
{% 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 %}
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.
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.
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
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.
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.
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>
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
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.
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!');
}
}
}