Still unable with Symfony to validate collection form

I’m using SYMFONY 5 and have setup a collection form where the user is enabled to create a service and add numerous sub-services to the service. This is working fine and the user can add/edit/show/delete services and also sub-services.

Screen-Shot with one service and two assigned sub-services where the second one is not valid because the language is not equal to the one of the service

Now I want to validate if a new sub-service is added and the form is submitted, the language of the sub-service item must be the one of the service item. If not, the database will not be updatet and the user will get an error message (see screen-shot). This is also working fine with one exception:
I cannot achieve to stick the error-message to the failed sub-service! It appears on every sub-service.

Here the definition of my entity service:

namespace AppEntity;

    use SymfonyComponentValidatorConstraints as Assert;
    use DoctrineCommonCollectionsArrayCollection;
    use DoctrineCommonCollectionsCollection;
    use DoctrineORMMapping as ORM;

    /**
     * @ORMEntity(repositoryClass="AppRepositoryServicesRepository")
     */
    class Services
    {
/**
 * @ORMId()
 * @ORMColumn(type="integer")
 */
private $id;

/**
 * @ORMColumn(type="string", length=3)
 */
private $sprache;
/**
 * @ORMColumn(type="integer", nullable=true)
 */
private $transid;

/**
 * @ORMColumn(type="string", length=200)
 */
private $header;

/**
 * @ORMColumn(type="text", nullable=true)
 */
private $body;

/**
 * @ORMOneToMany(targetEntity="AppEntitySubServices", mappedBy="services",cascade={"persist"})
 * @AssertValid()
 */
private $subServices;

public function __construct()
{
    $this->subServices = new ArrayCollection();
}

public function getId(): ?int
{
    return $this->id;
}
public function setId(int $id): self
{
    $this->id = $id;

    return $this;
}
public function getTransId(): ?int
{
    return $this->transid;
}
public function setTransId(int $transid): self
{
    $this->transid = $transid;

    return $this;
}
public function getSprache(): ?string
{
    return $this->sprache;
}

public function setSprache(string $sprache): self
{
    $this->sprache = $sprache;

    return $this;
}

public function getHeader(): ?string
{
    return $this->header;
}

public function setHeader(string $header): self
{
    $this->header = $header;

    return $this;
}

public function getBody(): ?string
{
    return $this->body;
}

public function setBody(?string $body): self
{
    $this->body = $body;

    return $this;
}

/**
 * @return Collection|SubServices[]
 */
public function getSubServices(): Collection
{
    return $this->subServices;
}

public function addSubService(SubServices $subService): self
{
    if (!$this->subServices->contains($subService)) {
        $this->subServices[] = $subService;
        $subService->setServices($this);
    }

    return $this;
}

public function removeSubService(SubServices $subService): self
{
    if ($this->subServices->contains($subService)) {
        $this->subServices->removeElement($subService);
        // set the owning side to null (unless already changed)
        if ($subService->getServices() === $this) {
            $subService->setServices(null);
        }
    }

    return $this;
}
    }

As you can see, in the service entity I have put @AssertValid() for the subservices.

Here the definition of the sub-service entity:

<?php

    namespace AppEntity;

    use AppValidatorSubServiceSprache;
     use DoctrineORMMapping as ORM;

    /**
     * @ORMEntity(repositoryClass="AppRepositorySubServicesRepository")
     * @SubServiceSprache()
     */
     class SubServices
      {
        /**
         * @ORMId()
         * @ORMColumn(type="integer")
          */
         private $id;

/**
 * @ORMColumn(type="integer", nullable=true)
 */
private $transsubid;

/**
 * @ORMColumn(type="string", length=3)
 */
private $sprache;

/**
 * @ORMManyToOne(targetEntity="AppEntityServices", inversedBy="subServices")
 */
private $services;


/**
 * @ORMColumn(type="string", length=200)
 */
private $header;

/**
 * @ORMColumn(type="text", nullable=true)
 */
private $body;

public function getId(): ?int
{
    return $this->id;
}

public function setId(int $id)
{
    $this->id = $id;
}
public function getTransSubId(): ?int
{
    return $this->transsubid;
}
public function setTransSubId(int $transsubid): self
{
    $this->transsubid = $transsubid;

    return $this;
}
public function getsprache(): ?string
{
    return $this->sprache;
}


public function setsprache(string $sprache): self
{
    $this->sprache = $sprache;
    return $this;
}
public function getServices(): ?Services
{
    return $this->services;
}

public function setServices(?Services $services): self
{
    $this->services = $services;

    return $this;
}

public function getHeader(): ?string
{
    return $this->header;
}

public function setHeader(string $header): self
{
    $this->header = $header;

    return $this;
}

public function getBody(): ?string
{
    return $this->body;
}

public function setBody(?string $body): self
{
    $this->body = $body;

    return $this;
}
    }

