What i want to do
In a form I have a nested OneToMany-Collection ProspektDates
with a DateField in a nested OneToMany-Collection Prospektlaendervarianten
of Entity Prospekt
, that i want to validate with a class validator against properties of Entity Prospekt
dependent of a property in parent Collection Prospektaendervarianten
.
Problem
When I edit an existing property date
of ProspektDate
, everything's fine. The problem is, that when the class validator is called and i have added a new date
in the Form, the property laendervarianten
in the collection ProspektDates
isn't set and i don't have access to the parent and can't validate against their properties.
The adding of a date in ProspektDate
is much like described here:
https://symfony.com/doc/current/form/form_collections.html
Solutions i thought about
I probably could add a hidden entity field to the form of ProspektDates
, but that sounds like a workaround to me, since Doctrine does at one point set the property laendervarianten
, only too late for me and i wonder if there's any better possibility without adding a field to the form.
I also thought about a Class Validator for ProspektLaendervarianten
in which i have access to everything, but that also sounds like a workaround for something i missed. Also then i always have to validate the whole collection i want to be able to validate a single date
of ProspektDates
.
Setup
Entity Prospekt
/**
* Prospekt
*
* @ORM\Table(name="prospekt")
* @ORM\Entity()
*/
class Prospekt
{
/**
* @var \DateTime
*
* @ORM\Column(name="du", type="datetime", nullable=true)
*/
private $du;
/**
* @var \DateTime
*
* @ORM\Column(name="uebersetzung_an_druckerei_date", type="datetime", nullable=true)
*/
private $uebersetzungAnDruckereiDate;
/**
*
* @Assert\Valid
* @ORM\OneToMany(targetEntity="ProspektLaendervarianten", mappedBy="prospekt", orphanRemoval=true, cascade={"all"}, fetch="EAGER")
*/
private $laendervarianten;
}
Entity ProspektLaendervarianten
/**
* ProspektLaendervarianten
*
* @ORM\Table(name="prospekt_laendervarianten")
* @ORM\Entity()
*/
class ProspektLaendervarianten
{
/**
* @ORM\ManyToOne(targetEntity="Prospekt", inversedBy="laendervarianten")
* @ORM\JoinColumn(name="prospekt_id", referencedColumnName="id", nullable=true)
*/
private $prospekt;
/**
* @ORM\ManyToOne(targetEntity="BaseCountries")
* @ORM\JoinColumn(name="base_country_id", referencedColumnName="id", nullable=true)
*/
private $baseCountry;
/**
*
* @Assert\Valid()
* @ORM\OneToMany(targetEntity="ProspektDates", mappedBy="laendervarianten", orphanRemoval=true, cascade={"all"}, fetch="EAGER")
* @ORM\OrderBy({"date" = "ASC"})
*/
protected $prospektDates;
}
Entity ProspektDates
/**
* ProspektDates
*
* @CustomAssert\EtGermany
*
* @ORM\Table(name="prospekt_date)
* @ORM\Entity()
*/
class ProspektDates
{
/**
* @var \DateTime|null
* @Assert\Valid()
* @ORM\Column(name="date", type="datetime", nullable=true)
*/
private $date;
/**
* @var ProspektLaendervarianten
*
* @ORM\ManytoOne(targetEntity="ProspektLaendervarianten", inversedBy="prospektDates")
* @ORM\JoinColumn(name="prospekt_laender_varianten_id", referencedColumnName="id", nullable=false)
*/
private $laendervarianten;
}
Part of the Form
{% for land in form.laendervarianten %}
<div class="row">
<div class="col-md-4">
<div class="row">
<div class="col-md-6 aktiv">
{{ land.vars.data.baseCountry.title }}
</div>
</div>
</div>
<div class="col-md-8 without-padding">
<div class="row">
<div class="col-md-4">
<div class="row prospekt-dates"
data-prototype="{{ form_widget(land.prospektDates.vars.prototype)|e('html_attr') }}"
data-index="{{ land.prospektDates|length }}">
{{ form_widget(land.prospektDates) }}
<div class="form-group">
<a href="#" class="prospekt-date-add">
<span class="fa fa-plus-circle"></span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
Class Validator for Entity ProspektDates
class EtGermanyValidator extends ConstraintValidator
{
public function validate($protocol, Constraint $constraint)
{
/* @var $protocol ProspektDates */
if (!$constraint instanceof EtGermany) {
throw new UnexpectedTypeException($constraint, EtGermany::class);
}
if (null === $protocol->getDate() || '' === $protocol->getDate()) {
return;
}
//when saving new date laendervarianten is null. Here's my problem!
if (empty($protocol->getLaendervarianten())) {
return;
}
// only validate if BaseCountry of parent Collection is Germany
if (BaseCountries::DE !== $protocol->getLaendervarianten()->getBaseCountry()->getId()) {
return;
}
// check if property Date (DateTime) of ProspektDates is later than property Du (DateTime) of Entity Prospekt
$du = $protocol->getLaendervarianten()->getProspekt()->getDu();
$et = $protocol->getDate();
if ($et < $du) {
$this->context->buildViolation($constraint->message)
->atPath('date')
->addViolation();
}
/*
if (!is_string($value)) {
throw new UnexpectedTypeException($value, 'string');
}
*/
}
}
FormType for Collection ProspektDates
class ProspektDateType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('date',
DateType::class,
array(
'attr' => [
'class' => 'form-control input-inline date_custom input-sm date_et',
'html5' => false,
'readonly' => true
],
'required' => false,
'widget' => 'single_text',
'format' => 'ccc, dd.MM.y',
'label' => 'ET',
'model_timezone' => 'Europe/Berlin',
)
);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => ProspektDates::class,
'error_bubbling'=>false
));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'prospektDates';
}
}
FormType for Collection ProspektLaendervarianten
class ProspektLaendervariantenType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('prospektDates', CollectionType::class, [
'label' => 'ETs',
'entry_type' => ProspektDateType::class,
'entry_options' => [
'label' => false,
'attr' => [
'class' => 'date-item',
],
],
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'required' => false,
'by_reference' => false,
'delete_empty' => true,
'prototype_name' => '__prospektDates-collection__',
'attr' => [
'class' => 'prospektDates-collection',
],
'block_name' => 'prospektDates',
]);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'ProspektLaendervarianten::class',
));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'laendervarianten';
}
}