Красивые url ссылки на yii2 или делаем контролируемые SEF ссылки.

24.09.2015

В один из этапов изучения yii2 Вам точно может понадобиться сделать ЧПУ на yii2. Причем красивые, еще и с возможностью личного создания/хранения в БД. Вообще у многих CMS и их компонентов используется подобная схема. Есть длинная не красивая ссылка содержащая GET данные и др., а ей соответствует красивый алиас. Который можно отредактировать как захочется без изменений правил перенаправлений в apache2 или самом фреймворке.

И вот я задался таким вопросом, с решением которого мне помогали на форуме. Поиск такого решения растянулся на 3 страницы и 4 месяца. Теперь же я просто оставлю это решение здесь.

Придется расширить правила urlManager двумя классами. Принцип работы таков: у нас есть ссылка вида контроллер/действие или контроллер/действие/параметр и т.п. Теперь нам нужно превратить ее в желаемую SEF ссылку, чтобы при обращении по алиасу (короткой ссылке) мы попадали на наш контроллер/действие... Пример: /registration => /user/registration или /login => /user/login/param или /blog/butiful-alias => /blog/123/534

Из примера видно превращение казалось бы не очень красивых ссылок в ЧПУ. Причем очень удачно это делать на примере блога, материалов, статей. Так же нужна и обратная совместимость, чтобы не было несколько точек входа на одну и туже страницу (дубликаты страниц). Это правило делает редирект на ЧПУ ссылку с обычной ссылки если такое правило есть в базе.

И так, нам нужно создать два класса которые будут управлять нашими ЧПУ на сайте.

А именно это:

  • первый класс забирает наши правила из БД, кэширует и загоняет их в urlManager. Он же поможет получить доступ к не SEF ссылкам через SEF ссылки:) Как-то так.
  • второй класс будет следить за обращениями к не SEF ссылкам и если для запрашиваемой страницы есть ЧПУ ссылка, он произведет редирект на SEF ссылку.

 

Для примера используется шаблон advanced фреймворка yii2.

Создаем в папке frontend\components\Pages\ (если папки components нет, создаем ее, а внутри нее папку Pages) файл с именем PagesUrlRule.php с содержимым:

 

<?php

namespace frontend\components\Pages;
use Yii;
use frontend\models\Pages\Pages;
use yii\caching\DbDependency;
use yii\web\CompositeUrlRule;
use yii\web\UrlRuleInterface;
use yii\base\InvalidConfigException;

/**
 * Class PagesUrlRule
 *
 * @package frontend\components\Pages
 */
class PagesUrlRule extends CompositeUrlRule{

    public $cacheComponent = 'cache';

    public $cacheID = 'PagesUrlRules';

    public $ruleConfig = ['class' => 'yii\web\UrlRule'];

    /**
     * Creates the URL rules that should be contained within this composite rule.
     *
     * @return \yii\web\UrlRuleInterface[] the URL rules
     * @throws \yii\base\InvalidConfigException
     */
    protected function createRules()
    {
        $cache = \Yii::$app->get($this->cacheComponent)->get($this->cacheID);
        if(!empty($cache))
            return $cache;

        $pages = Pages::find()->asArray(true)->all();

        $rules = [];
        foreach ($pages as $page) {

            $rule = [
                'pattern' => ltrim($page['alias'], '/'),
                'route' => ltrim($page['route'], '/'),
            ];


            $rule = \Yii::createObject(array_merge($this->ruleConfig, $rule));
            if (!$rule instanceof UrlRuleInterface) {
                throw new InvalidConfigException('URL rule class must implement UrlRuleInterface.');
            }
            $rules[] = $rule;
        }

        $cd= new DbDependency();
        $cd->sql='SELECT MAX(id) FROM '.Pages::tableName();

        Yii::$app->get($this->cacheComponent)->set($this->cacheID, $rules, 60,$cd);

        return $rules;
    }

    public function __wakeup()
    {
        $this->init();
    }

}

 

Далее в папке frontend\components\Pages\ создаем файл StrictParseRequest.php с содержимым:

 

<?php

namespace frontend\components\Pages;


use yii\web\CompositeUrlRule;

class StrictParseRequest extends CompositeUrlRule
{
    public $ruleConfig = ['class' => 'yii\web\UrlRule'];
    public $onlyGET = true;

