"(My) Best Pratices in Symfony" è una parte delle slides utilizzate durante un Train to Symfony2 organizzato in una web agency nel Giugno 2014.
Non è un elenco di best practices in senso stretto, sono semplicemente spunti dai quali partire per affrontare alcune problematiche pratiche.
Scopri su http://traintosymfony.com cos'è Train to Symfony, e quanto possa essere utile alla tua azienda.
5. Train to Symfony
5http://traintosymfony.comIl codice nel posto giusto
Dove scrivo il codice che risolve questo problema?
Quanti controller posso creare?
È normale creare tanti servizi?
Ho codice che si ripete in alcuni controller, è normale?
Come chiamo i templates TWIG?
Il progetto del collega è strutturato diversamente dal mio, come mai?
7. Train to Symfony
7http://traintosymfony.comIl codice nel posto giusto
Symfony propone già delle convenzioni:
●
struttura delle cartelle di un progetto
●
struttura delle cartelle di un bundle
●
configurazione dei bundles
●
routing
●
composer
●
[...]
10. Train to Symfony
10http://traintosymfony.comIl codice nel posto giusto
controller
primo punto in cui verrebbe da mettere codice
forse non è il posto giusto:
●
se mi servirà in altri punti dell'applicazione
●
se posso delegare della logica in un servizio (es.
mandare un'email)
11. Train to Symfony
11http://traintosymfony.comIl codice nel posto giusto
/**
* @Route(“/{category_id}/{product_id}”)
*/
public function showAction($category_id, $product_id)
{
$em = $this->getDoctrine()->getManager();
$category = $em->getRepository('FooBarBundle:Category')->find($category_id);
if (!$category) { throw $this->createNotFoundException('Unable to find Category.'); }
$product = $em->getRepository('FooBarBundle:Product')->find($product_id);
if (!$product) { throw $this->createNotFoundException('Unable to Product.'); }
if (!$product->isInCategory($category)) {
throw new HTTPException(500, “Product does not belong to Category {$category}”);
}
[...]
}
/**
* @Route(“/{category_id}/{product_id}”)
*/
public function showAction($category_id, $product_id)
{
$em = $this->getDoctrine()->getManager();
$category = $em->getRepository('FooBarBundle:Category')->find($category_id);
if (!$category) { throw $this->createNotFoundException('Unable to find Category.'); }
$product = $em->getRepository('FooBarBundle:Product')->find($product_id);
if (!$product) { throw $this->createNotFoundException('Unable to Product.'); }
if (!$product->isInCategory($category)) {
throw new HTTPException(500, “Product does not belong to Category {$category}”);
}
[...]
}
Il controller lancia eccezioni
12. Train to Symfony
12http://traintosymfony.comIl codice nel posto giusto
/**
* @Route(“/{category_id}/{product_id}”)
*/
public function showAction($category_id, $product_id)
{
// throws exceptions
$this->container->get('url_checker')->checkProductUrl($product_id, $category_id);
[...]
}
/**
* @Route(“/{category_id}/{product_id}”)
*/
public function showAction($category_id, $product_id)
{
// throws exceptions
$this->container->get('url_checker')->checkProductUrl($product_id, $category_id);
[...]
}
Cerco di delegare più logica possibile ai servizi
14. Train to Symfony
14http://traintosymfony.comIl codice nel posto giusto
entity
logica che opera su un singolo oggetto
funzioni che operano su entity collegate
funzioni che formattano uno o più campi
class Product {
public function getFullPrice() {
}
public function getSpecialPrice() {
}
}
class Product {
public function getFullPrice() {
}
public function getSpecialPrice() {
}
}
16. Train to Symfony
16http://traintosymfony.comIl codice nel posto giusto
class ContactType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('email', 'email', array(
'label' => 'Email',
'attr' => array('placeholder' => 'Il tuo indirizzo email'),
'constraints' => array(
new Email(array('message' => 'Inserisci un indirizzo email valido.'))
)
))
->add('message', 'textarea', array(
'label' => 'Messaggio',
'constraints' => array(
new NotBlank(array('message' => 'Inserisci un messaggio.')),
)
));
}
public function getName() {
return 'foo_barbundle_contact';
}
}
class ContactType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('email', 'email', array(
'label' => 'Email',
'attr' => array('placeholder' => 'Il tuo indirizzo email'),
'constraints' => array(
new Email(array('message' => 'Inserisci un indirizzo email valido.'))
)
))
->add('message', 'textarea', array(
'label' => 'Messaggio',
'constraints' => array(
new NotBlank(array('message' => 'Inserisci un messaggio.')),
)
));
}
public function getName() {
return 'foo_barbundle_contact';
}
}
# src/Foo/BarBundle/Form/ContactType.php
17. Train to Symfony
17http://traintosymfony.comIl codice nel posto giusto
listener
catturare eventi lanciati da Symfony, da bundles
di terze parti (FOSUserBundle), o custom
sviluppare un'architettura orientata ad eventi
difficoltà nel (ri)trovare il codice
18. Train to Symfony
18http://traintosymfony.comIl codice nel posto giusto
http://symfony.com/it/doc/current/book/internals.html#evento-kernel-response
class MyResponseListener
{
public function onKernelResponse(FilterResponseEvent $event)
{
$response = $event->getResponse();
[..]
}
}
class MyResponseListener
{
public function onKernelResponse(FilterResponseEvent $event)
{
$response = $event->getResponse();
[..]
}
}
# src/Foo/BarBundle/Listener/MyResponseListener.php
19. Train to Symfony
19http://traintosymfony.comIl codice nel posto giusto
repository
logica per estrarre informazioni dal database
non eseguire operazioni su dati già disponibili
all'interno dispongo solamente dell'entity manager
20. Train to Symfony
20http://traintosymfony.comIl codice nel posto giusto
NO
SI
$em->getRepository('FooBarBundle:Product')->findSpecialOffers();
$em->getRepository('FooBarBundle:Product')->search(array(
'q' => $q,
'special_offer' => true,
'return_qb' => true
))
$em->getRepository('FooBarBundle:Product')->findSpecialOffers();
$em->getRepository('FooBarBundle:Product')->search(array(
'q' => $q,
'special_offer' => true,
'return_qb' => true
))
$em->getRepository('FooBarBundle:Product')->isInCategory($product, $category);$em->getRepository('FooBarBundle:Product')->isInCategory($product, $category);
21. Train to Symfony
21http://traintosymfony.comIl codice nel posto giusto
template
logica estremamente limitata (if)
se un template mostra contenuto molto diverso in base a
della logica, valutare se creare diversi templates (es.
risultati di una ricerca, form contatti simili)
22. Train to Symfony
22http://traintosymfony.comIl codice nel posto giusto
servizi
accesso al container e agli altri servizi
spesso fanno da “ponte” tra controller, custom repository
e altri servizi
acquistare molta familiarità con i servizi
creare tutti quelli necessari
nome e id del servizio molto importanti
23. Train to Symfony
23http://traintosymfony.comIl codice nel posto giusto
/**
* @Route(“/catalog/{productPath}”, requirements={“productPath” = “.+”})
*/
public function showAction($productPath)
{
$catalogService = $this->container->get('catalog.service');
/*
* check if $productPath is a valid url
* throws exception
*/
$product = $catalogService->getProductFromPath($productPath);
return array(
'product' => $product
);
}
/**
* @Route(“/catalog/{productPath}”, requirements={“productPath” = “.+”})
*/
public function showAction($productPath)
{
$catalogService = $this->container->get('catalog.service');
/*
* check if $productPath is a valid url
* throws exception
*/
$product = $catalogService->getProductFromPath($productPath);
return array(
'product' => $product
);
}
# src/Foo/BarBundle/Controller/ProductController.php
example.com/catalog/category1/subcategory1/product1example.com/catalog/category1/subcategory1/product1
25. Train to Symfony
25http://traintosymfony.comIl codice nel posto giusto
use [...]
class CatalogService
{
public function __construct($em, $router) {
[...]
}
public function getProductFromPath($productPath)
{
// divide $productPath in tokens
// controlla che l'ultimo token sia lo slug di un prodotto, altrimenti lancia un'eccezione
// controlla che gli altri token siano slug di categorie, altrimenti lancia un'eccezione
// controlla che le categorie siano corrette, altrimenti lancia un'eccezione
return $product;
}
}
use [...]
class CatalogService
{
public function __construct($em, $router) {
[...]
}
public function getProductFromPath($productPath)
{
// divide $productPath in tokens
// controlla che l'ultimo token sia lo slug di un prodotto, altrimenti lancia un'eccezione
// controlla che gli altri token siano slug di categorie, altrimenti lancia un'eccezione
// controlla che le categorie siano corrette, altrimenti lancia un'eccezione
return $product;
}
}
# src/Foo/BarBundle/Service/CatalogService.php
27. Train to Symfony
27http://traintosymfony.comIl codice nel posto giusto
Devo (sempre) considerare che:
qualcun altro metterà mano al nostro codice
anch'io riprenderò in mano il mio codice
se adotto delle soluzioni standard, il mio codice
è più comprensibile
29. Train to Symfony
29http://traintosymfony.comOrganizzazione dei bundles
Quali bundles creo?
SiteBundle (FrontendBundle)
UserBundle (AdminBundle, BackendBundle)
Creo un altro bundle:
per una funzionalità particolare
se devo estenderne uno dei vendor
se mette a disposizione una funzionalità trasversale
(es. RedirectBundle in SymfonyBricks)
31. Train to Symfony
31http://traintosymfony.comOrganizzazione dei bundles
/Controller
nessun limite sul numero di controller
meglio molti controller che pochi ma molto lunghi
attenzione ai nomi dei controller, si riflettono in Resources/views
33. Train to Symfony
33http://traintosymfony.comOrganizzazione dei bundles
class BricksSiteExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new LoaderYamlFileLoader($container, new
FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
}
class BricksSiteExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new LoaderYamlFileLoader($container, new
FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
}
# src/Bricks/SiteBundle/DependencyInjection/BricksSiteExtension.php
*Extension.php si occupa di caricare i files di configurazione (servizi)
34. Train to Symfony
34http://traintosymfony.comOrganizzazione dei bundles
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('bricks_site');
// Here you should define the parameters that are allowed to
// configure your bundle. See the documentation linked above for
// more information on that topic.
return $treeBuilder;
}
}
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('bricks_site');
// Here you should define the parameters that are allowed to
// configure your bundle. See the documentation linked above for
// more information on that topic.
return $treeBuilder;
}
}
# src/Bricks/SiteBundle/DependencyInjection/Configuration.php
Configuration.php carica i parametri nel container
43. Train to Symfony
43http://traintosymfony.comDesign di controllers
Buone pratiche nei controller:
●
meno logica possibile
●
evitare ripetizione di codice
●
delegare il più possibile a servizi e repositories
●
valutare se una pagina = una action
(es. submit di un form)
●
lanciare eccezioni
45. Train to Symfony
45http://traintosymfony.comDesign di controllers
Al posto di:
use FooBarBundleEntityProduct;
/**
* @Route("/product/{id}")
*/
public function showAction($id)
{
$em = $this->getDoctrine()->getManager();
$product = $em->getRepository('FooBarBundle:Product')->find($id);
if (!$product) {
throw new NotFoundHttpException(“Unable to find entity”);
}
$price = $product->getPrice();
}
use FooBarBundleEntityProduct;
/**
* @Route("/product/{id}")
*/
public function showAction($id)
{
$em = $this->getDoctrine()->getManager();
$product = $em->getRepository('FooBarBundle:Product')->find($id);
if (!$product) {
throw new NotFoundHttpException(“Unable to find entity”);
}
$price = $product->getPrice();
}
# src/Foo/BarBundle/Controller/DefaultController.php
46. Train to Symfony
46http://traintosymfony.comDesign di controllers
use FooBarBundleEntityProduct;
/**
* @Route("/product/{id}")
* @ParamConverter("product", class="FooBarBundle:Product")
*/
public function showAction(Product $product)
{
$price = $product->getPrice();
[...]
}
use FooBarBundleEntityProduct;
/**
* @Route("/product/{id}")
* @ParamConverter("product", class="FooBarBundle:Product")
*/
public function showAction(Product $product)
{
$price = $product->getPrice();
[...]
}
# src/Foo/BarBundle/Controller/DefaultController.php
utilizzo @ParamConverter:
48. Train to Symfony
48http://traintosymfony.comFiles di configurazione custom
È bene tenere in ordine i file di configurazione in app/config
●
creare un file per ogni bundle da configurare
●
inserire parametri nel container tramite
files di configurazione custom
50. Train to Symfony
50http://traintosymfony.comFiles di configurazione custom
In un file custom di configurazione definisco:
●
parametri per un bundle, che voglio tenere
in un file separato (bundle_*)
●
parametri per il container
(chiave “parameters”)
●
parametri disponibili nei templates TWIG
(chiave “twig.globals”)
51. Train to Symfony
51http://traintosymfony.comFiles di configurazione custom
Per le configurazioni che cambiano in base alla macchina
è consigliato tenere una versione .dist
●
condivisione repository git
●
ambiente locale/remoto
●
opensource: informazioni sensibili
●
es: parameters.yml e parameters.yml.dist
53. Train to Symfony
53http://traintosymfony.comDal service container a TWIG
Come configuro variabili perché siano disponibili in TWIG?
Con la chiave twig.globals una variabile è accessibile in tutti i templates
54. Train to Symfony
54http://traintosymfony.comDal service container a TWIG
parameters:
# array of available interface translations
interface_translation_locales:
en:
code: en
flag: gb.png
it:
code: it
flag: it.png
es:
code: es
flag: es.png
twig:
globals:
# parameter accessible from twig templates
interface_translation_locales: "%interface_translation_locales%"
parameters:
# array of available interface translations
interface_translation_locales:
en:
code: en
flag: gb.png
it:
code: it
flag: it.png
es:
code: es
flag: es.png
twig:
globals:
# parameter accessible from twig templates
interface_translation_locales: "%interface_translation_locales%"
# SymfonyBricks/app/config/locales.yml
56. Train to Symfony
56http://traintosymfony.comDal service container a TWIG
services:
# estensione twig per il catalogo
catalog.twig.extension:
class: FooBarBundleExtensionCatalogExtension
arguments: [@catalog.service]
tags:
- { name: twig.extension }
services:
# estensione twig per il catalogo
catalog.twig.extension:
class: FooBarBundleExtensionCatalogExtension
arguments: [@catalog.service]
tags:
- { name: twig.extension }
# src/Foo/BarBundle/Resources/config/services.yml
1) definisco un'estensione TWIG custom
57. Train to Symfony
57http://traintosymfony.comDal service container a TWIG
class CatalogExtension extends Twig_Extension
{
public function __construct($catalogService) {
$this->catalogService = $catalogService;
}
public function getFunctions() {
return array(
'getCatalogService' => new Twig_Function_Method($this, 'getCatalogService')
);
}
public function getCatalogService() {
return $this->catalogService;
}
}
class CatalogExtension extends Twig_Extension
{
public function __construct($catalogService) {
$this->catalogService = $catalogService;
}
public function getFunctions() {
return array(
'getCatalogService' => new Twig_Function_Method($this, 'getCatalogService')
);
}
public function getCatalogService() {
return $this->catalogService;
}
}
# src/Foo/BarBundle/Extension/CatalogExtension.php
2) implemento l'estensione TWIG CatalogExtension
58. Train to Symfony
58http://traintosymfony.comDal service container a TWIG
{% set catalogService = getCatalogService() %}
{% for category in rootCategory.children %}
<a href="{{ catalogService.generatePath(category) }}">
{{ category.name }}
</a>
{% endfor %}
{% set catalogService = getCatalogService() %}
{% for category in rootCategory.children %}
<a href="{{ catalogService.generatePath(category) }}">
{{ category.name }}
</a>
{% endfor %}
3) lo utilizzo nel template, senza che il servizio sia passato da un controller
62. Train to Symfony
62http://traintosymfony.comEnvironments
AppKernel.php carica la configurazione dell'environment
class AppKernel extends Kernel
{
public function registerBundles()
{
[...]
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
}
}
class AppKernel extends Kernel
{
public function registerBundles()
{
[...]
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
}
}
# app/AppKernel.php
63. Train to Symfony
63http://traintosymfony.comEnvironments
Creare un ambiente aggiuntivo è semplice:
●
creo web/previewfeatures.php
●
inizializzo l'environment “preview_features” tramite
●
creo app/config/dev_preview_features.yml
$kernel = new AppKernel('preview_features', false);$kernel = new AppKernel('preview_features', false);