Запрет на сохранение xPDO-объекта, если не заполнены необходимые данные

Материал для экспертов. Многие сложные моменты не будут подробно объясняться, требуется хорошее знание xPDO.

Небольшое вступление. Заморочился я тут на сайте реализовать теги для топиков (для SEO отличная штука). Создал новый класс SocietyTopicTag со следующими колонками: id, topic_id, tag, active. Какие тут главные моменты, побудившие написать топик?

  1. Поле tag не может быть пустым.
  2. Поле topic_id так же не может быть пустым.

И вот далее мы разберемся с тем, как это предусмотреть, и какие тут есть подводные камни...

Начнем с простого - с проверки поля tag и запрета сохранения. Сразу посмотрим конечный код класса SocietyTopicTag:

class SocietyTopicTag extends xPDOSimpleObject {

    public function save($cacheFlag= null) {

        $this->tag = trim($this->tag);

        if(!$this->tag){

            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "[- ". __CLASS__ ." -]. Column tag can not be empty");

            return false;

        }

        if(!$this->getOne('Topic')){

            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "[- ". __CLASS__ ." -]. Topic required");

            return false;

        }

        return parent::save($cacheFlag);

    }    

}
  1. Перегружаем оригинальный метод save(). Теперь, если что-то не так, мы можем вернуть false, что будет означать, что объект не был сохранен.
  2. Проверяем наличие значения у поля tag.
       if(!$this->tag){

            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "[- ". __CLASS__ ." -]. Column tag can not be empty");

            return false;

        }

Теперь вот такой код не создаст нового объекта:

$tag = $modx->newObject('SocietyTopicTag', array(

    "tag"   => "",

));

if(!$tag->save()){

    print "Error";

}

else{

    print "Saved";

}

Так как tag не заполнен, то сохранение объекта прервется. Это исключит сохранение объекта с незаполненым полем.

А вот с topic_id несколько сложнее... Почему вот мы не стали проверять поле topic_id так же, как и tag, просто на наличие значения?

Во-первых, топик для этого тега может быть еще новый и не сохранен в БД, и соответственно у него и не будет id. А если у топика нет id, то мы просто не можем записать его в качестве topic_id. К примеру, в таком случае бы вот этот код не сработал:

$tag = $modx->newObject('SocietyTopicTag', [

    "tag"   => "DSfdsfdsF", 

]);

$r = $modx->newObject('modResource');

$r->addMany($tag);

$r->save();

Во-вторых, можно было бы указать id несуществующего топика. В случае проверки this>topicidунаспростовыполняетсяпроверказначениянаналичие,нонетпроверкиналичиядокумента.Авотвслучаепроверкиобъектаthis->topic_id у нас просто выполняется проверка значения на наличие, но нет проверки наличия документа. А вот в случае проверки объекта this->Topic, даже если мы просто передали значение topic_id, xPDO попытается выполнить запрос к БД и получить объект топика, и если его не получит, то тег не будет сохранен.

В итоге, представленная проверка сработает во всех случаях попытки сохранения как нового тега, так и обновления существующего.

P.S. Понятное дело, что в данном случае важную роль играет описание composite/aggregates-связей классов modResource и SocietyTopicTag.

В SocietyTopicTag map-файл дописываем:

  'aggregates' => 

  array (

    'Topic' => 

    array (

      'class' => 'modResource',

      'local' => 'topic_id',

      'foreign' => 'id',

      'cardinality' => 'one',

      'owner' => 'foreign',

    ),

  ),

А вот в modResource мы просто так не вклинимся, так как это ядро MODX-а, и мы его не можем трогать. Но есть лазейка - meta-файл пакета (metadata.mysql.php). Вот туда мы и дописываем связь для modResource:

$this->map['modResource']['composites']['TopicTags'] = array(

    'class' => 'SocietyTopicTag',

    'local' => 'id',

    'foreign' => 'topic_id',

    'cardinality' => 'many',

    'owner' => 'local',

);

Чтобы этот момент был чуть более понятен, я специально перенес свой старый топик.