As you can see, for the whole class subservices I have put the validation @SubServiceSprache().

Here is the definition of the validator SubServiceSprache:

    <?php

     namespace AppValidator;


     use SymfonyComponentValidatorConstraint;

     /**
      * @Annotation
      */
    class SubServiceSprache extends Constraint
     {
        public function validatedBy()
         {
             return get_class($this).'Validator';
         }


public function getTargets()
{
    //PROPERTY_CONSTRAINT wenn zB. EMAIL geprüft werden soll
    //CLASS_CONSTRAINT wenn ganze Entity geprüft werden soll
    // jeweils das Objekt (EMAIL od. ganzes Klassenobjekt wird übergeben
    return self::CLASS_CONSTRAINT;
}
    }

And here the validation logic in SubServiceSpracheValidator:

<?php


    namespace AppValidator;

    use AppEntityServices;
    use AppEntitySubServices;
    use SymfonyComponentValidatorConstraintValidator;
    use DoctrineORMEntityManagerInterface;
    use SymfonyComponentValidatorConstraint;
    use SymfonyContractsTranslationTranslatorInterface;



    class SubServiceSpracheValidator extends ConstraintValidator
    {

       private $em;
       private $subservice;
       private $translator;



       public function __construct(EntityManagerInterface $em, TranslatorInterface $translator)
       {
          $this->em = $em;
          $this->translator = $translator;
          $this->subservice = new SubServices();
       }

      public function validate($object, Constraint $constraint)
      {
          // Ist die Sprache des SubService die des Service?

          if ($object instanceof SubServices) {


             if($object->getServices()->getSprache() != $object->getsprache()){
                // Message Translation
                  $message = $this->translator->trans('subservice_sprachcheck',
                                                        ['subsprache' =>  object->getsprache(),'servsprache' => $object->getServices()->getsprache()]
           );
           // Assign message
           $this->context->buildViolation($message)
               ->atPath('sprache')
               ->addViolation();
       }
   }
       }
    }

Here is a snippet of the form class for service:

          ->add('subservices', CollectionType::class,
            array('entry_type' => SubservicesFormType::class,
                  'label' => false,
                  'entry_options' => array('label' => false),
                  'allow_add' => true,
                  'allow_delete' => true,
                  'by_reference' => false,
                  'error_bubbling' => false,
            ))
        ->add('save', SubmitType::class,
            array('label' => 'Sichern',
                'attr' => array('class' => 'buttonsave')
            ))
    ;
}
public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'data_class' => Services::class,
        'error_bubbling' => false,
        //'newid' => false,
    ]);
 }

and here the one for the subservices:

class SubservicesFormType extends AbstractType
    {

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder
        ->add('sprache', LanguageType::class,
            array('label' => 'Sprache',
                'disabled' => false,
                'attr' => array('class' => 'form-control'),
                'choice_loader' => NULL,
                'choices' => ['DEUTSCH' => 'de', 'ENGLISCH' => 'en'],
                'choice_translation_domain' => true,
            ))
        ->add('header', TextType::class,
            array('label' => 'Überschrift',
                  'attr' => array('class' => 'form-control')))
        ->add('body', TextareaType::class,
            array('label' => 'Beschreibung',
                  'attr' => array('class' => 'form-control')))
    ;
   }
   public function configureOptions(OptionsResolver $resolver)
   {
    $resolver->setDefaults([
        'data_class' => SubServices::class,
        'validation_groups' => ['Default'],
    ]);

}

}

and at last my twig-file:

