Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Buenas prácticas en el desarrollo de aplicaciones con Symfony2

Buenas prácticas en el desarrollo de aplicaciones con Symfony2

Slides de la charla en deSymfony 2013

Fran Moreno

June 22, 2013
Tweet

Other Decks in Programming

Transcript

  1. • Trabajo en Tierra Virtual • Miembro Symfony Valencia •

    @franmomu • showmethecode.es FRAN MORENO
  2. • Buenas prácticas • Bundles • Skinny Controllers, fat model

    • Model Managers • Eventos • Formularios ÍNDICE
  3. BUENAS PRÁCTICAS • Conjunto de acciones que han funcionado bien

    en un contexto • Criticables • Dependen del contexto
  4. BÁSICAS • RTFM (Read The Fucking Manual) • Mirar el

    código de Symfony2 • Mirar el código de los bundles
  5. BÁSICAS • RTFM (Read The Fucking Manual) • Mirar el

    código de Symfony2 • Mirar el código de los bundles • Cambiar el favicon y el apple touch icon
  6. BUNDLES Imitar la estructura de directorios MmsfBundle !"" Command/ !""

    Controller/ !"" Event/ !"" EventListener/ !"" Form/ # !"" EventListener/ # $"" Type/ !"" Resources/ !"" Security/ !"" Tests/ !"" Twig/ $"" Validator/
  7. class AppMmsfExtension extends Extension { public function load(array $configs, ContainerBuilder

    $container) { // ... $loader->load('forms.xml'); $loader->load('validation.xml'); $loader->load('twig.xml'); } } Separar las declaraciones de los servicios
  8. Usar Configuration class Configuration implements ConfigurationInterface { public function getConfigTreeBuilder()

    { $treeBuilder = new TreeBuilder(); $rootNode = $treeBuilder->root('app_mmsf'); $this->addPaginatorSection($rootNode); return $treeBuilder; } public function addPaginatorSection(ArrayNodeDefinition $rootNode) { $rootNode ->children() ->arrayNode('paginator') ->addDefaultsIfNotSet() ->children() ->arrayNode('template') // ... ->end() ->scalarNode('page_range') ->defaultValue(5) ->validate() ->ifTrue(function ($v) { return ((int) $v !== $v);}) ->thenInvalid('Page range must be an integer') ->end() ->end() ->end() ->end(); } } Añadir los parámetros en la Extension!
  9. SKINNY CONTROLLERS, FAT MODEL • Controladores pequeños • Symfony2 no

    provee Model • Capa Model • Objetos de dominio • Capa de persistencia • Entity • Repository • Servicios
  10. /** * @Route("/team/{id}/add_player", name="mmsf_main_team_add_player") * @Template */ public function addPlayerToTeamAction($id)

    { $em = $this->getDoctrine()->getManager(); $team = $em->getRepository('MmsfBundle:Team')->find($id); if (!$team) { throw $this->createNotFoundException('Unable to find Team entity.'); } $player = new Player(); $form = $this->createFormBuilder($player) ->add('name') ->add('surname') //... ; $request = $this->getRequest(); if ($request->isMethod('POST')) { $form->bind($request); if($form->isValid()) { // Amazing things... } } return array('form' => $form->createView(), 'team' => $team); } Fat Controller
  11. /** * @Route("/team/{id}/add_player", name="mmsf_main_team_add_player") * @Template */ public function addPlayerToTeamAction($id)

    { $em = $this->getDoctrine()->getManager(); $team = $em->getRepository('MmsfBundle:Team')->find($id); if (!$team) { throw $this->createNotFoundException('Unable to find Team entity.'); } $player = new Player(); $form = $this->createFormBuilder($player) ->add('name') ->add('surname') //... ; $request = $this->getRequest(); if ($request->isMethod('POST')) { $form->bind($request); if($form->isValid()) { // Amazing things... } } return array('form' => $form->createView(), 'team' => $team); } Fat Controller
  12. /** * @Route("/team/{id}/add_player", name="mmsf_main_team_add_player") * @Template */ public function addPlayerToTeamAction($id)

    { $em = $this->getDoctrine()->getManager(); $team = $em->getRepository('MmsfBundle:Team')->find($id); if (!$team) { throw $this->createNotFoundException('Unable to find Team entity.'); } $player = new Player(); $form = $this->createFormBuilder($player) ->add('name') ->add('surname') //... ; $request = $this->getRequest(); if ($request->isMethod('POST')) { $form->bind($request); if($form->isValid()) { // Amazing things... } } return array('form' => $form->createView(), 'team' => $team); } Fat Controller
  13. /** * @Route("/team/{id}/add_player", name="mmsf_main_team_add_player") * @Template */ public function addPlayerToTeamAction($id)

    { $em = $this->getDoctrine()->getManager(); $team = $em->getRepository('MmsfBundle:Team')->find($id); if (!$team) { throw $this->createNotFoundException('Unable to find Team entity.'); } $player = new Player(); $form = $this->createFormBuilder($player) ->add('name') ->add('surname') //... ; $request = $this->getRequest(); if ($request->isMethod('POST')) { $form->bind($request); if($form->isValid()) { // Amazing things... } } return array('form' => $form->createView(), 'team' => $team); } Fat Controller
  14. /** * @Route("/team/{id}/add_player", name="mmsf_main_team_add_player") * @Template */ public function addPlayerToTeamAction($id)

    { $em = $this->getDoctrine()->getManager(); $team = $em->getRepository('MmsfBundle:Team')->find($id); if (!$team) { throw $this->createNotFoundException('Unable to find Team entity.'); } $player = new Player(); $form = $this->createFormBuilder($player) ->add('name') ->add('surname') //... ; $request = $this->getRequest(); if ($request->isMethod('POST')) { $form->bind($request); if($form->isValid()) { // Amazing things... } } return array('form' => $form->createView(), 'team' => $team); } Fat Controller
  15. No crear los formularios en el Controller $form = $this->createFormBuilder($player)

    ->add('name') ->add('surname') //... ; $form = $this->createForm(new PlayerType(), $player);
  16. /** * @Route("/team/{id}/add_player", name="mmsf_main_team_add_player") * @Template */ public function addPlayerToTeamAction($id,

    Request $request) { //... $request = $this->getRequest(); // .. } Pasar la petición como parámetro
  17. /** * @Route("/team/{id}/add_player", name="mmsf_main_team_add_player") * @Template */ public function addPlayerToTeamAction($id,

    Request $request) { $em = $this->getDoctrine()->getManager(); $team = $em->getRepository('MmsfBundle:Team')->find($id); if (!$team) { throw $this->createNotFoundException('Unable to find Team entity.'); } // ... }
  18. ParamConverter /** * @Route("/team/{id}/add_player", name="mmsf_main_team_add_player") * @ParamConverter("team", class="MmsfBundle:Team") * @Template

    */ public function addPlayerToTeamAction(Team $team, Request $request) { $em = $this->getDoctrine()->getManager(); // ... }
  19. ParamConverter /** * @Route("/team/{id}/add_player", name="mmsf_main_team_add_player") * @ParamConverter("team", class="MmsfBundle:Team") * @Template

    */ public function addPlayerToTeamAction(Team $team, Request $request) { $em = $this->getDoctrine()->getManager(); // ... }
  20. ParamConverter /** * @Route("/team/{id}/add_player", name="mmsf_main_team_add_player") * @ParamConverter("team", class="MmsfBundle:Team") * @Template

    */ public function addPlayerToTeamAction(Team $team, Request $request) { $em = $this->getDoctrine()->getManager(); // ... }
  21. /** * @Route("/team/{id}/add_player", name="mmsf_main_team_add_player") * @ParamConverter("team", class="MMSFBundle:Team") * @Template */

    public function addPlayerToTeamAction(Team $team, Request $request) { // ... if($form->isValid()) { $em = $this->getDoctrine()->getManager(); $team->addPlayer($player); $em->persist($team); $em->flush(); $mailer = $this->get('app.mmsf.mailer'); $mailer->sendWelcomeToTeamPlayer($team, $player); return $this->redirect(/* ... */); } // ... } Persistencia
  22. class TeamManager { protected $objectManager; protected $class; protected $repository; protected

    $mailer; public function __construct(ObjectManager $om, $class, MailerInterface $mailer) { $this->objectManager = $om; $this->repository = $om->getRepository($class); $metadata = $om->getClassMetadata($class); $this->class = $metadata->getName(); $this->mailer = $mailer; } // ... } TeamManager - Constructor App\MmsfBundle\Entity\Team
  23. class TeamManager { // ... public function createTeam() { $class

    = $this->getClass(); $team = new $class; return $team; } public function findTeamBy(array $criteria) { return $this->repository->findOneBy($criteria); } public function updateTeam(TeamInterface $team, $andFlush = true) { $this->objectManager->persist($team); if ($andFlush) { $this->objectManager->flush(); } } public function addPlayerToTeam(PlayerInterface $player, TeamInterface $team) { $team->addPlayer($player); $this->updateTeam($team); $this->mailer->sendWelcomeToTeamPlayer($team, $player); } } TeamManager - Métodos
  24. interface UserManagerInterface { public function createUser(); public function deleteUser(UserInterface $user);

    public function findUserBy(array $criteria); public function findUserByUsername($username); public function findUserByEmail($email); public function findUserByUsernameOrEmail($usernameOrEmail); public function findUserByConfirmationToken($token); public function findUsers(); public function getClass(); public function reloadUser(UserInterface $user); public function updateUser(UserInterface $user); public function updateCanonicalFields(UserInterface $user); public function updatePassword(UserInterface $user); } FOSUserBundle - UserManagerInterface
  25. /** * @Route("/team/{id}/add_player", name="mmsf_main_team_add_player") * @ParamConverter("team", class="MMSFBundle:Team") * @Template */

    public function addPlayerToTeamAction(Team $team) { // ... if($form->isValid()) { $teamManager = $this->get('app.mmsf.manager.team'); $teamManager->addPlayerToTeam($player, $team); return $this->redirect(/* ... */); } // ... } TeamManager en el Controller
  26. /** * @Route("/team/{id}/add_player", name="mmsf_main_team_add_player") * @ParamConverter("team", class="MmsfBundle:Team") * @Template */

    public function addPlayerToTeamAction(Team $team, Request $request) { $player = new Player(); $form = $this->createForm(new PlayerType(), $player); if ($request->isMethod('POST')) { $form->bind($request); if($form->isValid()) { $teamManager = $this->get('app.mmsf.manager.team'); $teamManager->addPlayerToTeam($player, $team); return $this->redirect(/* ... */); } } return array('form' => $form->createView(), 'team' => $team); } Skinny Controller
  27. ¿Y si queremos añadir más acciones cuando se añade un

    jugador a un equipo? • Enviar un sms al usuario • Notificar a los demás miembros del equipo • ...
  28. EVENTOS MmsfBundle !"" ... !"" Event/ # !"" TeamPlayerEvent.php !""

    EventListener/ # $"" SendWelcomeToNewPlayerListener.php !"" Resources/ # $"" config/ # $"" listeners.xml $"" TeamEvents.php
  29. namespace App\MmsfBundle; final class TeamEvents { /** * The TEAMPLAYER_ADDED

    event occurs when a player is added to a team * * The event listener method * receives a App\MmsfBundle\Event\TeamPlayerEvent * instance. * * @var string * */ const TEAMPLAYER_ADDED = 'app.mmsf.player.added'; } Nombre del evento
  30. namespace App\MmsfBundle\Event; use Symfony\Component\EventDispatcher\Event; use App\MmsfBundle\Model\TeamInterface; use App\MmsfBundle\Model\PlayerInterface; class TeamPlayerEvent

    extends Event { protected $team; protected $player; public function __construct(TeamInterface $team, PlayerInterface $player) { $this->team = $team; $this->player = $player; } public function getTeam() { return $this->team; } public function getPlayer() { return $this->player; } } Objeto Event
  31. namespace App\MmsfBundle\EventListener; class SendWelcomeToNewPlayerListener implements EventSubscriberInterface { private $mailer; public

    function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function onTeamPlayerAdded(TeamPlayerEvent $event) { $team = $event->getTeam(); $player = $event->getPlayer(); $this->mailer->sendWelcomeToTeamPlayer($team, $player); } public static function getSubscribedEvents() { return array(TeamEvents::TEAMPLAYER_ADDED => 'onTeamPlayerAdded'); } } Listeners
  32. class TeamManager { protected $dispatcher; public function __construct(ObjectManager $om, $class,

    EventDispatcherInterface $dispatcher) { // ... $this->dispatcher = $dispatcher; } // ... public function addPlayerToTeam(PlayerInterface $player, TeamInterface $team) { $team->addPlayer($player); $this->updateTeam($team); $event = new TeamPlayerEvent($team, $player); $this->dispatcher->dispatch(TeamEvents::TEAMPLAYER_ADDED, $event); } } Lanzar Eventos
  33. FOSUserBundle class RegistrationController extends ContainerAware { public function registerAction(Request $request)

    { //.. $dispatcher->dispatch(FOSUserEvents::REGISTRATION_INITIALIZE, new UserEvent($user, $request)); //.. if ('POST' === $request->getMethod()) { $form->bind($request); if ($form->isValid()) { $dispatcher->dispatch(FOSUserEvents::REGISTRATION_SUCCESS, new FormEvent($form, $request)); //.. $dispatcher->dispatch(FOSUserEvents::REGISTRATION_COMPLETED, new FilterUserResponseEvent($user, $request, $response)); return $response; } } //.. } }
  34. FOSCommentBundle abstract class CommentManager implements CommentManagerInterface { // ... public

    function saveComment(CommentInterface $comment) { if (null === $comment->getThread()) { throw new InvalidArgumentException('The comment must have a thread'); } $event = new CommentPersistEvent($comment); $this->dispatcher->dispatch(Events::COMMENT_PRE_PERSIST, $event); if ($event->isPersistenceAborted()) { return; } $this->doSaveComment($comment); $event = new CommentEvent($comment); $this->dispatcher->dispatch(Events::COMMENT_POST_PERSIST, $event); } }
  35. FORMULARIOS MmsfBundle $"" Form/ !"" DataTransformer/ !"" EventListener/ !"" Extension/

    !"" Model/ $"" Type/ Tip: Usar el campo intention para más seguridad (CSRF) public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => $this->class, 'intention' => 'change_password', )); }
  36. $builder->add('name', 'text', array('label' => 'Nombre')) $builder->add('name' new TextType(), array('label' =>

    'Nombre')) namespace Symfony\Component\Form\Extension\Core\Type; use ... class TextType extends AbstractType { public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'compound' => false, )); } public function getParent() { return 'field'; } public function getName() { return 'text'; } }
  37. Formato de los datos DateType string DateTime array integer choice

    (array) text (array) single_text DateTime
  38. EJEMPLO - TeamType class Team { /** * @ORM\ManyToOne(targetEntity="App\MmmsfBundle\Entity\Player") *

    @ORM\JoinColumn(name="first_player_id", referencedColumnName="id") */ protected $firstPlayer; /** * @ORM\ManyToOne(targetEntity="App\MmmsfBundle\Entity\Player") * @ORM\JoinColumn(name="second_player_id", referencedColumnName="id") */ protected $secondPlayer; // ... }
  39. class TeamType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array

    $options) { $builder ->add('name') ->add('firstPlayerEmail', 'email', array('mapped' => false)) ->add('secondPlayerEmail', 'email', array('mapped' => false)) ; } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'App\MmsfBundle\Entity\Team' )); } public function getName() { return 'team_type'; } }
  40. class PlayerToEmailTransformer implements DataTransformerInterface { private $om; public function __construct(ObjectManager

    $om) { $this->om = $om; } public function transform(Player $player = null) { if (null === $player) { return ""; } return $player->getEmail(); } public function reverseTransform($email) { if (!$email) { return null; } $player = $this->om->getRepository('AppMmsfBundle:Player') ->findOneBy(array('email' => $email)) ; if (null === $player) { throw new TransformationFailedException("..."); } return $player; } }
  41. class PlayerSelectorType extends AbstractType { private $om; public function __construct(ObjectManager

    $om) { $this->om = $om; } public function buildForm(FormBuilderInterface $builder, array $options) { $transformer = new PlayerToEmailTransformer($this->om); $builder->addModelTransformer($transformer); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'invalid_message' => 'El email introducido no existe', )); } public function getParent() { return 'email'; } public function getName() { return 'player_selector'; } }
  42. class TeamType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array

    $options) { $builder ->add('name') ->add('firstPlayer', 'player_selector') ->add('secondPlayer', 'player_selector') ; } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'App\MmsfBundle\Entity\Team' )); } public function getName() { return 'team_type'; } }
  43. Formularios dinámicos class PlayerType extends AbstractType { protected $player; public

    function __construct(Player $player = null) { $this->player = $player; } public function buildForm(FormBuilderInterface $builder, array $options) { // ... if ($this->player) { $builder->add('level'); } } // ... }
  44. Formularios dinámicos class PlayerType extends AbstractType { protected $player; public

    function __construct(Player $player = null) { $this->player = $player; } public function buildForm(FormBuilderInterface $builder, array $options) { // ... if ($this->player) { $builder->add('level'); } } // ... } Sólo dependencias globales! EntityManager SecurityContext ...
  45. Eventos en formularios Model Norm View $form->setData($player) $form->bind($request) PRE_SET_DATA POST_SET_DATA

    PRE_BIND POST_BIND BIND POST_SUBMIT SUBMIT PRE_SUBMIT Symfony 2.3+ $form->submit($request)
  46. class PlayerType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array

    $options) { // ... $builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event){ $data = $event->getData(); $form = $event->getForm(); if ($data && $data->getId()) { $form->add('level'); } }); } } Formularios dinámicos
  47. REFERENCIAS • Documentación Symfony http://symfony.com/doc/ • FOSUserBundle https://github.com/FriendsOfSymfony/FOSUserBundle • FOSCommentBundle

    https://github.com/FriendsOfSymfony/FOSCommentBundle • 3 steps to Symfony2 Form Mastery - Bernhard Schussek https://speakerdeck.com/bschussek/3-steps-to-symfony2-form-mastery