diff --git a/common/models/Book.php b/common/models/Book.php index 0e5c4d8..4f53eba 100644 --- a/common/models/Book.php +++ b/common/models/Book.php @@ -2,10 +2,11 @@ namespace common\models; + use artbox\core\models\Alias; + use frontend\behaviors\SlugBehavior; use Yii; use yii\behaviors\TimestampBehavior; use yii\helpers\FileHelper; - use yii\helpers\Json; use yii\web\UploadedFile; /** @@ -42,8 +43,15 @@ [ 'class' => TimestampBehavior::className(), 'updatedAtAttribute' => false, + ], + [ + 'class' => SlugBehavior::className(), + 'action' => 'book/view', + 'params' => [ + 'id' => 'id', + ], ] - ]; + ]; } /** @@ -66,6 +74,7 @@ 'author_id', 'created_at', 'status', + 'alias_id' ], 'integer', ], @@ -151,4 +160,12 @@ return true; } + public function getAlias(){ + return $this->hasOne(Alias::className(), ['id' => 'alias_id']); + } + + public function getActiveComments(){ + return $this->hasMany(Comment::className(), ['book_id' => 'id'])->where(['status' => true])->orderBy('created_at'); + } + } diff --git a/common/models/Comment.php b/common/models/Comment.php new file mode 100644 index 0000000..65dc7e5 --- /dev/null +++ b/common/models/Comment.php @@ -0,0 +1,133 @@ + TimestampBehavior::className(), + 'updatedAtAttribute' => false, + ], + ]; + + } + + /** + * {@inheritdoc} + */ + public function rules() + { + return [ + [ + [ + 'book_id', + 'parent_id', + ], + 'default', + 'value' => null, + ], + [ + [ + 'book_id', + 'parent_id', + ], + 'integer', + ], + [ + [ 'comment' ], + 'string', + ], + [ + 'email', + 'email', + ], + [ + [ + 'name', + 'email', + ], + 'string', + 'max' => 255, + ], + [ + [ 'book_id' ], + 'exist', + 'skipOnError' => true, + 'targetClass' => Book::className(), + 'targetAttribute' => [ 'book_id' => 'id' ], + ], + [ + [ 'parent_id' ], + 'exist', + 'skipOnError' => true, + 'targetClass' => Comment::className(), + 'targetAttribute' => [ 'parent_id' => 'id' ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function attributeLabels() + { + return [ + 'id' => Yii::t('app', 'ID'), + 'book_id' => Yii::t('app', 'Book ID'), + 'name' => Yii::t('app', 'Name'), + 'email' => Yii::t('app', 'Email'), + 'comment' => Yii::t('app', 'Comment'), + 'parent_id' => Yii::t('app', 'Parent ID'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getBook() + { + return $this->hasOne(Book::className(), [ 'id' => 'book_id' ]); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getParent() + { + return $this->hasOne(Comment::className(), [ 'id' => 'parent_id' ]); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getComments() + { + return $this->hasMany(Comment::className(), [ 'parent_id' => 'id' ]); + } + } diff --git a/console/migrations/m180613_133549_create_comment_table.php b/console/migrations/m180613_133549_create_comment_table.php new file mode 100644 index 0000000..29b7449 --- /dev/null +++ b/console/migrations/m180613_133549_create_comment_table.php @@ -0,0 +1,50 @@ +createTable('comment', [ + 'id' => $this->primaryKey(), + 'book_id' => $this->integer(), + 'name' => $this->string(), + 'email' => $this->string(), + 'comment' => $this->text(), + 'parent_id' => $this->integer() + ]); + + $this->addForeignKey('comment_book_fk', + 'comment', + 'book_id', + 'book', + 'id', + 'CASCADE', + 'CASCADE'); + + $this->addForeignKey('comment_comment_fk', + 'comment', + 'parent_id', + 'comment', + 'id', + 'CASCADE', + 'CASCADE'); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->dropForeignKey('comment_book_fk', 'comment'); + $this->dropForeignKey('comment_comment_fk', 'comment'); + $this->dropTable('comment'); + } +} diff --git a/console/migrations/m180613_140138_alter_table_book.php b/console/migrations/m180613_140138_alter_table_book.php new file mode 100644 index 0000000..226cae6 --- /dev/null +++ b/console/migrations/m180613_140138_alter_table_book.php @@ -0,0 +1,35 @@ +addColumn('book', 'alias_id', $this->integer()); + + $this->addForeignKey('book_alias_fk', + 'book', + 'alias_id', + 'alias', + 'id', + 'CASCADE', + 'CASCADE'); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->dropForeignKey('book_alias_fk', 'book'); + $this->dropColumn('book', 'alias_id'); + } + +} diff --git a/console/migrations/m180613_144215_alter_table_comment.php b/console/migrations/m180613_144215_alter_table_comment.php new file mode 100644 index 0000000..d73c501 --- /dev/null +++ b/console/migrations/m180613_144215_alter_table_comment.php @@ -0,0 +1,42 @@ +addColumn('comment', 'status', $this->boolean()); + $this->addColumn('comment', 'created_at', $this->integer()); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->dropColumn('comment', 'status'); + $this->dropColumn('comment', 'created_at'); + } + + /* + // Use up()/down() to run migration code without a transaction. + public function up() + { + + } + + public function down() + { + echo "m180613_144215_alter_table_comment cannot be reverted.\n"; + + return false; + } + */ +} diff --git a/frontend/behaviors/SlugBehavior.php b/frontend/behaviors/SlugBehavior.php new file mode 100755 index 0000000..e473429 --- /dev/null +++ b/frontend/behaviors/SlugBehavior.php @@ -0,0 +1,420 @@ + 'getSlug', + ActiveRecord::EVENT_AFTER_UPDATE => 'getSlug', + ActiveRecord::EVENT_AFTER_DELETE => 'deleteSlug', + ]; + } + + /** + * @inheritdoc + */ + public function attach($owner) + { + parent::attach($owner); + if (!$owner instanceof ActiveRecord) { + throw new yii\base\InvalidConfigException( + \Yii::t('core', 'SlugBeahvior can only be attached to the instance of ActiveRecord') + ); + } + } + + /** + * Generate slug + * + * @param yii\base\Event $event + * + * @return void + */ + public function getSlug($event) + { + /** + * @var string $attribute_value + */ + $attribute_value = $this->owner->getAttribute($this->inAttribute); + if (!empty($attribute_value)) { + /** + * @var string $alias_value + */ + $alias_value = $this->aliasValue; + $this->currentAlias = $this->findCurrentAlias(); + if (empty($alias_value)) { + $result = $this->generateSlug($attribute_value); + } else { + $result = $this->generateSlug($alias_value); + } + $suffix = 2; + $new_result = $result; + while (!empty(Alias::find()->where(['value' => $new_result])->one())) { + $new_result = $result . '-' . $suffix; + $suffix++; + } + $this->saveSlug($new_result); + } + } + + /** + * Delete slug + * + * @param yii\base\Event $event + * + * @return void + */ + public function deleteSlug($event) + { + if (!empty($current = $this->findCurrentAlias())) { + + $current->delete(); + } + } + + /** + * Get current Alias. Db query will be made first time or if $force flag is set. + * + * @param bool $force + * + * @return \artbox\core\models\Alias|null + */ + public function findCurrentAlias($force = false) + { + if ($force || !$this->executed) { + /** + * @var int $alias_id + */ + $alias_id = $this->owner->getAttribute($this->outAttribute); + $currentAlias = null; + if (!empty($alias_id)) { + /** + * @var Alias|null $currentAlias + */ + $currentAlias = Alias::find() + ->where( + [ + 'id' => $alias_id, + ] + ) + ->one(); + } + $this->executed = true; + return $currentAlias; + } else { + return $this->currentAlias; + } + } + + /** + * Generate unique slug. + * + * @param string $slug + * + * @return string + */ + private function generateSlug($slug) + { + $slug = $this->slugify($slug); + $languageId = null; + if ($this->owner->hasAttribute('language_id')) { + $languageId = $this->owner->getAttribute('language_id'); + } + return SlugHelper::unify($slug, $this->currentAlias, $languageId); + } + + /** + * Generate slug from string according to $translit flag. + * + * @param string $slug + * + * @return string + */ + private function slugify($slug) + { + if ($this->translit) { + return SlugHelper::slugify($slug); + } else { + return $this->slug($slug, '-', true); + } + } + + /** + * Replace non-alphabetic symbols with $replcement symbol. + * + * @param string $string + * @param string $replacement + * @param bool $lowercase + * + * @return string + */ + private function slug($string, $replacement = '-', $lowercase = true) + { + $string = preg_replace('/[^\p{L}\p{Nd}]+/u', $replacement, $string); + $string = trim($string, $replacement); + return $lowercase ? strtolower($string) : $string; + } + + /** + * Check if $slug differs from current Alias value + * + * @param $slug + * + * @return bool + */ + private function hasChanged($slug) + { + if (!empty($this->currentAlias) && $this->currentAlias->value === $slug) { + return false; + } else { + return true; + } + } + + /** + * If $slug differs from Db Alias value, updates current if exists or create new Alias row. + * + * @param $slug + * + * @throws \yii\base\ErrorException + */ + protected function saveSlug($slug) + { + if (!$this->hasChanged($slug)) { + return; + } else { + $this->populateFields(); + $route = $this->buildRoute(); + $owner = $this->owner; + if (empty($alias = $this->findCurrentAlias())) { + + $alias = \Yii::createObject( + Alias::className(), + [ + [ + 'value' => $slug, + 'route' => $route, + 'entity' => $owner::className(), + 'title' => ( !empty($title) ) ? $title : $owner->getAttribute($this->inAttribute), + 'h1' => ( !empty($h1) ) ? $h1 : $owner->getAttribute($this->inAttribute), + 'fields' => Json::encode($this->fields), + 'language_id' => Language::getCurrent()->id, + ], + ] + ); + + } else { + $alias->value = $slug; + $alias->route = $route; + $alias->entity = $owner::className(); + $alias->language_id = Language::getCurrent()->id; + } + if ($alias->save()) { + $owner->{$this->outAttribute} = $alias->id; + $this->detach(); + $owner->save(false, [ $this->outAttribute ]); + } else { + $alias = Alias::find() + ->where( + [ + 'route' => $alias->route, + 'language_id' => $alias->language_id, + ] + ) + ->one(); + if ($alias) { + $owner->{$this->outAttribute} = $alias->id; + $this->detach(); + $owner->save(false, [ $this->outAttribute ]); + } else { + throw new ErrorException( + \Yii::t( + 'core', + 'Alias cannot be saved: ' . Json::encode($alias) . '. Errors: ' . Json::encode( + $alias->getErrors() + ) + ) + ); + } + } + } + } + protected function generateData(string $attribute, int $lang) + { + if (!isset($this->meta[ $lang ][ $attribute ])) { + return null; + } + $value = $this->meta[ $lang ][ $attribute ]; + foreach ($this->attributes as $search => $replace) { + $value = str_replace($search, $replace, $value); + + } + return $value; + } + + protected function populateFields() + { + $fields = array_keys($this->owner->attributes); + foreach ($fields as $field) { + + $chain = $this->owner; + + if (!empty($chain->$field)) { + $chain = $chain->$field; + } else { + $chain = ''; + } + $this->attributes[ '{{' . $field . '}}' ] = $chain; + } + } + + + /** + * Get $aliasValue. Try to get from cached value and tries to get from Db if empty. + * + * @return string + */ + public function getAliasValue() + { + if (empty($this->aliasValue) && !$this->executed) { + $currentAlias = $this->findCurrentAlias(); + if (!empty($currentAlias)) { + $this->aliasValue = $currentAlias->value; + return $currentAlias->value; + } + } + return $this->aliasValue; + } + + /** + * Set $aliasValue + * + * @param $value + */ + public function setAliasValue($value) + { + $this->aliasValue = $value; + } + + /** + * Build Json route from $action and $params fields. + * + * @return null|string + */ + protected function buildRoute() + { + $owner = $this->owner; + $route = []; + $action = $this->action; + if (empty($action)) { + return null; + } else { + $route[ 0 ] = $action; + } + $params = $this->params; + if (!empty($params) && is_array($params)) { + foreach ($params as $param => $attribute) { + $attributeValue = $owner->getAttribute($attribute); + if (!empty($attributeValue)) { + $route[ $param ] = $attributeValue; + } + } + } + return Json::encode($route); + } + } + \ No newline at end of file diff --git a/frontend/controllers/BookController.php b/frontend/controllers/BookController.php index 6d52755..c3a7bc6 100644 --- a/frontend/controllers/BookController.php +++ b/frontend/controllers/BookController.php @@ -38,7 +38,6 @@ } $model = new Book(); if (\Yii::$app->request->isPost) { - if ($model->load(\Yii::$app->request->post(), '') and $model->validate()) { $model->author_id = $user->id; $model->status = $model::STATUS_MODERATION; @@ -84,7 +83,7 @@ $dataProvider = new ActiveDataProvider( [ 'query' => Book::find() - ->with('author') + ->with(['author', 'alias']) ->where([ 'status' => Book::STATUS_ACTIVE ]), 'pagination' => [ 'pageSize' => 10, diff --git a/frontend/views/book/_book.php b/frontend/views/book/_book.php index 1f9d1e6..ad29a8f 100644 --- a/frontend/views/book/_book.php +++ b/frontend/views/book/_book.php @@ -2,7 +2,7 @@ /** * @var \common\models\Book $model; */ - use yii\helpers\Url; + use artbox\core\helpers\Url; ?>