{% extends 'base.html.twig' %}
{% import _self as formMacros %}
{% block title %}UB Mollekopf{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="{{ absolute_url('/css/ub_styles.css') }}"  type="text/css" media="all">
<link rel="stylesheet" href="{{ absolute_url('css/font-awesome.css') }}">
{% endblock %}
{% macro printSubserviceRow(SubservicesFormType) %}
   <td class="subserviceformsprache">{{ form_widget(SubservicesFormType.sprache) }}</td>
   <td class="subserviceformheader">{{ form_widget(SubservicesFormType.header) }}</td>
   <td class="subserviceformbody">{{ form_widget(SubservicesFormType.body) }}</td>
   <td class="subserviceformaction"></td>
{% endmacro %}
{% block body %}
  <div class="tableserviceedit">
   {{ form_start(form) }}
    <div class="tableheadereditservice">
        <table id="editserviceheader">
            <tr style="white-space: nowrap">
                <th style="width: 100%; padding-left: 0.5em">{% trans %}Ändern Service{% endtrans %}  {{ form_widget(form.id) }}</th>
            </tr>
        </table>
    </div>
    <div class="tablebodyeditservice">
        <table id="editservicesingleheader">
            <tr>
                <th style="width: 3.5em;">{% trans %}Sprache{% endtrans %}</th>
                <th style="width: 12em">{% trans %}Überschrift{% endtrans %}</th>
                <th style="width: 15em">{% trans %}Beschreibung{% endtrans %}</th>
            </tr>

            <tr class="editserviceheader">
                <td class="serviceformsprache">
                    {{ form_errors(form.sprache) }}
                    {{ form_widget(form.sprache) }}
                </td>
                <td class="serviceformheader">
                    {{ form_errors(form.header) }}
                    {{ form_widget(form.header) }}
                </td>
                <td class="serviceformbody">
                    {{ form_errors(form.body) }}
                    {{ form_widget(form.body) }}
                </td>
            </tr>

        </table>
        <div class="tablebodysubservices">
        <table id="subservices">
            <thead>
                <tr>
                    <th style="width: 6em;">{% trans %}Sprache{% endtrans %}</th>
                    <th style="width: 22.2em">{% trans %}Überschrift{% endtrans %}</th>
                    <th style="width: 15em">{% trans %}Beschreibung{% endtrans %}</th>
                </tr>
            </thead>

              <tbody id="collector" data-prototype="{{ formMacros.printSubserviceRow(form.subservices.vars.prototype)|e('html_attr') }}">
                {% for subservice in form.subservices %}
                <tr>
                  <td colspan="4">
                    <span style="color:red" > {{ form_errors(form) }}</span>
                  </td>
                </tr>
                <tr>
                  <td class="subserviceformsprache">
                     {{ form_widget(subservice.sprache) }}
                  </td>
                  <td class="subserviceformheader">
                    {{ form_widget(subservice.header) }}
                  </td>
                  <td class="subserviceformbody">
                    {{ form_widget(subservice.body) }}
                  </td>
                  <td class="subserviceformaction"></td>
                </tr>
                {% endfor %}

              </tbody>
            </table>
            </div>
        <div class="tablefooter" id="fussbereichnewservice">
            <div class="btnfooter">{{ form_widget(form.save) }} <button type="" class="buttonabort"><a href="{{path('services_maintain', { _locale: locale }) }}" style="color: white">{% trans %}Abbruch{% endtrans %}</a></button></div>
        </div>
         {{ form_end(form) }}
    {#</div>#}
{{ include('inc/navbar_bottom.html.twig') }}
{% endblock %}

{% block javascripts %}
    <script src="{{ absolute_url('/js/main.js') }}"></script>
{%  if locale == 'en' %}
   <script src="{{ absolute_url('/js/subservicesen.js') }}"></script>
{%  else %}
   <script src="{{ absolute_url('/js/subservices.js') }}"></script>
 {% endif %}
{% endblock %}

In the twig-template file I have tried serveral posibilities:
if I code {{ form_errors(form) }} the error-message will appear on every sub-service, if I code {{ form_errors(form.sprache) }} no error message will appear at all.

Does anybody have an idea to solve this?

Source: Symfony Questions

Was this helpful?

0 / 0

Leave a Reply 0

Your email address will not be published. Required fields are marked *