(my) best practices in symfony
DESCRIPTION
"(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.TRANSCRIPT
Train to Symfony 1http://traintosymfony.com
TRAINTO SYMFONY
(My) Best Practices in Symfony
the frameworkshop
http://traintosymfony.com @TrainToSymfony
BUSINESS CLASS
Train to Symfony 2http://traintosymfony.com
(MY) BEST PRACTICESIN SYMFONY
Train to Symfony 3http://traintosymfony.com(MY) BEST PRACTICES IN SYMFONY
Conoscere le best practices di Symfony
non per sapere “come si fa”
ma per capire “a cosa mi serve”
Train to Symfony 4http://traintosymfony.com(MY) BEST PRACTICES IN SYMFONY
Il codice nel posto giusto
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?
Train to Symfony 6http://traintosymfony.comIl codice nel posto giusto
È utile conoscere ed adottare convenzioniper poi sapere dove trovare e scrivere codice
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● [...]
Train to Symfony 8http://traintosymfony.comIl codice nel posto giusto
Ok, ma dove scrivo effettivamente il mio codice?
Train to Symfony 9http://traintosymfony.comIl codice nel posto giusto
controller
service
listener
repository
form builder
entity
template
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)
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
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
Train to Symfony 13http://traintosymfony.comIl codice nel posto giusto
Nel controller cerco di fare meno operazioni possibili
Delego più logica possibile ad altri componenti
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() {}
}
Train to Symfony 15http://traintosymfony.comIl codice nel posto giusto
form builder
definizione di un form specificandone i campi
scelta dei campi da gestire in base a della logica
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
Train to Symfony 17http://traintosymfony.comIl codice nel posto giusto
listener
catturare eventi lanciati da Symfony, da bundlesdi terze parti (FOSUserBundle), o custom
sviluppare un'architettura orientata ad eventi
difficoltà nel (ri)trovare il codice
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
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
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);
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)
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 repositorye altri servizi
acquistare molta familiarità con i servizi
creare tutti quelli necessari
nome e id del servizio molto importanti
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
Train to Symfony 24http://traintosymfony.comIl codice nel posto giusto
services:
# catalog service catalog.service: class: Foo\BarBundle\Service\CatalogService arguments: [@doctrine.orm.entity_manager, @router]
services:
# catalog service catalog.service: class: Foo\BarBundle\Service\CatalogService arguments: [@doctrine.orm.entity_manager, @router]
# src/Foo/BarBundle/Resources/config/services.yml
<service id="catalog.service" class="Foo\BarBundle\Service\CatalogService"><argument type="service" id="doctrine.orm.entity_manager" /><argument type="service" id="router" />
</service>
<service id="catalog.service" class="Foo\BarBundle\Service\CatalogService"><argument type="service" id="doctrine.orm.entity_manager" /><argument type="service" id="router" />
</service>
# src/Foo/BarBundle/Resources/config/services.xml
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
Train to Symfony 26http://traintosymfony.comIl codice nel posto giusto
Quali sono le convenzioni che definiamo noi?
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
Train to Symfony 28http://traintosymfony.com(MY) BEST PRACTICES IN SYMFONY
Organizzazione dei bundles
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)
Train to Symfony 30http://traintosymfony.comOrganizzazione dei bundles
Organizzazione internadi un bundle
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
Train to Symfony 32http://traintosymfony.comOrganizzazione dei bundles
/DependencyInjection
configura il bundle
classi chiamate al bootstrap del bundle
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 Loader\YamlFileLoader($container, newFileLocator(__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 Loader\YamlFileLoader($container, newFileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml'); }}
# src/Bricks/SiteBundle/DependencyInjection/BricksSiteExtension.php
*Extension.php si occupa di caricare i files di configurazione (servizi)
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
Train to Symfony 35http://traintosymfony.comOrganizzazione dei bundles
/Entity
meglio (?) definire tutte le entities in un solo posto
solitamente nel frontend (problema generazione CRUD)
problema inglese/italiano
Train to Symfony 36http://traintosymfony.comOrganizzazione dei bundles
/Extension (/TWIG)
contiene le estensioni custom di TWIG
filtri e funzioni TWIG
Train to Symfony 37http://traintosymfony.comOrganizzazione dei bundles
/Form
usare i FormType anche per form semplici
validazione possibile nel FormType
usare i FormType anche come servizi
Train to Symfony 38http://traintosymfony.comOrganizzazione dei bundles
/Listener
contiene i miei custom Listener
scegliere una convenzione per i nomi
Train to Symfony 39http://traintosymfony.comOrganizzazione dei bundles
/Resources
configurazione del bundle (config/services.xml)
templates
assets (sì js e css, no immagini)
Train to Symfony 40http://traintosymfony.comOrganizzazione dei bundles
/Service
contiene tutti i servizi creati
nient'altro che semplici classi PHP con un namespace
aspetto più critico di un servizio: il nome e l'id
Train to Symfony 41http://traintosymfony.com(MY) BEST PRACTICES IN SYMFONY
Design di controllers
Train to Symfony 42http://traintosymfony.comDesign di controllers
Funzionalità del sito
Sintassi degli url
Caricamento delle routes in routing.yml
Quanti controller creo? Come raggruppo le actions?
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
Train to Symfony 44http://traintosymfony.comDesign di controllers
Utilizzo di @ParamConverter
http://symfony.com/it/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
Train to Symfony 45http://traintosymfony.comDesign di controllers
Al posto di:
use Foo\BarBundle\Entity\Product;
/** * @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 Foo\BarBundle\Entity\Product;
/** * @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
Train to Symfony 46http://traintosymfony.comDesign di controllers
use Foo\BarBundle\Entity\Product;
/** * @Route("/product/{id}") * @ParamConverter("product", class="FooBarBundle:Product") */public function showAction(Product $product){
$price = $product->getPrice();
[...]}
use Foo\BarBundle\Entity\Product;
/** * @Route("/product/{id}") * @ParamConverter("product", class="FooBarBundle:Product") */public function showAction(Product $product){
$price = $product->getPrice();
[...]}
# src/Foo/BarBundle/Controller/DefaultController.php
utilizzo @ParamConverter:
Train to Symfony 47http://traintosymfony.com(MY) BEST PRACTICES IN SYMFONY
Files di configurazione custom
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 tramitefiles di configurazione custom
Train to Symfony 49http://traintosymfony.comFiles di configurazione custom
La direttiva imports include un qualsiasi file yml:
imports: - { resource: parameters.yml } - { resource: security.yml }
# bundles config - { resource: bundle_assetic.yml } - { resource: bundle_fos_user.yml } - { resource: bundle_hwi_oauth.yml } - { resource: bundle_stof_doctrine_extensions.yml } - { resource: bundle_vich_uploads.yml }
# project config - { resource: conf_custom_templates.yml } - { resource: conf_email_addresses.yml } - { resource: conf_locales.yml } - { resource: conf_special_product_categories.yml }
imports: - { resource: parameters.yml } - { resource: security.yml }
# bundles config - { resource: bundle_assetic.yml } - { resource: bundle_fos_user.yml } - { resource: bundle_hwi_oauth.yml } - { resource: bundle_stof_doctrine_extensions.yml } - { resource: bundle_vich_uploads.yml }
# project config - { resource: conf_custom_templates.yml } - { resource: conf_email_addresses.yml } - { resource: conf_locales.yml } - { resource: conf_special_product_categories.yml }
# app/config/config.yml
Train to Symfony 50http://traintosymfony.comFiles di configurazione custom
In un file custom di configurazione definisco:
● parametri per un bundle, che voglio tenerein un file separato (bundle_*)
● parametri per il container(chiave “parameters”)
● parametri disponibili nei templates TWIG(chiave “twig.globals”)
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
Train to Symfony 52http://traintosymfony.com(MY) BEST PRACTICES IN SYMFONY
Dal service container a TWIG
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
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
Train to Symfony 55http://traintosymfony.comDal service container a TWIG
Posso rendere disponibile anche un servizio(da usare con discrezione)
Train to Symfony 56http://traintosymfony.comDal service container a TWIG
services:
# estensione twig per il catalogo catalog.twig.extension: class: Foo\BarBundle\Extension\CatalogExtension arguments: [@catalog.service] tags: - { name: twig.extension }
services:
# estensione twig per il catalogo catalog.twig.extension: class: Foo\BarBundle\Extension\CatalogExtension arguments: [@catalog.service] tags: - { name: twig.extension }
# src/Foo/BarBundle/Resources/config/services.yml
1) definisco un'estensione TWIG custom
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
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
Train to Symfony 59http://traintosymfony.com(MY) BEST PRACTICES IN SYMFONY
Environments
Train to Symfony 60http://traintosymfony.comEnvironments
prod, dev e test sono i 3 ambienti predefiniti
● app.php● app_dev.php
Train to Symfony 61http://traintosymfony.comEnvironments
Il front controller definisce l'ambiente corrente:
$kernel = new AppKernel('prod', false);$kernel = new AppKernel('prod', false);
# web/app.php
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
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);
Train to Symfony 64http://traintosymfony.com
the frameworkshop
http://traintosymfony.com @TrainToSymfony
(My) Best Practices in Symfony
TRAINTO SYMFONY
BUSINESS CLASS