Modified fields for Zend_Db_Table

by Naneau

Earlier today, I wrote a little extension to Zend_Db_Table that allows for “custom” fields to be added to it’s rows. When it was done some other ideas came floating up. Why wouldn’t I make my life even easier, by creating fields that don’t output their values directly, but in some meaningfully modified way.

Why would I want to do that, you ask? Well, let’s assume you have a database definition that stores a created_on value for every row. It contains a UNIX timestamp. Like most people, I don’t know what time it is by looking at a very long number (at the time of writing it is 11784070556). But what if I could make my created_on field not contain a timestamp, but a Zend_Date object.

Internally it should still work with the timestamp, as it is the “pure” form that computers like. Another field type that may need modification is that for passwords. In that case you need a one way modification. You want to retrieve a encrypted password, but set a plain text one. You only need modification (encryption) while setting it.

Now, I’ll admit that this idea is a little more far-fetched and less useful as the one described in the previous post. Most of this could be done in various custom row classes and domain logic in the table class. Yet I like to play around and decided to go ahead anyway, so I needed some kind of “modifier” for fields. Something a little like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Naneau_Db_Table_Modifier {
    /**
     * get the modified value
     * @param mixed $value
     * @param Naneau_Db_Table_Row $row
     * @return mixed
     */

    public function modify($value, $row);

    /**
     * get the original value
     * @param mixed $value
     * @param Naneau_Db_Table_Row $row
     * @return void
     */

    public function unModify($value, $row);
}

I modified my earlier extensions to check for the existence of such a modifier and call the relevant methods when getting and setting a row field. They now are:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<?php
require_once 'Zend/Db/Table/Abstract.php';
require_once 'Naneau/Db/Table/Row.php';

class Naneau_Db_Table extends Zend_Db_Table_Abstract {

    /**
     * array of extra fields
     *
     * @var array
     */

    private $_extraFields = array();

    /**
     * modifiers
     *
     * @var array
     */

    private $_modifiers = array();

    public function __construct(array $config = array())
    {
        if (!isset($config['rowClass'])) {
            $config['rowClass'] = 'Naneau_Db_Table_Row';
        }
        //boeh!

        parent::__construct($config);

        if (method_exists($this, 'setup')) {
            $this->setup();
        }
    }

    /**
     * add an extra field to the row definition
     *
     * @param string $name
     * @param string|array $fieldFunc
     */

    protected function addExtraField($name, $fieldFunc)
    {
        $this->_extraFields[$name] = $fieldFunc;
    }

    /**
     * get an extra field
     *
     * @param Naneau_Db_Row $row
     * @param string $what
     * @return mixed
     * @throws Zend_Db_Table_Row_Exception
     */

    public function getExtraField($row, $what)
    {
        if (!array_key_exists($what, $this->_extraFields)) {
            //field doesn't exist in extra fields
            require_once 'Zend/Db/Table/Row/Exception.php';
            throw new Zend_Db_Table_Row_Exception("Specified column \"$what\" is not in the row");
        }

        $func = $this->_extraFields[$what];
        //the function
        $args = array($row, $what);
        //the arguments (an copy of this row is given to the function to work with)

        if (is_array($func)) {
            //method of some class or object
            return call_user_method_array($func[1], $func[0], $args);
        } else {
            //plain function
            return call_user_func_array($func, $args);
        }
        //return the result of a function
    }

    /**
     * get a modified field value, if a modifier has been registered
     *
     * @param Naneau_Db_Table_Row $row
     * @param string $what
     * @param mixed $value
     * @return mixed
     */

    public function getModifiedValue($row, $what, $value) {
        if (array_key_exists($what, $this->_modifiers)) {

            $modifier = $this->_modifiers[$what];
            return $modifier->modify($value, $row);
        }
        return $value;
    }

    /**
     * get an unModified value, if a modifier has been registered
     *
     * @param Naneau_Db_Table_Row $row
     * @param string $what
     * @param mixed $value
     * @return mixed
     */

    public function getUnModifiedValue($row, $what, $value) {
        if (array_key_exists($what, $this->_modifiers)) {

            $modifier = $this->_modifiers[$what];
            $value = $modifier->unModify($value, $row);
        }
        return $value;
    }

    /**
     * add a modifier to the model
     *
     * @param string $field
     * @param Naneau_Db_Table_Modifier $modifier
     */

    protected function addModifier($field, $modifier) {
        $this->_modifiers[$field] = $modifier;
    }
}

and

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

require_once 'Zend/Db/Table/Row.php';

class Naneau_Db_Table_Row extends Zend_Db_Table_Row_Abstract {

    /**
     * overridden __get
     *
     * @param string $what
     * @return mixed
     * @throws Zend_Db_Table_Row_Exception
     */

    public function __get($what)
    {
        try {
            $return  = parent::__get($what);
            //it exists as a 'normal' field
        } catch (Zend_Db_Table_Row_Exception $e) {
            //field doesn't exist as 'normal' field

            $return = $this->_getTable()->getExtraField($this, $what);
            //try to get it from the extra fields in the table definition
        }

        return $this->_getTable()->getModifiedValue($this, $what, $return);
        //return modified value (if it can be modified, that is)
    }

    public function __set($what, $value) {
        $value = $this->_getTable()->getUnModifiedValue($this, $what, $value);
       
        parent::__set($what, $value);
    }
}

You can set modified fields in the same “setup” method that you can use for the “extra” fields.

1
2
3
4
protected function setup() {
    Zend_Loader::loadClass('Naneau_Db_Table_Modifier_Date');
    $this->addModifier('timestamp', new Naneau_Db_Table_Modifier_Date());
}

Where the date modifier is:

1
2
3
4
5
6
7
8
9
class Naneau_Db_Table_Modifier_Date implements Naneau_Db_Table_Modifier {
    public function modify($timestamp, $row) {
        return new Zend_Date($timestamp, null, Zend_Registry::get('locale'));
    }

    public function unModify($date, $row) {
        return $date->get();
    }
}

After you have registered a date modifier for a field, you can work with it as if it were a Zend_Date object:

1
2
3
//fetch a db row
echo $row->created_on;
//outputs something like: 6-apr-2007 6:20:00

At the moment it really is just a modifier. You can get and set fields it as if they were objects, but you can’t call methods on them changing their internal state, which you could then save. I’m thinking up a solution for that, using some kind of object store/cache. Also please note that the toArray methods of both Row and Rowset still return “internal” values.

And that’s a lot of code for a single post. But as this is more of an experiment than anything else, I decided to just post it inline, and not as an attachment. I’d love comments on this. Preferably comments on why it’s evil and shouldn’t be done this way, because for me this is just an attempt at learning.