1

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.

ndm
  • 59,784
  • 9
  • 71
  • 110
Lajos Arpad
  • 64,414
  • 37
  • 100
  • 175

1 Answers1

2

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

ndm
  • 59,784
  • 9
  • 71
  • 110
  • Excellent point! If there is no direct solution, then this kind of active error raising on rule violation is a second-best approach. Will wait for other answers for a while though. – Lajos Arpad Feb 25 '22 at 10:53
  • @LajosArpad I don't think there really is a good solution for this, given how the query instantiation is hard coded, and how overriding in PHP works. The only thing I could imagine, would be to use a custom table factory, which wraps all table instances in a proxy that could "override" the `query()` method and conditionally return values based on whether the behavior is loaded or not. But that feels like a dirty hack. – ndm Feb 25 '22 at 16:10
  • I'm sure there is no good solution, this was also my suspicion. The only possibility was that Cake would take the PHP files and "prework" them. Example: if foo.php has a bar class and we tell CakePHP that bar should be different, then some engine could recreate foo accordingly. I hoped there was such a feature, but I considered it to be unlikely, since if such a feature existed, some kind of build operation would probably exist. – Lajos Arpad Feb 26 '22 at 17:08
  • In lack of that (at least in the project I'm working on), the second-best is to automatically poke developers violating these terms with a meaningful and easy-to-understand error message. This is why I have accepted your answer. – Lajos Arpad Feb 26 '22 at 17:08