Constructing Dynamic Assets - PHP Project pt. I
Join the DZone community and get the full member experience.
Join For FreeIn the last post
I introduced a feature in my team's current project that will allow our
users (and us!) to dynamically define "assets", and I explained the
syntax for defining an asset.
Now that we have our definition, we need to actually construct an asset
instance. There are several classes involved here: the AssetField,
which stores the display and validation information contained in the
definition for a single field; the Asset which is basically a container
wrapped around a list of fields and field values; and the AssetFactory,
which reads a definition and constructs an Asset by hanging fields on
it.
In the code below, I'm intentionally leaving out most of the gory
details because they're boring and I'm focusing more on the design than
the implementation.
AssetField is essentially a code translation of the JSON definition. Here's some of what it looks like:
class AssetField { const TypeString = 'string'; const TypeInteger = 'int'; // etc. for each type protected $name = null; protected $display = null; protected $type = self::TypeString; // etc. for each attribute that can exist in a field definition public function getName() { return $this->name; } // All setters return $this to provide a fluent interface public function setName($name) { $this->name = $name; return $this; } // ... other getters and setters for each property // listed in the definition attributes ... } // Create a field named "power_sources" whose value is a unique array // of one or more of the values "Gas", "Electric", "Solar" // which defaults to having Gas and Solar turned on // and allows the user to enter their own value if they need to. $powerSourceField = new AssetField(); $powerSourceField->setName("power_sources") ->setUnique(true) ->setCollection(true) ->setOptions(array("Gas", "Electric", "Solar")) ->setDefault(array("Gas", "Solar")); ->setOther(true);
Every Asset object will carry around its AssetField objects, so that it
can provide information about how it is built. This allows our form
construction and validation code to be very generic.
Since Assets don't know what fields will be hung on them, we use PHP's
magic `__get` and `__set` methods to set and return field values.
However, during implementation we realized that the times we want to
know the value of a field are very rare; more often, we want to know the
properties of a field. So we also utilize the magic `__call` to give
our code access to the underlying field object.
Here is what most of the Asset class looks like:
class Asset { protected $type = null; protected $display = null; protected $fields = array(); protected $values = array(); // Return the field object when its name is called as a class method public function __call($fieldName, $args) { return isset($this->fields[$fieldName]) ? $this->fields[$fieldName] : null; } // Get the value of the named field public function __get($fieldName) { return isset($this->fields[$fieldName]) ? $this->values[$fieldName] : null; } // Set the value of the named field public function __set($fieldName, $value) { if (isset($this->fields[$fieldName])) { $this->values[$fieldName] = value; } } // Hang a new field on this asset public function addField(AssetField $field) { $name = $field->getName(); $this->fields[$name] = $field; $this->values[$name] = $field->getDefault(); return $this; } // ... Helper methods for returning the list of all fields // setting the Asset type, display string and instance name format ... } // Get an instantiation of our plumbing system example $plumbing = new Asset(); $plumbing->setType("plumbing") ->setDisplay("Home Plumbing") ->setInstanceNameFormat("Installed %installation_date%"); $waterSource = new AssetField(); $waterSource->setName("water_source") ->setOptions(array("city", "well")) ->setOther("Where does the water come from") ->setDefault("city"); $installationDate = new AssetField(); $installationDate->setName("installation_date") ->setType(AssetField::TypeDate) ->setRequired(true); $waterHeater = new AssetField(); $waterHeater->setName("water_heater") ->setType(AssetField::TypeSub) ->setOptions(array("gas_heater", "electric_heater")); $showers = new AssetField(); $showers->setName("showers") ->setType(AssetField::TypeSub) ->setOptions(array("shower")) ->setCollection(true) ->setMax(5) ->setDefault(array()); $plumbing->addField($waterSource) ->addField($installationDate) ->addField($waterHeater) ->addField($showers); // Use the asset $plumbing->water_source = "well"; $plumbing->installation_date = "06/05/2004"; echo $plumbing->getDisplay() . ": $plumbing"; // "Home Plumbing: Installed 06/05/2004" <--- comes from an overloaded __toString method echo "My water comes from a {$plumbing->water_source}" // "My water comes from a well" // Instantiate a new shower asset $shower = new Asset(); // ... set up the asset ... // add the shower to the plumbing system $plumbing->showers[] = $shower; // What is the default value for the water source? $field = $plumbing->water_source(); $default = $field->getDefault();
Calling a field as a method will return the AssetField object for that property. The field object can then be used in forms and validation. Another benefit of dynamically constructing our Assets in this way is that we can customize any asset on the fly without affecting any other asset of that type. From our plumbing example, let's suppose one user wants to track the serial number of their water heater, but no one else does. We just make sure that any instantiation of a water_heater asset for that user gets an additional "serial_number" field:
// Continuing from above if ($userId == $customAssetUserId) { $serialNumber = new AssetField(); $serialNumber->setName("serial_number"); $plumbing->addField($serialNumber); }
Any form generation and validation code will automatically pick that field up and display it for that user.
The last important class for constructing Assets is the AssetFactory.
All Asset instances are constructed through this factory. It takes a
type definition, which in the plumbing example is a JSON string. The
factory doesn't actually care where the definitions come from or how
they are stored, as long as if receives a properly formatted array.
AssetFactory is given definitions and then uses the definitions to
construct Assets on demand:
class AssetFactory { // List of asset recipes this factory knows how to bake protected $definitions = array(); public function define($definition) { // ... Validate proper formed-ness of the definition ... // ... Set some reasonable defaults for non-specified field attributes ... // All assets get an id field $definition['fields']['id'] = array( 'type' => DW_Asset_Field::TypeString, 'hidden' => true, ); $this->definitions[$definition['type']] = $definition; } // Construct an asset of the given type public function build($type) { $definition = $this->definitions[$type]; $asset = new Asset(); $asset->setType($definition['type']) ->setDisplay($definition['display']) ->setInstanceNameFormat($format); foreach ($definition['fields'] as $name => $fieldDef) { $field = new AssetField(); $field->setName($name) ->setType($fieldDef['type']) ->setDisplay($fieldDef['display']) ->setHidden($fieldDef['hidden']) ->setUnique($fieldDef['unique']) ->setRequired($fieldDef['required']) ->setMin($fieldDef['min']) ->setMax($fieldDef['max']) ->setOptions($fieldDef['options']) ->setOther($fieldDef['other']) ->setDefault($fieldDef['default']); $asset->addField($field); $asset->$name = $fieldDef['default']; } return $asset; } } $factory = new AssetFactory(); $factory->define(json_decode('{ "type" : "plumbing", "display" : "Home Plumbing", "instance_name" : "Installed %installation_date%", "fields" : { "water_source" : { "type" : "string", "options" : ["city", "well"], "other" : "Where does the water come from", "default" : "city" }, "installation_date" : { "type" : "date", "required" : true }, "water_heater" : { "type" : "subasset", "options" : ["gas_heater", "electric_heater"], }, "showers" : { "type" : "subasset", "options" : ["shower"], "collection" : true, "max" : 5 } } }')); $plumbing = $factory->build("plumbing");
If we have any customizations, like our serial_number from above, we can
call a `customize()` method on the factory from within the `build()`
method, or pass the result of the `build()` to some other customization
class. At the moment, we don't have any requirements like that. The
important thing is that we now have an Asset object that we can pass
around to our generic Asset handling code.
Next up, a description of how we generate a form from a generic Asset object.
Published at DZone with permission of Josh Adell, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments