В теме о событиях натолкнулся на вопрос о том, как имея объект комментария на сайте можно получить ссылку на материал, к которому этот комментарий оставлен, при условии, что комментарии могут иметь привязку к разным сущностям. Сам я тоже использую в своих проектах комментарии к разнородным сущностям, поэтому хочу поделиться своей реализацией.

Представим, что в нашем проекте есть некоторые комментируемые сущности (Новость, Пост, Работа в портфолио, Рецепт и т.п.), для которых нужно сделать возможность оставлять комментарии. Для универсальности было бы лучше хранить все комментарии в одной таблице. Естественно предположить, что эта таблица для разделения комментариев должна содержать поляmaterial_id и type.

Автор спрашивает:

…как в таком случае я получаю объект для которого был добавлен комментарий?

Задача (для примера): при добавлении нового комментария, мне необходимо проапдеэйтить запись для которой этот комментарий добавлялся (обновить счетчик комментариев в нем). Но такая модель не одна (Post, News, Blog и т.д для примера). Ну и хотелось бы сделать все это “красиво”; без лишней завязки на сущности.

С одной комментируемой сущностью никаких проблем нет. Отношение к материалам можно прописать явно в классе комментария:

class Comment extends CActiveRecord
{
    // ...
 
    public function relations(){
        return array(
            'material' => array(self::BELONGS_TO, 'Post', 'material_id'),
        ));
    }
}

При наличии не только модели Post, но и ещё нескольких так уже сделать нельзя.

Конечно, в модели комментария можно сделать метод Comment::getMaterial(), который бы на основании типа данного комментария искал и возвращал необходимый объект:

class Comment extends CActiveRecord
{
    // ...
 
    public function getMaterial(){
        $className = $this->type;
        $material = CActiveRecord::model($className)->findByPk($this->material_id);
    }
}

 

Но тогда мы лишимся прелестей жадной загрузки Comment::model()->with('material')->findAll(), и число запросов к БД возрастёт до числа комментариев.

Для решения данной проблемы можно поступить следующим образом:

  1. Создать базовый класс Comment с общей логикой работы;
  2. Для каждой комментируемой сущности создать лёгкие подклассы NewsCommentPostComment и т.д., в которых объявить свой тип и свою связь с материалом;
  3. Настроить отношения сущностей к своим комментариям по соответствующим типам.

Итак, для комментариев создаём базовый класс Comment:

/**
 * This is the model class for table "{{comment}}".
 *
 * The followings are the available columns in table '{{comment}}':
 * @property string $id
 * @property string $type
 * @property integer $material_id
 * @property string $user_id
 * @property integer $parent_id
 * @property string $date
 * @property string $name
 * ...
 * @property string $text
 * @property integer $public
 * @property integer $moder
 */
class Comment extends CActiveRecord
{
    protected $type_of_comment = '';
 
    // ...
    public function tableName()
    {
        return '{{comment}}'; // Общая таблица
    }   
 
    // Для автовыбора классов при поиске
    protected function instantiate($attributes)
    {
        $class = $attributes['type'] . 'Comment'; // Класс выбирается по полю type
        $model = new $class(null);
        return $model;
    }
 
    public function rules()
    {
        return array(
            array('material_id, type, text', 'required'),
            // закрываем поля от комментатора
            array('material_id, type', 'unsafe', 'on'=>'insert'),
 
            array('material_id, user_id, type', 'safe', 'on'=>'search'),
            // ...
        );
    }
 
    public function relations()
    {
        return array(       
            // Автор (если есть)
            'user'=>array(self::BELONGS_TO, 'User', 'user_id'),
            // Родитель (для древовидных комментариев, если Вы их используете)
            // 'parent'=>array(self::BELONGS_TO, 'Comment', 'parent_id'),
        );
    }
 
    public function search()
    {
        $criteria = new CDbCriteria;
 
        $criteria->compare('id',$this->id);
        if ($this->type_of_comment)
            $criteria->compare('type',$this->type_of_comment);
        else
            $criteria->compare('type',$this->type);
        $criteria->compare('material_id',$this->material_id);
        // ...
        $criteria->compare('public',$this->public);
        $criteria->compare('moder',$this->moder);
 
        return new CActiveDataProvider($this, array(
            'criteria'=>$criteria,
        ));
    }
 
    // scope
    public function material($id)
    {
        if ($id){
            $this->getDbCriteria()->mergeWith(array(
                'condition' => 'material_id=:id',
                'params'=>array(':id'=>$id),
            ));
        }
        return $this;
    }
 
    // scope
    public function type($type)
    {
        if ($type){
            $this->getDbCriteria()->mergeWith(array(
                'condition'=>'type=:type',
                'params'=>array(':type'=>$type),
            ));
        }
        return $this;
    }
 
    // переопределяем для поиска по типу
    public function find($condition='', $params=array())
    {
        $this->type($this->type_of_comment);
        return parent::find($condition, $params);
    }
 
