2. References, metadata, and the database

The data and object implementation within the portal generally follows one simple rule: make everything as abstract as possible. The more abstract it is, the easier it should be to extend and because none of the data in the portal is speed critical, we can afford to trade some amount of optimization for flexibility.

Data in the portal is almost always abstracted into two categories: references and metadata. The former containing no more than an ID and a type, the latter containing all of the information associated with that reference. For example, let’s say we need some way to represent a vehicle. The reference would simply contain an automatically assigned ID, and the type vehicle while the metadata would include things such as the make, model, color, year, mileage, and other pertinent bits of data. To better understand this, let’s take a look at the database schemas for each type. Here is the refs table schema:

+-------+------------------+------+-----+---------+----------------+
| Field | Type             | Null | Key | Default | Extra          |
+-------+------------------+------+-----+---------+----------------+
| id    | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| type  | varchar(128)     | NO   | MUL | NULL    |                |
+-------+------------------+------+-----+---------+----------------+

And here is the meta table schema:

+-------+------------------+------+-----+---------+----------------+
| Field | Type             | Null | Key | Default | Extra          |
+-------+------------------+------+-----+---------+----------------+
| id    | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| type  | varchar(128)     | NO   | MUL | NULL    |                |
| slug  | varchar(128)     | NO   | MUL | NULL    |                |
| refid | int(10) unsigned | NO   | MUL | NULL    |                |
| value | longtext         | YES  |     | NULL    |                |
+-------+------------------+------+-----+---------+----------------+

Using our above example, let’s presume we need to store data that represents a 1994 Honda Civic. Once created let’s say we have an id of 42. Here would be the example reference and metadata as stored in the database:

+----+---------+
| id | type    |
+----+---------+
| 42 | vehicle |
+----+---------+

+------+---------+-----------+-------+---------+
| id   | type    | slug      | refid | value   |
+------+---------+-----------+-------+---------+
| 3121 | vehicle | type      |    42 | vehicle |
| 3121 | vehicle | make      |    42 | Honda   |
| 3122 | vehicle | model     |    42 | Civic   |
| 3123 | vehicle | year      |    42 | 1994    |
| 3124 | vehicle | color     |    42 | green   |
| 3125 | vehicle | mileage   |    42 | 127235  |
| 3126 | vehicle | trimLevel |    42 | CX      |
+------+---------+-----------+-------+---------+

So why not create a vehicles table with the relevant columns? The answer is that in this paradigm, anyone or anything can then add more metadata without there having to be any changes to the original code or to the database structure, and all the base level data access and manipulation functions can be contained within a couple of classes. Also note that the indexes in the meta table schema should provide sufficient performance for most use cases.

Of course there’s nothing stopping a developer from creating their own database tables if they require optimization beyond the reference/metadata paradigm. Creating and dropping those tables would be a good use case for the onActivate and onDeactivate parameters specified in the module’s meta.txt file.

There are classes specifically for interacting with the database and for handling references and metadata. Unless you have very particular needs, it’s extremely unlikely that you’ll ever need to interact with the \Papal\Database class directly. Rather most everything written to or read from the database will happen via the \Papal\Reference class, a child of it, or the \Papal\Meta class.

The Reference Class

Almost every “thing” that you’ll represent within the portal is done so via the \Papal\Reference class or a child class which extends it. If you look in the code of most existing modules, you’ll notice that there are many instances of classes extending the Reference class to provide implementation specific functionality or to override the default functions. The Reference class or an extension of it provides a clean and simple way of creating just about any sort of object representation within the portal.

The fundamental anatomy of a Reference object and how it maps to the database is incredibly simple: the reference object represents an item in the refs table and all of the object properties represent the associated items in the meta table. Consider the following example:

$ref = \Papal\Reference::new('vehicle');
$ref->make = 'Honda';
$ref->model = 'Civic';
$ref->year = '1994';
$ref->save();
echo $ref->id;

This creates a new reference of type vehicle. Note that the new() static method writes the reference to the database and returns the object with a couple of default properties: id and type. Next we set a few of the values used in our previous example and call the save() object method to write the object properties to the database as metadata.

