
CNC (SEF URLs) in Symfony 3 - slug auto-generation, configuration and routing
- Tutorial
Good day to all!
On the third day, I needed a blitz webinar on the topic of CNC in Symfony. In general, my webinar time is limited to two hours, while I also had to talk about auto-generation of CRUD functionality (scaffolding) in the same Symfony, and about the simplest way to create paging. This created a problem, since I know how to make CNC “pens” without resorting to tools automated for this task, but the story would be long and extra topics would be drawn into the discussion. So I went to ask the Internet how to make things easier. And so I found myself in that rare situation when such a popular platform as Symfony does not have banal training material on the subject of “CNC in three clicks”. I looked the same in English, but it is also empty there (maybe I was looking badly - time was limited).

Terminology
I do not know who will read my article, so for a start we will understand the terminology.
CNC is the abbreviation for Human-readable URLs. Translated into English as Friendly URL or Semantic URL . However, it is more often used as a similar abbreviation: SEF URLs - Search Engine Friendly URLs.
What gives you the CNC?
The most obvious is that the URLs of your site will be understandable to the user. Why, only, should he read them? Most clients of my customers are not even aware of the presence of a browser address bar. If in doubt, then see how many results the query will display in Google "Where is the address bar of the browser . "
However, there is an undeniable plus - correctly compiled CNCs are one of the important elements of SEO optimization, due to which the pages of your site will appear in the search engine on the first page. To do this, the URLs on your site should contain information relevant to the search engine about the pages to which they lead and have a well thought out nesting. All this is great, but this article is not about SEO optimization. It is assumed that you have already decided to get CNC on your site and you no longer need additional motivation.
CNC, non-CNC
CNC URLs are page addresses that describe all the necessary information about the page requested from the server in the form of path segments, that is, GET parameters in such a URL are very rare.
You can usually find path patterns like these:
http (s): // Domain / slug-category / slug-subcategory / slug-product-or-article
http (s): // Domain / Profile / slug-owner-profile
Another term appears here - Slug, which is important for further understanding of the article:
Slug - (from Wiktionary ) alternative human-friendly - the alphanumeric part of the universal address of the Internet link (URL) to the content being categorized. That is, if on a simple basis, then slug replaces all sorts of signs and id-shniki resources of our site in the URL with human-readable text.
Let us give an example
To whom it is so clear what CNC is - we move on.
How to get CNC in Symfony
I will explain on the example of a fresh installation of Symfony. At the time of writing, the version of Symfony 3.3.0 was taken. It is assumed that you installed Symfony and configured access to the database.
Before the bottom line, yes, the thing is, we need to make friends of our Symfony 3.3.0 with phpunit so that it does not crash after auto-generation of controllers. Complete the composer.json project with two lines:
composer.json
And update the dependencies:
Or so, if you have a composer archive in the project:
Generate the essence of the product inside the AppBundle bundle with the console command:
Surely you noticed that in addition to the rest of the fields, there is an interesting slug field. I made it unique, and without the ability to be null. The fact is that in our new project we will have to be able to select products from the database both by id-identifiers and by slug-am. Slug is now our second unique record identifier after id.
For convenience of presentation and for your convenience, testing the material I have set up, we will generate a CRUD controller based on the AppBundle: Product entity created in the previous step. To do this, execute the console commands:
Now after starting the server
We can visit the page http: // localhost: 2020 / products / and see an empty list of products and a link to the page for creating a new product: We will

