This code has never been run through php. It's a first draft. It's here because this needs to be worked out before real implementation, and, maybe someone will find it useful.
It's a sketch of a way to manage small configurations within a web app, so that the user can alter them through the app. Notes below.
<? // vim:ts=4 sw=4:ai
/**
* A spec is an array that's passed to a function that interprets
* it for its own use. They are like small configuration files.
* This class helps manage them.
*/
class spec
{
var $id;
var $spec;
var $dirty;
function spec( $id=0, $get=false )
{
$this->id = $id;
$this->spec = unserialize(query_get_string("SELECT spec FROM specs WHERE id=$id"));
$this->dirty = false;
if ($get) return $this->get();
}
function create( $data )
{
return $id;
}
function get()
{
return $this->spec;
}
/**
* Duplicates the current spec and returns it.
* Note that duplicates are marked writable.
*/
function clone()
{
$spec = $this->spec;
$spec['name'] = 'Copy of '.$spec['name'];
$spec['readonly'] = 0;
query_insert("INSERT INTO specs spec=".serialize($spec));
return new spec(get_insert_id());
}
function save( $data )
{
if ($this->dirty)
{
query_update("UPDATE .... ");
$this->dirty = false;
}
}
/**
* If a spec has its readonly field set to true, then
* it can't be deleted. This is useful if you have some
* specs that ship with your app.
*/
function delete()
{
$s = $this->get();
if ($s['readonly']) return false;
else
query_delete("DELETE FROM ...");
return true;
}
function update( $name, $value )
{
if ( $this->spec[$name] == $value )
{
$this->spec[$name] = $value;
$this->dirty = true;
}
}
}
// --- shorthand usage
$id = $_GET('id');
$r = new report();
$r->report( new spec($id) );
// --- normal usage
$id = $_GET('id');
$s = new spec($id);
if ($_GET['clone'])
{
$s = $s->clone();
}
$spec = $s->get();
$s->update('term',$_POST['term']);
$s->save();
print "<form>";
print "<h1>{$spec[name]}</h1>";
// renaming widget could go here so user can rename the clone
print "<input type='text' name='search' value='{$spec[term]}'>";
print "</form>";
$results = search($s['term']);
print $results;
print "<a href='/?clone=1'>make a copy</a>";
I came up with this idea because there's a sql-based report generator I whipped up that takes a spec, which is just an array, as an argument. The report generator runs the query and decorates it according to the spec, so it'll look nicer than what I usually produce and add some features.
That was fine, but, I also wanted to export the data. The easiest way to do this turned out to be to save the spec out, and then feed it into another class that would produce the exported format (CSV mostly). I could have done it through sessions, but got paranoid about having multiple windows open, and ended up saving the spec into a table. It worked okay, but required sweeping the table of old specs.
That little decision opened up some ideas. For example, now, the spec can be used to export the report to an outside party. Just add a random key to the table, and email it to the recipient of the report.
The problem with that, was, you probably want to hide some of the proprietary data from the recipient. The shortest path to this is to allow the staffer reading the report to choose what to export. (Yes, there are risks, but data is a kind of leverage you can use to your advantage. Let them hide more, and then release some later.) Let them clone the spec, manipulate it, and then email them the key.
You can't make reports with this class. What you can do is make a spec, and then write a program to inject it into the system.
Writing it is done in an app like phpMyAdmin, not in your code, unless you really like writing all the code. Code it, then insert variables as needed, and debug by passing it into a report. Then, you write some code to inject your query into the system.
From that point on, you can retrieve your spec through the class, and it's not in your code. (And if I ever write the tools, I should be able to clone and alter the spec, export to phpmyadmin, fix it up, and put it back into the spec. There'll be a magic button that does all this.)
A spec looks something like this:
function search( $search)
{
$spec = array(
'name' => 'some report',
'sql' => "select b.id,a.x, a.y, b.z FROM a JOIN b ON a.b_id=b.a_id WHERE a.data LIKE '$search'",
'hidden_columns' = array( 'b.id' ),
);
report($spec);
}
Well, it's not quite that simple, but something like that.
Note that the spec has a variable in the query. That value is going to be "baked in" to the spec. That can be a problem, so some kind of variable interpolation method will have to be implemented.
Maybe something like
'sql' => ".... LIKE '__search__' ",
That would be interpolated like this:
$s->get( array('search'=>$search) );
Kinda ugly, but if you sanitize $_GET, you can $s->get($_GET), which is shorter.
Sometimes, though, you want to bake some of that information in. You don't want someone to be able to change the ID in a query if you're sharing the report with an outside party.
Maybe:
$s->finalize('id', $id);
$s->finalize('id2',$id2','sql');
$s->save();
The first call replaces __id__ with $id's value throughout the entire spec. The second replaces __id2__ with $id2's value in the 'sql' field in the spec. (There might need to be some path spec thing if the spec is a hierarchy of arrays.)
When the spec is saved, those variables can't be altered anymore. Thus, it's safer to share it. A user would have to hack the spec directly, not by passing in some variables that happen to appear in some URL or form.
Note, however, that other variables are not finalized. I use different variables to allow users to sort the report, filter the report, and will probably add more features. There are also parts of the spec that link columns to other reports. Those variables need to remain "variable".
Also, there is a readonly feature but I think I implemented it wrong. It's more of a no-delete feature in the code. It should really be a read-only and disallow altering the spec through the class. Of course, a spec is just an array, so you can alter the array directly and subvert the protection. It's only there to prevent accidental deletion or overwriting of a spec that should ship with the app.