We have moved our forum to GitHub Discussions. For questions about Phalcon v3/v4/v5 you can visit here and for Phalcon v6 here.

Mimic Doctrine Table inheritance

I'd like to mimic Doctrine Table inheritance (https://the-phpjs-ldc.rgou.net/symfony1/more-with-symfony/en/09-Doctrine-Form-Inheritance.markdown )

I've manage to do it for findFirst method

public static function findFirst($parameters = null)
    {
        $object = parent::findFirst($parameters);

        if (!$object) {
            return false;
        }

        switch ($object->type) {
            case 'Offer':
                $return = new Offer();
                break;
            default:
                return $object;
        }
        $return->assign($object->toArray());
        return $return;
    }

(here we assume that the column type can have an Offer value and we want the model associated to be Offer (which inherit our own class)

It works.

But i don't find a good way to do this for find method, the idea would be to do the same kind on things while iterating in resultset. But how to hook query to return our own resultset and not the ResultSet\Simple ?

Thanks in advance

Olivier

edited Aug '14

update, here is the hack done on query on the _executeSelect

I had to do an hack because my standard find (without any column selected was bugging :

protected function _executeSelect(array $intermediate, array $bindParams, array $bindTypes = null)
    {
        $models = array_keys($this->_models);
        $model = $models[0];
        $object = new $model();
        $dialect = $object->getReadConnection()->getDialect();

        $sql = $dialect->select($intermediate);

        // hack due to unknown bug in select
        $sql = str_replace('`' . $this->_models[$model]. '` FROM', '* FROM', $sql);

        return new ResultSet(
            $this->_metaData->getColumnMap($object),
            $object,
            $object->getReadConnection()->query(
                $sql,
                $bindParams,
                $bindTypes
            ),
            $this->getCache()
        );
    }

the find method in the Model object is

public static function find($parameters = null)
    {
        if (!is_array($parameters)) {
            $parameters = [$parameters];
        }

        $builder = new Builder($parameters);
        $builder->from(get_called_class());

        if (isset($parameters['bind'])) {
            return $builder->getQuery()->execute($parameters['bind']);
        } else {
            return $builder->getQuery()->execute();
        }
    }

builder is a basic class with only one overrided method

    public function getQuery()
    {
        $query = new Query($this->getPhql());
        $query->setDI($this->getDI());
        return $query;
    }

Do you know why i had to do this horrible hack ?

    // hack due to unknown bug in select
   $sql = str_replace('`' . $this->_models[$model]. '` FROM', '* FROM', $sql);

Thanks !!

New question : how to hook the relation to do the same ?



24.1k
Accepted
answer
edited Sep '14

Complete new thinking on the problem :

change in Phalcon 1.3.3 ( https://github.com/phalcon/cphalcon/pull/2789 ) and 2.0.0 ( https://github.com/phalcon/cphalcon/pull/2790 ) to use late state binding on cloneResultMap than override cloneResultMap on the "parent" model class :

/**
     * Assigns values to a model inherited class from an array returning a new model.
     *
     * @param \Phalcon\Mvc\Model $base          object to hydrate
     * @param array              $data          data to use for hydration
     * @param array              $columnMap     column mapping
     * @param int                $dirtyState    object state
     * @param boolean            $keepSnapshots keep snapshot
     *
     * @return \Phalcon\Mvc\Model
     */
    public static function cloneResultMap(
        $base,
        $data,
        $columnMap,
        $dirtyState = null,
        $keepSnapshots = null
    ) {
        if (isset($data['type'])) {
            $class = '\Ournamespace\Models\\'.$data['type'];
            if (class_exists($class)) {
                $base = new $class();
            }
        }
        return parent::cloneResultMap(
            $base,
            $data,
            $columnMap,
            $dirtyState,
            $keepSnapshots
        );
    }

Very neat approach!

This does not seem to work when getting related objects. Olivier, do you have a solution for this as well?

edited Sep '14

My bad, it does work! I created an example that everyone can use if they want to use single table inheritence for their project, which in my opinion is very usefull.

See Single table inheritance why this is usefull.

To we need to have a Base class which extends the model.

class BaseModel extends \Phalcon\Mvc\Model
{
    public static function cloneResultMap(
        $base,
        array $data,
        $columnMap,
        $dirtyState = null,
        $keepSnapshots = null
    ) {
            if ($base->getInheritanceColumn() && isset($data[$base->getInheritanceColumn()])) {
                $class = get_class($base).'\\' . $data[$base->getInheritanceColumn()];
                    if (class_exists($class)) {
                            $base = new $class();
                    }
            }
        return parent::cloneResultMap(
            $base,
            $data,
            $columnMap,
            $dirtyState,
            $keepSnapshots
        );
    }

}

in a class where you want single table inheritenace to work just add the following to the onConstruct method of the base class

class Notification extends BaseModel
{
    public function onConstruct(){
        $this->setInheritanceColumn('type');
    }

    public function getMessage(){
        return "I'm an object of class " . get_class($this);
    }
}

Now we can add custom behavior for a like notificaton in a seperate class instead of using some ugly switch statement in the Notification class. See code below.

namespace notification;

class Like extends  \Notification{

    public function initialize(){
        parent::initialize();
        $this->setSource("notification");
    }

    public function onConstruct(){
        //type should be identicial to the class name. This could be replaced by function which return the class name.
        $this->type = 'Like';
    }

} 

No when we run the following code::

$notification = Notification::findFirst();
print $notification->getObject();

this will output

I'm an object of class notification\Like

Hope @Olivier Garbé pull request gets merged into the main branche. Thanks anyway.

It seems this change was merged in, but there's not a lot of documentation out there, so I was lead to this thread.

What I'm wondering about is whether this approach would also work with resultsets;

Eg. I have a message table. Message has several sub-types such as Discussion, Comment, Event, Poll. When creating a list of the 10 latest messages, I am selecting from Message, but I would like to receive instances of Discussion, Comment, Event, Poll as per their type.

Is this possible at the moment, and if so: how?

I've also tried a PHQL query approach, selecting from all types I'm interested in, but this does not work for me ( issue being all these types map to the same table, 'message', and this causes an error ).

edited Sep '15

hello, it works with resultset yes, as resultset use object hydration to returns results.

you have to override the cloneResultMap of your Message object and return Discussion / Comment / Event depending on Message type :

here a part of our code as an example :

    /**
     * Assigns values to a Prestation inherited class from an array returning a new model.
     *
     * @param \Phalcon\Mvc\Model $base          object to hydrate
     * @param array              $data          data to use for hydration
     * @param array              $columnMap     column mapping
     * @param int                $dirtyState    object state
     * @param boolean            $keepSnapshots keep snapshot
     *
     * @return \Phalcon\Mvc\Model
     */
    public static function cloneResultMap(
        $base,
        array $data,
        $columnMap,
        $dirtyState = null,
        $keepSnapshots = null
    ) {
        if (isset($data['type'])) {
            switch ($data['type']) {
                case 'TransportDP':
                    $class = '\Prestation\Models\Transport\Dynamic';
                    break;
                case 'TransportProd':
                    $class = '\Prestation\Models\Transport\Internal';
                    break;
                default:
                    $class = '\Prestation\Models\\' . $data['type'];
                    break;
            }
            if (class_exists($class)) {
                $base = new $class();
            }
        }
        return parent::cloneResultMap(
            $base,
            $data,
            $columnMap,
            $dirtyState,
            $keepSnapshots
        );
    }