I've a set of data (~30 properties, all having their own array of values) that I want to pass around to various classes in PHP and I want to also enforce the data's array structure. Multiple classes will be expecting this structure to be consistent.
Because of these facts I can't really rely on a standard array and so I decided to pass an object around. I looked into ArrayObject and while it allows me to set/get as if the class were an array I didn't see anything about enforcing the structure.
Is there an existing standard class that can handle enforcement of it's array-like structure while still being treated as an array, e.g., basically ArrayObject + enforcement?
An example of the array structure:
$item_type_props = array(
'phone' => array('speed' => 1000, 'self_label' => false, 'support_poe' => true, 'bluetooth' => false),
'pbx' => array('acd_support' => true, 'max_conn' => 300, max_phones => 600),
'adapter' => array('fxo' => 4, 'fxs' => 0, 't1_e1_pri' => 0),
etc...
);
I know each property in the array could be it's own class and enforce it's own fields through the constructor and set/get but then I suddenly have ~30 classes that are nothing but a bunch of attributes and that seems somewhat excessive for just holding data.
Alternatively, if I'm just approaching this from the wrong mindset and missing something really, really obvious then please do point it out. I get the feeling that I am but my brain might be having a vacation.
While you could roll your own, I encourage you to use an existing validation implementation. For example, Symfony\Validator
allows you to define nested structures and the requirements on each level:
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;
$validator = Validation::createValidator();
$constraint = new Assert\Collection(array(
// the keys correspond to the keys in the input array
'name' => new Assert\Collection(array(
'first_name' => new Assert\Length(array('min' => 101)),
'last_name' => new Assert\Length(array('min' => 1)),
)),
'email' => new Assert\Email(),
'simple' => new Assert\Length(array('min' => 102)),
'gender' => new Assert\Choice(array(3, 4)),
'file' => new Assert\File(),
'password' => new Assert\Length(array('min' => 60)),
));
$violations = $validator->validate($input, $constraint);
This lets you push the details of how to validate down to another (already tested) level, while letting your code focus on why it needs this data. For the Symfony case, you can use an array
as the storage mechanism, and use a design that firewalls unvalidated from validated data.
One way we might do this is notationally. Pretend we have implemented a method, perhaps using Symfony's validator, to return a validated array. We can use Hungarian notation to indicate our structure has passed through validation and is "safe":
<?php
$vInput = validate($_GET); // Hungarian notation: any variable beginning with "v" is "validated" and safe to use
function foobar(array $vInput) { ... }
While this is performant, it's not great for maintenance in the long term. So, you might consider an object wrapper that allows you to leverage the type system:
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;
class ValidatedArray extends \ArrayObject {
public function construct($input = [], $flags = 0, $iterator_class = 'ArrayIterator') {
$violations = Validation::createValidator()->validate($array, $this->constraints());
// throw exception if any violations
parent::__construct($input, $flags, $iterator_class);
}
public function __offsetSet($index, $value) {
$constraints = $this->constraints()[$index]; // specific constraints to this index
$violations = Validation::createValidator()->validate($array, $constraints);
// throw exception on violations
parent::__offsetSet($index, $value);
}
public function constraints() {
return new Assert\Collection(...);
}
}
$input = new ValidatedArray($_REQUEST); // first time validation
$input['foo'] = 'bar'; // also subject to validation
You might want to make this implementation an abstract
base class, with concrete descendants implementing the constraints
method to provide the specific limitations on the array object itself. This provides a flexible way to get dedicated Data Transfer Objects.