Solved thread

This post is marked as solved. If you think the information contained on this thread must be part of the official documentation, please contribute submitting a pull request to its repository.

How to make the resultset models be upcast to a more specific type?

I need to upcast models to a specific type from a general one. I have a Markup model and Note and Tag inherit from this. I want to be able to do call Markup::find and have it return both Notes and Tags. Sometimes though I will want to only get Notes or Tags with Note::find to be more specific. I need to detect a field to see which type of model it is before creating it as that.

Does Phalcon support this sort of thing?



48.4k

I'm thinking that if the Model class had a static function that would return the type of model to create based upon the data from the database then it would accomplish what I'm trying to do. If that static function was missing then it would use the class name itself.

So it would be great if my Markup class had a static method that looked for the type field from the database result row and then was able to return model class names as it saw fit. This would allow me to call BaseClass::find to return types that inherit it.

Are Note and Tag stored in different tables?



48.4k

Same table.

Are Note and Tag stored in different tables?



56.7k
Accepted
answer

I don't think there's anything built-in that will do this.

If you've got small resultsets, you can overwrite Markup::find() to iterate through the retrieved resultset and build a new result set with the appropriate models.

If you've got big resultsets and don't want to retrieve all the records at once, you can do what I've done and give the Markup class a getOfType() method that returns a new Note or Tag object based on the properties retrieved for the Markup object.



48.4k
edited Feb '15

Thanks. That is a pretty nice solution. Basically, in my project all user data goes into a single table. I suppose that if I created a new namespace for the classes to be used in this way then I wouldn't have to explicitly list each of the classes in that getoftype() method.

So do you manually pass in each attribute on an assign in the (kind of) getOfType() factory?

Also does Phalcon treat a model that was pulled from the database differently than a model that had its values assigned in user code? Do I need to pass any flags in?

I suppose that special attention would need to be given to ensure that hooks run at the appropriate level. I never want a model to be able to insert, delete or update at a lower class level if it hasn't been instantiated as that type. Have you figured out a solution to that puzzle?



48.4k

Also, would you effectively lose the ability to do batch operations to on the Resultset if you want to use appropriate hooks?

Hmm, I've already created a Sequence class that wraps the Resultset. I could make all of the typical interface methods that map over the Resultset ones return objects called from getoftype().



48.4k

If you've got small resultsets, you can overwrite Markup::find() to iterate through the retrieved resultset and build a new result set with the appropriate models.

I don't see how that would be possible with a vanilla Resultset. It looks like I'd have to wrap the Resultset, which is something that I already starting doing for another reason.



48.4k

Also, I suppose that I'd optimize the base class so that it doesn't do snapshots and any other thing that would take up more resources. I don't need to take up 4x the memory.

Hmm, what about assigning values by reference? Would that get me into trouble? I'm considering always using the Resultset wrapper and hiding the base model records behind it and reusing their variables by assigning them by reference to the objects create with getoftype().

I probably need to chew on this for some days.

1) This code is what creates a new typed Page class from an instance of my BasePage class:

    public function loadPageOfType(){
        switch($this->type){
            case 'Home':
                $Type = new \MW\Model\Page\Home();
                break;
            case 'Content':
                $Type = new \MW\Model\Page\Content();
                break;
            case 'App':
                $Type = new \MW\Model\Page\App();
        }

        $Type->importPage($this);
        return $Type;
    }

    public function importPage($Page){
        foreach($Page->toArray() as $key=>$value){
            $this->{$key} = $value;
        }
    }

This doesn't trigger the afterFetch() hook on the newly typed object. That's not a big deal for me though because in my case, the typed classes interact with the database identically to each other. So, the database interactivity behaviour defined in BasePage applies to all the typed classes as well.

3) This is how that find() could be overwritten:

class Markup(){
    public function find($parameters){
        $Resultset = parent::find($parameters);
        $typed_results = [];
        if($Resultset != NULL){
            foreach($Resultset as $Record){
                $typed_results[] = $Record->loadOfType();
            }
        }

        return $typed_results;
    }
}

You can see the code converts a Resultset into an array. This will cause you to lose the ability to delete() or update() en masse. I haven't looked into it, but there's bound to be a way to build a new Resultset object rather than an array.



48.4k

I haven't looked into it, but there's bound to be a way to build a new Resultset object rather than an array.

I was just looking at some Zephir code today and it appears that Phalcon isn't currently built to handle these edge cases and that a lot of things like the query builder would need to be extended. I'm not sure that I feel comfortable with that.



48.4k

This doesn't trigger the afterFetch() hook on the newly typed object. That's not a big deal for me though because in my case, the typed classes interact with the database identically to each other. So, the database interactivity behaviour defined in BasePage applies to all the typed classes as well.

For my case as well. The insert, update and delete hooks vary wildly in my case though.

The insert, update and delete hooks vary wildly in my case though.

That's fine. When you call save() on these typed objects, all the necessary beforeSave(), afterSave() etc methods will still be called. These are fully fledged instances of the typed classes, so all the appropriate hooks will be called for those 3 actions. I specifically mentioned afterFetch() because these typed objects aren't "fetched".



48.4k
edited Feb '15

Understood.

My table and user data is capable of building and searching for an inheritance chain (with the MySQL dialect class for FULLTEXT support and soon to be newer MariaDB stuff) and I'd like to be able to call from the more specific type like File::find, Doc::find, Markup::find, Note::find, etc. So I just have to check in the find method as to which current class the type is and then to work down the inheritance tree. Hell, I might end up doing it in third normal form but I'm to get there is the same method.

I really like your approach here because it frees me from the tyranny of the Resultset object. So I suppose that the Resultset class is never meant to be anything more than a nice form to receive data in. It sure would be nice to be able to pass in a lambda to have the query return types based on an evaluation of the DB record data though.

[edit] Rambling... Time for bed. Thanks for the help.

The trouble with a lambda like that is it would have to be run over every record as it was pulled out. That would cause chaos for thinks like the ArrayIterator interface it implements, and the count() function.