Are recursive, dynamic models possible in Phalcon?

I've been building out a fairly large project using Phalcon 4.0.5 and just hit a major snag.

Our models in some cases are dynamic, so we will create an instance, and set the source table after creation, which has been working fine until its done recursively, here is an example:

class DynamicModel extends \Phalcon\Mvc\Model
{
    public function setTable($table)
    {
        $this->setSource($table);
    }
}

$originalModel = new \Model\DynamicModel;
$originalModel->setTable('original');
$originalModel::find();

This all works great, until...

class DynamicModel extends \Phalcon\Mvc\Model
{
    public function setTable($table)
    {
        $this->setSource($table);
    }

    public function afterFetch(): void
    {
        $someOtherTable = new \Model\DynamicModel;
        $someOtherTable->setTable('some_new_table');

        // this works, but now the source for any instance
        // of \Model\DyanmicModel is 'some_new_table'
        $someOtherTable::find();
    }
}

I can use this line to fix the issue of it having the wrong table, and force it to refresh in setTable()

$this->getModelsManager()->__destruct();  

But the issue remains for saving it... for example:

$originalModel = new \Model\DynamicModel;
$originalModel->setTable('original');
$originalModel::find();

this fires afterFetch() and changes table to some_new_table

So now when you go to save the model instance like:

$originalModel->some_field = 123;
$originalModel->save();

Its now trying to save to some_new_table

I've tried a number of different approaches to overcome this, the most promising seems to be anonymously subclassing DynamicModel, which seems to work until you try to run a query.

$dynamicModelAnonymousClassInstance = new class extends \Model\DynamicModel {
    public function initialize(): void
    {
        $this->setSource('some_other_table');
        parent::initialize();
    }
};

With the above, $dynamicModelAnonymousClassInstance appears to be a perfectly usable model, but when you run ::find() you get:

Scanning error before 'anonymous...' when parsing: SELECT [email protected] (155)

So it seems to discard the source set behind the scenes, but only once you try to do a query. I'm not sure if that is a bug, or I'm just not using it as intended so its behaving unpredictably.

When I define the class normally, for instance:

class SomeOtherTable extends \Model\ManagedModel {
    public function initialize(): void
    {
        $this->setSource('some_other_table');
        parent::initialize();
    }
};

\Model\SomeOtherTable::find();

This works fine, but the problem is I can't predefine the names of the classes on the filesystem for this project, I need to be able to generate them dynamically by the name provided by the user.

I would really appreciate any ideas on how to overcome the issue, or other approaches that might avoid the issue altogether.

Thanks

It's not immediately obvious to me why you're running that code in afterFetch(). You're not returning anything - you're just querying the DB again, but doing nothing with the result.

Just thinking out loud (so to speak), but what if you added a static property to DynamicModel that gets set to whatever you set in setTable(). Then, add a beforeSave() handler that calls $this->setSource(self::$originallySetTableSource). That would revert the change done in afterFetch(). You could probably tie that in to the constructor actually, rather than needing a separate setTable() method. It might make invocation of DynamicModel a little clearer:

$SomeDynamicModel = new \Model\DynamicModel('table_name');

