Create a behavior for Yii2
post/view/1 => post/view/testovaya-novost(view should be removed from the url, but the lesson is not)
The most primitive way is to create the slug field in the post table, a new attribute appears in the Post model, add a new input to the view, into which we drive the slug .
field( $model, 'name' )->textInput( [ 'maxlength' => 255 ] ) ?>
field( $model, 'slug' )->textInput( [ 'maxlength' => 255 ] ) ?>
field( $model, 'content' )->textarea( [ 'rows' => 6 ] ) ?>
isNewRecord ? Yii::t( 'app', 'Create' ) : Yii::t( 'app', 'Update' ), [ 'class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary' ] ) ?>
But it’s not always interesting to do it with pens (it’s not interesting at all to whom I’m fooling), so we add methods to the model that, when the model is saved, generate slug automatically from name, check its uniqueness in the table (because we will extract post from slug base, and, therefore, slug cannot be unique), and, perhaps, 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, when developing the site, we are faced with the fact that slugs are still needed in the Page model. And also in the catalog for the goods - let it be an Item model. You can follow the path of least resistance - copy-paste. But…
In Yii, there is such a thing as behaviors - a functional that allows you to use the same functions in different models. So, let's write the behavior for slug'ification.
In our Post model (it’s also \ 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 connection, registered the class in which the behavior will be located and passed three attributes to this class:
1. in_attribute - the 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 the slug, respectively (slug or alias)
3. translit - everything is clear here
When creating the behavior there was another fourth attribute - unique, but then I excluded this functionality, because it is very rare that slug is not unique.
Let me remind you that I use the structure of the yii2-app-advanced application, that is, I have the backend and frontend folders in which the controllers and views are located, and the common folder, in which there are common models and behaviors.
Create common / behaviors / Slug.php:
'getSlug'
];
}
}We inherit the class from yii \ base \ Behavior, prescribe three attributes with initial settings, create the events method, which will attach the behavior to some event when saving the model. Since slug is usually necessary and can be spelled out in rules as required, then we will bind slug generation before 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 into the behavior as $ this-> owner. Thus, 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 variable $ this-> out_attribute.
We do a check to see if slug is empty when saving and, if empty, then we generate it from name (the title of the record). If it is not empty, then we process the received 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 remove the unwanted characters with the slugify function and translate it into transliteration, if necessary. Let's immediately look at it:
private function slugify( $slug )
{
if ( $this->translit ) {
return Inflector::slug( TransliteratorHelper::process( $slug ), '-', true );
} else {
return $this->slug( $slug, '-', true );
}
}What is transliteration? This is the transfer of national symbols by their analogues in the standard Latin alphabet. Most of the snippets found on the foreign Internet only clear the text from umlauts, caps and other characters ('À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' = > 'A',), that is, from the "dirty" Latin make "clean". The standard helper yii2 yii \ helpers \ Inflector :: slug also does this (by the way, this method was incompatibly changed during the creation of the behavior - development over yii2 is still ongoing). In RuNet, respectively, they add another replacement of the Cyrillic alphabet with the Latin alphabet. But I would like to create the most flexible transliteration. The latest version of yii \ helpers \ Inflector :: slug uses the php extension intl, including transliterating even Chinese characters, but, as I understand it, it is not enabled by default (php 5.5.6). But a wonderful 2amigos developer, familiar to everyone interested in yii, found the transliterator-helper add-on (it in turn uses ideas from drupal, as far as I remember). It represents a certain number of php files, which describe most of the characters and their replacement in the Latin alphabet.
Add a dependency to composer.json
"2amigos/transliterator-helper": "2.0.*", update it, and now dosamigos \ helpers \ TransliteratorHelper is available to us:return Inflector::slug( TransliteratorHelper::process( $slug ), '-', true );We transliterate, clear non-alphabetic characters, replace spaces with a dash “-”.
If we don’t need transliteration, then:
return $this->slug( $slug, '-', true );Slug method (truncated 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;
}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 slug for uniqueness in the database. Let me remind you that we do not need a non-unique slug, since we will use it to extract data from the table.
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();
}In theory, the primary key may not be id, so we find it with the primaryKey () function. Next, we make a request to the table for the existence of such a slug. If the record is not new, and we do update (! $ This-> owner-> isNewRecord), then slug may already exist and make an exception for this id:
$condition .= ' and ' . $pk . ' != :pk';The function returns true if slug is unique, and false if not. Further:
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 it is not unique, then add the digital suffix testovaya-novost-2
for ( $suffix = 2; !$this->checkUniqueSlug( $new_slug = $slug . '-' . $suffix ); $suffix++ ) {}Using the brute force method, we find the first free suffix and add it to the slug. The solution is spied up in WordPress, but I don’t like that for each suffix we do it on demand, respectively when busy testovaya-novost, testovaya-novost-2, testovaya-novost-3, testovaya-novost-4, testovaya-novost-5 to us You will need to make 6 queries to verify uniqueness. If anyone can offer a better solution, I will be grateful.
So, slug is generated, transferred to the model, saved to the database, and we use the resulting behavior in other models.
'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 code for connecting behavior is given above. A example of transliteration:
тест Тест й test 我爱 中文 Ψ ᾉ Ǽ ß Ц => test-test-y-test-wo-ai-zhong-wen-ps-a-ae-ss-cReferences: