La primera versión del framework Symfony2 se publicó hace más de tres años. Durante este tiempo, la comunidad de programadores Symfony ha originado una serie de buenas prácticas oficiosas que han sido adoptadas por la mayoría de aplicaciones.
Lamentablemente muchas de estas prácticas tienen poco que ver con la visión original de los creadores de Symfony y complican en exceso el desarrollo de las aplicaciones.
En esta sesión se presentarán muchas de las buenas prácticas oficiales recomendadas por Fabien Potencier, creador de Symfony. Sorpréndete con una visión totalmente renovada y pragmática del desarrollo de aplicaciones Symfony profesionales.
9. ¿Por qué?
Las buenas prácticas oficiosas
complican mucho el desarrollo
de aplicaciones y no siguen la
filosofía pragmática de los
creadores de Symfony.
10. Definición de “Best Practice”
A well defined procedure
that is known to produce
near-optimum results.
21. No uses las buenas prácticas …
• En bundles compartidos (públicos o
privados).
22. No uses las buenas prácticas …
• En bundles compartidos (públicos o
privados).
• En aplicaciones muy complejas o con
arquitecturas muy especiales.
23. No uses las buenas prácticas …
• En bundles compartidos (públicos o
privados).
• En aplicaciones muy complejas o con
arquitecturas muy especiales.
• Si tienes tus propias buenas prácticas.
32. ¿Cómo crear un sistema de plugins?
• Deben funcionar de manera
autónoma.
33. ¿Cómo crear un sistema de plugins?
• Deben funcionar de manera
autónoma.
• Pueden definir su propia
configuración.
34. ¿Cómo crear un sistema de plugins?
• Deben funcionar de manera
autónoma.
• Pueden definir su propia
configuración.
• Pueden incluir plantillas y assets.
59. No añadas un vendor a los bundles privados
AcmeNetworksAcmeWebsiteMarketingBundle
!
AcmeNetworksAcmeWebsiteMarketingBundle:Default:index.html.twig
!
{{ render(controller(
'AcmeNetworksAcmeWebsiteMarketingBundle:Default:latestNews'
)) }}
60. No añadas un vendor a los bundles privados
AcmeNetworksAcmeWebsiteMarketingBundle
!
AcmeNetworksAcmeWebsiteMarketingBundle:Default:index.html.twig
!
{{ render(controller(
'AcmeNetworksAcmeWebsiteMarketingBundle:Default:latestNews'
)) }}
Esto lo he visto con
mis propios ojos
69. Nueva organización de plantillas
aplicacion/
$" app/Resources/views/
!" contact/
# !" index.html.twig
# $" show.html.twig
. . .
!
$" product/
!" index.html.twig
!" category.html.twig
$" show.html.twig
70. La nueva notación de las plantillas
$this->render('AcmeDemoBunde:Default:index.html.twig');
$this->render('default/index.html.twig');
!
$this->render('AcmeDemoBundle::index.html.twig');
$this->render('index.html.twig');
71. La nueva notación de las plantillas
{% extends '::layout.html.twig' %}
{% extends 'layout.html.twig' %}
!
{{ include('AcmeDemoBundle:Default:subdir/index.html.twig') }}
{{ include('default/subdir/index.html.twig') }}
!
{{ include('AcmeDemoBundle:Default/subdir:index.html.twig') }}
{{ include('default/subdir/index.html.twig') }}
72. Los problemas de la notación tradicional
AcmeDemoBundle:Default:subdir/index.html.twig
73. Los problemas de la notación tradicional
• Requiere explicársela a cada diseñador/programador.
AcmeDemoBundle:Default:subdir/index.html.twig
74. Los problemas de la notación tradicional
• Requiere explicársela a cada diseñador/programador.
• Tiene excepciones (alguna de sus partes puede estar
vacía) e inconsistencias (subdirectorios).
AcmeDemoBundle:Default:subdir/index.html.twig
75. Los problemas de la notación tradicional
• Requiere explicársela a cada diseñador/programador.
• Tiene excepciones (alguna de sus partes puede estar
vacía) e inconsistencias (subdirectorios).
• No es inmediato saber dónde está la plantilla (debes
traducir la notación a un directorio).
AcmeDemoBundle:Default:subdir/index.html.twig
80. En resumen
• Si utilizas mal los bundles, estás repitiendo
la estructura de la aplicación sin necesidad
• Si no vas a compartir tus bundles, utiliza
los directorios de la aplicación (app/
Resources/ y web/).
• El número de archivos se mantiene, los
directorios y la complejidad se reducen.
83. Escala de sensibilidad
para programadores
Política
Religión
Fútbol
Estándar de código
Editor de código
Anotaciones
84. Anotaciones PHP
use AppBundleEntityPost;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
!
class CommentController extends Controller
{
/**
* @Route("/edit/{id}", name = "post_edit")
*/
public function editAction(Post $post)
{ ... }
!
/*
* @Route("/edit/{id}", name = "post_edit")
*/
public function editAction(Post $post)
{ ... }
}
85. Anotaciones PHP
use AppBundleEntityPost;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
!
class CommentController extends Controller
{
/**
* @Route("/edit/{id}", name = "post_edit")
*/
public function editAction(Post $post)
{ ... }
!
/*
* @Route("/edit/{id}", name = "post_edit")
*/
public function editAction(Post $post)
{ ... }
}
Anotación
86. Anotaciones PHP
use AppBundleEntityPost;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
!
class CommentController extends Controller
{
/**
* @Route("/edit/{id}", name = "post_edit")
*/
public function editAction(Post $post)
{ ... }
!
/*
* @Route("/edit/{id}", name = "post_edit")
*/
public function editAction(Post $post)
{ ... }
}
Anotación
Comentario
94. Los ParamConvertes en la práctica
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use AppBundleEntityPost;
!
class CommentController extends Controller
{
/**
* @Route("/edit/{id}", name = "post_edit")
*/
public function editAction(Post $post)
{
}
}
use SensioBundleFrameworkExtraBundleConfigurationRoute;
!
!
class CommentController extends Controller
{
/**
* @Route("/edit/{id}", name = "post_edit")
*/
public function editAction($id)
{
$em = $this->getDoctrine()->getManager();
$post = $em->getRepository('AppBundle:Post')->find($id);
!
if (!$post) {
throw $this->createNotFoundException();
}
}
}
95. Un controlador Symfony de ejemplo
namespace AppBundleController;
!
use AppBundleEntityPost;
use SymfonyBundleFrameworkBundleControllerController;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
!
class BlogController extends Controller
{
/**
* @Route("/edit/{id}", name="post_edit")
*/
public function editAction(Post $post)
{
return $this->render('blog/edit.html.twig', array(
'post' => $post
));
}
}
97. Los atajos de los controladores
$this->forward();
$this->redirect();
$this->redirectToRoute();
$this->getUser();
$this->getDoctrine();
$this->generateUrl();
$this->createNotFoundException();
$this->createAccessDeniedException()
102. Restricciones sencillas y comunes
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use SensioBundleFrameworkExtraBundleConfigurationSecurity;
!
/**
* @Route("/new", name="admin_post_new")
* @Security("has_role('ROLE_ADMIN')")
*/
public function newAction()
{
// ...
}
103. Restricciones sencillas y comunes
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use SensioBundleFrameworkExtraBundleConfigurationSecurity;
!
/**
* @Route("/new", name="admin_post_new")
*/
public function newAction()
{
if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) {
throw $this->createAccessDeniedException();
}
!
!
// ...
}
104. Restricciones sencillas y comunes
use AppBundleEntityPost;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use SensioBundleFrameworkExtraBundleConfigurationSecurity;
!
/**
* @Route("/{id}/edit", name="admin_post_edit")
* @Security("user.getEmail() == post.getAuthorEmail()")
*/
public function editAction(Post $post)
{
// ...
}
105. Restricciones sencillas y comunes
// src/AppBundle/Entity/Post.php
// ...
!
class Post
{
// ...
!
public function isAuthor(User $user = null)
{
return $user && $user->getEmail() == $this->getAuthorEmail();
}
}
106. Restricciones sencillas y comunes
use AppBundleEntityPost;
use SensioBundleFrameworkExtraBundleConfigurationSecurity;
!
/**
* @Route("/{id}/edit", name="admin_post_edit")
* @Security("post.isAuthor(user)")
*/
public function editAction(Post $post)
{
// ...
}
!
!
{% if post.isAuthor(app.user) %}
<a href=""> ... </a>
{% endif %}
107. Restricciones avanzadas (voters)
namespace AppBundleSecurity;
!
use SymfonyComponentSecurityCoreAuthorizationVoterAbstractVoter;
use SymfonyComponentSecurityCoreUserUserInterface;
!
class PostVoter extends AbstractVoter
{
protected function getSupportedAttributes()
{
return array('create', 'edit');
}
!
protected function getSupportedClasses()
{
return array('AppBundleEntityPost');
}
!
protected function isGranted($attribute, $post, $user = null)
{
// ...
}
}
109. Nombres de servicios en apps Symfony
// Servicios Symfony
$this->get('doctrine')
$this->get('logger')
$this->get('session')
$this->get('validator')
!
// Servicios de terceros
$this->get('imagine.filter.loader.thumbnail')
$this->get('knp_menu.renderer_provider')
$this->get('sonata.admin.form.filter.type.datetime_range')
110. Nombres de servicios propios
$this->get('slugger')
$this->get('parser')
$this->get('markdown_parser')
$this->get('stats_aggregator')
!
// Aceptable también
$this->get('app.slugger')
$this->get('app.parser')
$this->get('app.markdown_parser')
$this->get('app.stats_aggregator')
111. No definas parámetros para las clases
# app/config/services.yml
parameters:
slugger.class: AppBundleUtilsSlugger
!
services:
slugger:
class: "%slugger.class%"
112. No definas parámetros para las clases
# app/config/services.yml
parameters:
slugger.class: AppBundleUtilsSlugger
!
services:
slugger:
class: "%slugger.class%"
Innecesario y
poco útil en la
práctica
113. No definas parámetros que no cambian
# app/config/config.yml
parameters:
homepage.num_items: 10
!
// src/AppBundle/Entity/Post.php
class Post {
const NUM_ITEMS = 10;
!
// ...
}
114. No definas parámetros que no cambian
# app/config/config.yml
parameters:
homepage.num_items: 10
!
// src/AppBundle/Entity/Post.php
class Post {
const NUM_ITEMS = 10;
!
// ...
}
Este valor
seguramente no
cambia nunca
115. No añadas botones en los formularios
class PostType extends AbstractType {
public function buildForm($builder, $options) {
$builder
// ...
->add('save', 'submit', array('label' => 'Create Post'))
;
}
!
// ...
}
116. No añadas botones en los formularios
class PostType extends AbstractType {
public function buildForm($builder, $options) {
$builder
// ...
->add('save', 'submit', array('label' => 'Create Post'))
;
}
!
// ...
}
Te dificulta
reutilizar los
formularios
118. No generes las URLs en los tests
public function testBlogArchives()
{
$client = self::createClient();
$url = $client->getContainer()->get('router')->generate('blog_archives');
$client->request('GET', $url);
// ...
}
!
!
public function testBlogArchives()
{
$client = self::createClient();
$client->request('GET', '/blog/archives/');
// ...
}
119. No generes las URLs en los tests
public function testBlogArchives()
{
$client = self::createClient();
$url = $client->getContainer()->get('router')->generate('blog_archives');
$client->request('GET', $url);
// ...
}
!
!
public function testBlogArchives()
{
$client = self::createClient();
$client->request('GET', '/blog/archives/');
// ...
}
Si rompes la URL
no te enteras
121. Configuración
no cambia de un ordenador a otro
proyecto/app/config/
!" config.yml
!" parameters.yml
$" parameters.yml.dist
122. Configuración
no cambia de un ordenador a otro
cambia de un ordenador a otro
no se sube al repositorio
proyecto/app/config/
!" config.yml
!" parameters.yml
$" parameters.yml.dist
123. Configuración
no cambia de un ordenador a otro
cambia de un ordenador a otro
no se sube al repositorio
este sí se sube al repositorio
proyecto/app/config/
!" config.yml
!" parameters.yml
$" parameters.yml.dist
124. No utilices una configuración semántica
public function getConfigTreeBuilder() {
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('framework');
!
$rootNode
->children()
->scalarNode('secret')->end()
->scalarNode('http_method_override')
->info("Set true to enable support for ...”)
->defaultTrue()
->end()
->arrayNode('trusted_proxies')
->beforeNormalization()
->ifTrue(function ($v) { return !is_array($v) && null !== $v; })
->then(function ($v) { return is_bool($v) ? array() : preg_split('/s*,s*/', $v); })
->end()
->prototype('scalar')
->validate()
->ifTrue(function ($v) {
if (empty($v)) {
return false;
}
!
if (false !== strpos($v, '/')) {
Sólo es útil en
configuraciones
muy complejas