📜 ⬆️ ⬇️

Create a behavior for Yii2

Often, but in fact almost always, when creating a site, it is necessary that the pages of the site are opened not by id entities in the database, but by text identifier, let's call it slug.

post/view/1 => post/view/testovaya-novost 


(it would be worth removing the view from the url, but the lesson is not about that)
')
In the most primitive way, you can create a slug field in the post table, a new attribute appears in the Post model, add a new input to the view, and punch the slug with pens.

 <?php use yii\helpers\Html; use yii\widgets\ActiveForm; /** * @var yii\web\View $this * @var common\models\Post $model * @var yii\widgets\ActiveForm $form */ ?> <div class="post-form"> <?php $form = ActiveForm::begin(); ?> <?= $form->field( $model, 'name' )->textInput( [ 'maxlength' => 255 ] ) ?> <?= $form->field( $model, 'slug' )->textInput( [ 'maxlength' => 255 ] ) ?> <?= $form->field( $model, 'content' )->textarea( [ 'rows' => 6 ] ) ?> <div class="form-group"> <?= Html::submitButton( $model->isNewRecord ? Yii::t( 'app', 'Create' ) : Yii::t( 'app', 'Update' ), [ 'class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary' ] ) ?> </div> <?php ActiveForm::end(); ?> </div> 




But it’s not always interesting to use pens (yes, I’m cheating whom, it’s not interesting at all), so we add methods to the model that, when the model is saved, generate a slug automatically from the name, check its uniqueness in the table (after all, we’ll retrieve the post from base, and, therefore, slug can not be not unique), and, perhaps, we transliterate it (test-news => testovaya-novost) - this can also be useful.
Well, we write, we become attached to the event, we test - everything works. And here in the development of the site we are faced with the fact that slug'i still needed in the model Page. And also in the product catalog - let it be the Item model. You can take the path of least resistance - copy-paste. But…

In Yii, there is such a thing as behaviors (functional) that allows you to use the same functions in different models. So, we write the behavior for slug'ification.

In our Post model (it is \ commoin \ models \ Post just in case) we connect the behavior that has not yet been created:

 public function behaviors() { return [ 'slug' => [ 'class' => 'common\behaviors\Slug', 'in_attribute' => 'name', 'out_attribute' => 'slug', 'translit' => true ] ]; } 


We created the behaviors function necessary for the connection, assigned a class in which we will have the behavior, and transferred three attributes to this class:
1. in_attribute - an attribute of the model from which slug will be generated (it may differ in different models, for example name or title)
2. out_attribute is an attribute of slug respectively (slug or alias)
3. translit - everything is clear

When creating the behavior, there was another fourth attribute - unique, but then I excluded this functional, since It is very rarely necessary that the slug is not unique.

I will mention that I use the yii2-app-advanced application structure, that is, I have backend and frontend folders, in which there are controllers and views, and a common folder, in which there are common models and behaviors.

Create common / behaviors / Slug.php:

 <?php namespace common\behaviors; use yii; use yii\base\Behavior; use yii\db\ActiveRecord; class Slug extends Behavior { public $in_attribute = 'name'; public $out_attribute = 'slug'; public $translit = true; public function events() { return [ ActiveRecord::EVENT_BEFORE_VALIDATE => 'getSlug' ]; } } 


We inherit the class from yii \ base \ Behavior, prescribe three attributes with initial settings, create the events method, which will bind the behavior to some event when the model is saved. Since slug is usually necessary and can be registered in rules as required, we will tie the slug generation to validation.

 ActiveRecord::EVENT_BEFORE_VALIDATE => 'getSlug' 


Now create the getSlug method:

 public function getSlug( $event ) { if ( empty( $this->owner->{$this->out_attribute} ) ) { $this->owner->{$this->out_attribute} = $this->generateSlug( $this->owner->{$this->in_attribute} ); } else { $this->owner->{$this->out_attribute} = $this->generateSlug( $this->owner->{$this->out_attribute} ); } } 


The model object itself is passed to the behavior as $ this-> owner. Thus, the slug will be available to us through a call to $ this-> owner-> slug or in our case $ this-> owner -> {$ this-> out_attribute}, since the name of the slug attribute is passed to the $ this-> variable out_attribute.
Whether we check whether the slug is empty when saved and, if empty, then generate it from the name (record header). If it is not empty, then we process the incoming slug.

 private function generateSlug( $slug ) { $slug = $this->slugify( $slug ); if ( $this->checkUniqueSlug( $slug ) ) { return $slug; } else { for ( $suffix = 2; !$this->checkUniqueSlug( $new_slug = $slug . '-' . $suffix ); $suffix++ ) {} return $new_slug; } } 


In the first line of the method, we use the slugify function to remove unnecessary characters and translate them into translit if necessary. Let's take it at once and consider:

 private function slugify( $slug ) { if ( $this->translit ) { return Inflector::slug( TransliteratorHelper::process( $slug ), '-', true ); } else { return $this->slug( $slug, '-', true ); } } 


