phpcakephpormbehaviorcakephp-3.x

How can I define a replicate behavior for the execute method without changing all the models' namespace?


I have a class like this:

<?php

namespace App\ORM;

use Cake\ORM\Query as ORMQuery;
use Cake\Database\ValueBinder;
use Cake\Datasource\ConnectionManager;

class Query extends ORMQuery
{

    /*Some stuff which has no relevance in this question*/

    protected $mainRepository;

    protected function isReplicate()
    {
        return ($this->mainRepository && $this->mainRepository->behaviors()->has('Replicate') && ($this->getConnection()->configName() !== 'c'));
    }

    public function __construct($connection, $table)
    {
        parent::__construct($connection, $table);
        $this->mainRepository = $table;
    }

    public function execute()
    {
        if ($this->isReplicate()) {
            $connection = $this->getConnection();
            $replica = clone $this;
            $replica->setConnection(ConnectionManager::get('c'));
            $replica->execute();
        }
        $result = parent::execute();
        return $result;
    }
}

This works well, that is, there is a central c server and there are other servers that are separated by districts. There are some tables that are to be refreshed on c when something was executed for them on a district server. Table models that are to be replicated look like this:

<?php

namespace App\Model\Table;

use App\ORM\Table;

class SomeNameTable extends Table
{
    public function initialize(array $config)
    {
        /*Some initialization that's irrelevant from the problem's perspective*/
    }
}

Everything works, but there is a catch. The use App\ORM\Table; statement specifies that my own Table implementation should be used and that Table implementation ensures that once query is being called, my Query class will be used, described in the first code chunk from this question. This is my Table class:

<?php

namespace App\ORM;

use Cake\ORM\Table as ORMTable;

class Table extends ORMTable
{
    public function query()
    {
        return new Query($this->getConnection(), $this);
    }
}

As mentioned earlier, everything works as expected, but this approach effectively means that I will need to change the namespace of the base class for all the models where this replication behavior is needed. This is surely easy to do now, but in the future I worry that some new tables are to be replicated and developers working on that will not read documentation and best practices section, inheriting the model directly from Cake's class with the same name. Is there a way to tell Cake that I would like all models fulfilling some logical validation, (for example for models that have a behavior) will use the overriden execute method? In general, the answer to such questions would be a "no", but I wonder whether Cake has a feature for overriding code behavior based on some rules.

Ex.

function isReplicate($model) {
    return ($model->behaviors()->has('Replicate'));
}

The function above could determine whether the new execute is preferred or the old, Cake one.


Solution

  • Not really, no, at least as far as I understand your question.

    There's events that you could utilize, Model.initialize for example, it would allow you to check whether the initialized model has a specific behavior loaded, but you would not be able to change how the inherited query() method behaves.

    You could use it for validation purposes though, so that developers that don't follow your documentation get slapped in the face when they don't extend your base table class, eg check whether it's being extended and throw an exception if that's not the case, something along the lines of this:

    // in `Application::bootstrap()`
    
    \Cake\Event\EventManager::instance()->on(
        'Model.initialize',
        function (\Cake\Event\EventInterface $event) {
            $table = $event->getSubject();
            assert($table instanceof \Cake\ORM\Table);
    
            if (
                $table->behaviors()->has('Replicate') &&
                !is_subclass_of($table, \App\Model\Table\AppTable::class)
            ) {
                throw new \LogicException(
                    'Tables using the Replicate behavior must extend \App\Model\Table\AppTable'
                );
            }
        }
    );
    

    See also