what’s this? data about data?
the classic set up
so, i’m sure you’ve had that problem. no, not that one, the one where you want to store a bunch of things that are similar, but not identical, in the same database table. for instance, you want to store files uploaded to your site in an “uploads” table. since you are a normalization freak like myself, you put the common fields in the “uploads” table, then happily start to make helper/lookup tables that will contain the “weird” fields. so, when you look up a particular upload record, you can easily select (or LEFT JOIN) all the “weird” fields associated with that particular record. works great. but then, you perhaps want to do this for another table. rinse, repeat. since you are exactly like me, you probably thought to yourself “well, time to make a polymorphic table to contain all this mess”. so, you create One Table To Rule Them All, with model/foreign_id fields, and all is well. until you, like me, decide that you want hierarchical data. “no problem” you think, “that’s why God (or the Devil, if you ask most programmers) invented tree structures!”. so you update your OTTRTA (see above for witty acronym) table to actAs Tree. fine, works great. until you get millions of rows and something goes corrupt (it always does, trust me).
well, since it is my mission in life to suffer so that you don’t, i have experienced all of the above and more so that i could present to you:
wtf does it do, i hear you asking? good thing i made a github project page, because it tells me the metadata plugin is “A CakePHP 1.3 plugin that provides arbitrary metadata storage for model records”.
simple enough. so, how to set it up:
- check out the code from github:
$ cd /path/to/your/app/plugins && git clone git://github.com/jmcneese/metadata.git
if you already have your project under git you can do this (it’s call sub-tree’ing), or muddle through setting up and using submodules. pick your poison.
- create the required db table. you can either do this via the SQL files included in metadata/config/schema (there are two, those who prefer UUID ids, and those who prefer INT), or you can use the schema shell to do this:
$ cake schema create Metadata.metadata
- set metadata as a behavior on the models you wish to use it with, ala:
var $actsAs = array('Metadata.Metadata' => array( 'validate' => array( 'fieldName' => array( 'rule' => 'postal', 'message' => 'Must be valid postal code', ), 'some' => array( 'nested' => array( 'array' => array( array( 'rule' => 'numeric', 'message' => 'Must be numeric' ), array( 'rule' => array( 'decimal', 2 ), 'message' => 'Must be decimal' ) ) ) ) ) ));
- Define any validation rules that you need, in a similar fashion to model validation rules (all the built-in Validator rules work). Multiple rules per node are supported, and nodes can be as deep as required.
so, #1 and #2 are pretty cut and dry, but #3 and #4 probably merit more explanation:
you can specify whatever nodes you want validated, so long as they are “leaf” nodes. a leaf node is one that does not have children underneath it. take the following example:
$some_metadata = array( 'settings' => array( 'profile' => array( 'show_gravatar' => true, 'hide_email' => false ), 'anonymous' => false ) );
the nodes ‘settings.anonymous’, ‘settings.profile.hide_email’ and ‘settings.profile.show_gravatar’ would be “leaf” nodes, in that they have no children. ‘settings.profile’ would not be a leaf, and thus cannot have validation rules associated with it (can’t really think of any you would need/want anyhow).
all the rules that are built in to the core Validation class are supported. unlike model validation, you cannot (currently) specify your own custom rules, as this still needs to be built out.
multiple rules per node are supported, so long as you format them as shown above.
the ‘last’ parameter is supported for validation rules, but that’s it (no “on” or any other model validation parameter).
how to shake your meta-thing
first off, you can use the methods getMeta() and setMeta(). for this to work, the model must be referencing a valid model record, ala:
$this->Stuff->id = 1; // or whatever you use for primary key ids $this->Stuff->setMeta('foo','bar');
now, the metadata “foo” with the value of “bar” is set for the record with the id of the record for the model “Stuff”. now, to retrieve the metadata:
$this->Stuff->id = 1; // or whatever you use for primary key ids $meta_foo = $this->Stuff->getMeta('foo'); // $meta_foo now equals 'bar'
much like the Configure class, you can set n-level deep keys:
$this->Stuff->id = 1; // or whatever you use for primary key ids $this->Stuff->setMeta('some.deeply.nested.setting','shoop');
doing this creates a nested structure that holds all these keys, and allows you to query as deep as you wish:
$this->Stuff->id = 1; // or whatever you use for primary key ids $meta = $this->Stuff->getMeta('some.deeply'); // this results in: $meta = array( 'nested' => array( 'setting' => 'shoop' ) );
if you pass no path to getMeta, it will return all metadata associated with the model record in question. alternately, you can pass an entire array to setMeta() and the entire structure will be saved. pre-existing keys will be merged with what you pass.
another way to save metadata (particularly when creating a new record) would be to include it in the data you pass to your primary model when saving. to illustrate:
$this->Stuff->create(); $this->Stuff->save(array( 'Stuff' => array( 'name' => 'blah', 'Metadatum' => array( 'settings' => array( 'show' => false ) ) ) ));
this would cause the new Stuff record to be created, and the metadata saved along with it. after that saved, you’d be able to:
$show_stuff = $this->Stuff->getMeta('settings.show');
don’t look behind the curtain
this all works by utilizing the TreeBehavior, specifically with the scoping configurations that limit the tree calculations to just a small section of the overall table. this means that you may have hundreds of thousands of (or millions even) trees in your metadatum table with no noticeable degradation in performance (other than the natural one that happens when your tables get large). it’s conceivable that any of these mini-trees could become corrupt, and for that there are some custom methods for dealing with this, verifyMeta() and recoverMeta(). these work pretty much exactly like the TreeBehavior methods verify() and recover(), except that they set the scope of the tree first.
also, if you are feeling particularly brave, you can use the Metadatum model directly via setKey()/getKey() (both of these work just like get/setMeta(), except they don’t scope the internal tree). this can be useful for application-wide settings/metadata, but i would really recommend using Configure for that.
now, go fork it and fork it well!
this works for my purposes as it stands, but i imagine that there may be those out there who would like it to exactly mimic model validaton (what with the “on” parameter, or custom validation). as with other projects, i accept bug reports, suggestions, donations, patches and/or feature requests.
one of the biggest things that still needs to be implemented is the auto-inclusion of metadata when finding primary model records. as it stands now, you have to retrieve any and all metadata via the getMeta() method, they are never automatically included, as when a model hasAndBelongsToMany some other model. this is because the Metadatum model is not bound/associated in any way to the primary model. i’ll look into finding some way to configure this sort of behavior.