By Reference Model Caching?

Hi everyone,

There is more than likely a better way to achieve what I want, by all means, please suggest an alternative method - I'm all ears.

I use afterUpdate to trigger a request to an external API. This trigger can be disabled via a private model property. As you can see in the "renameBreed" method I call $this->update() which will save the breed model to save in the "_postSaveRelated" method in \Phalcon\Mvc\Model. Because Phalcon does a findFirst to re-retrieve the relationship, it creates a new instance of the model meaning the value of any private properties are reset.

I've tried model caching hoping it would help preserve this but it still seems to cache only the data (obvisously of only public properties). The cache is started prior to "intialize" as well - so any userland data transformation is lost. If it were by reference then this would let us work around that. Unfortuntaly you can't save objects by reference if they have magic methods :|

Effectively, what I'm asking for is a way to retrieve the same object when you get a relation;

$dog = \Dog::findFirst();
var_dump(spl_object_hash($dog->breed), spl_object_hash($dog->breed));

My way of trying to get around this problem is without doubt the wrong approach. So if some kind soul can suggest a better way around naviating my problem that'd be wonderful. I feel like events might be a good place to start but I haven't used them enough to know if it's the right place to start.

Thanks everyone :)

$dog = \Dog::findFirst();
$dog->renameBreed('potato');
class Base extends \Phalcon\Mvc\Model
{
    public $id;

    public function initialize()
    {
        $this->useDynamicUpdate(true);
        $this->keepSnapshots(true);

        $this->setReadConnectionService('db');
        $this->setWriteConnectionService('db');

        $eventsManager = $this->getDI()->getShared('eventsManager');
        $eventsManager->attach('model', function($event, $model) {
            return true;
        });
        $this->setEventsManager($eventsManager);
        unset($eventsManager);
    }
}
class Dog extends \Base
{
    public $name;
    public $breed_id;

    private $localUpdate = false;

    public function initialise()
    {
        $this->hasOne('breed_id', 'Dog/Breed', 'id', ['alias' => 'breed']);
    }

    public function renameBreed($breedName)
    {
        $breed = $this->breed;
        $breed->localUpdate(); // Only update locally
        $breed->name = $breedName;

        return $this->update();
    }
}
namespace Dog;
class Breed extends \Base
{
    public $name;

    private $localUpdate = false;

    public function localUpdate()
    {
        $this->localUpdate = !$this->localUpdate;
    }

    public function afterUpdate()
    {
        if (!$this->localUpdate) {
            // Update dog breed at animal registry
            // Or just do stuff
        }
    }
}
edited 27d ago

If I understand correctly, you'd like to serialize protected members, namely all of this: https://github.com/phalcon/cphalcon/blob/master/phalcon/mvc/model.zep#L88-L114
PHP gives you a __sleep() magic method that can do this.
All you do is return an array with all the properties it should serialize, whether it's private, protected, or public, and it will serialize for you.

The model's serialize method may be of interest toyou:
https://github.com/phalcon/cphalcon/blob/master/phalcon/mvc/model.zep#L4518

You can use __wakeup() to re-establish the relationships.

All this is really hacky, and you should ask yourself whether you're pushing the ORM too far. Just write this stuff by hand if you need more flexibility.

I don't however understand what you mean by this:

    public function afterUpdate()
    {
        if (!$this->localUpdate) {
            // Update dog breed at animal registry
            // Or just do stuff
        }
    }

Is your issue trying to write logic to go in there?

Or are you asking how you can cache the model to save yourself database hits?

If it's the latter, I'd suggest writing another layer of logic for your application. Only use the ORM on a cache miss.

Utilizing the repository pattern for your business logic, is probably the way to go here so you can handle cache e.g. Redis or Memcached, and your Model Entities that interact with the database, at the same time.

You should be caching the end result so you never have to interact with the ORM in the first place. If there's a cache miss later, then utilize the ORM then but not before.

So rather than trying to fake an ORM state through cache, you'd use the repository pattern that would functon equally through either cache or through the ORM.

If I'm way off, please clarify a bit further what you mean by "reference model caching".

What happens at this line?

// Update dog breed at animal registry

If you're updating an external API, why are you pulling from it again? Is the external API and your database in sync?
How does that work?

edited 27d ago

Hi Kevin,

I appreciate your reply and apologise for making my post so confusing.

So I'm sending to an external API after the ORM performs a successful update. The part that sends to an external API is wrapped in a condition.

if ($this->localUpdate)

In the renameBreed method it can be expected that when you call;

$this->update();

It'll updated the related model (breed) as per _postSaveRelatedRecords. This method fetches the relationship "breed" with getRelated which will fetch a fresh instance of the model/relationship despite it being retrieved once already. So regardless what I set the localUpdate variable to it'll be false because it fetches a new instance of the relationship.

Hopefully I misunderstand, but this can't be overcome by reusing related records because this only caches the results that would be assigned to that model. So any other 'getRelated' calls would return a fresh instance of the model with the data that was retreived from the DB initially. Any in memory changes aren't there.

If I misunderstand and I'm hoping I do; the cache is kept up to date and I could use __sleep() to save the private property.

What I meant when I said "by reference caching" is that the model is saved in memory by reference and when you do any new calls to it you get the same object which you could confirm by this;

spl_object_hash($this->breed) == spl_object_hash($this->breed);

I hope that made sense and I didn't go off on a tangent :/

Thanks :)

edited 19d ago

Let's see if I understand this: You want to be able to rename a breed, and have a 3rd-Party API be updated if the update works successfully. You also want to be able to disable the third-party updating.

I think you have some fundamental, but minor problems you need to get around.

You shouldn't be using the Dog model to rename a Breed. That's muddying the waters and creating unnecessary dependencies.

I'm also not sure why Breed::$localUpdate needs to be a private variable, when you've got a public method that simply sets the variable anyway. Further, I strongly suggest you explicitly set true/false for localUpdate instead of a toggle. That's asking for trouble if somewhere you accidentally call localUpdate() one to many times.

I think this should be how you start this action:

$Dog = \Dog::findFirst();
$Dog->breed->rename('potato');

Or, to update the API:

$Dog = \Dog::findFirst();
$Dog->breed->localUpdate = true;
$Dog->breed->rename('potato');

And I don't think you really need to care about the cache:

class Breed extends \Base{

    public $name;
    public $localUpdate = false;

    public function rename($newName){
        $this->name = $newName;
        if($this->update()){
            if($this->localUpdate){
                $this->renameRemote($newName);
            }
            else{
                // other stuff
            }
        }
    }
}

where renameRemote() is the method that talks to the remote API.