Issues

ZF-3399: Subclassing Zend_Db_Select/Zend_Db_Table_Select

Description

Subclasses of Zend_Db_Select cannot be used by Zend_Db_Table_Abstract::select() because it instantiates Zend_Db_Table_Select (which in turn extends Zend_Db_Select).

The only way around this is to first copy/paste the contents of Zend/Db/Table/Select.php and change the class it extends to your custom class. Then, you need to subclass Zend_Db_Table_Abstract and overload Zend_Db_Table_Abstract::select() to instantiate My_Db_Table_Select instead of Zend_Db_Table_Select.

Comments

Rather than copy/paste, we are using a subclass of Zend_Db_Table, which applies the same approach as is used for the row and rowset subclasses:

New:


    /**
     * Classname for Table Select statement
     *
     * @var string
     */
    protected $_selectClass = 'Zend_Db_Table_Select';

Updated:


    /**
     * Returns an instance of a Zend_Db_Table_Select object.
     *
     * @return Zend_Db_Table_Select
     */
    public function select()
    {
        @Zend_Loader::loadClass($this->_selectClass);
        return new $this->_selectClass($this);
    }

I think this code could be copied straight into Zend_Db_Table_Abstract.

This allows users to override the Table_Select class in the same way they override the row or rowset classes, and would make the class more consistent.

_where() might also need to be updated. Currently the class is fixed:

    /**
     * Generate WHERE clause from user-supplied string or array
     *
     * @param  string|array $where  OPTIONAL An SQL WHERE clause.
     * @return Zend_Db_Table_Select
     */
    protected function _where(Zend_Db_Table_Select $select, $where)

If we are allowed to specify our own class, it might not be a sub-class of Zend_Db_Table_Select.

I know that the normal practice would be to subclass Zend_Db_Table_Select and then make any changes necessary, but Zend_Db_Table_Select is a sub-class of Zend_Db_Select and this is also frequently sub-classed (e.g. to support bind variables). I have found it easier to incorporate the functionality I need by sub-classing Zend_Db_Select first (e.g. My_Db_Select extends Zend_Db_Select), and then sub-classing that to inherit the additional functionality at the table level (My_Db_Table_Select extends My_Db_Select).

This means that I have to keep My_Db_Table_Select as a verbatim copy of Zend_Db_Table_Select, except for the extends clause! It would be better if these classes used composition rather than inheritance, but that is another battle! I'm hoping that Zend_Db_Select will be enhanced to bind variables and I can forget about my sub classes.

In any case, for people in my situation who are deliberately using a Table Select that is not sub-classed from Zend_Db_Table_Select, the following might be better:

    /**
     * Generate WHERE clause from user-supplied string or array
     *
     * @param  string|array $where  OPTIONAL An SQL WHERE clause.
     * @return Zend_Db_Table_Select
     */
    protected function _where($select, $where)
    {
        if (! $select instanceof $this->_selectClass) {
            $type = gettype($select);
            if ($type == 'object') {
                $type = 'an object of class ' . get_class($select);
            }
            require_once 'Zend/Exception.php';
            throw new Zend_Exception('Select must be a ' . $this->_selectClass . ', but it is ' . $type);
        }
        
        $where = (array) $where;

        foreach ($where as $key => $val) {
            // is $key an int?
            if (is_int($key)) {
                // $val is the full condition
                $select->where($val);
            } else {
                // $key is the condition with placeholder,
                // and $val is quoted into the condition
                $select->where($key, $val);
            }
        }

        return $select;
    }

There are other places where a function requires a Zend_Db_Table_Select, e.g. Zend_Db_Table_Row_Abstract->findDependentRowset, so removing the class requirement and replacing it with the check I suggest above is probably not a good idea as it will be required in many places.

A better approach might be to create Zend_Db_Table_Select_Interface and require that instread. Then our custom classes could just implement the interface.

Zend_Db_Adapter is similarly affected. It has variables for $_defaultStmtClass and $_defaultProfilerClass but not one for $_defaultSelectClass. It would be better if the select class could also be changed easily.

I think this requires the addition of:

 
    /**
     * Default class name for a Select statement.
     *
     * @var string
     */
    protected $_defaultSelectClass = 'Zend_Db_Select';

and:

 
    /**
     * Get the default select class.
     *
     * @return string
     */
    public function getSelectClass()
    {
        return $this->_defaultSelectClass;
    }

    /**
     * Set the default select class.
     *
     * @return Zend_Db_Adapter_Abstract Fluent interface
     */
    public function setSelectClass($class)
    {
        $this->_defaultSelectClass = $class;
        return $this;
    }

And updating:

 
    /**
     * Creates and returns a new Zend_Db_Select object for this adapter.
     *
     * @return Zend_Db_Select
     */
    public function select()
    {
        return new Zend_Db_Select($this);
    }

To:

 
    /**
     * Creates and returns a new Zend_Db_Select object for this adapter.
     *
     * @return Zend_Db_Select
     */
    public function select()
    {
        Zend_Loader::loadClass($this->_defaultSelectClass);
        return new $this->_defaultSelectClass($this);
    }

For consistency and completeness we also need getter and setter methods for the $_select in Zend_Db_Table_Abstract:

    /**
     * @param  string $classname
     * @return Zend_Db_Table_Abstract Provides a fluent interface
     */
    public function setSelectClass($classname)
    {
        $this->_selectClass = (string) $classname;

        return $this;
    }

    /**
     * @return string
     */
    public function getSelectClass()
    {
        return $this->_selectClass;
    }

I agree that for such uses cases (that I understand), some interfaces are highly needed.

Zend_Db_Select_Interface should then define all the Zend_Db_Select methods that are actually used by Zend classes which use Zend_Db_Select typed params (all ?) The Interface should also define a public function __construct(Zend_Db_Adapter_Abstract $adapter) signature

We should also create a Zend_Db_Table_Select_Interface extending Zend_Db_Select_Interface and adding Zend_Db_Table_Select methods to it for Zend object using Zend_Db_Table_Select objects as input method params

When extending {{Zend_Db_Select::assemble()}} (to make it able to return only one part of generated SQL SELECT query) I ran into the same problem than Roger Hunwicks's {quote} I know that the normal practice would be to subclass Zend_Db_Table_Select and then make any changes necessary, but Zend_Db_Table_Select is a sub-class of Zend_Db_Select and this is also frequently sub-classed (e.g. to support bind variables). I have found it easier to incorporate the functionality I need by sub-classing Zend_Db_Select first (e.g. My_Db_Select extends Zend_Db_Select), and then sub-classing that to inherit the additional functionality at the table level (My_Db_Table_Select extends My_Db_Select).

This means that I have to keep My_Db_Table_Select as a verbatim copy of Zend_Db_Table_Select, except for the extends clause! It would be better if these classes used composition rather than inheritance, but that is another battle! I'm hoping that Zend_Db_Select will be enhanced to bind variables and I can forget about my sub classes. {quote} And copy/pasting {{Zend_Db_Table_Select}} proper code (the one that differs from {{Zend_Db_Select}}) into {{My_Db_Table_Select}} is quite "ugly" but seems to be the only way.

I think using interfaces in {{Zend_Db_Table_Abstract}} instead of concrete classes would be a good step forward.

I agree, but this type of change would have to wait till 2.0 time as it would be a mostly-complete rewrite that is needed to facilitate this.