    /**
     * @inheritdoc
     */
    protected function createRules()
    {
        $verb = null;
        if($this->onlyGET)
            $verb = 'GET';

        return [
            \Yii::createObject(array_merge($this->ruleConfig, [
                'pattern' => '<m>/<c>/<a>',
                'route' => '<m>/<c>/<a>',
                'verb' => $verb
            ])),
            \Yii::createObject(array_merge($this->ruleConfig, [
                'pattern' => '<c>/<a>',
                'route' => '<c>/<a>',
                'verb' => $verb
            ]))
        ];
    }

    /**
     * @inheritdoc
     */
    public function __wakeup()
    {
        $this->init();
    }

    /**
     * @inheritdoc
     */
    public function parseRequest($manager, $request){
        $result = parent::parseRequest($manager, $request);

        if(empty($result))
            return $result;

        $url = array_merge(["/".$result[0]], $result[1], $request->getQueryParams());

        $canonical = $manager->createUrl($url);

        if($request->url != $canonical){
            \Yii::$app->response->redirect($canonical, 301);
        }

        return $result;
    }
}

 

Открываем файл frontend\config\main.php и в return секции componetns находим (если нет, то добавляем) urlManager. Добавляем внутрь наши классы в строгой последовательности. В результате должно получиться примерно вот так:

return [
    'id' => '',
    'name' => '',
    'basePath' => dirname(__DIR__),
    'bootstrap' => ['log'],
    'controllerNamespace' => 'frontend\controllers',
    'components' => [
        'user' => [
            'identityClass' => 'common\models\User',
            'enableAutoLogin' => true,
        ],
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'rules'=> [
                /* Ваши правила  */

                ['class' => 'frontend\components\Pages\PagesUrlRule'], // первый
                ['class' => 'frontend\components\Pages\StrictParseRequest'], // второй
            ],
        ],
        'log' => [
            'traceLevel' => YII_DEBUG ? 3 : 0,
            'targets' => [
                [
                    'class' => 'yii\log\FileTarget',
                    'levels' => ['error', 'warning'],
                ],
            ],
        ],
        'errorHandler' => [
            'errorAction' => 'site/error',
        ],
    ],
    'params' => $params,
];

 

Создаем в БД таблицу с именем pages, в которой будут храниться наши правила:

 

CREATE TABLE `pages` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `alias` varchar(190) CHARACTER SET utf8mb4 NOT NULL,
  `route` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `alias_UNIQUE` (`alias`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

 

Далее создаем Active Record для frontend стороны сайта. Создаем папку frontend\models\Pages\ и в ней файл Pages.php с содержимым:

<?php

namespace frontend\models\Pages;

use Yii;

/**
 * This is the model class for table "pages".
 *
 * @property integer $id
 * @property string $alias
 * @property string $route
 */
class Pages extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'pages';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['alias', 'route'], 'required'],
            [['alias'], 'string', 'max' => 190],
            [['route'], 'string', 'max' => 255],
            [['alias'], 'unique']
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'alias' => 'Alias',
            'route' => 'Route',
        ];
    }
}



На этом этапе основная часть закончена. Теперь осталось создать Active Record для backend части, через которую мы будет добавлять наши ЧПУ ссылки.

Создаем в папке backend\controllers\ файл PagesController.php с содержимым:

 

<?php

namespace backend\controllers;

use Yii;
use app\models\Pages;
use app\models\PagesSearch;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
use yii\filters\AccessControl;

/**
 * PagesController implements the CRUD actions for Pages model.
 */
class PagesController extends Controller
{
    public function behaviors()
    {
        return [
            /*'access' => [
                'class' => AccessControl::className(),
                'rules' => [
                    [
                        'allow' => true,
                        'roles' => ['superadmin'],
                    ],
                ],
            ],*/
            'verbs' => [
                'class' => VerbFilter::className(),
                'actions' => [
                    'delete' => ['post'],
                ],
            ],
        ];
    }

    /**
     * Lists all Pages models.
     * @return mixed
     */
    public function actionIndex()
    {
        $searchModel = new PagesSearch();
        $dataProvider = $searchModel->search(Yii::$app->request->queryParams);

        return $this->render('index', [
            'searchModel' => $searchModel,
            'dataProvider' => $dataProvider,
        ]);
    }