    // переопределяем для поиска по типу
    public function findAll($condition='', $params=array())
    {
        $this->type($this->type_of_comment);
        return parent::findAll($condition, $params);
    }
 
    // переопределяем для поиска по типу
    public function findAllByAttributes($attributes, $condition='', $params=array())
    {
        $this->type($this->type_of_comment);
        return parent::findAllByAttributes($attributes, $condition, $params);
    }
 
    // переопределяем для поиска по типу
    public function count($condition='', $params=array())
    {
        $this->type($this->type_of_comment);
        return parent::count($condition, $params);
    }
 
    // храним $this->isNewRecord для проверки в afterSave()
    protected $_isNew; 
 
    protected function beforeSave()
    {
        $this->initType();
 
        // комментарии без типа в базу не пройдут!
        if (!$this->type) 
            return false;
 
        $this->_isNew = $this->isNewRecord;
        return parent::beforeSave();
    }
 
    protected function initType()
    {
        if (!$this->type)
            $this->type = $this->type_of_comment;
    }
 
    protected function afterSave()
    {
        if ($this->_isNew){
            $this->sendNotifications();
        }
        $this->updateMaterial();
        parent::afterSave();
    }
 
    protected function afterDelete()
    {
        $this->updateMaterial();
        parent::afterDelete();
    }
 
    // отправка уведомлений пользователям
    protected function sendNotifications()
    {
        // ...
    }
 
    // вызов обновления материала
    protected function updateMaterial()
    {
        if ($this->type && $this->material instanceof ICommentDepends){
            $this->material->updateCommentsState($this);
        }
    }
 
    // другие общие методы
    // ...
}

…и сколько угодно наследников (по числу наших комментируемых сущностей), в которых определены типы и ссылки на связанный материал:

class PostComment extends Comment
{
    // совпадает с префиксом подкласса комментария и именем класса сущности
    const TYPE_OF_COMMENT = 'Post';
 
    public static function model($className=__CLASS__){
        return parent::model($className);
    }
 
    public function __construct($scenario='insert'){
        // устанавливаем наш тип полю базового класса
        $this->type_of_comment = self::TYPE_OF_COMMENT;
        parent::__construct($scenario);
    }
 
    // Добавим ссылку на нашего специфического владельца
    public function relations(){ 
        return array_merge(parent::relations(), array(
            'material'=>array(self::BELONGS_TO, self::TYPE_OF_COMMENT, 'material_id'),
        ));
    }
}

Так как мы будем теперь работать с разными классами, то для универсальности (да и просто для удобства), создадим отдельную форму комментария:

class CommentForm extends CFormModel
{
    public $name;
    public $email;
    public $site;
    public $text;
 
    public function rules() {
        return array(
            array('text', 'required', 'message' => 'Напишите текст комментария'),
 
            array('name', 'length', 'max'=>255),
            array('name', 'required', 'message' => 'Представьтесь, пожалуйста', 'on'=>'anonim'),
 
            array('email', 'length', 'max'=>255),
            array('email', 'email', 'message' => 'Неверный формат E-mail адреса'),
            array('email', 'required', 'message' => 'Введите Email', 'on'=>'anonim'),
 
            array('site', 'url'),
            array('site', 'length', 'max'=>255),
        );
    }
 
    public function attributeLabels(){
        return array(
            'name' => 'Ваше имя',
            'email' => 'Ваш Email',
            'site' => 'Ваш сайт',
            'text' => 'Комментарий',
        );
    }
}

Теперь мы можем легко работать как с NewsCommentPostComment и т.д., так и непосредственно с базовым классом Comment:

// Вернёт все комментарии
$allComments = Comment::model()->findAll();
 
// Вернёт комментарии блога, так как мы переопределили метод findAll()
$postComments = PostComment::model()->findAll();
 
// Тоже вернёт комментарии блога, но уже тип можно выбирать динамически
$postComments = Comment::model()->type(PostComment::TYPE_OF_COMMENT)->findAll(); 
 
// Вернёт комментарии к записи блога, так как имена типа и класса сущности совпадают  
$сomments = Comment::model()->type(get_class($post))->material($post->id)->findAll();
// Создаём комментарий нужного типа статически
$comment = new PostComment();
 
// Создаём комментарий нужного типа данимически
$comment = new Comment();
$comment->type = PostComment::TYPE_OF_COMMENT;

Добавление комментария для записи блога в контроллере может выглядеть так:

