Extending on 2 previous questions about form structure and validating collections I've run into the next issue.
My form validates properly. Including included collections by way of Fieldsets. But the innermost Fieldset should not result in an Entity and a FK association to the parent if its values are not set.
An Address may or may not have linked Coordinates. It's possible to create all of these in the same Form.
However, the Coordinates should not be created and should not be linked from the Address if no coordinates have been given in the Form. They're not required in the Form and the Entity Coordinates itself requires both properties of Latitude and Longitude to be set.
Below, first the Entities. Following are the Fieldsets used for the AddressForm. I've removed stuff from both unrelated to the chain of Address -> Coordinates.
Address.php
class Address extends AbstractEntity
{
// Properties
/**
* @var Coordinates
* @ORM\OneToOne(targetEntity="Country\Entity\Coordinates", cascade={"persist"}, fetch="EAGER", orphanRemoval=true)
* @ORM\JoinColumn(name="coordinates_id", referencedColumnName="id", nullable=true)
*/
protected $coordinates;
// Getters/Setters
}
Coordinates.php
class Coordinates extends AbstractEntity
{
/**
* @var string
* @ORM\Column(name="latitude", type="string", nullable=false)
*/
protected $latitude;
/**
* @var string
* @ORM\Column(name="longitude", type="string", nullable=false)
*/
protected $longitude;
// Getters/Setters
}
As is seen in the Entities above. An Address has a OneToOne uni-directional relationship to Coordinates. The Coordinates entity requires both latitude and longitude properties, as seen with the nullable=false.
It's there that it goes wrong. If an Address is created, but no Coordinates's properties are set in the form, it still creates a Coordinates Entity, but leaves the latitude and longitude properties empty, even though they're required.
So, in short:
- A
CoordinatesEntity is created where non should exist - A link to
Coordinatesis created fromAddresswhere non should exist
Below the Fieldsets and InputFilters to clarify things further.
AddressFieldset.php
class AddressFieldset extends AbstractFieldset
{
public function init()
{
parent::init();
// Other properties
$this->add([
'type' => CoordinatesFieldset::class,
'required' => false,
'name' => 'coordinates',
'options' => [
'use_as_base_fieldset' => false,
],
]);
}
}
CoordinatesFieldset.php
class CoordinatesFieldset extends AbstractFieldset
{
public function init()
{
parent::init();
$this->add([
'name' => 'latitude',
'required' => true,
'type' => Text::class,
'options' => [
'label' => _('Latitude'),
],
]);
$this->add([
'name' => 'longitude',
'required' => true,
'type' => Text::class,
'options' => [
'label' => _('Longitude'),
],
]);
}
}
AddressFieldsetInputFilter.php
class AddressFieldsetInputFilter extends AbstractFieldsetInputFilter
{
/** @var CoordinatesFieldsetInputFilter $coordinatesFieldsetInputFilter */
protected $coordinatesFieldsetInputFilter;
public function __construct(
CoordinatesFieldsetInputFilter $filter,
EntityManager $objectManager,
Translator $translator
) {
$this->coordinatesFieldsetInputFilter = $filter;
parent::__construct([
'object_manager' => $objectManager,
'object_repository' => $objectManager->getRepository(Address::class),
'translator' => $translator,
]);
}
/**
* Sets AddressFieldset Element validation
*/
public function init()
{
parent::init();
$this->add($this->coordinatesFieldsetInputFilter, 'coordinates');
// Other filters/validators
}
}
CoordinatesFieldsetInputFilter.php
class CoordinatesFieldsetInputFilter extends AbstractFieldsetInputFilter
{
public function init()
{
parent::init();
$this->add([
'name' => 'latitude',
'required' => true,
'allow_empty' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 2,
'max' => 255,
],
],
[
'name' => Callback::class,
'options' => [
'callback' => function($value, $context) {
//If longitude has a value, mark required
if(empty($context['longitude']) && strlen($value) > 0) {
$validatorChain = $this->getInputs()['longitude']->getValidatorChain();
$validatorChain->attach(new NotEmpty(['type' => NotEmpty::NULL]));
$this->getInputs()['longitude']->setValidatorChain($validatorChain);
return false;
}
return true;
},
'messages' => [
'callbackValue' => _('Longitude is required when setting Latitude. Give both or neither.'),
],
],
],
],
]);
// Another, pretty much identical function for longitude (reverse some params and you're there...)
}
}
EDIT: Adding a DB Dump image. Shows empty latitude, longitude.
EDIT2: When I remove 'allow_empty' => true, from the AddressFieldsetInputFilter inputs and fill a single input (latitude or longitude), then it validates correctly, unless you leave both inputs empty, then it breaks off immediately to return that the input is required. (Value is required and can't be empty).

By chance did I stumple upon this answer, which was for allowing a Fieldset to be empty but validate it if at least a single input was filled in.
By extending my own
AbstractFormInputFilterandAbstractFieldsetInputFilterclasses from anAbstractInputFilterclass, which incorporates the answer, I'm now able to supply FielsetInputFilters, such as theAddressFieldsetInputFilter, with an additional->setRequired(false). Which is then validated in theAbstractInputFilter, if it actually is empty.The linked answer gives this code:
As I mentioned I used this code to extend my own
AbstractInputFilter, allowing small changes in*FieldsetInputFilterFactoryclasses.AddressFieldsetInputFilterFactory.phpMight not be a good idea for everybody's projects, but it solves my problem of not always wanting to validate a Fieldset and it definitely solves the original issue of not creating an Entity with just an ID, as shown in the screenshot in the question.