Defining Dynamic Assets - PHP Project pt. II
I am currently working on a feature of a project that allows users to
track assets that exist at a location. The interesting part of the
feature is that an asset can be almost anything, and that the properties
of a single instance of an asset may have little or no overlap with the
properties of a different asset.
Properties in this context refer not just to the attributes of an asset,
but also to how those attributes are displayed, entered by the user
when adding/editing an asset, and validated when the user is saving an
asset. Assets can have sub-assets, forming a tree. And the most
interesting wrinkle: users should be able to define their own assets,
with properties' data-types, display and validation all user-controlled.
I'm not going to talk about how the assets are stored (until a few days ago it was a toss-up between schema-less MySQL or MongoDB.)
Instead, what follows is series of posts describing our solution for
defining an asset, displaying the add and edit forms and validating an
asset. This first post covers how asset types are defined.
An asset is basically a bag of properties, and each property has
attributes that define what values it can hold and hints about how it
should be presented to the user when displaying or editing.
For example: the plumbing system in your home may have properties like
"installation date" or "water source" (where water source may be either
"city" or "well".) The system may also have sub-assets, like "water
heater", which has properties like "type" (gas, electric, solar) and
"last maintenance date". There might also be a list of "showers", which
are also sub-assets, and have properties like "location", "size" and
"needs to be cleaned".
Our first pass was to define a class for each asset type, which would
include the type's properties and how those properties should be
displayed. The problem with this solution was two-fold: first, we don't
currently know what properties each of our assets will or won't need to
have (so an asset type's schema will be in flux); and second, we don't
want our users to have to write code in order to define their own asset
types. In fact, we don't know all the assets we will need to track for
our own purposes yet.
Our solution was to store asset definitions as data instead of code.
Then, all we need is a mechanism that reads a definition and builds a
dynamic "class-less" asset object. The definition syntax is JSON,
though the code that actually constructs concrete Asset instances
doesn't care how the definitions are stored.
Here is some of the definition for the above plumbing system (I've
intentionally added some constraints to demonstrate other bits of the
definition syntax):
{
"plumbing" : {
"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
}
}
},
"another_type" : {
...
},
...
}All assets must have a "type", which must be unique among all asset
definitions. "display" is an optional field; if not specified, the
display is the "type" string upper camel-cased and with underscores
replaced with spaces (e. g. "plumbing_system" becomes "Plumbing
System"). The display is used to identify an asset instance's type to
the user, mainly on entry/edit forms and reporting.
The "instance_name" is the formatting string used to present a specific
asset to the user. Wrapping a field name in %'s will substitute the
actual value of that field when displaying the asset to the user. In
the above example, if a plumbing asset has an installation date of
"6/12/2009", then the asset would be displayed to the user as "Installed
6/12/2009". If no instance_name is given, the instance name defaults
to the asset's id.
The rest of the asset is a list of fields, indexed by the internal field
name, and each having some combination of descriptive attributes. The
"type" attribute is the only required attribute for a field, and must
have one of the values "string", "int", "boolean", "datetime", "date",
"float" or "subasset". The difference between date and datetime is
purely in the way that they are presented to the user (and how
granularly the value is stored: dates are always rounded to midnight.)
"required" is a boolean flag indicating whether the asset can be saved
without a value in that field or not. "hidden" is another boolean flag,
specifying if the field should be presented to the user. We have to be
careful that if a field is required and hidden the application must
provide a value. A good way to do that is with the "default" attribute,
which specifies the default value for that field. If the field is not
hidden, the default value will be pre-entered or selected for the user
on an "Add Asset" form.
The "options" attribute lists the valid values for a field. The
exception is for "subasset" fields, in which case options is a list of
valid subasset types (in our plumbing example, gas heaters and electric
heaters could be their own asset types with their own set of fields, and
an instance of either would be a valid value for the "water_heater"
field.) If no type options are listed for a subasset field, any asset
type is assumed to be valid.
If a user should be allowed to enter their own value in addition to the,
then the "other" attribute can be used. If "other" is a boolean true,
then an "Other" option will be added to the list of options, and the
user will be able to enter whatever value they like. "other" can also
be a string, in which case it is the text to display as the "Other"
option, and the label on whatever form field is used to capture the
user's input.
Some fields hold multiple values. In these cases, the "collection"
attribute is set to boolean true. By default, the same value can appear
in the collection multiple times. If that is not desired, the "unique"
attribute can be used to validate that each value appears only once.
Using combinations of "collection", "unique" and "options" can drive
some pretty complex form behaviors (which will be shown in a future
post.)
There are also "max" and "min" attributes. On a collection, the value
of the attributes are integers specifying how many values are allowed to
be in that collection. Otherwise, they represent upper and lower
bounds on integer, float, date and datetime values.
The definition syntax so far gives us all the flexibility we need to
define our project's known asset types, and we believe it will be useful
in the future for easily adding new types and allowing our users to
define their own assets types (probably through a form that translates
into the JSON definition.)
(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)