What is translit? This is the transfer of national symbols to their counterparts in standard Latin. Most snippets found on the foreign Internet only clear text from umlauts, caps and other symbols ('À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' = > 'A',), that is, from the "dirty" Latin do "clean." This makes the standard helper yii2 yii \ helpers \ Inflector :: slug (by the way, during the creation of the behavior, this method was incompatibly changed - development over yii2 is still ongoing). In RuNet, respectively, they add another replacement of the Cyrillic alphabet in the Latin alphabet. But I would like to create the most flexible transliteration. The latest version of yii \ helpers \ Inflector :: slug uses the intl php extension, including transliterating even Chinese characters, but, as I understand it, it is not enabled by default (php 5.5.6). But a great developer 2amigos, familiar to everyone interested in yii, had the addition of transliterator-helper (it in turn uses the ideas from drupal, as I recall). It represents a number of php-files, which describe most of the characters and their replacement in the Latin alphabet.
Add the "2amigos/transliterator-helper": "2.0.*" dependency to composer.json "2amigos/transliterator-helper": "2.0.*" , Update, and now dosamigos \ helpers \ TransliteratorHelper is available:

 return Inflector::slug( TransliteratorHelper::process( $slug ), '-', true ); 


We are transliterating, clearing non-alphabetic characters, replacing spaces with a dash "-".

If we do not need translit, then:

 return $this->slug( $slug, '-', true ); 


The slug method (a trimmed version of yii \ helpers \ Inflector :: slug without transliteration):

 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; } 


Let's go back to generateSlug:

 private function generateSlug( $slug ) { $slug = $this->slugify( $slug ); if ( $this->checkUniqueSlug( $slug ) ) { return $slug; } else { for ( $suffix = 2; !$this->checkUniqueSlug( $new_slug = $slug . '-' . $suffix ); $suffix++ ) {} return $new_slug; } } 


The second line, we check the slug for uniqueness in the database. Let me remind you that we do not need a non-unique slug, since we will extract data from the table using it.

 private function checkUniqueSlug( $slug ) { $pk = $this->owner->primaryKey(); $pk = $pk[0]; $condition = $this->out_attribute . ' = :out_attribute'; $params = [ ':out_attribute' => $slug ]; if ( !$this->owner->isNewRecord ) { $condition .= ' and ' . $pk . ' != :pk'; $params[':pk'] = $this->owner->{$pk}; } return !$this->owner->find() ->where( $condition, $params ) ->one(); } 


Our primary key may theoretically have no id, so we find it with the function primaryKey (). Then we make a request to the table for the existence of such a slug. If the entry is not new, and we update (! $ This-> owner-> isNewRecord), then slug may already exist and make an exception to this id:

 $condition .= ' and ' . $pk . ' != :pk'; 


The function returns true if the slug is unique, and false if not. Farther:

 if ( $this->checkUniqueSlug( $slug ) ) { return $slug; } else { for ( $suffix = 2; !$this->checkUniqueSlug( $new_slug = $slug . '-' . $suffix ); $suffix++ ) {} return $new_slug; } 


If slug is unique, we return it, assign it to the model attribute, and save the model to the database. If not unique, then add a digital suffix testovaya-novost-2

 for ( $suffix = 2; !$this->checkUniqueSlug( $new_slug = $slug . '-' . $suffix ); $suffix++ ) {} 


Using the search method, we find the first free suffix and add it to the slug. The solution was overlooked in WordPress, but I don’t like what we do for each suffix on request, respectively, with testovaya-novost, testovaya-novost-2, testovaya-novost-3, testovaya-novost-4, testovaya-novost-5, us You will need to make 6 requests to verify uniqueness. If anyone can offer a better solution, I will be grateful.

So, the slug is gendered, transferred to the model, saved to the base, and we use the resulting behavior in other models.

Full text of behavior:
 <?php namespace common\behaviors; use dosamigos\helpers\TransliteratorHelper; use yii; use yii\base\Behavior; use yii\db\ActiveRecord; use yii\helpers\Inflector; class Slug extends Behavior { public $in_attribute = 'name'; public $out_attribute = 'slug'; public $translit = true; public function events() { return [ ActiveRecord::EVENT_BEFORE_VALIDATE => 'getSlug' ]; } public function getSlug( $event ) { if ( empty( $this->owner->{$this->out_attribute} ) ) { $this->owner->{$this->out_attribute} = $this->generateSlug( $this->owner->{$this->in_attribute} ); } else { $this->owner->{$this->out_attribute} = $this->generateSlug( $this->owner->{$this->out_attribute} ); } } private function generateSlug( $slug ) { $slug = $this->slugify( $slug ); if ( $this->checkUniqueSlug( $slug ) ) { return $slug; } else { for ( $suffix = 2; !$this->checkUniqueSlug( $new_slug = $slug . '-' . $suffix ); $suffix++ ) {} return $new_slug; } } private function slugify( $slug ) { if ( $this->translit ) { return Inflector::slug( TransliteratorHelper::process( $slug ), '-', true ); } else { return $this->slug( $slug, '-', true ); } } 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; } private function checkUniqueSlug( $slug ) { $pk = $this->owner->primaryKey(); $pk = $pk[0]; $condition = $this->out_attribute . ' = :out_attribute'; $params = [ ':out_attribute' => $slug ]; if ( !$this->owner->isNewRecord ) { $condition .= ' and ' . $pk . ' != :pk'; $params[':pk'] = $this->owner->{$pk}; } return !$this->owner->find() ->where( $condition, $params ) ->one(); } } 



The connection code is given above. And an example of transliteration:

    test 我爱 中文 Ψ ᾉ Ǽ ß  => test-test-y-test-wo-ai-zhong-wen-ps-a-ae-ss-c 


References:

Source: https://habr.com/ru/post/208328/


All Articles