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.

24d20907afea0f684e62d620b886af16?s=128

Bernhard Schussek

April 29, 2016
Tweet

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