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

Symfony2 Forms: Do's and Don'ts

Symfony2 Forms: Do's and Don'ts

The Symfony Form component is one of the most complex components of the whole framework and often leaves developers startled. Let me give you a few guidelines that make development of forms much easier than you would have thought.

Bernhard Schussek

April 29, 2016
Tweet

More Decks by Bernhard Schussek

Other Decks in Programming

Transcript

  1. Bernhard Schussek · webmozart.io 2/122 http://slightlyviral.com/23-examples-of-why-women-live-longer-than-men/ Symfony2 Forms Symfony2 Forms

    Dos And Don'ts Dos And Don'ts Bernhard Schussek (@webmozart) Bernhard Schussek (@webmozart) Symfony Live Köln 2015 Symfony Live Köln 2015
  2. Bernhard Schussek · webmozart.io 5/122 Bernhard Schussek Freelancer, Trainer and

    Coach Symfony Architecture Coding Practices webmozart.io
  3. Bernhard Schussek · webmozart.io 6/122 @webmozart

  4. Bernhard Schussek · webmozart.io 8/122 What's the hardest part of

    Symfony?
  5. Bernhard Schussek · webmozart.io 9/122

  6. Bernhard Schussek · webmozart.io 10/122

  7. Bernhard Schussek · webmozart.io 11/122

  8. Bernhard Schussek · webmozart.io 12/122

  9. Bernhard Schussek · webmozart.io 13/122 Make simple cases simple, make

    complex cases possible ✓
  10. Bernhard Schussek · webmozart.io 14/122 New in 2.7/2.8/3.0

  11. Bernhard Schussek · webmozart.io 15/122 No More Type Names (>=

    2.8)
  12. Bernhard Schussek · webmozart.io 16/122 $form = $this->createForm(new PlaceOrderType()); $form

    = $this->createForm('app_place_order');
  13. Bernhard Schussek · webmozart.io 17/122 $form = $this->createForm(PlaceOrderType::class);

  14. Bernhard Schussek · webmozart.io 18/122 $builder ->add('firstName', 'text') ->add('lastName', 'text')

    ->add('address', 'app_address') ->add('shippingMethod', 'entity', [ // ... ]) ;
  15. Bernhard Schussek · webmozart.io 19/122 $builder ->add('firstName', TextType::class) ->add('lastName', TextType::class)

    ->add('address', AddressType::class) ->add('shippingMethod', EntityType::class, [ // ... ]) ;
  16. Bernhard Schussek · webmozart.io 20/122

  17. Bernhard Schussek · webmozart.io 21/122

  18. Bernhard Schussek · webmozart.io 22/122 class PlaceOrderType extends AbstractType {

    public function buildForm(FormBuilderInterface $buil { // ... } public function getName() { return 'app_place_order'; } }
  19. Bernhard Schussek · webmozart.io 23/122 class PlaceOrderType extends AbstractType {

    public function buildForm(FormBuilderInterface $buil { // ... } }
  20. Bernhard Schussek · webmozart.io 24/122 app.form.place_order: class: AppBundle\Form\PlaceOrderType tags: -

    { name: form.type, alias: app_place_order } app.form.submit_payment: class: AppBundle\Form\SubmitPaymentType tags: - { name: form.type, alias: app_submit_payment }
  21. Bernhard Schussek · webmozart.io 25/122 app.form.process_order: class: AppBundle\Form\ProcessOrderType arguments: -

    @app.shipping_service tags: - { name: form.type, alias: app_process_order }
  22. Bernhard Schussek · webmozart.io 26/122 app.form.process_order: class: AppBundle\Form\ProcessOrderType arguments: -

    @app.shipping_service tags: - { name: form.type }
  23. Bernhard Schussek · webmozart.io 27/122 {% block app_place_order_widget %} {#

    ... #} {% endblock %} class PlaceOrderType extends AbstractType { // ... }
  24. Bernhard Schussek · webmozart.io 28/122 {% block place_order_widget %} {#

    ... #} {% endblock %} class PlaceOrderType extends AbstractType { // ... }
  25. Bernhard Schussek · webmozart.io 29/122 {% block app_place_order_widget %} {#

    ... #} {% endblock %} class PlaceOrderType extends AbstractType { // ... public function getBlockPrefix() { return 'app_place_order'; } }
  26. Bernhard Schussek · webmozart.io 30/122 configureOptions() (>= 2.7)

  27. Bernhard Schussek · webmozart.io 31/122 class PlaceOrderType extends AbstractType {

    public function setDefaultOptions( OptionsResolverInterface $resolver) { // ... } }
  28. Bernhard Schussek · webmozart.io 32/122 class PlaceOrderType extends AbstractType {

    public function configureOptions( OptionsResolver $resolver) { // ... } }
  29. Bernhard Schussek · webmozart.io 33/122 $resolver->setRequired('payment_method'); $resolver->setAllowedTypes('is_payable', 'bool'); $resolver->setAllowedValues( 'payment_method',

    PaymentMethod::all() ); // ...
  30. Bernhard Schussek · webmozart.io 34/122 "prototype_data" option (>= 2.8)

  31. Bernhard Schussek · webmozart.io 35/122 $builder->add('lineItems', CollectionType::class, [ 'type' =>

    OrderLineType::class, 'prototype_data' => new OrderLine( 'New item', 120.00 ), ]);
  32. Bernhard Schussek · webmozart.io 36/122 Return null from "query_builder" (>=

    2.8)
  33. Bernhard Schussek · webmozart.io 37/122 $builder->add('shippingMethod', EntityType::class, [ 'class' =>

    ShippingMethod::class, 'query_builder' => function ($repo) use ($address) { if (!$address) { return null; } return $repo->queryByAddress($address); ), ]);
  34. Bernhard Schussek · webmozart.io 38/122 Flipped "choices" option (>= 2.7)

  35. Bernhard Schussek · webmozart.io 39/122 $builder->add('expressShipping', ChoiceType::class, [ 'choices' =>

    [ '' => 'Decide later', 0 => 'No', 1 => 'Yes', ], ]);
  36. Bernhard Schussek · webmozart.io 40/122 $builder->add('expressShipping', ChoiceType::class, [ 'choices' =>

    [ 'Decide later' => null, 'No' => false, 'Yes' => true, ], ]);
  37. Bernhard Schussek · webmozart.io 41/122 $builder->add('expressShipping', ChoiceType::class, [ 'choices' =>

    [ 'Decide later' => Selection::get(LATER), 'No' => Selection::get(NO), 'Yes' => Selection::get(YES), ], ]);
  38. Bernhard Schussek · webmozart.io 42/122 $builder->add('expressShipping', ChoiceType::class, [ 'choices' =>

    [ 'Decide later' => Selection::get(LATER), 'No' => Selection::get(NO), 'Yes' => Selection::get(YES), ], 'choices_as_values' => true, ]); 2.8 only
  39. Bernhard Schussek · webmozart.io 43/122 New "choice_label" option (>= 2.7)

  40. Bernhard Schussek · webmozart.io 44/122 $builder->add('expressShipping', ChoiceType::class, [ // ...

    // $shipping->getTitle() 'choice_label' => 'title', ]); <select> <option value="0">Decide later</option> <option value="1">No</option> <option value="2">Yes</option> </select>
  41. Bernhard Schussek · webmozart.io 45/122 $builder->add('expressShipping', ChoiceType::class, [ // ...

    'choice_label' => function (Selection $selection) { return humanize($selection->getType()); }, ]); <select> <option value="0">Decide later</option> <option value="1">No</option> <option value="2">Yes</option> </select>
  42. Bernhard Schussek · webmozart.io 46/122 New "choice_value" option (>= 2.7)

  43. Bernhard Schussek · webmozart.io 47/122 $builder->add('expressShipping', ChoiceType::class, [ // ...

    ]); <select> <option value="0">Decide later</option> <option value="1">No</option> <option value="2">Yes</option> </select>
  44. Bernhard Schussek · webmozart.io 48/122 $builder->add('expressShipping', ChoiceType::class, [ // ...

    // $selection->getType() 'choice_value' => 'type' ]); <select> <option value="later">Decide later</option> <option value="no">No</option> <option value="yes">Yes</option> </select>
  45. Bernhard Schussek · webmozart.io 49/122 $builder->add('expressShipping', ChoiceType::class, [ // ...

    'choice_value' => function (Selection $selection) { return strtoupper($selection->getType()); }, ]); <select> <option value="LATER">Decide later</option> <option value="NO">No</option> <option value="YES">Yes</option> </select>
  46. Bernhard Schussek · webmozart.io 50/122 New "choice_attr" option (>= 2.7)

  47. Bernhard Schussek · webmozart.io 51/122 $builder->add('expressShipping', ChoiceType::class, [ // ...

    'choice_attr' => ['data-reference' => 1], ]); <select> <option data-reference="1">Decide later</option> <option data-reference="1">No</option> <option data-reference="1">Yes</option> </select>
  48. Bernhard Schussek · webmozart.io 52/122 $builder->add('expressShipping', ChoiceType::class, [ // ...

    'choice_attr' => function (Selection $selection) { return ['data-reference' => $selection->getRef() }, ]); <select> <option data-reference="1">Decide later</option> <option data-reference="2">No</option> <option data-reference="3">Yes</option> </select>
  49. Bernhard Schussek · webmozart.io 53/122 New "group_by" option (>= 2.7)

  50. Bernhard Schussek · webmozart.io 54/122 $builder->add('paymentMethod', ChoiceType::class, [ 'choices' =>

    [ 'Credit Card' => [ 'VISA' => PaymentMethod::VISA, 'MasterCard' => PaymentMethod::MASTER_CARD, ], 'Bank Transfer' => [ // ... ], ], ]); <select> <optgroup label="Credit Card"> ... </optgroup> ... </select>
  51. Bernhard Schussek · webmozart.io 55/122 $builder->add('paymentMethod', ChoiceType::class, [ // ...

    // $paymentMethod->getCategory() 'group_by' => 'category', ]); <select> <optgroup label="Credit Card"> ... </optgroup> ... </select>
  52. Bernhard Schussek · webmozart.io 56/122 $builder->add('paymentMethod', ChoiceType::class, [ // ...

    // $paymentMethod->getCategory()->getName() 'group_by' => 'category.name', ]); <select> <optgroup label="Credit Card"> ... </optgroup> ... </select>
  53. Bernhard Schussek · webmozart.io 57/122 $builder->add('paymentMethod', ChoiceType::class, [ // ...

    'group_by' => function (PaymentMethod $method) { return sprintf( '%s (%s)', $method->getCategory()->getName(), $method->getCategory()->getFees() ); }, ]); <select> <optgroup label="Credit Card (5%)"> ... </optgroup> ... </select>
  54. Bernhard Schussek · webmozart.io 58/122 Forms are not as hard

    as you think (even in 2.7)
  55. Bernhard Schussek · webmozart.io 59/122 PlaceOrderType Controller FormFactory createForm() buildForm()

    FormBuilder Form
  56. Bernhard Schussek · webmozart.io 60/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() FormBuilder Form Order FormView update validate()
  57. Bernhard Schussek · webmozart.io 61/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  58. Bernhard Schussek · webmozart.io 62/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  59. Bernhard Schussek · webmozart.io 63/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  60. Bernhard Schussek · webmozart.io 64/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  61. Bernhard Schussek · webmozart.io 65/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  62. Bernhard Schussek · webmozart.io 66/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  63. Bernhard Schussek · webmozart.io 67/122 My Best Practices

  64. Bernhard Schussek · webmozart.io 68/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  65. Bernhard Schussek · webmozart.io 69/122 Where to Create Forms? In

    the Controller Custom Form Type Custom Form Type
  66. Bernhard Schussek · webmozart.io 70/122 How to Create Form Types?

    doctrine:generate:form by hand by hand
  67. Bernhard Schussek · webmozart.io 71/122 ✓ Use PhpStorm

  68. Bernhard Schussek · webmozart.io 72/122 Where to Submit Forms to?

    Same Action Different Action Same Action
  69. Bernhard Schussek · webmozart.io 73/122 $form = $this->createForm(PaymentType::class, $order, [

    'action' => $this->generateUrl('payment_gw'), ]); ✓ Use "action" option in the controller
  70. Bernhard Schussek · webmozart.io 74/122 $form = $this->createForm(PaymentType::class, $order, [

    'method' => 'PUT', ]); ✓ Use "method" option in the controller
  71. Bernhard Schussek · webmozart.io 75/122 $defaultShipping = $this->getDefaultShipping($customer); $form =

    $this->createForm( new PlaceOrderType($defaultShipping), ); ✗ Don't Pass Dynamic Data to Constructor
  72. Bernhard Schussek · webmozart.io 76/122 $defaultShipping = $this->getDefaultShipping($customer); $form =

    $this->createForm(PlaceOrderType::class, .., [ 'default_shipping' => $defaultShipping, ]); ✓ Use Custom Options
  73. Bernhard Schussek · webmozart.io 77/122 parameters: app.us_shipping.enabled: true services: app.form.shipping:

    class: AppBundle\Form\ShippingType arguments: - %app.us_shipping.enabled% tags: - { type: form.type } ✓ Pass Global Settings To Constructor
  74. Bernhard Schussek · webmozart.io 78/122 class ShippingType extends AbstractType {

    private $usShipping; public function __construct($usShipping) { $this->usShipping = (bool) $usShipping; } // ... } ✓ Pass Global Settings To Constructor
  75. Bernhard Schussek · webmozart.io 79/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  76. Bernhard Schussek · webmozart.io 80/122 {{ form(form) }} ✓ Use

    form(form) for Rapid Prototyping
  77. Bernhard Schussek · webmozart.io 81/122 {{ form_row(form.firstName) }} {{ form_row(form.lastName)

    }} {{ form_row(form.address) }} {{ form_row(form.submit) }} ✓ Use form_row() to Control Field Order
  78. Bernhard Schussek · webmozart.io 82/122 {{ form_row(form.address, { 'label': 'Your

    Address', 'attr': {'class': 'shippable-address'}, 'label_attr': {'data-id': 5} }) }} ✓ Set Labels/Attributes in the View
  79. Bernhard Schussek · webmozart.io 83/122 ✓ Use the Form Debugger

  80. Bernhard Schussek · webmozart.io 84/122 <div class="form-group"> {{ form_errors(form.paymentMethod) }}

    <label for="{{ form.paymentMethod.vars.id }}"> Payment Method {{ form_widget(form.paymentMethod) }} </label> </div> ✓ Write Custom HTML By Hand
  81. Bernhard Schussek · webmozart.io 85/122 {% form_theme form 'my-theme.html.twig' %}

    {{ form(form) }} ✗ (Mostly) Don't Use Themes
  82. Bernhard Schussek · webmozart.io 86/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  83. Bernhard Schussek · webmozart.io 87/122 /** * @Assert\NotNull * @Assert\GreaterThan(0)

    */ private $cost; ✗ Don't Add "Assert" Alias
  84. Bernhard Schussek · webmozart.io 88/122 ✓ Use PhpStorm with the

    Annotation Plugin
  85. Bernhard Schussek · webmozart.io 89/122 /** * @NotNull * @GreaterThan(0)

    */ private $cost; ✓ Auto-Import Annotations
  86. Bernhard Schussek · webmozart.io 90/122 /** * @NotNull * @Expression("value

    < this.getThreshold()") */ private $memory; ✓ Use the @Expression Constraint
  87. Bernhard Schussek · webmozart.io 91/122 /** * @Callback */ public

    function validateTotal( ExecutionContextInterface $context) { if ($this->total !== $this->getProductsTotal()) { $this->context->addViolation('Invalid.'); } } ✓ Use the @Callback Constraint
  88. Bernhard Schussek · webmozart.io 92/122 $context->addViolation('product.total.invalid'); ✓ Use Message Keys

  89. Bernhard Schussek · webmozart.io 93/122 $context->buildViolation('product.total.invalid') ->setParameter('{{ expected }}', $expected)

    ->addViolation(); ✓ Use Message Parameters
  90. Bernhard Schussek · webmozart.io 94/122 /** * @NotNull * @GreaterThan(0,

    groups="Checkout") */ private $total; ✓ Use Validation Groups for Partial Validation
  91. Bernhard Schussek · webmozart.io 95/122 public function configureOptions(OptionsResolver $resolv {

    $resolver->setDefaults([ 'validation_groups' => new GroupSequence([ 'Default', 'VatCheck' ]), ]); } ✓ Use Group Sequences for Sequential Validation
  92. Bernhard Schussek · webmozart.io 96/122 class ProductName extends Regex {

    public function __construct($options = []) { parent::__construct(array_replace([ 'pattern' => '/^\w.*$/' ], $options); } public function validatedBy() { return RegexValidator::class; } } ✓ Create Domain-Specific Constraints
  93. Bernhard Schussek · webmozart.io 97/122 class ProductNameValidator extends ConstraintValidator {

    public function validated($value, Constraint $constr { $this->context->getValidator() ->inContext($this->context) ->validate($value, new Regex(self::PATTERN)) ->validate($value, new NotEqualTo('keyword') ; } } ✓ Create Composite Constraints
  94. Bernhard Schussek · webmozart.io 98/122 class ProductNameValidator extends ConstraintValidator {

    public function validated($value, Constraint $constr { if (null === $value) { return; } // ... } } ✓ Ignore Null Values
  95. Bernhard Schussek · webmozart.io 99/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  96. Bernhard Schussek · webmozart.io 100/122 Clark Kent ... Submit Name

    VAT $order->getCustomerName() $order->setCustomerName() Order +orderNumber +customerName +vat 1 2 3
  97. Bernhard Schussek · webmozart.io 101/122 $builder->add('vat', TextType::class, [ 'property_path' =>

    'customer.vatId' ]); ✓ Decouple Forms from the Model Structure
  98. Bernhard Schussek · webmozart.io 102/122 'customer.name' == $data->getCustomer()->setName() 'items[0].amount' ==

    $data->getItems()[0]->setAmount() ✓ Learn Property Paths
  99. Bernhard Schussek · webmozart.io 103/122 class Customer { public function

    __construct($name, Address $address) { // ... } } What About Non-Empty Constructors?
  100. Bernhard Schussek · webmozart.io 104/122 public function configureOptions(OptionsResolver $resolv {

    $resolver->setDefaults([ 'empty_data' => function (FormInterface $form) { return new Customer( $form->get('name')->getData(), $form->get('address')->getData() ); }, ]); } ✓ Use "empty_data" for Non-Empty Constructors
  101. Bernhard Schussek · webmozart.io 105/122 class Money { public function

    __construct($amount) { $this->amount = $amount; } public function getAmount() { return $this->amount; } } What About Value Objects?
  102. Bernhard Schussek · webmozart.io 106/122 Model Format View Format DateTime

    string DateTimeType
  103. Bernhard Schussek · webmozart.io 107/122 Model Format View Format Money

    string MyMoneyType
  104. Bernhard Schussek · webmozart.io 108/122 class MyMoneyType extends AbstractType implements

    DataTransformerInterface { public function buildForm( FormBuilderInterface $builder, array $options) { $builder->addModelTransformer($this); } public function getParent() { return MoneyType::class; } // ... } ✓ Use Data Transformers (1/3)
  105. Bernhard Schussek · webmozart.io 109/122 class MyMoneyType extends AbstractType implements

    DataTransformerInterface { // ... public function transform($money) { if (null === $money) { return null; } return $money->getAmount(); } // ... } ✓ Use Data Transformers (2/3)
  106. Bernhard Schussek · webmozart.io 110/122 class MyMoneyType extends AbstractType implements

    DataTransformerInterface { // ... public function reverseTransform($amount) { if (null === $amount) { return null; } return new Money($amount); } } ✓ Use Data Transformers (3/3)
  107. Bernhard Schussek · webmozart.io 111/122 class Customer { public function

    relocate(Address $address) { // ... } } What About Non-Standard Methods?
  108. Bernhard Schussek · webmozart.io 112/122 Clark Kent ... Submit Name

    VAT $order->getCustomerName() $order->setCustomerName() Order +orderNumber +customerName +vat 1 2 3
  109. Bernhard Schussek · webmozart.io 113/122 class CustomerType extends AbstractType implements

    DataMapperInterface { public function buildForm( FormBuilderInterface $builder, array $options) { $builder->setDataMapper($this); } // ... } ✓ Use Data Mappers (1/3)
  110. Bernhard Schussek · webmozart.io 114/122 class CustomerType extends AbstractType implements

    DataMapperInterface { // ... public function mapDataToForms($customer, $forms) { $forms = iterator_to_array($forms); $forms['address']->setData($customer->getAddress // ... } // ... } ✓ Use Data Mappers (2/3)
  111. Bernhard Schussek · webmozart.io 115/122 class CustomerType extends AbstractType implements

    DataMapperInterface { // ... public function mapFormsToData($forms, &$customer) { $forms = iterator_to_array($forms); $customer->relocate($forms['address']->getData() // ... } // ... } ✓ Use Data Mappers (3/3)
  112. Bernhard Schussek · webmozart.io 116/122 class Order { public function

    setCustomerNumber(int $orderNumber) { // ... } } What About Strict Models?
  113. Bernhard Schussek · webmozart.io 117/122 public function configureOptions(OptionsResolver $resolv {

    $resolver->setDefaults([ 'data_class' => PlaceOrder::class, ]); } class PlaceOrder { public $orderNumber; public $customerNumber; // ... } ✓ Map to DTOs
  114. Bernhard Schussek · webmozart.io 118/122 class PlaceOrder { /** @OrderNumber

    */ public $orderNumber; } /** @Entity */ class Order { /** @OrderNumber */ private $orderNumber; } ✓ Use Domain-Specific Constraints
  115. Bernhard Schussek · webmozart.io 119/122 public function configureOptions(OptionsResolver $resolv {

    $resolver->setDefaults([ 'data_class' => Order::class, ]); } /** * @Entity */ class Order { ... } ✓ Map to Doctrine Entities in RAD Applications
  116. Bernhard Schussek · webmozart.io 120/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  117. Bernhard Schussek · webmozart.io 121/122 $builder ->add('country', 'entity', [ ...

    ]) ->add('province', 'entity', [ ... ]) ; What About Field Dependencies?
  118. Bernhard Schussek · webmozart.io 122/122 $builder->get('country')->addEventListener( FormEvents::POST_SET_DATA, $addProvinceField ); $builder->get('country')->addEventListener(

    FormEvents::POST_SUBMIT, $addProvinceField ); ✓ Use POST_* Hooks On Fields (1/2)
  119. Bernhard Schussek · webmozart.io 123/122 $addProvinceField = function (FormEvent $event)

    { $form = $event->getForm()->getParent(); $country = $event->getData(); $form->add('province', EntityType::class, [ 'class' => Province::class, 'query_builder' => function ($repo) use ($countr // query provinces by $country }, ]); }; ✓ Use POST_* Hooks On Fields (2/2)
  120. Bernhard Schussek · webmozart.io 124/122 Forms can be simple if

    done right!
  121. Bernhard Schussek · webmozart.io 125/122 Complex things are complex...

  122. Bernhard Schussek · webmozart.io 126/122 Questions? Questions? joind.in/talk/4beec joind.in/talk/4beec Bernhard

    Schussek Bernhard Schussek @webmozart @webmozart webmozart.io