delay the creation of new products. After all, we are waiting for the connection of Doctrine extensions.
Connecting Doctrine Behavioral Extensions
Why do we need Doctrine extensions? Can't we generate slug for the product ourselves? In general, yes. All this can be done with your own hands: generate a slug based on a field or a set of fields, take care of the uniqueness of the slug, always keep in mind the need to fill it, otherwise the site will crash. But we are not here for this. So we read the official documentation on how to use Doctrine extensions :
→ How to use Doctrine Extensions
Here we are advised to use the StofDoctrineExtensionsBundle bundle, which will ensure that Doctrine extensions are connected correctly. Read the documentation on it:
→ StofDoctrineExtensionsBundle
Install the StofDoctrineExtensionsBundle bundle:
We connect the downloaded bundle:
app / AppKernel.php
Of all the wealth of the Doctrine extensions we have involved in the project, we need only one thing - the Sluggable behavior extension . So we configure StofDoctrineExtensionsBundle so that this extension is enabled:
app / config / config.yml
The Sluggable behavior extension is connected. We must now tell him exactly what is required of him. To do this, read the documentation on it:
→ Sluggable behavior extension for Doctrine 2
It turns out that we do not have much to do. All you need is to connect the class of annotations that the extension provides us with in the essence of the product, and indicate with these annotations the Product: slug field that it should automatically fill in as slug based on the fields that we select:
src / AppBundle / Entity / Product.php
Here I indicated with an annotation
It's time to create products. But before that, you need to remove the extra field from the form, because the slug Doctrine field will fill in independently:
src / AppBundle / Form / ProductType.php
We go to the product creation form ( http: // localhost: 2020 / products / new ). We

save and see that slug is generated. It is suitable for use in the routes of your application:

It remains to verify the CNC in practice.
First CNC Route
Let's do it simple. Namely

, we’ll redo the routes products_show and products_edit: so that they show us the product not by id, but by slug. We will not change the products_delete route, since it is not visible to either the user or the search engine.
src / AppBundle / Controller / ProductController.php
app / Resources / views / product / index.html.twig
app / Resources / views / product / show.html.twig
It turned out like this:

Now the route to a detailed view of the product looks like this:
Route to edit the product:
Uniqueness of slugs The
question asked me in the comments by psycho-coder user helped me to add an article. But what if I want to create several products with the same name? After all, Symfony allows you to do this. What will happen then to slugs that are written in a field with a unique key in the database?
As I said above, the Doctrine Sluggable behavior extension takes responsibility for building unique slugs.
For example, I created a product three times in a row with the same name: “Something Meaningful”. Automatically generated slugs turned out like this:
If this option is not pleasant, then it is possible to specify generation for the slug field on the basis of not one field, but two. An example of a similar annotation for the slug field:
Three times create a product with an interval of one minute and get slug-s:
If you don’t like it either, then we’ll come up with your own option and share in the comments.
Finally,
we have achieved our goal:
It's time to go buy beer and think about how to translate all this into a large project. If I was useful to you, then I am glad to be of service to you.
I attach the archive with the Symfony project created during the writing of the article here .
By the way, I myself rendered the 3d picture specifically for this article. I liked her, and I didn’t take much effort either.
All the good routes!
On the third day, I needed a blitz webinar on the topic of CNC in Symfony. In general, my webinar time is limited to two hours, while I also had to talk about auto-generation of CRUD functionality (scaffolding) in the same Symfony, and about the simplest way to create paging. This created a problem, since I know how to make CNC “pens” without resorting to tools automated for this task, but the story would be long and extra topics would be drawn into the discussion. So I went to ask the Internet how to make things easier. And so I found myself in that rare situation when such a popular platform as Symfony does not have banal training material on the subject of “CNC in three clicks”. I looked the same in English, but it is also empty there (maybe I was looking badly - time was limited).

Terminology
I do not know who will read my article, so for a start we will understand the terminology.
CNC is the abbreviation for Human-readable URLs. Translated into English as Friendly URL or Semantic URL . However, it is more often used as a similar abbreviation: SEF URLs - Search Engine Friendly URLs.
What gives you the CNC?
The most obvious is that the URLs of your site will be understandable to the user. Why, only, should he read them? Most clients of my customers are not even aware of the presence of a browser address bar. If in doubt, then see how many results the query will display in Google "Where is the address bar of the browser . "
However, there is an undeniable plus - correctly compiled CNCs are one of the important elements of SEO optimization, due to which the pages of your site will appear in the search engine on the first page. To do this, the URLs on your site should contain information relevant to the search engine about the pages to which they lead and have a well thought out nesting. All this is great, but this article is not about SEO optimization. It is assumed that you have already decided to get CNC on your site and you no longer need additional motivation.
CNC, non-CNC
CNC URLs are page addresses that describe all the necessary information about the page requested from the server in the form of path segments, that is, GET parameters in such a URL are very rare.
You can usually find path patterns like these:
http (s): // Domain / slug-category / slug-subcategory / slug-product-or-article
http (s): // Domain / Profile / slug-owner-profile
Another term appears here - Slug, which is important for further understanding of the article:
Slug - (from Wiktionary ) alternative human-friendly - the alphanumeric part of the universal address of the Internet link (URL) to the content being categorized. That is, if on a simple basis, then slug replaces all sorts of signs and id-shniki resources of our site in the URL with human-readable text.
Let us give an example
To whom it is so clear what CNC is - we move on.
Analysis of an example of how website URLs might look if modified to CNC
For example, the site of the store rozetka.com.ua (the first site that came to hand). The CNC is in its infancy. Let's try to bring their links to mind manually:
I went to the "Table Tennis Balls" page and the address was in it:
rozetka.com.ua/t_balls/c81265 It is
clear that the "c81265" first character indicates that the requested object is product category, and the number after it is the category id in the database.
Remaking it under the CNC, it would have turned out simply:
rozetka.com.ua/t_balls
Just deleted the id-shnik? How so? But what about the content pages (http://rozetka.com.ua/contacts/)?
Yes, no problem. Just put all the content pages so that the current path in the request is checked first of all with them. In Symfony, this is done only by the fact that routes for these paths are declared first.
If it still doesn’t work out, or you have something else important on the site other than content pages and product categories, then we
’ll take a more unambiguous path: rozetka.com.ua/category/t_balls
Next, I switched to the Donic Table Tennis Balls product itself Elit 1 * 6 pcs white (618016): rozetka.com.ua/198578/p198578
Here it’s just a disaster. The CNC even stopped smelling.
What should this URL look like? Depending on how your site is arranged, there may be several options. According to the degree of congestion, URL segments that reduce the ambiguity of the path:
Here:
t_balls - slug of category
donic-elit-1-6-beliy - product slug
I think we have finished with clarity.
I went to the "Table Tennis Balls" page and the address was in it:
rozetka.com.ua/t_balls/c81265 It is
clear that the "c81265" first character indicates that the requested object is product category, and the number after it is the category id in the database.
Remaking it under the CNC, it would have turned out simply:
rozetka.com.ua/t_balls
Just deleted the id-shnik? How so? But what about the content pages (http://rozetka.com.ua/contacts/)?
Yes, no problem. Just put all the content pages so that the current path in the request is checked first of all with them. In Symfony, this is done only by the fact that routes for these paths are declared first.
If it still doesn’t work out, or you have something else important on the site other than content pages and product categories, then we
’ll take a more unambiguous path: rozetka.com.ua/category/t_balls
Next, I switched to the Donic Table Tennis Balls product itself Elit 1 * 6 pcs white (618016): rozetka.com.ua/198578/p198578
Here it’s just a disaster. The CNC even stopped smelling.
What should this URL look like? Depending on how your site is arranged, there may be several options. According to the degree of congestion, URL segments that reduce the ambiguity of the path:
- rozetka.com.ua/t_balls/donic-elit-1-6-beliy
- rozetka.com.ua/category/t_balls/donic-elit-1-6-beliy
- rozetka.com.ua/category/t_balls/product/donic-elit-1-6-beliy
Here:
t_balls - slug of category
donic-elit-1-6-beliy - product slug
I think we have finished with clarity.
How to get CNC in Symfony
I will explain on the example of a fresh installation of Symfony. At the time of writing, the version of Symfony 3.3.0 was taken. It is assumed that you installed Symfony and configured access to the database.
Before the bottom line, yes, the thing is, we need to make friends of our Symfony 3.3.0 with phpunit so that it does not crash after auto-generation of controllers. Complete the composer.json project with two lines:
composer.json
...
"require-dev": {
...
"phpunit/phpunit": "^6.2.1"
...
},
...
"config": {
"platform": {
"php": "7.0.15"
},
...
},
...
And update the dependencies:
composer update
Or so, if you have a composer archive in the project:
php composer.phar update
Generate the essence of the product inside the AppBundle bundle with the console command:
php bin/console doctrine:generate:entity --entity=AppBundle:Product --fields="name:string description:text seller:string publishDate:datetime slug:string(length=128 nullable=false unique=true)" -q
Surely you noticed that in addition to the rest of the fields, there is an interesting slug field. I made it unique, and without the ability to be null. The fact is that in our new project we will have to be able to select products from the database both by id-identifiers and by slug-am. Slug is now our second unique record identifier after id.
For convenience of presentation and for your convenience, testing the material I have set up, we will generate a CRUD controller based on the AppBundle: Product entity created in the previous step. To do this, execute the console commands:
php bin/console doctrine:database:create #создаем базу данных
php bin/console doctrine:schema:create #создаем структуру данных в базе данных
php bin/console doctrine:generate:crud --entity="AppBundle:Product" --route-prefix=products --with-write -n #генерируем CRUD контроллер
Now after starting the server
php bin/console server:run localhost:2020
We can visit the page http: // localhost: 2020 / products / and see an empty list of products and a link to the page for creating a new product: We will

delay the creation of new products. After all, we are waiting for the connection of Doctrine extensions.
Connecting Doctrine Behavioral Extensions
Why do we need Doctrine extensions? Can't we generate slug for the product ourselves? In general, yes. All this can be done with your own hands: generate a slug based on a field or a set of fields, take care of the uniqueness of the slug, always keep in mind the need to fill it, otherwise the site will crash. But we are not here for this. So we read the official documentation on how to use Doctrine extensions :
→ How to use Doctrine Extensions
Here we are advised to use the StofDoctrineExtensionsBundle bundle, which will ensure that Doctrine extensions are connected correctly. Read the documentation on it:
→ StofDoctrineExtensionsBundle
Install the StofDoctrineExtensionsBundle bundle:
composer require stof/doctrine-extensions-bundle
We connect the downloaded bundle:
app / AppKernel.php
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
// ...
new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
);
// ...
}
// ...
}
Of all the wealth of the Doctrine extensions we have involved in the project, we need only one thing - the Sluggable behavior extension . So we configure StofDoctrineExtensionsBundle so that this extension is enabled:
app / config / config.yml
...
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
sluggable : true
...
The Sluggable behavior extension is connected. We must now tell him exactly what is required of him. To do this, read the documentation on it:
→ Sluggable behavior extension for Doctrine 2
It turns out that we do not have much to do. All you need is to connect the class of annotations that the extension provides us with in the essence of the product, and indicate with these annotations the Product: slug field that it should automatically fill in as slug based on the fields that we select:
src / AppBundle / Entity / Product.php
...
use Gedmo\Mapping\Annotation as Gedmo;
...
/**
* Product
*
* @ORM\Table(name="product")
* @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
*/
class Product
{
...
/**
* @var string
*
* @Gedmo\Slug(fields={"name"})
* @ORM\Column(name="slug", type="string", length=128, nullable=false, unique=true)
*/
private $slug;
...
}
Here I indicated with an annotation
@Gedmo\Slug(fields={"name"})
that I want slug to be generated based on the name field. You can specify multiple fields so that they are constant during generation. For example, often instead of named entities indicate the creation date: @Gedmo\Slug(fields={"publishDate", "name"})
. It's time to create products. But before that, you need to remove the extra field from the form, because the slug Doctrine field will fill in independently:
src / AppBundle / Form / ProductType.php
...
class ProductType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name')->add('description')->add('seller')->add('publishDate'); //Удалили ->add('slug')
}
...
}
We go to the product creation form ( http: // localhost: 2020 / products / new ). We

save and see that slug is generated. It is suitable for use in the routes of your application:

It remains to verify the CNC in practice.
First CNC Route
Let's do it simple. Namely

, we’ll redo the routes products_show and products_edit: so that they show us the product not by id, but by slug. We will not change the products_delete route, since it is not visible to either the user or the search engine.
src / AppBundle / Controller / ProductController.php
...
class ProductController extends Controller
{
...
/**
* Finds and displays a product entity.
*
* @Route("/{slug}", name="products_show")
* @Method("GET")
* @param string $slug
* @return \Symfony\Component\HttpFoundation\Response
*/
public function showAction(string $slug)
{
$product = $this->getDoctrine()
->getRepository('AppBundle:Product')
->findOneBySlug($slug);
$deleteForm = $this->createDeleteForm($product);
return $this->render('product/show.html.twig', array(
'product' => $product,
'delete_form' => $deleteForm->createView(),
));
}
/**
* Displays a form to edit an existing product entity.
*
* @Route("/{slug}/edit", name="products_edit")
* @Method({"GET", "POST"})
* @param Request $request
* @param string $slug
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function editAction(Request $request, string $slug)
{
$product = $this->getDoctrine()
->getRepository('AppBundle:Product')
->findOneBySlug($slug);
$deleteForm = $this->createDeleteForm($product);
$editForm = $this->createForm('AppBundle\Form\ProductType', $product);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('products_edit', array('slug' => $product->getSlug()));
}
return $this->render('product/edit.html.twig', array(
'product' => $product,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
));
}
...
}
app / Resources / views / product / index.html.twig
{% extends 'base.html.twig' %}
{% block body %}
Products list
{% for product in products %}
{% endfor %}
Id Name Description Seller Publishdate Actions {{ product.id }} {{ product.name }} {{ product.description }} {{ product.seller }} {% if product.publishDate %}{{ product.publishDate|date('Y-m-d H:i:s') }}{% endif %}
{% endblock %}
app / Resources / views / product / show.html.twig
{% extends 'base.html.twig' %}
{% block body %}
Product
Id {{ product.id }} Name {{ product.name }} Description {{ product.description }} Seller {{ product.seller }} Publishdate {% if product.publishDate %}{{ product.publishDate|date('Y-m-d H:i:s') }}{% endif %} Slug {{ product.slug }}
- Back to the list
- Edit
-
{{ form_start(delete_form) }}
{{ form_end(delete_form) }}
{% endblock %}
It turned out like this:

Now the route to a detailed view of the product looks like this:
@Route("/{slug}", name="products_show")
Route to edit the product:
@Route("/{slug}/edit", name="products_edit")
Uniqueness of slugs The
question asked me in the comments by psycho-coder user helped me to add an article. But what if I want to create several products with the same name? After all, Symfony allows you to do this. What will happen then to slugs that are written in a field with a unique key in the database?
As I said above, the Doctrine Sluggable behavior extension takes responsibility for building unique slugs.
For example, I created a product three times in a row with the same name: “Something Meaningful”. Automatically generated slugs turned out like this:
- chto-to-osmyslennoe
- chto-to-osmyslennoe-1
- chto-to-osmyslennoe-2
If this option is not pleasant, then it is possible to specify generation for the slug field on the basis of not one field, but two. An example of a similar annotation for the slug field:
@Gedmo\Slug(fields={"name", "publishDate"})
Three times create a product with an interval of one minute and get slug-s:
- chto-to-osmyslennoe-2015-05-05-04-04
- chto-to-osmyslennoe-2015-05-05-04-05
- chto-to-osmyslennoe-2015-05-05-04-06
If you don’t like it either, then we’ll come up with your own option and share in the comments.
Finally,
we have achieved our goal:
- slug is generated automatically when saving an entity
- routes work taking into account slug instead of id-shnik
- The slug field in the database has a unique key, which allows you to level the brakes when selecting products for this field
It's time to go buy beer and think about how to translate all this into a large project. If I was useful to you, then I am glad to be of service to you.
I attach the archive with the Symfony project created during the writing of the article here .
By the way, I myself rendered the 3d picture specifically for this article. I liked her, and I didn’t take much effort either.
All the good routes!