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:
Coordinates
Entity is created where non should existCoordinates
is created from Address
where non should existBelow 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 AbstractFormInputFilter
and AbstractFieldsetInputFilter
classes from an AbstractInputFilter
class, which incorporates the answer, I'm now able to supply FielsetInputFilters, such as the AddressFieldsetInputFilter
, with an additional ->setRequired(false)
. Which is then validated in the AbstractInputFilter
, if it actually is empty.
The linked answer gives this code:
<?php
namespace Application\InputFilter;
use Zend\InputFilter as ZFI;
class InputFilter extends ZFI\InputFilter
{
private $required = true;
/**
* @return boolean
*/
public function isRequired()
{
return $this->required;
}
/**
* @param boolean $required
*
* @return $this
*/
public function setRequired($required)
{
$this->required = (bool) $required;
return $this;
}
/**
* @return bool
*/
public function isValid()
{
if (!$this->isRequired() && empty(array_filter($this->getRawValues()))) {
return true;
}
return parent::isValid();
}
}
As I mentioned I used this code to extend my own AbstractInputFilter
, allowing small changes in *FieldsetInputFilterFactory
classes.
AddressFieldsetInputFilterFactory.php
class AddressFieldsetInputFilterFactory extends AbstractFieldsetInputFilterFactory
{
/**
* @param ServiceLocatorInterface|ControllerManager $serviceLocator
* @return InputFilter
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
parent::setupRequirements($serviceLocator, Address::class);
/** @var CoordinatesFieldsetInputFilter $coordinatesFieldsetInputFilter */
$coordinatesFieldsetInputFilter = $this->getServiceManager()->get('InputFilterManager')
->get(CoordinatesFieldsetInputFilter::class);
$coordinatesFieldsetInputFilter->setRequired(false); // <-- Added option
return new AddressFieldsetInputFilter(
$coordinatesFieldsetInputFilter,
$this->getEntityManager(),
$this->getTranslator()
);
}
}
Might 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.