$commentForm = new CommentForm();
if (isset($_POST['CommentForm']){
    $commentForm->attributes = $_POST['CommentForm'];
    if ($commentForm->validate()){
        $comment = new PostComment();
        $comment->attributes = $_POST['CommentForm'];
        $comment->material_id = $post->id;
        if ($comment->save()){
            Yii::app()->user->setFlash('comment-form', 'Комментарий отправлен');
        }
    }
}

Теперь нам нужно немного видоизменить модели материалов, а именно в условия отношений добавить поиск по типу:

class Post extends CActiveRecord implements ICommentDepends
{
    public function relations()
    {
        return array(
            /* если пригодится отношение $model->comments, то раскомментируйте 
            'comments' => array(self::HAS_MANY, 'Comment', 'material_id',
                'condition'=>'comments.public=1 AND type=:type',
                'params'=>array(':type'=>PostComment::TYPE_OF_COMMENT),
                'order'=>'comments.id DESC'
            ),
            */
        );
    }
 
    // Реализуйте интерфейс ICommentDepends, если хотите получать оповещения от ваших комментариев
    // для своевременного обновления их числа в соответствующем поле или любых других целей
    public function updateCommentsState($comment)
    {
        $comments_count = BlogPostComment::model()->material($this->id)->count('public=1');
        $comments_new_count = BlogPostComment::model()->material($this->id)->count('public=1 AND moder=0');
 
        $this->updateByPk($this->id, array('comments_count' => $comments_count));
        $this->updateByPk($this->id, array('comments_new_count' => $comments_new_count));
    }
}

Интерфейс ICommentDepends:

interface ICommentDepends
{
    public function updateCommentsState($comment);
}

При выводе комментариев нам больше не нужно самим заботиться об их типах. При поиске экземпляров методами Comment::model()->findAll()Comment::model()->find() иComment::model()->findByPk() всю магическую работу выполняет переопределённый методComment::instantiate(), подсказанный в форуме. Именно он создаёт экземпляр нужного класса для каждого комментария в выборке.

Таким образом, отношение material будет всегда вести на экземпляр своей сущности, и мы можем работать с любыми комментариями полиморфно.

Вывод в одной ленте последних комментариев разных типов со ссылкой на якорь:

<?php
$criteria = new CDBCriteria();
$criteria->limit = 10;
$criteria->order = 'date DEC';
$criteria->with = array('material');
$comments = Comment::model()->findAll($criteria);
?>
<ul class="last_comments">
<?php foreach ($comments as $comment): ?>
    <li><a href="<?php echo $comment->material->url; ?>#comment_<?php echo $comment->id; ?>"><?php echo strip_tags($comment->text); ?></a></li>
<?php endforeach; ?>
</ul>

Для лёгкого получения ссылки $material->url нужно определить метод-геттер getUrl() в модели каждой сущности.

Для дальнейшего повышения универсальности полезно вывести добавление комментариев из кода контроллера в отдельный виджет. Это сильно облегчит организацию комментирования для новых сущностей.

class CommentsWidget extends СWidget
{
    public $material_id;
    public $type;
 
    protected $user;
 
    public function run()
    {
        if (!$this->user) $this->user = User::model()->findByPk(Yii::app()->user->getId());
        if (!$this->material_id) throw new CException('Not setted a Material_ID');
        if (!$this->type) throw new CException('Not setted a TYPE of comments');
 
        $form = new CommentForm();
        if (!$this->user) $form->scenario = 'anonim';
 
        if(isset($_POST['CommentForm']))
        {           
            $form->attributes = $_POST['CommentForm'];
 
            if($form->validate()){
 
                $className = $this->type . 'Comment';
 
                $comment = new $className;
                $comment->attributes = $_POST['CommentForm'];
                $comment->material_id = $this->material_id;
                $comment->public = 1;
                $comment->moder = 0;                
                if ($this->user)
                    $comment->user_id = $this->user->id;
 
                if ($comment->save()){
                    Yii::app()->user->setFlash('comment-form','Ваш коментарий добавлен');
                    Yii::app()->controller->refresh();
                }
            }
        }
 
        $comments = Comment::model()
            ->type($this->type)
            ->material($this->material_id)
            ->with('user')
            ->findAll(array('order'=>'t.id ASC'));
 
        $this->render('Comments/comments', array(
            'comments'=>$comments,
            'form'=>$form,
            'user'=>$this->user,
            'material_id'=>$this->material_id,
            'type'=>$this->type,
        ));
    }
}

Этот виджет в своих представлениях должен полностью содержать вывод комментариев и формы комментирования.

Таким образом, для добавления комментирования к записям блога необходимо всего лишь создать подкласс PostComment с типом Post и отношением material, и в представление view.php после вывода содержимого записи добавить вызов виджета комментариев:

<?php $this->widget('CommentsWidget', array(
    'material_id'=>$post->id,
    'type'=>PostComment::TYPE_OF_COMMENT,
)); ?>

И так со всеми сущностями. Код контроллера дополнять не нужно:

class PostController extends Controller
{
    // ...
 
    public function actionView()
    {
        $post = $this->loadModel();
 
        $this->render('view', array(
            'post'=>$post,
        ));
    }
}

Вспомним, что если сущность должна получать уведомление об изменениях среди её комментариев, то её модель должна реализовать интерфейс ICommentDepends.

Теперь все комментарии на сайте администратор может обслуживать в том же списке и работая с классом Comment как и раньше (используя тот же самый методComment::model()->with('material', 'user')->findAll() или CGridView с провайдером$model->search(), и те же отношения $comment->material и $comment->user) не обращая внимания на типы комментариев.

 

Взято отсюда

 

 

 

Комментарии

comments powered by Disqus