    /**
     * Displays a single Pages model.
     * @param integer $id
     * @return mixed
     */
    public function actionView($id)
    {
        return $this->render('view', [
            'model' => $this->findModel($id),
        ]);
    }

    /**
     * Creates a new Pages model.
     * If creation is successful, the browser will be redirected to the 'view' page.
     * @return mixed
     */
    public function actionCreate()
    {
        $model = new Pages();

        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['view', 'id' => $model->id]);
        } else {
            return $this->render('create', [
                'model' => $model,
            ]);
        }
    }

    /**
     * Updates an existing Pages model.
     * If update is successful, the browser will be redirected to the 'view' page.
     * @param integer $id
     * @return mixed
     */
    public function actionUpdate($id)
    {
        $model = $this->findModel($id);

        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['view', 'id' => $model->id]);
        } else {
            return $this->render('update', [
                'model' => $model,
            ]);
        }
    }

    /**
     * Deletes an existing Pages model.
     * If deletion is successful, the browser will be redirected to the 'index' page.
     * @param integer $id
     * @return mixed
     */
    public function actionDelete($id)
    {
        $this->findModel($id)->delete();

        return $this->redirect(['index']);
    }

    /**
     * Finds the Pages model based on its primary key value.
     * If the model is not found, a 404 HTTP exception will be thrown.
     * @param integer $id
     * @return Pages the loaded model
     * @throws NotFoundHttpException if the model cannot be found
     */
    protected function findModel($id)
    {
        if (($model = Pages::findOne($id)) !== null) {
            return $model;
        } else {
            throw new NotFoundHttpException('The requested page does not exist.');
        }
    }
}

 

Здесь я за комментировал секцию access метода behaviors т.к. я использую RBAC для доступа к контроллеру. 

Далее в папке backend\models\ создаем файл PagesSearch.php с содержимым:

<?php

namespace app\models;

use Yii;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use app\models\Pages;

/**
 * PagesSearch represents the model behind the search form about `app\models\Pages`.
 */
class PagesSearch extends Pages
{
    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['id'], 'integer'],
            [['alias', 'route'], 'safe'],
        ];
    }

    /**
     * @inheritdoc
     */
    public function scenarios()
    {
        // bypass scenarios() implementation in the parent class
        return Model::scenarios();
    }

    /**
     * Creates data provider instance with search query applied
     *
     * @param array $params
     *
     * @return ActiveDataProvider
     */
    public function search($params)
    {
        $query = Pages::find();

        $dataProvider = new ActiveDataProvider([
            'query' => $query,
        ]);

        $this->load($params);

        if (!$this->validate()) {
            // uncomment the following line if you do not want to return any records when validation fails
            // $query->where('0=1');
            return $dataProvider;
        }

        $query->andFilterWhere([
            'id' => $this->id,
        ]);

        $query->andFilterWhere(['like', 'alias', $this->alias])
            ->andFilterWhere(['like', 'route', $this->route]);

        return $dataProvider;
    }
}



В этой же папке backend\models\ создаем еще один файл Pages.php с содержимым:

<?php

namespace app\models;

use Yii;

/**
 * This is the model class for table "pages".
 *
 * @property integer $id
 * @property string $alias
 * @property string $route
 */
class Pages extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'pages';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['alias', 'route'], 'required'],
            [['alias'], 'string', 'max' => 190],
            [['route'], 'string', 'max' => 255],
            [['alias'], 'unique', 'message' => 'Такой алиас уже существует']
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'alias' => 'Alias',
            'route' => 'Route',
        ];
    }
}



И напоследок нужно создать view

Создаем папку backend\views\pages\ где так же создадим 6 файлов:

Создаем файл _form.php с содержимым:

<?php

use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */
/* @var $model app\models\Pages */
/* @var $form yii\widgets\ActiveForm */
?>

<div class="pages-form">

    <?php $form = ActiveForm::begin(); ?>

    <?= $form->field($model, 'alias')->textInput(['maxlength' => true]) ?>

    <?= $form->field($model, 'route')->textInput(['maxlength' => true]) ?>

    <div class="form-group">
        <?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update', ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?>
    </div>

    <?php ActiveForm::end(); ?>

