The document offers a comprehensive collection of tips and tricks related to Symfony, presented by Javier Eguiluz at SymfonyCon Paris in 2015. It covers various topics including Doctrine, Twig, Composer, Monolog, security practices, and testing strategies, along with code examples and best practices. The document highlights new features, recommended practices, and improvements in the Symfony ecosystem that can benefit developers.
Introduction to the presentation and the speaker, Javier Eguiluz, covering the agenda and thanking contributors.
Mini-tips for accessing request parameters using Symfony's ParameterBag, highlighting methods like get(), getInt(), and getBoolean().
Overview of Doctrine, discussing naming strategies for tables and properties in entities with examples.
Creating a generic InvoiceBundle that can work with multiple entities, defining interfaces and associations.
Details on the improved handling of collections in Doctrine 2.5, simplifying IN queries.
Guidance on setting default table options in configuration for compatibility with emojis and database engines.
Introducing save points in Doctrine transactions for better rollback management and transaction handling.
Mention of optional details in annotations for route definitions and controllers.
Managing email delivery in development environment with options for whitelisting.
Using environment variables for configuration management and exploring alternative variable loading.Introduction to creating custom channels and formatters with Monolog, structured logging practices.
Customization of Symfony's web debug toolbar and reordering/removing panels in the debug interface.
Implementing success and failure handlers for login events in Symfony security.
Best practices in data provider usage to enhance the readability and maintainability of tests.
Usage of PHP generators for generating data providers and reducing memory consumption.
Using @before annotations for setup in PHPUnit tests for better readability.
Using PHPUnit Bridge to detect deprecations and manage their stack traces effectively.
Implementing smoke tests for service validation and performance benchmarks for service instantiation.
Enhancements in command lifecycle and styling in Symfony console commands for better interaction.
Accessing request parameters
useSensioBundleFrameworkExtraBundleConfigurationRoute;
use SymfonyBundleFrameworkBundleControllerController;
use SymfonyComponentHttpFoundationRequest;
/** @Route("/blog") */
class BlogController extends Controller
{
/** @Route("/", name="blog_index") */
public function indexAction()
{
$value1 = $request->query->get('parameter1');
$value2 = $request->request->get('parameter2');
// ...
}
}
15.
Accessing request parameters
useSensioBundleFrameworkExtraBundleConfigurationRoute;
use SymfonyBundleFrameworkBundleControllerController;
use SymfonyComponentHttpFoundationRequest;
/** @Route("/blog") */
class BlogController extends Controller
{
/** @Route("/", name="blog_index") */
public function indexAction()
{
$value1 = $request->query->get('parameter1');
$value2 = $request->request->get('parameter2');
// ...
}
}
everybody uses the get() method,
but there are other useful methods
Naming
strategies
I learned thisfrom
Ruud
Bijnen
More information
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/
namingstrategy.html
20.
Setting custom table/propertiesnames
namespace AppBundleEntity;
use DoctrineORMMapping as ORM;
/** @ORMTable(name="api_users") */
class ApiUsers
{
/** @ORMColumn(type="string", name="api_token") */
private $apiToken;
/** @ORMColumn(type="datetime", name="created_at") */
private $createdAt;
}
21.
Setting custom table/propertiesnames
namespace AppBundleEntity;
use DoctrineORMMapping as ORM;
/** @ORMTable(name="api_users") */
class ApiUsers
{
/** @ORMColumn(type="string", name="api_token") */
private $apiToken;
/** @ORMColumn(type="datetime", name="created_at") */
private $createdAt;
}
Using a built-inDoctrine naming strategy
namespace AppBundleEntity;
use DoctrineORMMapping as ORM;
/** @ORMTable */
class ApiUsers
{
/** @ORMColumn(type="string") */
private $apiToken;
/** @ORMColumn(type="datetime") */
private $createdAt;
}
# app/config/config.yml
doctrine:
dbal:
# ...
orm:
naming_strategy: doctrine.orm.naming_strategy.underscore
this is automatically
translated into api_token
24.
Defining a customnaming strategy
namespace DoctrineORMMapping;
interface NamingStrategy
{
function classToTableName($className);
function propertyToColumnName($propertyName);
function referenceColumnName();
function joinColumnName($propertyName);
function joinTableName($sourceEntity, $targetEntity, $propertyName);
function joinKeyColumnName($entityName, $referencedColumnName);
}
What if thetarget entity can change?
Invoice
entity
User
entityManyToOne
Company
entity
28.
Our needs
• Createa generic InvoiceBundle able to
work both with User and Company
entities.
• No code change or special configuration is
needed for the bundle.
29.
1. Define anabstract "subject" interface
namespace AcmeInvoiceBundleModel;
interface InvoiceSubjectInterface
{
// Add here your custom methods
// ...
}
30.
2. Make entitiesimplement this interface
use DoctrineORMMapping as ORM;
use AcmeInvoiceBundleModelInvoiceSubjectInterface;
/** @ORMEntity */
class User implements InvoiceSubjectInterface
{
// ...
}
/** @ORMEntity */
class Company implements InvoiceSubjectInterface
{
// ...
}
different
applications
31.
3. Configure theentity association
namespace AcmeInvoiceBundleEntity;
use DoctrineORMMapping AS ORM;
use AcmeInvoiceBundleModelInvoiceSubjectInterface;
/** @ORMEntity */
class Invoice
{
/**
* @ORMManyToOne(
targetEntity="AcmeInvoiceBundleModelInvoiceSubjectInterface")
*/
protected $subject;
}
32.
3. Configure theentity association
namespace AcmeInvoiceBundleEntity;
use DoctrineORMMapping AS ORM;
use AcmeInvoiceBundleModelInvoiceSubjectInterface;
/** @ORMEntity */
class Invoice
{
/**
* @ORMManyToOne(
targetEntity="AcmeInvoiceBundleModelInvoiceSubjectInterface")
*/
protected $subject;
}
abstract target entity
33.
4. Define thetarget entity (at each application)
# app/config/config.yml
doctrine:
# ...
orm:
# ...
resolve_target_entities:
AcmeInvoiceBundleModelInvoiceSubjectInterface:
AcmeAppBundleEntityCustomer
this is where magic
happens: dynamic entity
resolution at runtime
34.
Improved
WHERE … IN
Ilearned this from More information
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/changelog/
migration_2_5.html#query-api-where-in-query-using-a-collection-as-parameter
Marco
Pivetta
35.
Common code whenusing collections
$categories = ...
$categoryIds = array();
foreach ($categories as $category) {
$categoryIds[] = $category->getId();
}
$queryBuilder = $this
->where('model.category IN (:category_ids)')
->setParameter('category_ids', $categoryIds)
;
36.
Common code whenusing collections
$categories = ...
$categoryIds = array();
foreach ($categories as $category) {
$categoryIds[] = $category->getId();
}
$queryBuilder = $this
->where('model.category IN (:category_ids)')
->setParameter('category_ids', $categoryIds)
;
transform the
ArrayCollection
into an array of IDs
37.
In Doctrine 2.5this is no longer needed
$categories = ...
$categoryIds = array();
foreach ($categories as $category) {
$categoryIds[] = $category->getId();
}
$queryBuilder = $this
->where('model.category IN (:categories)')
->setParameter('categories', $categories)
;
WHERE..IN
supports the use of
ArrayCollection
Define the defaulttable options
# app/config/config.yml
doctrine:
dbal:
default_table_options:
charset: utf8mb4
collate: utf8mb4_unicode_ci
engine: InnoDB
DoctrineBundle 1.6NEW
required to
support emojis
41.
Savepoints
I learned thisfrom More information
https://github.com/doctrine/DoctrineBundle/pull/451
http://doctrine.readthedocs.org/en/latest/en/manual/transactions.html
Christopher
Davis
42.
Normal Doctrine transaction
try{
$conn->beginTransaction();
// do something ...
$conn->commit();
} catch(Exception $e) {
$conn->rollback();
}
43.
Doctrine transaction withsave points
try {
$conn->beginTransaction('create_user');
// do something ...
$conn->commit('create_user');
} catch(Exception $e) {
$conn->rollback('create_user');
}
Give a name to a
transaction to create
a "save point"
44.
Nesting Doctrine transactions
try{
$conn->beginTransaction();
// do something ...
$conn->beginTransaction('create_user');
try {
// do something ...
$conn->commit('create_user');
} catch(Exception $e) { $conn->rollback('create_user'); }
$conn->commit();
} catch(Exception $e) { $conn->rollback(); }
Nested transaction
with a save point
The "action" suffixis optional for annotations
class DefaultController extends Controller
{
/**
* @Route("/", name="homepage")
*/
public function index(Request $request)
{
// ...
}
}
no need to call this
method indexAction()
50.
The "action" suffixis optional for annotations
class DefaultController extends Controller
{
/**
* @Route("/", name="homepage")
*/
public function index(Request $request)
{
// ...
}
}
no need to call this
method indexAction()
this only works when using
routing annotations
51.
The "action" suffixis optional for annotations
class DefaultController extends Controller
{
/**
* @Route("/", name="homepage")
*/
public function index(Request $request)
{
// ...
}
}
no need to call this
method indexAction()
this only works when using
routing annotations
best used when the
controller class only
contains action methods
52.
The "method" paramis optional for listeners
# app/config/services.yml
services:
app.exception_listener:
class: AppBundleEventListenerExceptionListener
tags:
- {
name: kernel.event_listener,
event: kernel.exception,
method: 'onKernelException'
}
no need to define the
method parameter
53.
The "method" paramis optional for listeners
# app/config/services.yml
services:
app.exception_listener:
class: AppBundleEventListenerExceptionListener
tags:
- {
name: kernel.event_listener,
event: kernel.exception,
method: 'onKernelException'
}
no need to define the
method parameter
on + camelCased event name
54.
The is_granted() checkin error pages
{# app/Resources/TwigBundle/views/Exception/error.html.twig #}
{% if app.user and is_granted('ROLE_ADMIN') %}
...
{% endif %}
Symfony 2.7 and previous need
this check to avoid issues in
pages not covered by a firewall.
55.
The is_granted() checkin error pages
{# app/Resources/TwigBundle/views/Exception/error.html.twig #}
{% if app.user and is_granted('ROLE_ADMIN') %}
...
{% endif %}
Symfony 2.7 and previous need
this check to avoid issues in
pages not covered by a firewall.
{% if is_granted('ROLE_ADMIN') %}
...
{% endif %}
Symfony 2.8 no longer
requires this check
This is whatwe
said during
SymfonyCon 2014
Don't load test classes in production
{
"autoload": {
"psr-4": { "MyLibrary": "src/" }
},
"autoload-dev": {
"psr-4": { "MyLibraryTests": "tests/" }
}
}
59.
New "exclude-from-classmap" option
//composer.json
{
"autoload": {
"exclude-from-classmap": ["/Tests/", "/test/", "/tests/"]
}
}
These classes are excluded
from the optimized autoloader
60.
New "exclude-from-classmap" option
//composer.json
{
"autoload": {
"exclude-from-classmap": ["/Tests/", "/test/", "/tests/"]
}
}
$ composer dump-autoload --optimize
These classes are excluded
from the optimized autoloader
Deprecated
filters
I learned thisfrom More information
http://twig.sensiolabs.org/doc/advanced.html#deprecated-filtersFabien
Potencier
65.
Applications evolve intime…
class AppExtension extends Twig_Extension
{
public function getFilters()
{
return array(
new Twig_SimpleFilter('old_filter', ...),
new Twig_SimpleFilter('new_filter', ...),
);
}
}
How can you deprecate
"old_filter" without
breaking the application?
66.
Filters can bedeprecated
class AppExtension extends Twig_Extension
{
public function getFilters()
{
return array(
new Twig_SimpleFilter('old_filter', ..., array(
'deprecated' => true,
'alternative' => 'new_filter',
)),
new Twig_SimpleFilter('new_filter', ...),
);
}
}
Twig 1.23NEW
This is how you deprecate
filters and alert developers
without breaking things
67.
Check if some
blockexists
I learned this from More information
https://github.com/twigphp/Twig/pull/1831Martin
Hasoň
68.
Avoid missing blocks
{%if 'title' is block %}
<title>{{ block('title') }}<title>
{% endif %}
Twig 1.2XNEW
not merged yet
The problem withfilter arguments
{{ product.photo|image(400, 150, 0.9) }}
71.
The problem withfilter arguments
{{ product.photo|image(400, 150, 0.9) }}
What if I need to define more arguments?
72.
The problem withfilter arguments
{{ product.photo|image(400, 150, 0.9) }}
{{ product.photo|image(
width = 400, height = 150, opacity = 0.9
) }}
What if I need to define more arguments?
this is a valid solution for Twig, but the
underlying PHP code is still very complex
73.
Defining a filterwith lots of arguments
<?php
$filter = new Twig_SimpleFilter('image', function (
$path, $width, $height, $opacity
) {
$path = ...
$width = ...
$height = ...
$opacity = ...
});
74.
Defining a variadicfilter
$filter = new Twig_SimpleFilter('image', function (
$path, $options = array()
) {
// ...
}, array('is_variadic' => true));
75.
Defining a variadicfilter
$filter = new Twig_SimpleFilter('image', function (
$path, $options = array()
) {
// ...
}, array('is_variadic' => true));
a single variadic parameter holds any
number of passed parameters (unlimited)
Template
source code
I learnedthis from More information
https://github.com/twigphp/Twig/pull/1813
https://github.com/twigphp/Twig/pull/1807
Nicolas
Grekas
78.
The Template classadded a new method
abstract class Twig_Template implements Twig_TemplateInterface
{
// ...
public function getSource()
{
// ...
}
}
Twig 1.22NEW
79.
How to displaythe source code of the template
{% set template = _self %}
{{ template.source|e }}
80.
How to displaythe source code of the template
{% set template = _self %}
{{ template.source|e }}
having the source code allows for
example to test if the template is
backwards/forwards compatible
81.
The (dirty) secretof the getSource() method
/* default/index.html.twig */
class __TwigTemplate_c917e55e0a4ad9b0ed28003c15c48de756d875a69e121aca97d5a53e84eaef4f extends Twig_Template
{
public function __construct(Twig_Environment $env) { }
protected function doDisplay(array $context, array $blocks = array()) { // ... }
public function getTemplateName()
{
return "default/index.html.twig";
}
// ...
public function getDebugInfo()
{
return array ( 115 => 54, 109 => 53, 93 => 43, 68 => 22, 66 => 21, 57 => 15, 46 => 7, 41 => 4, 35 => 3, 11 => 1,);
}
}
/* {% extends 'base.html.twig' %}*/
/* */
/* {% block body %}*/
/* <div id="wrapper">*/
/* <div id="container">*/
/* <div id="welcome">*/
/* <h1><span>Welcome to</span> Symfony {{ constant('SymfonyComponentHttpKernelKernel::VERSION') }}</h1>*/
/* </div>*/
/* ... */
Compiled Twig templates include
its full source code at the bottom
Email delivery
whitelist
I learnedthis from More information
https://github.com/symfony/symfony-docs/pull/4924
https://github.com/symfony/symfony-docs/pull/4925
Terje
Bråten
E.
Weimann
84.
In dev environment,redirect all emails
# app/config/config_dev.yml
swiftmailer:
delivery_address: dev@example.com
85.
Whitelist some emailsin dev environment
# app/config/config_dev.yml
swiftmailer:
delivery_address: dev@example.com
delivery_whitelist:
- "/notifications@example.com$/"
- "/^admin@*$/""
86.
Whitelist some emailsin dev environment
# app/config/config_dev.yml
swiftmailer:
delivery_address: dev@example.com
delivery_whitelist:
- "/notifications@example.com$/"
- "/^admin@*$/""
email addresses that match
the regular expression will be
sent (even in dev environment)
Setting ENV variablesvia the web server
<VirtualHost *:80>
ServerName Symfony
DocumentRoot "/path/to/symfony_2_app/web"
DirectoryIndex index.php index.html
SetEnv SYMFONY__DATABASE_USER user
SetEnv SYMFONY__DATABASE_PASSWORD secret
<Directory "/path/to/symfony_2_app/web">
AllowOverride All
Allow from All
</Directory>
</VirtualHost>
same as setting
database_user and
database_password
in parameters.yml
ENV variables aredumped into the container
// app/cache/dev/appDevDebugProjectContainer.xml
<container>
<parameters>
<parameter key="database_user">root</parameter>
<parameter key="database_password"> __secret__ </parameter>
<!-- ... -->
</parameters>
</container>
// app/cache/prod/appProdProjectContainer.php
protected function getDefaultParameters()
{
return array(
// ...
'database_user' => 'root',
'database_password' => __secret__,
);
}
DEV
environment
PROD
environment
You can see the password.
92.
ENV variables aredumped into the container
// app/cache/dev/appDevDebugProjectContainer.xml
<container>
<parameters>
<parameter key="database_user">root</parameter>
<parameter key="database_password"> __secret__ </parameter>
<!-- ... -->
</parameters>
</container>
// app/cache/prod/appProdProjectContainer.php
protected function getDefaultParameters()
{
return array(
// ...
'database_user' => 'root',
'database_password' => __secret__,
);
}
DEV
environment
PROD
environment
You can see the password.
It's static (if ENV variable
changes, app breaks)
93.
This is whatyou want…
// app/cache/prod/appProdProjectContainer.php
protected function getDefaultParameters()
{
return array(
// ...
'database_user' => 'root',
'database_password' => getenv("SYMFONY__DATABASE_PASSWORD");
);
}
94.
This is whatyou want…
// app/cache/prod/appProdProjectContainer.php
protected function getDefaultParameters()
{
return array(
// ...
'database_user' => 'root',
'database_password' => getenv("SYMFONY__DATABASE_PASSWORD");
);
}
… but we failed at
implementing this feature.
3. In "dev"machine
• Credentials file doesn't exist but no error is
triggered (because of ignore_errors).
• App uses the regular parameters.yml file.
101.
4. In "prod"machine
• Credentials file overrides parameters.yml
• The right parameters are available
anywhere (e.g. console commands)
• Developers can't see the production
configuration options.
• Requires discipline to add/remove options.
Creating a customchannel is painless
# app/config/config.yml
monolog:
channels: ["marketing"]
105.
Creating a customchannel is painless
# app/config/config.yml
monolog:
channels: ["marketing"]
$this->get('monolog.logger.marketing')->info('......');
106.
Creating a customchannel is painless
# app/config/config.yml
monolog:
channels: ["marketing"]
$this->get('monolog.logger.marketing')->info('......');
# app/logs/dev.log
[2015-11-30 17:44:19] marketing.INFO: ...... [] []
107.
Creating a customchannel is painless
# app/config/config.yml
monolog:
channels: ["marketing"]
now you can process these
messages apart from the
rest of logs (e.g. save them
in a different file)
$this->get('monolog.logger.marketing')->info('......');
# app/logs/dev.log
[2015-11-30 17:44:19] marketing.INFO: ...... [] []
108.
The channel namecan be a parameter
# app/config/config.yml
parameters:
app_channel: 'marketing'
monolog:
channels: [%app_channel%]
services:
app_logger:
class: ...
tags: [{ name: monolog.logger, channel: %app_channel% }]
MonologBundle 2.8NEW
Creating simple customformatters is painless
# app/config/config.yml
services:
app.log.formatter:
class: 'MonologFormatterLineFormatter'
public: false
arguments:
- "%%level_name%% [%%datetime%%] [%%channel%%] %%message%%n
%%context%%nn"
- 'H:i:s'
- false
the format of
each log line
113.
Creating simple customformatters is painless
# app/config/config.yml
services:
app.log.formatter:
class: 'MonologFormatterLineFormatter'
public: false
arguments:
- "%%level_name%% [%%datetime%%] [%%channel%%] %%message%%n
%%context%%nn"
- 'H:i:s'
- false
the format of
each log line
the format of
%datetime% placeholder
114.
Creating simple customformatters is painless
# app/config/config.yml
services:
app.log.formatter:
class: 'MonologFormatterLineFormatter'
public: false
arguments:
- "%%level_name%% [%%datetime%%] [%%channel%%] %%message%%n
%%context%%nn"
- 'H:i:s'
- false
the format of
each log line
the format of
%datetime% placeholder
115.
Creating simple customformatters is painless
# app/config/config.yml
services:
app.log.formatter:
class: 'MonologFormatterLineFormatter'
public: false
arguments:
- "%%level_name%% [%%datetime%%] [%%channel%%] %%message%%n
%%context%%nn"
- 'H:i:s'
- false
the format of
each log line
the format of
%datetime% placeholder
ignore n inside the
log messages?
Custom log formatDefault log format
INFO [18:38:21] [php] The SymfonyComponent
DependencyInjectionReference::isStrict method is
deprecated since version 2.8 and will be removed in 3.0.
{"type":16384,"file":"myproject/vendor/symfony/symfony/
src/Symfony/Component/DependencyInjection/
Reference.php","line":73,"level":28928}
INFO [18:38:21] [request] Matched route "homepage".
{"route_parameters":{"_controller":"AppBundle
Controller
DefaultController::indexAction","_route":"homepage"},"r
equest_uri":"http://127.0.0.1:8000/"}
INFO [18:38:21] [security] Populated the TokenStorage
with an anonymous Token.
[]
DEBUG [18:38:21] [event] Notified event "kernel.request"
to listener "SymfonyComponentHttpKernelEventListener
DebugHandlersListener::configure".
[]
DEBUG [18:38:21] [event] Notified event "kernel.request"
to listener "SymfonyComponentHttpKernelEventListener
ProfilerListener::onKernelRequest".
[2015-11-30 18:38:21] php.INFO: The SymfonyComponent
DependencyInjectionReference::isStrict method is
deprecated since version 2.8 and will be removed in 3.0.
{"type":16384,"file":"myproject/vendor/symfony/symfony/
src/Symfony/Component/DependencyInjection/
Reference.php","line":73,"level":28928} []
[2015-11-30 18:38:21] request.INFO: Matched route
"homepage". {"route_parameters":
{"_controller":"AppBundleController
DefaultController::indexAction","_route":"homepage"},"r
equest_uri":"http://127.0.0.1:8000/"} []
[2015-11-30 18:38:21] security.INFO: Populated the
TokenStorage with an anonymous Token. [] []
[2015-11-30 18:38:21] event.DEBUG: Notified event
"kernel.request" to listener "SymfonyComponent
HttpKernelEventListener
DebugHandlersListener::configure". [] []
[2015-11-30 18:38:21] event.DEBUG: Notified event
"kernel.request" to listener "SymfonyComponent
HttpKernelEventListener
ProfilerListener::onKernelRequest". [] []
[2015-11-30 18:38:21] event.DEBUG: Notified event
"kernel.request" to listener "SymfonyComponent
HttpKernelEventListenerDumpListener::configure". []
[]
The security.interactive_login event
services:
login_listener:
class:AppBundleListenerLoginListener
arguments: ['@security.token_storage', '@doctrine']
tags:
- { name: 'kernel.event_listener', event: 'security.interactive_login' }
the most common event to "do things"
after the user successfully logs in
139.
Defining a successhandler
namespace AppBundleSecurity;
use SymfonyComponentSecurityHttpAuthenticationAuthenticationSuccessHandlerInterface;
class LoginHandler implements AuthenticationSuccessHandlerInterface
{
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
// do something ...
return $this->redirect('...');
return new Response('...');
}
}
140.
Defining a successhandler
namespace AppBundleSecurity;
use SymfonyComponentSecurityHttpAuthenticationAuthenticationSuccessHandlerInterface;
class LoginHandler implements AuthenticationSuccessHandlerInterface
{
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
// do something ...
return $this->redirect('...');
return new Response('...');
}
}
you just need to implement
this interface…
141.
Defining a successhandler
namespace AppBundleSecurity;
use SymfonyComponentSecurityHttpAuthenticationAuthenticationSuccessHandlerInterface;
class LoginHandler implements AuthenticationSuccessHandlerInterface
{
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
// do something ...
return $this->redirect('...');
return new Response('...');
}
}
you just need to implement
this interface…
…and return a Response
instance
Enabling the successhandler
# app/config/services.yml
services:
app.login_handler:
class: AppBundleSecurityLoginHandler
arguments: ...
# app/config/security.yml
firewalls:
main:
pattern: ^/
form_login:
success_handler: app.login_handler
the LoginHandler class is
executed when the user logs
in successfully and after the
event.interactive_login
Meaningful
data providers
I learnedthis from More information
http://www.thisprogrammingthing.com/2015/making-dataproviders-more-maintainableScott
Warren
146.
Common data providers
publicfunction getUserData()
{
return array(
array(true, false, false),
array(true, true, false),
array(true, true, true),
);
}
/** @dataProvider getUserData */
public function testRegistration($register, $enable, $notify)
{
// ...
}
147.
When an errorhappens…
$ phpunit -c app
F..
Time: 137 ms, Memory: 12.50Mb
There was 1 failure:
1) AppBundleTestsControllerDefaultControllerTest::testRegistration
with data set #0 (true, false, false)
very hard to understand
the exact error
148.
Meaningful data providers
publicfunction getUserData()
{
return array(
'register user only' => array(true, false, false),
'register and enable' => array(true, true, false),
'register, enable and notify' => array(true, true, true),
);
}
/** @dataProvider getUserData */
public function testRegistration($register, $enable, $notify)
{
// ...
}
array keys are the
labels of each data set
149.
When an errorhappens…
$ phpunit -c app
F..
Time: 137 ms, Memory: 12.50Mb
There was 1 failure:
1) AppBundleTestsControllerDefaultControllerTest::testRegistration
with data set "register user only" (true, false, false)
meaningful error
messages
Common data providers
publicfunction dataProvider()
{
public function providerBasicDrivers()
{
return array(
array('doctrine.orm.cache.apc.class', array('type' => 'apc')),
array('doctrine.orm.cache.array.class', array('type' => 'array')),
array('doctrine.orm.cache.xcache.class', array('type' => 'xcache')),
array('doctrine.orm.cache.wincache.class', array('type' => 'wincache')),
array('doctrine.orm.cache.zenddata.class', array('type' => 'zenddata')),
);
}
}
152.
Data providers usingPHP generators
public function dataProvider()
{
public function providerBasicDrivers()
{
yield ['doctrine.orm.cache.apc.class', ['type' => 'apc']];
yield ['doctrine.orm.cache.array.class', ['type' => 'array']];
yield ['doctrine.orm.cache.xcache.class', ['type' => 'xcache']];
yield ['doctrine.orm.cache.wincache.class', ['type' => 'wincache']];
yield ['doctrine.orm.cache.zenddata.class', ['type' => 'zenddata']];
}
}
PHP 5.5NEW
153.
Data providers usingPHP generators
public function dataProvider()
{
public function providerBasicDrivers()
{
yield ['doctrine.orm.cache.apc.class', ['type' => 'apc']];
yield ['doctrine.orm.cache.array.class', ['type' => 'array']];
yield ['doctrine.orm.cache.xcache.class', ['type' => 'xcache']];
yield ['doctrine.orm.cache.wincache.class', ['type' => 'wincache']];
yield ['doctrine.orm.cache.zenddata.class', ['type' => 'zenddata']];
}
}
PHP 5.5NEW
it reduces memory
consumption
154.
Better setUp
and tearDown
Ilearned this from More information
https://phpunit.de/manual/current/en/appendixes.annotations.html
#appendixes.annotations.before
Sebastian
Bergmann
155.
Common use ofsetUp( ) method in tests
class IntegrationTest extends PHPUnit_Framework_TestCase
{
private $rootDir;
private $fs;
public function setUp()
{
$this->rootDir = realpath(__DIR__.'/../../../../');
$this->fs = new Filesystem();
if (!$this->fs->exists($this->rootDir.'/symfony.phar')) {
throw new RuntimeException("...");
}
}
}
156.
Common use ofsetUp( ) method in tests
class IntegrationTest extends PHPUnit_Framework_TestCase
{
private $rootDir;
private $fs;
public function setUp()
{
$this->rootDir = realpath(__DIR__.'/../../../../');
$this->fs = new Filesystem();
if (!$this->fs->exists($this->rootDir.'/symfony.phar')) {
throw new RuntimeException("...");
}
}
}
public function setUp()
public function tearDown()
public static function setUpBeforeClass()
public static function tearDownAfterClass()
157.
Better alternative: @beforeannotation
class IntegrationTest extends PHPUnit_Framework_TestCase
{
private $rootDir;
private $fs;
/** @before */
public function initialize()
{
$this->rootDir = realpath(__DIR__.'/../../../../');
$this->fs = new Filesystem();
}
/** @before */
public function checkPharFile() {
if (!$this->fs->exists($this->rootDir.'/symfony.phar')) {
throw new RuntimeException("...");
}
}
}
158.
Better alternative: @beforeannotation
class IntegrationTest extends PHPUnit_Framework_TestCase
{
private $rootDir;
private $fs;
/** @before */
public function initialize()
{
$this->rootDir = realpath(__DIR__.'/../../../../');
$this->fs = new Filesystem();
}
/** @before */
public function checkPharFile() {
if (!$this->fs->exists($this->rootDir.'/symfony.phar')) {
throw new RuntimeException("...");
}
}
}
you can define any number
of @before methods
159.
Better alternative: @beforeannotation
class IntegrationTest extends PHPUnit_Framework_TestCase
{
private $rootDir;
private $fs;
/** @before */
public function initialize()
{
$this->rootDir = realpath(__DIR__.'/../../../../');
$this->fs = new Filesystem();
}
/** @before */
public function checkPharFile() {
if (!$this->fs->exists($this->rootDir.'/symfony.phar')) {
throw new RuntimeException("...");
}
}
}
you can define any number
of @before methods
order matters (first
method is executed first)
160.
Alternatives to setUpand tearDown
class IntegrationTest extends PHPUnit_Framework_TestCase
{
/** @before */
public function initialize() { }
/** @after */
public function clean() { }
/** @beforeClass */
public static function initialize() { }
/** @afterClass */
public static function initialize() { }
}
161.
Avoid risky
tests
I learnedthis from More information
https://phpunit.de/manual/current/en/risky-tests.htmlSebastian
Bergmann
Common PHPUnit configuration
<?xmlversion="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
beStrictAboutTestsThatDoNotTestAnything="true"
checkForUnintentionallyCoveredCode="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestSize="true"
beStrictAboutChangesToGlobalState="true"
>
...
</phpunit>
this strict configuration
will force you to create
better tests
Use the PHPUnitBridge to detect deprecations
$ composer require --dev symfony/phpunit-bridge
$ phpunit -c app
PHPUnit by Sebastian Bergmann and contributors.
.................
Time: 3.7 seconds, Memory: 75.00Mb
OK (17 tests, 21 assertions)
Remaining deprecation notices (368)
OK (17 tests, 21 assertions)
Remaining deprecation notices (368)
167.
When a deprecationoccurs…
This information is
useful, but incomplete.
Often you need the
stack trace of the code
that triggered the
deprecation.
168.
On-demand deprecation stacktraces
// BEFORE
$ phpunit -c app
// AFTER
$ SYMFONY_DEPRECATIONS_HELPER=/Blog::index/
phpunit -c app
A regular expression that
is matched against
className::methodName
169.
On-demand deprecation stacktraces
$ SYMFONY_DEPRECATIONS_HELPER=/.*/ phpunit -c app
The class "SymfonyBundleAsseticBundleConfigAsseticResource" is performing resource
checking through ResourceInterface::isFresh(), which is deprecated since 2.8 and will be removed in 3.0
Stack trace:
#0 vendor/symfony/symfony/src/Symfony/Component/Config/Resource/BCResourceInterfaceChecker.php(32):
trigger_error()
#1 app/bootstrap.php.cache(3061): SymfonyComponentConfigResourceBCResourceInterfaceChecker->isFresh()
#2 vendor/symfony/symfony/src/Symfony/Component/Config/ResourceCheckerConfigCacheFactory.php(45):
SymfonyComponentConfigResourceCheckerConfigCache->isFresh()
#3 vendor/symfony/symfony/src/Symfony/Component/Routing/Router.php(302): SymfonyComponentConfigResourceCheckerConfigCacheFactory->cache()
#4 vendor/symfony/symfony/src/Symfony/Component/Routing/Router.php(250): SymfonyComponentRoutingRouter->getMatcher()
#5 vendor/symfony/symfony/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php(154): SymfonyComponentRoutingRouter->matchRequest()
#6 [internal function]: SymfonyComponentHttpKernelEventListenerRouterListener->onKernelRequest()
#7 vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php(61): call_user_func()
#8 [internal function]: SymfonyComponentEventDispatcherDebugWrappedListener->__invoke()
#9 vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/EventDispatcher.php(181): call_user_func()
#10 vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/EventDispatcher.php(46): SymfonyComponentEventDispatcherEventDispatcher->doDispatch()
#11 vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php(132): SymfonyComponentEventDispatcherEventDispatcher-
>dispatch()
#12 app/bootstrap.php.cache(3178): SymfonyComponentEventDispatcherDebugTraceableEventDispatcher->dispatch()
#13 app/bootstrap.php.cache(3151): SymfonyComponentHttpKernelHttpKernel->handleRaw()
#14 app/bootstrap.php.cache(3302): SymfonyComponentHttpKernelHttpKernel->handle()
#15 app/bootstrap.php.cache(2498): SymfonyComponentHttpKernelDependencyInjectionContainerAwareHttpKernel->handle()
#16 vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Client.php(79): SymfonyComponentHttpKernelKernel->handle()
#17 vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Client.php(131): SymfonyComponentHttpKernelClient->doRequest()
#18 vendor/symfony/symfony/src/Symfony/Component/BrowserKit/Client.php(317): SymfonyBundleFrameworkBundleClient->doRequest()
#19 src/AppBundle/Tests/Controller/Admin/BlogControllerTest.php(42): SymfonyComponentBrowserKitClient->request()
#20 [internal function]: AppBundleTestsControllerAdminBlogControllerTest->testRegularUsersCannotAccessToTheBackend()
#21 {main}
It stops at the first
deprecation and shows
its stack trace
Automatic "smoke testing"for services
public function testContainerServices()
{
$client = static::createClient();
foreach ($client->getContainer()->getServiceIds() as $serviceId) {
$service = $client->getContainer()->get($serviceId);
$this->assertNotNull($service);
}
}
174.
Automatic "smoke testing"for services
public function testContainerServices()
{
$client = static::createClient();
foreach ($client->getContainer()->getServiceIds() as $serviceId) {
$service = $client->getContainer()->get($serviceId);
$this->assertNotNull($service);
}
}
get all services defined
by the container
175.
Automatic "smoke testing"for services
public function testContainerServices()
{
$client = static::createClient();
foreach ($client->getContainer()->getServiceIds() as $serviceId) {
$service = $client->getContainer()->get($serviceId);
$this->assertNotNull($service);
}
}
get all services defined
by the container
try to instantiate all of
them to look for errors
176.
Automatic "smoke testing"for services
public function testContainerServices()
{
$client = static::createClient();
foreach ($client->getContainer()->getServiceIds() as $serviceId) {
try {
$service = $client->getContainer()->get($serviceId);
$this->assertNotNull($service);
} catch(InactiveScopeException $e) { }
}
}
you may need this try … catch to avoid
issues with request related services
177.
Manual "smoke testing"for important services
public static function dataServices()
{
return [
['app.datetimepicker', 'AppBundleFormTypeDateTimePickerType'],
['app.redirect_listener', 'AppBundleEventListenerRedirectToPreferredLocaleListener'],
];
}
/** @dataProvider dataServices */
public function testImportantServices($id, $class)
{
$client = static::createClient();
$service = $client->getContainer()->get($id);
$this->assertInstanceOf($class, $service);
}
check that the service
is instantiable and that
the returned class is
the right one
Service instantiation shouldnot take too long
public function testContainerServices()
{
$client = static::createClient();
foreach ($client->getContainer()->getServiceIds() as $serviceId) {
try {
$startedAt = microtime(true);
$service = $client->getContainer()->get($serviceId);
$elapsed = (microtime(true) - $startedAt) * 1000;
$this->assertLessThan(50, $elapsed);
} catch(InactiveScopeException $e) {
}
}
}
180.
Service instantiation shouldnot take too long
public function testContainerServices()
{
$client = static::createClient();
foreach ($client->getContainer()->getServiceIds() as $serviceId) {
try {
$startedAt = microtime(true);
$service = $client->getContainer()->get($serviceId);
$elapsed = (microtime(true) - $startedAt) * 1000;
$this->assertLessThan(50, $elapsed);
} catch(InactiveScopeException $e) {
}
}
}
services should be
created in 50ms or less
181.
Most expensive
services in
SymfonyStandard
Service Time (ms)
cache_warmer 42.95
doctrine.orm.default_entity_manager 14.05
web_profiler.controller.router 11.88
swiftmailer.mailer.default 6.65
profiler 5.45
annotation_reader 4.53
doctrine.dbal.default_connection 4.22
form.type_extension.form.validator 4.13
routing.loader 4.02
form.type.choice 3.10
Common lifecycle ofa command
class MyCustomCommand extends ContainerAwareCommand
{
protected function configure()
{
}
protected function execute(InputInterface $input, OutputInterface $output)
{
}
}
185.
Full lifecycle ofa command
class MyCustomCommand extends ContainerAwareCommand
{
protected function configure()
{ }
protected function initialize(InputInterface $input, OutputInterface $output)
{ }
protected function interact(InputInterface $input, OutputInterface $output)
{ }
protected function execute(InputInterface $input, OutputInterface $output)
{ }
}
here you can ask the user for
any missing option or argument
186.
Ask for anymissing command argument
class MyCommand extends ContainerAwareCommand
{
// ...
protected function interact(InputInterface $input, OutputInterface $output)
{
$dialog = $this->getHelper('dialog');
foreach ($input->getArguments() as $argument => $value) {
if ($value === null) {
$input->setArgument($argument, $dialog->ask($output,
sprintf('<question>%s</question>: ', ucfirst($argument))
));
}
}
}
}
the interact() method is usually
very verbose. This is the minimal
useful interact() method.
Most commands area mess
$description = $this->formatSection(
'container', sprintf('Information for service <info>%s</info>', $options['id']))
."n".sprintf('<comment>Service Id</comment> %s', isset($options['id']) ? $options['id'] : '-')
."n".sprintf('<comment>Class</comment> %s', get_class($service)
);
$description[] = sprintf('<comment>Scope</comment> %s', $definition->getScope(false));
$description[] = sprintf('<comment>Public</comment> %s', $definition->isPublic() ? 'yes' : 'no');
$description[] = sprintf('<comment>Synthetic</comment> %s', $definition->isSynthetic() ? 'yes' : 'no');
$description[] = sprintf('<comment>Lazy</comment> %s', $definition->isLazy() ? 'yes' : 'no');
$this->writeText($description, $options);
you are mixing content with
presentation, like in the good
old days of HTML+CSS
191.
The new "StyleGuide"
makes built-in commands
visually consistent.
192.
You can useit in your own commands too
use SymfonyComponentConsoleStyleSymfonyStyle;
class MyCustomCommand
{
// ...
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$io->...
// ...
}
}
193.
Displaying a title(BEFORE)
// alternative 1
$formatter = $this->getHelperSet()->get('formatter');
$formattedBlock = $formatter->formatBlock($title, '', true);
$output->writeln($formattedBlock);
// alternative 2
$title = 'Lorem Ipsum Dolor Sit Amet';
$output->writeln('<info>'.$title.'</info>');
$output->writeln(str_repeat('=', strlen($title)));
// alternative 3
$output->writeln('');
$output->writeln('Add User Command Interactive Wizard');
$output->writeln('-----------------------------------');
194.
Displaying a title(AFTER)
use SymfonyComponentConsoleStyleSymfonyStyle;
$io = new SymfonyStyle($input, $output);
$io->title('Lorem Ipsum Dolor Sit Amet');
195.
Displaying a title(AFTER)
use SymfonyComponentConsoleStyleSymfonyStyle;
$io = new SymfonyStyle($input, $output);
$io->title('Lorem Ipsum Dolor Sit Amet');
196.
Displaying a table(BEFORE)
$table = new Table($this->getOutput());
$table->setStyle('compact');
$table->setHeaders(['Parameter', 'Value']);
$table->addRow(['...', '...']);
$table->addRow(['...', '...']);
$table->addRow(['...', '...']);
$table->render();
197.
Displaying a table(AFTER)
use SymfonyComponentConsoleStyleSymfonyStyle;
$headers = ['Parameter', 'Value'];
$rows = [ ... ];
$io = new SymfonyStyle($input, $output);
$io->table($headers, $rows);
198.
Displaying a table(AFTER)
use SymfonyComponentConsoleStyleSymfonyStyle;
$headers = ['Parameter', 'Value'];
$rows = [ ... ];
$io = new SymfonyStyle($input, $output);
$io->table($headers, $rows);
199.
All the commonfeatures are covered
$io = new SymfonyStyle($input, $output);
$io->title();
$io->section();
$io->text();
$io->listing();
$io->table();
$io->choice();
$io->success();
$io->error();
$io->warning();
$io->ask();
$io->askHidden();
$io->confirm();
200.
Create consistent commandseffortlesslyFull Sample Command
$ php app/console command
Lorem Ipsum Dolor Sit Amet
==========================
// Duis aute irure dolor in reprehenderit in voluptate velit esse
// cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
Name Method Scheme Host Path
----------------- ------ ------ ---- ---------------------
admin_post_new ANY ANY ANY /admin/post/new
admin_post_show GET ANY ANY /admin/post/{id}
admin_post_edit ANY ANY ANY /admin/post/{id}/edit
admin_post_delete DELETE ANY ANY /admin/post/{id}
----------------- ------ ------ ---- ---------------------
! [CAUTION] Lorem ipsum dolor sit amet, consectetur adipisicing elit,
! sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
! Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
Consectetur Adipisicing Elit Sed Do Eiusmod
-------------------------------------------
* Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua.
* Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo.
* Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
Bundle namespace:
> AppBundle <Enter>
Bundle name [AcmeAppBundle]:
> <Enter>
Configuration format (yml, xml, php, annotation) [annotation]:
> <Enter>
Command title
All types of questions
Section title
List
Info message
Table
Caution admonition
! Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
Consectetur Adipisicing Elit Sed Do Eiusmod
-------------------------------------------
* Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua.
* Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo.
* Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
Bundle namespace:
> AppBundle <Enter>
Bundle name [AcmeAppBundle]:
> <Enter>
Configuration format (yml, xml, php, annotation) [annotation]:
> <Enter>
Do you want to enable the bundle? (yes/no) [yes]:
> <Enter>
Configuration format (select one) [annotation]:
> yml
> xml
> php
> annotation
! [NOTE] Duis aute irure dolor in reprehenderit in voluptate velit esse
! cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat.
[OK] Lorem ipsum dolor sit amet, consectetur adipisicing elit
[ERROR] Duis aute irure dolor in reprehenderit in voluptate velit esse.
[WARNING] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi.
All ty
Secti
List
All ty
resul
Note