From 902ffdb71ac185cec4f85e7eeca7f552aa204e50 Mon Sep 17 00:00:00 2001 From: Zura Sekhniashvili Date: Thu, 26 Jan 2017 13:02:44 +0400 Subject: [PATCH] Add migration to convert article and article category to many to many --- controllers/ArticleController.php | 2 + ..._070327_article_category_article_table.php | 55 ++++++++++ ...rate_to_article_category_article_table.php | 48 +++++++++ models/Article.php | 100 +++++++++++++----- models/ArticleCategory.php | 77 +++++++------- models/ArticleCategoryArticle.php | 87 +++++++++++++++ models/query/ArticleQuery.php | 75 ++++++++++--- models/search/ArticleSearch.php | 3 +- views/article/_form.php | 12 ++- views/article/index.php | 7 -- 10 files changed, 369 insertions(+), 97 deletions(-) create mode 100644 migrations/m170126_070327_article_category_article_table.php create mode 100644 migrations/m170126_072252_migrate_to_article_category_article_table.php create mode 100644 models/ArticleCategoryArticle.php diff --git a/controllers/ArticleController.php b/controllers/ArticleController.php index 81ff82f..b657c54 100644 --- a/controllers/ArticleController.php +++ b/controllers/ArticleController.php @@ -8,6 +8,7 @@ use centigen\i18ncontent\models\ArticleCategory; use centigen\i18ncontent\web\Controller; use Yii; +use yii\helpers\ArrayHelper; use yii\web\NotFoundHttpException; use yii\filters\VerbFilter; @@ -99,6 +100,7 @@ public function actionCreate() public function actionUpdate($id) { $model = $this->findModel($id); + $model->category_ids = ArrayHelper::getColumn($model->articleCategoryArticles, 'category_id'); $articleCategories = ArticleCategory::getCategories(); $locales = BaseHelper::getAvailableLocales(); diff --git a/migrations/m170126_070327_article_category_article_table.php b/migrations/m170126_070327_article_category_article_table.php new file mode 100644 index 0000000..3270d71 --- /dev/null +++ b/migrations/m170126_070327_article_category_article_table.php @@ -0,0 +1,55 @@ +createTable(\centigen\i18ncontent\models\ArticleCategoryArticle::tableName(), [ + 'id' => Schema::TYPE_PK, + 'article_id' => Schema::TYPE_INTEGER . '(11) NOT NULL', + 'category_id' => Schema::TYPE_INTEGER . '(11) NOT NULL', + 'created_at' => Schema::TYPE_INTEGER, + 'updated_at' => Schema::TYPE_INTEGER, + ]); + +// $articles = \centigen\i18ncontent\models\Article::find()->all(); +// $trans = Yii::$app->db->beginTransaction(); +// foreach ($articles as $article) { +// $articleArticleCategory = +// } + + $this->createIndex('idx_article_category_article_article_id', \centigen\i18ncontent\models\ArticleCategoryArticle::tableName(), 'article_id'); + $this->addForeignKey('fk_article_category_article_article', \centigen\i18ncontent\models\ArticleCategoryArticle::tableName(), 'article_id', + \centigen\i18ncontent\models\Article::tableName(), 'id', 'cascade', 'cascade'); + $this->createIndex('idx_article_category_article_article_category_id', \centigen\i18ncontent\models\ArticleCategoryArticle::tableName(), 'article_category_id'); + $this->addForeignKey('fk_article_category_article_article_category', \centigen\i18ncontent\models\ArticleCategoryArticle::tableName(), 'article_category_id', + \centigen\i18ncontent\models\ArticleCategory::tableName(), 'id', 'cascade', 'cascade'); + + + } + + public function down() + { + $this->dropForeignKey('fk_article_category_article_article', \centigen\i18ncontent\models\ArticleCategoryArticle::tableName()); + $this->dropIndex('idx_article_category_article_article_id', \centigen\i18ncontent\models\ArticleCategoryArticle::tableName()); + + $this->dropForeignKey('fk_article_category_article_article_category', \centigen\i18ncontent\models\ArticleCategoryArticle::tableName()); + $this->dropIndex('idx_article_category_article_category_id', \centigen\i18ncontent\models\ArticleCategoryArticle::tableName()); + + $this->dropTable(\centigen\i18ncontent\models\ArticleCategoryArticle::tableName()); + } + + /* + // Use safeUp/safeDown to run migration code within a transaction + public function safeUp() + { + } + + public function safeDown() + { + } + */ +} diff --git a/migrations/m170126_072252_migrate_to_article_category_article_table.php b/migrations/m170126_072252_migrate_to_article_category_article_table.php new file mode 100644 index 0000000..c3bf0d9 --- /dev/null +++ b/migrations/m170126_072252_migrate_to_article_category_article_table.php @@ -0,0 +1,48 @@ +all(); + + $trans = Yii::$app->db->beginTransaction(); + foreach ($articles as $article){ + $articleCategoryArticle = new \centigen\i18ncontent\models\ArticleCategoryArticle(); + $articleCategoryArticle->article_category_id = $article->category_id; + $articleCategoryArticle->article_id = $article->id; + if (!$articleCategoryArticle->save()){ + $trans->rollBack(); + return false; + } + } + + $trans->commit(); + + $this->dropForeignKey('fk_article_category', \centigen\i18ncontent\models\Article::tableName()); + $this->dropIndex('idx_category_id', \centigen\i18ncontent\models\Article::tableName()); + $this->dropColumn(\centigen\i18ncontent\models\Article::tableName(), 'category_id'); + + return null; + } + + public function down() + { + echo "m170126_072252_migrate_to_article_category_article_table cannot be reverted.\n"; + + return false; + } + + /* + // Use safeUp/safeDown to run migration code within a transaction + public function safeUp() + { + } + + public function safeDown() + { + } + */ +} diff --git a/models/Article.php b/models/Article.php index 000dabb..16cbc9e 100644 --- a/models/Article.php +++ b/models/Article.php @@ -2,6 +2,7 @@ namespace centigen\i18ncontent\models; +use centigen\base\helpers\UtilHelper; use centigen\i18ncontent\models\query\ArticleQuery; use trntv\filekit\behaviors\UploadBehavior; use Yii; @@ -9,6 +10,7 @@ use yii\behaviors\SluggableBehavior; use yii\behaviors\TimestampBehavior; use yii\db\ActiveQuery; +use yii\helpers\ArrayHelper; /** * This is the model class for table "article". @@ -22,7 +24,6 @@ * @property array $attachments * @property integer $author_id * @property integer $updater_id - * @property integer $category_id * @property integer $status * @property integer $published_at * @property integer $position @@ -31,7 +32,7 @@ * * @property string $title * @property string $body - * @property ArticleCategory $category + * @property ArticleCategoryArticle[] $articleCategoryArticles * @property ArticleAttachment[] $articleAttachments * @property ArticleTranslation[] $translations * @property ArticleTranslation $activeTranslation @@ -56,6 +57,8 @@ class Article extends TranslatableModel public $articleCount = null; + public $category_ids = []; + /** * @author Zura Sekhniashvili * @var ArticleTranslation[] @@ -126,17 +129,16 @@ public function behaviors() public function rules() { return [ - [['category_id'], 'required'], [['slug'], 'unique'], [['published_at'], 'default', 'value' => time()], - [['published_at'], 'filter', 'filter' => 'strtotime', 'when' => function($model) { + [['published_at'], 'filter', 'filter' => 'strtotime', 'when' => function ($model) { return is_string($model->published_at); }], - [['category_id'], 'exist', 'targetClass' => ArticleCategory::className(), 'targetAttribute' => 'id'], [['author_id', 'updater_id', 'position', 'status'], 'integer'], [['slug', 'thumbnail_base_url', 'thumbnail_path', 'url'], 'string', 'max' => 2024], [['view'], 'string', 'max' => 255], - [['attachments', 'thumbnail', 'published_at'], 'safe'] + [['attachments', 'thumbnail', 'published_at'], 'safe'], + ['category_ids', 'each', 'rule' => ['integer']], ]; } @@ -149,12 +151,12 @@ public function attributeLabels() 'id' => Yii::t('i18ncontent', 'ID'), 'slug' => Yii::t('i18ncontent', 'Slug'), 'view' => Yii::t('i18ncontent', 'Article View'), + 'category_ids' => Yii::t('i18ncontent', 'Article Categories'), 'thumbnail' => Yii::t('i18ncontent', 'Thumbnail'), 'position' => Yii::t('i18ncontent', 'Position'), 'url' => Yii::t('i18ncontent', 'Url'), 'author_id' => Yii::t('i18ncontent', 'Author'), 'updater_id' => Yii::t('i18ncontent', 'Updater'), - 'category_id' => Yii::t('i18ncontent', 'Category'), 'status' => Yii::t('i18ncontent', 'Published'), 'published_at' => Yii::t('i18ncontent', 'Published At'), 'created_at' => Yii::t('i18ncontent', 'Created At'), @@ -181,9 +183,9 @@ public function getUpdater() /** * @return \yii\db\ActiveQuery */ - public function getCategory() + public function getArticleCategoryArticles() { - return $this->hasOne(ArticleCategory::className(), ['id' => 'category_id']); + return $this->hasMany(ArticleCategoryArticle::className(), ['article_id' => 'id']); } /** @@ -194,6 +196,55 @@ public function getArticleAttachments() return $this->hasMany(ArticleAttachment::className(), ['article_id' => 'id']); } + public function save($runValidation = true, $attributeNames = null) + { + $transaction = Yii::$app->db->beginTransaction(); + if (parent::save()){ + + $existingCategoryIds = ArrayHelper::getColumn($this->articleCategoryArticles, 'category_id'); + $toDeleteCategoryIds = array_diff($existingCategoryIds, $this->category_ids); + $toAddCategoryIds = array_diff($this->category_ids, $existingCategoryIds); +// \centigen\base\helpers\UtilHelper::vardump($toDeleteCategoryIds, $toAddCategoryIds); +// exit; + if ($this->removeCategories($toDeleteCategoryIds) && $this->addCategories($toAddCategoryIds)){ + $transaction->commit(); + return true; + } + } + $transaction->rollBack(); + return false; + } + + protected function removeCategories($categoryIds) + { + if (empty($categoryIds)){ + return true; + } + ArticleCategoryArticle::deleteAll(['category_id' => $categoryIds]); + return true; + } + + protected function addCategories($categoryIds) + { + if (empty($categoryIds)){ + return true; + } + $data = []; + foreach ($categoryIds as $category_id){ + $data[] = [ + 'article_id' => $this->id, + 'category_id' => $category_id, + 'updated_at' => time(), + 'created_at' => time() + ]; + } + + Yii::$app->db->createCommand()->batchInsert(ArticleCategoryArticle::tableName(), + ['article_id', 'category_id', 'updated_at', 'created_at'], $data)->execute(); + + return true; + } + /** * Find Article-s by category. Return array of Article or ActiveQuery * @@ -206,10 +257,8 @@ public static function getArticlesByCategory(ArticleCategory $cat, $getQuery = t { $query = Article::find() ->with('activeTranslation') - ->where([ - 'category_id' => $cat->id, - 'status' => self::STATUS_PUBLISHED - ]); + ->byCategoryId($cat->id) + ->published(); return $getQuery ? $query : $query->all(); } @@ -222,15 +271,11 @@ public static function getArticlesByCategory(ArticleCategory $cat, $getQuery = t */ public static function getByCategorySlug($slug) { - return Article::find() - ->from(self::tableName().' a') - ->innerJoin('{{%article_category}} ac', 'ac.id = a.category_id') + return Article::find()->byCategorySlug($slug) + ->categoryActive() + ->published() ->with('activeTranslation') - ->where([ - 'ac.slug' => $slug, - 'a.status' => self::STATUS_PUBLISHED, - 'ac.status' => self::STATUS_PUBLISHED - ])->all(); + ->all(); } /** @@ -244,10 +289,9 @@ public static function getBySlug($slug) { return Article::find() ->with('activeTranslation') - ->where([ - 'slug' => $slug, - 'status' => self::STATUS_PUBLISHED - ])->one(); + ->bySlug($slug) + ->published() + ->one(); } /** @@ -274,10 +318,10 @@ public function getShortDescription() return $this->activeTranslation ? $this->activeTranslation->getShortDescription() : ''; } - public function getThumbnailUrl() + public function getThumbnailUrl() { - if ($this->thumbnail_path){ - return Yii::getAlias('@storageUrl') . '/source/'.ltrim($this->thumbnail_path, '/'); + if ($this->thumbnail_path) { + return Yii::getAlias('@storageUrl') . '/source/' . ltrim($this->thumbnail_path, '/'); } return null; } diff --git a/models/ArticleCategory.php b/models/ArticleCategory.php index bc7f1ed..9a0a63e 100644 --- a/models/ArticleCategory.php +++ b/models/ArticleCategory.php @@ -15,7 +15,7 @@ * @property string $view * @property integer $status * - * @property Article[] $articles + * @property ArticleCategoryArticle[] $articlesCategoryArticles * @property ArticleCategory $parent * @property ArticleCategoryTranslation $activeTranslation * @property ArticleCategoryTranslation[] $translations @@ -53,42 +53,6 @@ public static function find() return (new ArticleCategoryQuery(get_called_class()))->with('activeTranslation'); } - /** - * Get ArticleCategory id by its slug - * - * @author Zura Sekhniashvili - * @param $slug - * @return int|mixed - */ - public static function getIdBySlug($slug) - { - $self = self::find()->where(['slug' => $slug])->one(); - return $self ? $self->id : -1; - } - - /** - * Get all ids of ArticleCategory which are direct children of given category plus id of given category - * - * @author Zura Sekhniashvili - * @param $slug - * @return array - */ - public static function getIdsBySlug($slug) - { - $ids = []; - $id = self::getIdBySlug($slug); - array_push($ids, $id); - $self = self::find()->select('id')->where(['parent_id' => $id])->asArray()->all(); - - if (!empty($self)) { - foreach ($self as $val) { - $ids[] = $val['id']; - } - } - return $ids; - } - - public function behaviors() { return [ @@ -131,9 +95,9 @@ public function attributeLabels() /** * @return \yii\db\ActiveQuery */ - public function getArticles() + public function getArticleCategoryArticles() { - return $this->hasMany(Article::className(), ['category_id' => 'id']); + return $this->hasMany(ArticleCategoryArticle::className(), ['category_id' => 'id']); } /** @@ -152,6 +116,41 @@ public function getChildCategories() return $this->hasMany(ArticleCategory::className(), ['parent_id' => 'id']); } + /** + * Get ArticleCategory id by its slug + * + * @author Zura Sekhniashvili + * @param $slug + * @return int|mixed + */ + public static function getIdBySlug($slug) + { + $self = self::find()->where(['slug' => $slug])->one(); + return $self ? $self->id : -1; + } + + /** + * Get all ids of ArticleCategory which are direct children of given category plus id of given category + * + * @author Zura Sekhniashvili + * @param $slug + * @return array + */ + public static function getIdsBySlug($slug) + { + $ids = []; + $id = self::getIdBySlug($slug); + array_push($ids, $id); + $self = self::find()->select('id')->where(['parent_id' => $id])->asArray()->all(); + + if (!empty($self)) { + foreach ($self as $val) { + $ids[] = $val['id']; + } + } + return $ids; + } + /** * Get article categories as map where key is `ArticleCategory::id` and value `activeTranslation->title` * diff --git a/models/ArticleCategoryArticle.php b/models/ArticleCategoryArticle.php new file mode 100644 index 0000000..9e1b7ae --- /dev/null +++ b/models/ArticleCategoryArticle.php @@ -0,0 +1,87 @@ + true, 'targetClass' => Article::className(), 'targetAttribute' => ['article_id' => 'id']], + [['category_id'], 'exist', 'skipOnError' => true, 'targetClass' => ArticleCategory::className(), 'targetAttribute' => ['category_id' => 'id']], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'id' => Yii::t('i18ncontent', 'ID'), + 'article_id' => Yii::t('i18ncontent', 'Article ID'), + 'category_id' => Yii::t('i18ncontent', 'Article Category ID'), + 'created_at' => Yii::t('i18ncontent', 'Created At'), + 'updated_at' => Yii::t('i18ncontent', 'Updated At'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getArticle() + { + return $this->hasOne(Article::className(), ['id' => 'article_id']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getArticleCategory() + { + return $this->hasOne(ArticleCategory::className(), ['id' => 'category_id']); + } +} \ No newline at end of file diff --git a/models/query/ArticleQuery.php b/models/query/ArticleQuery.php index 2ae7184..5e85ebf 100644 --- a/models/query/ArticleQuery.php +++ b/models/query/ArticleQuery.php @@ -4,27 +4,14 @@ use centigen\i18ncontent\models\Article; use centigen\i18ncontent\models\ArticleCategory; +use centigen\i18ncontent\models\ArticleCategoryArticle; use yii\db\ActiveQuery; use yii\db\Connection; class ArticleQuery extends ActiveQuery { private $joinedOnCategory = false; - - public function published() - { - $this->andWhere(['{{%article}}.status' => Article::STATUS_PUBLISHED]); - $this->andWhere(['<', '{{%article}}.published_at', time()]); - return $this; - } - - /** - * @param $categoryId - * @return $this - */ - public function byCategoryId($categoryId){ - return $this->andWhere(['{{%article}}.category_id' => $categoryId]); - } + private $joinedOnArticleCategoryArticle = false; /** * @author Zura Sekhniashvili @@ -56,6 +43,22 @@ public function bySlug($slug) return $this->andWhere(['{{%article}}.slug' => $slug]); } + public function published() + { + $this->andWhere(['{{%article}}.status' => Article::STATUS_PUBLISHED]); + $this->andWhere(['<', '{{%article}}.published_at', time()]); + return $this; + } + + /** + * @param $categoryId + * @return $this + */ + public function byCategoryId($categoryId){ + $this->joinOnArticleCategoryArticle(); + return $this->andWhere([ArticleCategoryArticle::tableName().'.category_id' => $categoryId]); + } + /** * @author Zura Sekhniashvili * @param $categorySlug @@ -64,9 +67,37 @@ public function bySlug($slug) public function byCategorySlug($categorySlug) { if (!$this->joinedOnCategory){ - $this->innerJoin(ArticleCategory::tableName().' ac', 'ac.id = {{%article}}.category_id'); + $this->joinOnCategory(); + } + return $this->andWhere([ArticleCategory::tableName().'.slug' => $categorySlug]); + } + + /** + * @author Zura Sekhniashvili + * @return self + */ + public function joinOnArticleCategoryArticle() + { + if (!$this->joinedOnArticleCategoryArticle){ + $this->joinedOnArticleCategoryArticle = true; + $this->innerJoin(ArticleCategoryArticle::tableName(), + ArticleCategoryArticle::tableName().'.article_id = '.Article::tableName().'.id'); } - return $this->andWhere(['ac.slug' => $categorySlug]); + return $this; + } + + /** + * @author Zura Sekhniashvili + * @return self + */ + public function joinOnCategory() + { + $this->joinOnArticleCategoryArticle(); + if (!$this->joinedOnCategory){ + $this->joinedOnCategory = true; + $this->innerJoin(ArticleCategory::tableName(), ArticleCategory::tableName().'.id = '.ArticleCategoryArticle::tableName().'.category_id'); + } + return $this; } /** @@ -77,4 +108,14 @@ public function orderByPosition() { return $this->orderBy(['{{%article}}.position' => SORT_ASC]); } + + /** + * @author Zura Sekhniashvili + * @return self + */ + public function categoryActive() + { + $this->joinOnCategory(); + return $this->andWhere([ArticleCategory::tableName().'.status' => ArticleCategory::STATUS_ACTIVE]); + } } diff --git a/models/search/ArticleSearch.php b/models/search/ArticleSearch.php index 4c9eb7e..a784aaf 100644 --- a/models/search/ArticleSearch.php +++ b/models/search/ArticleSearch.php @@ -26,7 +26,7 @@ class ArticleSearch extends Article public function rules() { return [ - [['id', 'category_id', 'status', 'published_at', 'created_at', 'position'], 'integer'], + [['id', 'status', 'published_at', 'created_at', 'position'], 'integer'], [['author', 'slug', 'title', 'catIds'], 'safe'], ]; } @@ -73,7 +73,6 @@ public function search($params) $query->andFilterWhere([ 'a.id' => $this->id, - 'a.category_id' => $this->category_id, 'a.status' => $this->status, 'a.position' => $this->position, 'a.published_at' => $this->published_at, diff --git a/views/article/_form.php b/views/article/_form.php index 7bb9026..c455f18 100644 --- a/views/article/_form.php +++ b/views/article/_form.php @@ -19,7 +19,11 @@ ->hint(Yii::t('i18ncontent', 'If you\'ll leave this field empty, slug will be generated automatically')) ->textInput(['maxlength' => true]) ?> - field($model, 'category_id')->dropDownList($categories, ['prompt' => '']) ?> + field($model, 'category_ids', [ + 'inputOptions' => [ + 'multiple' => 'multiple' + ] + ])->dropDownList($categories) ?> - isNewRecord): ?> + isNewRecord): ?>
$model->id], [ 'class' => 'btn btn-danger', 'data-confirm' => "Are you sure you want to delete this item?", - 'data-method'=>"post", + 'data-method' => "post", 'data-pjax' => "0" ]) ?>
- + diff --git a/views/article/index.php b/views/article/index.php index ac0ef9c..257ead7 100644 --- a/views/article/index.php +++ b/views/article/index.php @@ -48,13 +48,6 @@ 'style' => 'width: auto' ] ], - [ - 'attribute' => 'category_id', - 'value' => function ($model) { - return $model->category ? $model->category->activeTranslation->title : null; - }, - 'filter' => \centigen\i18ncontent\models\ArticleCategory::getCategories(), - ], [ 'attribute' => 'author', 'value' => function ($model) {