</div>



Создаем файл _search.php с содержимым:

<?php

use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */
/* @var $model app\models\PagesSearch */
/* @var $form yii\widgets\ActiveForm */
?>

<div class="pages-search">

    <?php $form = ActiveForm::begin([
        'action' => ['index'],
        'method' => 'get',
    ]); ?>

    <?= $form->field($model, 'id') ?>

    <?= $form->field($model, 'alias') ?>

    <?= $form->field($model, 'route') ?>

    <div class="form-group">
        <?= Html::submitButton('Search', ['class' => 'btn btn-primary']) ?>
        <?= Html::resetButton('Reset', ['class' => 'btn btn-default']) ?>
    </div>

    <?php ActiveForm::end(); ?>

</div>



Создаем файл create.php с содержимым:

<?php

use yii\helpers\Html;


/* @var $this yii\web\View */
/* @var $model app\models\Pages */

$this->title = 'Create Pages';
$this->params['breadcrumbs'][] = ['label' => 'Pages', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="pages-create">

    <h1><?= Html::encode($this->title) ?></h1>

    <?= $this->render('_form', [
        'model' => $model,
    ]) ?>

</div>



Создаем файл index.php с содержимым:

<?php

use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */
/* @var $searchModel app\models\PagesSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */

$this->title = 'Pages';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="pages-index">

    <h1><?= Html::encode($this->title) ?></h1>
    <?php // echo $this->render('_search', ['model' => $searchModel]); ?>

    <p>
        <?= Html::a('Create Pages', ['create'], ['class' => 'btn btn-success']) ?>
    </p>

    <?= GridView::widget([
        'dataProvider' => $dataProvider,
        'filterModel' => $searchModel,
        'columns' => [
            ['class' => 'yii\grid\SerialColumn'],

            'id',
            'alias',
            'route',

            ['class' => 'yii\grid\ActionColumn'],
        ],
    ]); ?>

</div>



Создаем файл update.php с содержимым:

<?php

use yii\helpers\Html;

/* @var $this yii\web\View */
/* @var $model app\models\Pages */

$this->title = 'Update Pages: ' . ' ' . $model->id;
$this->params['breadcrumbs'][] = ['label' => 'Pages', 'url' => ['index']];
$this->params['breadcrumbs'][] = ['label' => $model->id, 'url' => ['view', 'id' => $model->id]];
$this->params['breadcrumbs'][] = 'Update';
?>
<div class="pages-update">

    <h1><?= Html::encode($this->title) ?></h1>

    <?= $this->render('_form', [
        'model' => $model,
    ]) ?>

</div>



Создаем файл view.php с содержимым:

<?php

use yii\helpers\Html;
use yii\widgets\DetailView;

/* @var $this yii\web\View */
/* @var $model app\models\Pages */

$this->title = $model->id;
$this->params['breadcrumbs'][] = ['label' => 'Pages', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="pages-view">

    <h1><?= Html::encode($this->title) ?></h1>

    <p>
        <?= Html::a('Update', ['update', 'id' => $model->id], ['class' => 'btn btn-primary']) ?>
        <?= Html::a('Delete', ['delete', 'id' => $model->id], [
            'class' => 'btn btn-danger',
            'data' => [
                'confirm' => 'Are you sure you want to delete this item?',
                'method' => 'post',
            ],
        ]) ?>
    </p>

    <?= DetailView::widget([
        'model' => $model,
        'attributes' => [
            'id',
            'alias',
            'route',
        ],
    ]) ?>

</div>


И главное, не забываем создать backend\web\.htaccess и frontend\web\.htaccess с содержимым:

# use mod_rewrite for pretty URL support
RewriteEngine on
# If a directory or a file exists, use the request directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Otherwise forward the request to index.php
RewriteRule . index.php

 

На этом все! Открываем в backend`е /pages/index и создаем правило. В поле Alias вписываем желаемую ссылку ЧПУ, а в поле Route существующую ссылку на ваше действие.

Сохраняем и проверяем работоспособность на сайте. Вот пример:

Добавляем правило SEF в yii2 backend

Проверка SEF правила в yii2

comments powered by HyperComments