Views 2 is proceeding apace, mostly on the weekends. With the baby I'm just not having the time throughout the week to actually work on it, which is unfortunate. But I thought I'd share with the world at large some of the things that are going on under the hood. This might be especially interesting to people who have utilized the Views API in the past; I decided to go for Drastic Changes.
To start with, instead of providing function names to handlers that worked somewhat haphazardly, in part due to inconsistent arguments, I decided to go with handler objects using the delegation pattern. One effect is that pretty much the entirety of the actual code for building the view itself has bee moved into the handlers, something that makes custom handlers more powerful as there is a lot more they can override.
Here's the current build function (yes, there's a lot of TODOs in it still):
<?php
function build($display_id = NULL) {
if (!empty($this->built)) {
return;
}
$this->display_id = $display_id;
// Attempt to load from cache.
// TODO: Load a build_info from cache.
// Call a module hook and see if it wants to present us with a
// pre-built query or instruct us not to build the query for
// some reason.
// TODO: Implement this.
// If that fails, let's build!
$this->build_info = array();
$this->query = new views_query();
$this->_seed_handlers();
$this->_build('filter');
$this->build_sort = $this->build_fields = TRUE;
// build arguments.
foreach ($this->argument as $id => $argument) {
if (isset($this->args[$id])) {
// handle argument that is present.
// TODO: Do we want to put in argument placeholders here
// So that we can try to cache queries with arguments too?
$argument->handler->argument = $this->args[$id];
$argument->handler->query();
}
else {
// determine default condition and handle.
if (!$argument->handler->default_action()) {
return;
}
}
}
// Build our sort criteria if we were instructed to do so.
if (!empty($this->build_sort)) {
$this->_build('sort');
}
// TODO: Test display to see if it needs fields.
if (!empty($this->build_fields)) {
$this->_build('field');
}
$this->build_info['query'] = $this->query->query();
$this->build_info['count_query'] = $this->query->query(TRUE);
$this->build_info['query_args'] = $this->query->get_where_args();
$this->built = TRUE;
}
/**
* Internal method to build an individual set of handlers.
*/
function _build($key) {
foreach ($this->$key as $data) {
$data->handler->query();
}
}
?>As you can see (or, if you can't see, you're about to be told), the majority of the work is passed off onto handler->query().
All handlers are derived from the views_handler object:
<?php
/**
* Base handler, from which all the other handlers are derived.
* It creates a common interface to create consistency amongst
* handlers and data.
*
* The default handler has no constructor, so there's no need to jank with
* parent::views_handler() here.
*
* This class would be abstract in PHP5, but PHP4 doesn't understand that.
*
*/
class views_handler {
/**
* Seed the handler with necessary data.
* @param $view
* The $view object this handler is attached to.
* @param $data
* The item from the database; the actual contents of this will vary
* based upon the type of handler.
*/
function seed(&$view, &$data) {
$this->view = &$view;
$this->data = &$data;
// Mostly this exists to make things easier to reference. $this->options['...']
// is a little easier than $this->data->options['...'];
if (isset($data->options)) {
$this->options = $data->options;
}
else {
$this->options = array();
}
// This exist on most handlers, but not all. So they are still optional.
if (isset($data->table)) {
$this->table = $data->table;
}
if (isset($data->field)) {
$this->field = $data->field;
}
if (isset($data->relationship)) {
$this->relationship = $data->relationship;
}
if (!empty($view->query)) {
$this->query = &$view->query;
}
}
/**
* Provide a form for setting options.
*/
function options_form(&$form) { }
/**
* Validate the options form.
*/
function options_validate($form, &$form_state) { }
/**
* Perform any necessary changes to the form values prior to storage.
* There is no need for this function to actually store the data.
*/
function options_submit($form, &$form_state) { }
/**
* Add this handler into the query.
*
* If we were using PHP5, this would be abstract.
*/
function query() { }
}
?>Thanks to that seed function, all of the handler objects have intimate knowledge of everything they need at all times. Because they're objects with a reasonably long lifetime, they can keep private variables very easily. They can also communicate with each other a little bit if they need to, something that was almost impossible for handlers to do in the past. Other than that, this base handler doesn't do anything except to define the basic interface that all handlers must have. More specific handler types are derived from that:
<?php
/**
* Base field handler that has no options and renders an unformatted field.
*/
class views_handler_field extends views_handler {
/**
* Construct a new field handler.
*/
function views_handler_field($click_sortable = FALSE, $additional_fields = array()) {
$this->click_sortable = $click_sortable;
$this->additional_fields = $additional_fields;
}
/**
* Called to add the field to a query.
*/
function query() {
// Ensure the requested table is part of the query, and get the proper alias for it.
$alias = $this->query->ensure_table($this->table, $this->relationship);
// Add the field.
$this->field_alias = $this->query->add_field($alias, $this->field);
// Add any additional fields we are given.
if (!empty($this->additional_fields) && is_array($this->additional_fields)) {
foreach ($this->additional_fields as $this->field) {
$this->query->add_field($alias, $this->field);
}
}
}
/**
* Called to determine what to tell the clicksorter.
*/
function click_sort() {
// Ensure the requested table is part of the query, and get the proper alias for it.
$alias = $this->query->ensure_table($this->table, $this->relationship);
return "$alias.$this->field";
}
/**
* Render the field.
*
* @param $values
* The values retrieved from the database.
*/
function render($values) {
$value = $values->{$this->field_alias};
return check_plain($value);
}
}
?>That is both the base field handler and a perfectly usable handler for fields that don't need any special processing. For those that do, those will look more like this:
<?php
/**
* A handler to provide proper displays for dates.
*/
class views_handler_field_date extends views_handler_field {
/**
* Constructor; calls to base object constructor.
*/
function views_handler_field_date($click_sortable = FALSE, $additional_fields = array()) {
parent::views_handler_field($click_sortable, $additional_fields);
}
function options_form(&$form) {
$form['date_format'] = array(
'#type' => 'select',
'#title' => t('Date format'),
'#options' => array(
'small' => t('Small'),
'medium' => t('Medium'),
'large' => t('Large'),
'custom' => t('Custom'),
'time ago' => t('Time ago'),
),
'#default_value' => isset($this->options['date_format']) ? $this->options['date_format'] : 'small',
);
$form['custom_date_format'] = array(
'#type' => 'textfield',
'#title' => t('Custom date format'),
'#description' => t('If "Custom", see <a href="http://us.php.net/manual/en/function.date.php">the PHP docs</a> for date formats. If "Time ago" this is the the number of different units to display, which defaults to two.'),
'#default_value' => isset($this->options['custom_date_format']) ? $this->options['custom_date_format'] : '',
);
}
function render($values) {
$value = $values->{$this->field_alias};
$format = $this->options['date_format'];
$custom_format = $this->options['custom_date_format'];
switch ($format) {
case 'time ago':
return $value ? t('%time ago', array('%time' => format_interval(time() - $value, is_numeric($custom_format) ? $custom_format : 2))) : theme('views_nodate');
case 'custom':
return $value ? format_date($value, $format, $custom_format) : theme('views_nodate');
default:
return $value ? format_date($value, $format) : theme('views_nodate');
}
}
}
?>I haven't gotten to the part where you actually tell Views about your handler yet; I'm still vasillating on whether or not it will be involved in the schema, but either way I'm pretty sure it's going to look something like this:
<?php
$field_handlers['tablename']['fieldname'] = array(
'field' => 'real_fieldname', // just like in Views now,
'name' => t('the UI label'),
'group' => t('the UI group'), // this will be like an optgroup in the select
'help' => t('The traditional help text'),
'handler' => new views_handler_field_date(TRUE), // true means clicksortable
);
?>Or something along those lines, anyhow. Most of the information that will appear here will be about the UI, and then the handler object will deal with information about how to actually put stuff into the query.
This is very much still a work in progress. I have the very very basic handlers written, I can load/save views and I can build simple queries. I haven't done anything like work with the menu system, render a view, or even figure out exactly how this data will be acquired and cached (like I said, still dithering about the .schema stuff). And the UI? Heh. That's a project alllll it's own, and will probably end up taking up more time than all of this.


the OO hurdle
and when this mammoth rewrite is done, you get to teach all of us about how to extend classes and set properties. Kinda like teaching everyone about CVS branching and tagging. Joy!
thanks for the "in progress" peaks. i enjoy them a lot.
Post new comment