PHP Doctrine - adding automatic, simple CRUD

I just found myself wishing for automatic CRUD, for quick and simple administrative tasks, as it turned out a very easy thing to add. The first thing we have to do is move the label selection from having to be explicitly declared in the insert/update form - like we are doing now - to the model:

class Company extends Mdl{
	public $label = 'ticker';
...

In this case the short name (ticker) for the company will be used as the label in drop downs. The functionality itself has been implemented by extending Ctrl with AdminCtrl:

class AdminCtrl extends Ctrl{
	function init($name){
		parent::init($name);
		$tmp = array('id', 'created_at', 'updated_at');
		$this->hidden = array_combine($tmp, $tmp);
		$this->update_list = true;
		$this->delete_list = true;
		$this->setS('pop', NULL);
	}
...

First we create an array with fields we don’t want to mess with in the insert/update forms. Next we set default values of whether to show update and/or delete links for the current table, default is true/yes.

Lastly we do away with pre-population completely, in fact we don’t do any validating at all in the models at the moment, even though the system would allow it. Validation will work but nothing will re-populate.

function listAll(){
  $this->assign('update', $this->update_list);
  $this->assign('delete', $this->delete_list);
  $this->assign('columns', array_keys($this->t->getColumns()));
  $this->assign('items', $this->findAllArr());
  $this->assignRelations();
  return $this->fetch('../list.tpl');
}

Retrieval, we simply list everything without pagination or proper search forms. With the logic we already have in place in Ctrl it should be trivial to add, no need at the moment though.

First we assign the update/delete yes/no stuff. Next we assign various relations we need in the template:

function assignRelations(){
  foreach($this->t->getRelations() as $key => $value){
    $relations[ $value->getLocal() ]['label'] = $this->getMdl($value->getClass())->label;
    $relations[ $value->getLocal() ]['Mdl'] = $value->getClass();
  }
  $this->assign('relations', 	$relations);
}

We use Doctrine::Table ($this->t) to get the relations, currently only single relations - $this->hasOne(’Company’, array(’local’ => ‘company’, ‘foreign’ => ‘id’)) - have been tested since they are the ones commonly used in select boxes (we could even test for single and do nothing in case of multiple with isOneToOne()).

Anyway, we iterate through the relations and use the local field name as the key, in this case company. Next we get the label by getting the class name, Company, and using it to get the model object which has the label member variable.

We also store the class name in ‘Mdl‘. Time for the HTML (list.tpl):

<a href="{$base_dir}form">Insert new</a><br>
<table>
	<tr>
		{foreach from=$columns item=c}
			<td><strong>{$c}</strong></td>
		{/foreach}
	</tr>
	{foreach from=$items item=i}
	<tr class="{cycle values="tr_light,tr_dark"}">
		{foreach from=$i key=c item=val}
			<td>
				{if !empty($relations.$c)}
					{obj->getLbl lbl=$relations.$c.label mdl=$relations.$c.Mdl id=$i.$c}
				{else}
					{$val}
				{/if}
			</td>
		{/foreach}
		{if $update == true}
			<td><a href="{$base_dir}form/id/{$i.id}">Edit</a></td>
		{/if}
		{if $update == true}
			<td><a href="{$base_dir}delete/id/{$i.id}">Delete</a></td>
		{/if}
	</tr>
	{/foreach}
</table>

We draw the column names as headlines to the table, then we loop through all our items. If the entry for the current column, for instance company isn’t empty we assume a one to one relation and fetch the label value (ticker) we have fought so hard to get to, instead of the company id value stored in company. Note the dot syntax Smarty uses to access associative arrays, nice.

If we don’t have a relation we simply display the value as is.

Finally we show or don’t show the update/delete links we discussed earlier. Here’s getLbl() in AdminCtrl:

function getLbl($p, &$s){
  return $this->getCtrl($p['mdl'])->find($p['id'])->$p['lbl'];
}

Not a hell of a lot to say here, we get the controller in question and use that to find the record, and finally use the passed label information to get at the contents of the label variable (ticker). A less resource hungry alternative would be to use DQL here but I don’t need this code to be quick so I don’t care.

Let’s continue with more functionality in the AdminCtrl:

function formCommon($f_head, $type, $tpl = "../ui.tpl"){
  $this->assign('columns', array_diff_key($this->t->getColumns(), $this->hidden));
  $this->assignRelations();
  return parent::formCommon($f_head, $type, $tpl);
}

This is in fact the only change we need to make to the current logic, adding the relations and filtering out some columns.

In ui.tpl:

<form {$form_head} >
<table>
	{foreach from=$columns key=c item=info}
	<tr>
		<td>{$c}: </td>
		<td>
			{if !empty($relations.$c)}
				{obj->selBox lbl=$relations.$c.label mdl=$relations.$c.Mdl idname=$c}
			{elseif $info.length > 500}
				{obj->textArea idname=$c}
			{else}
				{obj->textBox idname=$c}
			{/if}
		</td>
	</tr>
	{/foreach}
</table>
<input type="submit" value="Submit">
</form>

Note the use of basically the same information we used in the listing, where we got a label, to fetch a proper select box. It’s the same principle, instead of a label for a specific record we now need to get labels for all entries in the whole table, selBox() has already been explained though so we don’t go there.

If the text is longer than 500 characters we go for a text area instead of a box, otherwise we go for a box. This is the place to for instance implement a radio group for booleans if it’s needed.

At last an example of a controller implementing this:

class CompanyCtrl extends AdminCtrl{
	function init($name){
		parent::init($name);
		$this->update_list = false;
		$this->delete_list = false;
	}
}

And that was that, the basics of automatic form generation using our Doctrine and Smarty combo.


Related Posts

Tags: , ,