It’s extremely unlikely that you’ll ever need to call the Reference class constructor directly. Doing so without the reference ID will simply return an empty object. Creating new Reference objects is almost always done via the aforementioned new() static method and retrieving existing ones from the database is almost always done with the constructFromId() static method as is done in the following example:

$car = \Papal\Reference::new('vehicle');
$car->make = 'Chevrolet';
$car->save();

$chevy = \Papal\Reference::constructFromId($car->id);
var_dump($car);
var_dump($chevy);

The above will result in the following output:

object(Papal\Reference)#8 (3) {
  ["id"]=>
  string(3) "625"
  ["type"]=>
  string(7) "vehicle"
  ["make"]=>
  string(5) "Chevy"
}
object(Papal\Reference)#7 (3) {
  ["id"]=>
  string(3) "625"
  ["type"]=>
  string(7) "vehicle"
  ["make"]=>
  string(5) "Chevy"
}

Note that you now have two objects that represent the same reference. While you’d probably not want to do this in the module you’re planning to write, it serves as an appropriate example of how to create and access Reference objects.

There are additional static methods and object methods for accomplishing various tasks with and within the Reference class. There are methods available to retrieve multiple objects, delete/purge objects, set timestamps, set object properties from JSON data, etc. For a complete reference please look at the code on Github.

Extending the Reference Class and Using Utility Classes

So what if your “thing” needs some additional functionality beyond what is found in the Reference class? Or what if all of your references have certain metadata values that need to be associated with them? There are two ways to accomplish these sorts of things. Consider the following example of a class that extends the Reference class:

namespace Papal;
class HondaCivic extends Reference {

  public static function new($placeholder = null) {
    $ref = parent::new('vehicle');
    $ref->make = 'Honda';
    $ref->model = 'Civic';
    return $ref;
  }

  public function setMileage(int $miles) {
    $this->mileage = $miles;
  }
}

And this example of a separate utility class:

namespace Papal;
class HondaUtility {

  public static function new() {
    $ref = Reference::new('vehicle');
    $ref->make = 'Honda';
    return $ref;
  }

  public static function newCivic() {
    $ref = self::new();
    $ref->model = 'Civic';
    return $ref;
  }

  public static function setMileage(Reference $ref, int $miles) {
    $ref->mileage = $miles;
  }
}

Both classes effectively accomplish the same things and one implementation is not necessarily better than the other. If you prefer a more object-oriented coding style, extending the Reference class might make more sense to you. If you prefer a more procedural approach, a utility class might suit your workflow better.

The Meta Class

While most of your interactions with metadata will be via Reference object properties, sometimes you may find that it makes sense to interact with the metadata directly. The \Papal\Meta class exists for this reason. There are also various static methods for retrieving reference IDs or multiple pieces of metadata. The Meta class isn’t intended to be an abstracted away, “lower level” class in the same vein that \Papal\Database is, rather it’s intended to be used anytime you need to manipulate or filter for metadata but when you don’t necessarily want or need the extra overhead of Reference objects. In fact the Reference class itself makes use of the Meta class for a lot of underlying functionality.

Say for instance we want an array of all references that represent green vehicles. We can leverage the Meta class to assist with this:

$refIds = \Papal\Meta::getRefIdArray('vehicle', 'color', 'green');
foreach ($refIds as $id) {
  $refs[] = \Papal\Reference::constructFromId($id);
}

Or let’s say we need an array of all vehicle colors currently in the database perhaps to display in something like a select input or a report:

$colorData = \Papal\Meta::getRefIdsAndValues('vehicle', 'color');
$colors = array_unique(array_values($colorData));

It should be noted that the Meta class is a final class and that it only contains static methods. Basically there’s no reason to ever try and instantiate a Meta object as metadata should only ever exist as primitives and Reference object properties. For a complete Meta class reference, check out the code on Github.

Next we’ll take a look at the portal’s powerful registration hooks, how they work, and how to use them:

Understanding and using registration hooks.