Finally, off-topic: If you append the three backticks for opening a code block with "php" (so '''php but with backticks instead of single quotes), you'll get syntax highlighting.

edited Jun '20

Hey Dylan, thanks for the quick reply.

You're spot on, the code in afterFetch() is for demonstration purposes to keep it as simple as possible. The real purpose is to get a hydrated model instance.

Your idea about the static property works in the above scenario, but I don't think can handle another level of recursion, I need to handle up to 4 levels of recursion. I tried a similar approach, but storing it as arrays and it got a unwieldy quickly.

Is there some reason an anonymous subclass won't allow ->setSource('table_name') but a pre-defined class will? This is my first time diving into anonymous classes, so I'm a bit out of my depth.

I've never used anonymous classes so I'm no help there.

You're right - static properties won't work if you need to store multiple levels. What about just a regular private property? That will be tied to the object, not the class. Since you're setting the source with every invocation of DynamicModel anyway, there's no need to persist the table source between objects of that class.

How do you determine the source of the recursive, inner model? ie: what some_new_tableis? What do you actually do with that inner model? Is it assigned to that outer model?

Can you give me the use case for why you're doing this? Maybe that'll help us come up with an ideal solution.

edited Jun '20

The use case is for a system that can create and manage tables dynamically.

If it helps to explain it conceptutally, an example might be a user created Blog model instance, which, when hydrated loads an instance of a Image model, which in turn loads a File instance of the file record.

The cut-off at the service level is 4 levels of recursion, so that would also force the cut-off on the recursive dynamic models as well.

So, in psuedo-ish-code:

class DynamicModel extends BaseModel {
    public function afterFetch()
    {
        if(property_exists($this, 'image')) {
            // lets say it was stored as an int
            $imageModelInstance = new \Model\DynamicModel;
            $imageModelInstance->setTable('images');
            $this->image = $imageModelInstance::findFirst($this->image);
        }

        if(property_exists($this, 'file')) {
            // lets say it was stored as an int
            $fileModelInstance = new \Model\DynamicModel;
            $fileModelInstance->setTable('files');
            $this->file = $fileModelInstance::findFirst($this->file);
        }
    }
}

$dynamicModelInstance = new \Model\DynamicModel;
$dynamicModelInstance->setTable('blogs');

$blogs = $dynamicModelInstance::find();

In a perfect world, the end result would be something like this

[
    blog {
        image {
            file {

            }
        }
    }
]
edited Jun '20

Ok, then yeah - I think setting a private property would work:


class DynamicModel{

    private $actual_source;

    public function setTable($table){
        $this->actual_source = $table;
        $this->setSource($table);
    }

    // Ensure the right table is used before validation (and saving) happens
    public function beforeValidation(){
        $this->setSource($this->actual_source);
    }

    // Ensure the right table is used when querying
    public function find($params){
        $this->setSource($this->actual_source);
        return parent::find($params);
    }

    public function afterFetch(){
        $sub_models = [
            'image' => 'images',
            'file'  => 'files'
        ];
        foreach($sub_models as $property,$table){
            if(property_exists($this,$property)){
                $SubModel = new \Model\DynamicModel;
                $SubModel->setTable($table);
                $this->{$property} = $SubModel::findFirst($this->{$property});
            }
        }
    }   
}

I feel like there should be a more elegant solution - maybe anonymous classes is the trick. What you're doing is pretty out there, so this may be as good as it gets.

edited Jun '20

Hey Dylan, first of all thanks for taking a look at this. After 10 hours of banging my head against it, at some point it just helps to have a second pair of eyes. I realize this problem isn't necessarily related to Phalcon, but more just my implementation of it, so I'm very appreciative of the help.

Anonymous subclassing doesn't work as it gets auto assigned a class name by the engine, and in Zephir, it simply calls get_class() on the instance so PHQL attempts to construct a query with an anonymous class name like SELECT field FROM [email protected]/iw9uh87gf8 I don't see anywhere you can override the internal class key, otherwise this would be the best option.

I had not considered making the query methods non-static. After going through it, it also has its pitfalls, such as:

1) ->find() cannot be made non-static when its extending Phalcon\Mvc\Model, a simple workaround is just to make up a new name like ->findDynamic() to wrap the original name.

2) I still need to make use of $this->getModelsManager()->__destruct(); to refresh the cache on each instance of ->setTable() otherwise it does not actually update the source, which means every query generates a new execution plan, which means anything in a loop would perform poorly.

3) The model instances returned by ->findDynamic() which call ::find() are generic instances of DynamicModel without the private property set, so now I have to save each model instance returned by find or findFirst and pass that table to it, this also seems like it will perform poorly.

After coming up with workarounds for all of the above shortcomings, it "works" but feels very fragile and since its doing all of the inspection of variables and passing things around in PHP it isn't taking advantage of Phalcon's performance benefits.

That all being said, I'm tinkering around with another wild idea:

What if I had a file with 1,000 spare placeholder classes, named things like DynamicModel0001, then as needed I can instantiate these, set the table, and maintain the table:model mapping and interface directly with the hydrated placeholder models.

In practice that might look like:

class DynamicModel0001 extends DynamicModel
{
    public $dynamicSource;

    public function initialize()
    {
        if(is_string($dynamicSource)) {
            $this->setSource($this->dynamicSource);
        }
    }
}

class DynamicModel0002 extends DynamicModel
{
    public $dynamicSource;

    public function initialize()
    {
        if(is_string($dynamicSource)) {
            $this->setSource($this->dynamicSource);
        }
    }
}

Then resolve the table name to placeholder class via a static method, such as:

class DynamicModelMapper
{
    static $existingModels = [];

    static $nextDynamicModelPosition = 1;

    static function getModel($table)
    {
        if(isset(self::$existingModels[$table])) {
            return self::$existingModels[$table];
        }

        $className = "\Model\DynamicModel000" . self::$nextDynamicModelPosition;

        self::$existingModels[$table] = new $className;
        self::$existingModels[$table]->setTable($table);
        self::$nextDynamicModelPosition += 1;

        return self::$existingModels[$table];
    }
}

$userTable = 'some_table';
DynamicModelMapper::getModel($userTable)::find()

This is just theory, this code may not be exactly what's needed, but its the general idea of having spare blank classes avilable on-demand. It seems kind of nutty, but also seems like it may work.

I am fairly certain it will work, its pretty weird, but seems more maintainable than the maze that I've created to handle the other side effects of mixing/matching static vs. non-static.

edited Jun '20

D'oh - you're right, I forgot the static nature of find()

You're also right that this is a wild idea. You've been really inventive but honestly, if I had to do this to solve my problem, I'd re-examine what the problem is. Because as inventive as this is....there's GOTTA be a better way.

You said you want to manage tables dynamically. What all does that entail? Have you considered not using models at all, but a type of Management component that can act on multiple tables - possibly dynamically creating SQL statements?

Something like:

$Manager = new Manager('blog');
$Manager->deleteWhere('id',23);
class Manager extends \Phalcon\Mvc\Component{
    private $table;

    public function __construct($table){
        $this->table = $table;
    }

    // Alternatively you could maybe have this accept an SQL clause and binding values
    public function deleteWhere($column,$value){
        // Obviously lots of room for SQL injections,
        // this is just a proof-of-concept
        $query = <<<SQL
            DELETE
            FROM
                $this->table
            WHERE
                $column = $value
        SQL;
        $this->DB->executeQuery($query);
    }
}

Another thought I had was - do you actually need multiple tables? Or can all this data sit in one table, use one Model, and have that Model's behaviour change depending on what type the row is (blog/image/file/etc)?

Necessity is the mother of all invention :)

For this project, the ability to manage multiple tables dynamically is not incidental, but rather the central purpose of the platform. Coming up with a maintainable solution is a must.

I have considered building something to manually construct queries, but I am quite fond of Phalcon's ORM, even more so with shiny new Phalcon 4 features.

Currently, its able to handle hundreds of other use cases in the context of this project brilliantly, this is the only place its stumbled so far.

I'm not sure if this is out of scope, but one potential option would be to make the internal class name customizable, so in Manager.zep instead of

public function setModelSource(<ModelInterface> model, string! source) -> void
{
    let this->sources[get_class_lower(model)] = source;
}

It could have a parameter for model class:

public function setModelSource(<ModelInterface> model, string! source, string! customClassName) -> void
{
    modelClass = customClassName ?? get_class_lower(model)

    let this->sources[class] = source;
}

I do not know Zephir, the above code is for demonstration purposes only

Alternatively, it could hash the anonymous class name and assign it a representitive name to refer to it keeping the model fully anonymous.

I'm fairly certain this would allow for anonymous subclasses. It could potentially have other side effects, but at least would fix the error I found.

If that was built into Phalcon, you could then create models completely programmatically, which would likely be helpful for a number of different use cases beyond mine.

Apparently, I'm not the only one running into this case, I found a thread on StackOverflow where they claimed to be able to solve the same issue in Laravel using anonymous subclasses: https://stackoverflow.com/a/53092769/225199

If you want to keep investigating anonymous classes, good luck. Not having any experience with them, I can't help with that.

Another thought:

Have you considered using the developer tools? https://docs.phalcon.io/4.0/en/devtools

Maybe instead of making 1000 dynamic models, you dynamically create the model definition for each table. So if a user wants to create a table called blogs, you create the table, then invoke the developer tools to create the model for it.

edited Jun '20

In that case the web server would need access to write to the filesystem with the new model definitions I believe, which would be a security concern. It would definitely make it a million times easier, but I don't think thats viable considering the risk it imposes.

I've thought about doing eval() as well, but that also has security and performance concerns.

Sorry, I know this is kind of a weird one

edited Jun '20

Web servers write to the file system all the time - thats how file uploads work. You could make it safe by restricting the characters of new table names to just [a-z0-9] - that's simple enough to regex.

Or, you could have a translation table that maps whatever the user entered to a safe, internal name. Then, when a Joe's table model is requested, a lookup is done that mapsJoe's table to user_23_table_98, and the User23Table98 model.