📜 ⬆️ ⬇️

CNC (SEF URLs) in symfony 3 - slug autogeneration, configuration and routing

Good day to all!

The third day it took me a blitz webinar on the topic of CNC in symfony. In general, the time of the webinar is limited to two hours, while I had to tell you about the autogeneration of CRUD functionality (scaffolding) in the same Symfony, and about the simplest way to create a page range. This created a problem, since I know how to make the CNC “handles”, without resorting to tools automated for this task, but the story would have turned out to be long and unnecessary topics would be drawn into the discussion. So I went to ask the Internet how to make everything easier. And so I found myself in that rare situation when such a popular platform as Symfony does not have a trivial educational material on the topic “CNC in three clicks”. I also looked in English, but it was also empty there (maybe I was looking badly - time was limited). In general, I coped with the search for scattered material on this topic, as well as with collecting it into a single narrative, so why not share it with everyone?



Terminology
I do not know who will read my article, so for a start we will understand the terminology.
')
CNC - an abbreviation of "human readable URL." In English translated as Friendly URL or Semantic URL . However, it is more often used as a similar abbreviation: SEF URLs - Search Engine Friendly URLs.

What does the CNC give you?

The most obvious is that the URLs of your site will be clear to the user. Why, only, him to read them? Most of the clients of my customers do not even suspect the presence of the browser address bar. If in doubt, then look at how many results will display the query in Google "Where is the browser address bar . "

However, there is an undeniable plus - correctly compiled CNC are one of the important elements of SEO optimization, thanks to which the pages of your site will appear in a search engine on the first page. To do this, the URL on your site should contain search-relevant information about the pages to which they lead and have thoughtful nesting. All this is great, but this article is not about SEO optimization. It is assumed that you have already decided to get the CNC on your website and additional motivation is no longer required.

CNC, not 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 this URL are very rare.

You can usually find path templates like this:
http (s): // Domain / slug-categories / slug-subcategories / slug-goods-or-articles
http (s): // Domain / Profile / slug-owner-profile

Then another term appears - Slug, which is important for further understanding of the article:

Slug - (from Wiktionary ) an alternative human-friendly perception - the alphanumeric part of the universal address of the Internet link (URL) to the content being categorized. That is, if in simple terms, slug replaces all sorts of signs and id-schniki of the resources of our site in the URL with human-readable text.

Let's look at an example

To whom it is clear what CNC is, we shake it further.

Parsing an example of how the site URLs might look like if finalized at the CNC
For example, the site of the store rozetka.com.ua (the first site that came to hand). The CNC is in its infancy here. Let's try to bring their links to mind manually:

I went to the “Table Tennis Balls” page and the address was:
rozetka.com.ua/t_balls/c81265

It is obvious that the first character “c81265” indicates that the requested object is the category of goods, and the number after it indicates the category id in the database.

Remade under the CNC at would have turned out just:
rozetka.com.ua/t_balls

Just deleted 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 query is checked first of all with them. In symfony, this is done only by the fact that the routes for these paths are first declared.

If it does not work out this way or there is something important on your site besides the content pages and product categories, then we do a more definitive way:
rozetka.com.ua/category/t_balls

Then I switched to the product “Donic Elit table tennis balls 1 * 6 pcs white (618016): rozetka.com.ua/198578/p198578

Here is just the trouble. 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 load of URL segments that reduce the ambiguity of the path:


Here:

t_balls - slug categories
donic-elit-1-6-beliy - slug product

I think with clarity, we are finished.

How to get CNC in symfony

I will explain on the example of a fresh install of symfony. At the time of writing, symfony version 3.3.0 was taken. It is assumed that you have installed symfony and configured access to the database.

Before the essence begins, you need to befriend our Symfony 3.3.0 with phpunit so that it does not collapse after the autogeneration of the controllers. Supplement the composer.json project with two lines:

composer.json
... "require-dev": { ... "phpunit/phpunit": "^6.2.1" ... }, ... "config": { "platform": { "php": "7.0.15" }, ... }, ... 

And update dependencies:

 composer update 

Or so, if your archive composer lies in the project:

 php composer.phar update 

We generate the product essence with the console command inside the AppBundle bundle:

 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 

You probably noticed that besides the other fields there is an interesting slug field. I made it unique, and without the possibility of being null. The fact is that in our new project we will have to be able to select products from the database, both by id and by slug. Slug is now our second unique identifier after the id.

For convenience of presentation and for your convenience of testing the outlined material, we will generate a CRUD controller based on the AppBundle: Product entity created in the previous step. To do this, run 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 http: // localhost: 2020 / products / and see an empty product list and a link to the new product creation page:



Let's wait with the creation of new products. After all, we are waiting for the connection extensions Doctrine.

Connect Doctrine Behavioral Extensions

Why do we need Doctrine extensions? Can we not generate a slug for the product? 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 collapse. But we are not here for this. So 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 the correct connection of Doctrine extensions. We read the documentation on it:

→ StofDoctrineExtensionsBundle

Install the StofDoctrineExtensionsBundle bundle:

 composer require stof/doctrine-extensions-bundle 

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 Doctrine extensions we are involved in, we need only one thing - the Sluggable behavior extension . So let's configure the StofDoctrineExtensionsBundle so that this extension is included:

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 don’t have much to do. All you need to do in the essence of the product is to connect the class of annotations that the extension provides to us, and indicate with these annotations the Product: slug field that it should be automatically filled in as a slug based on the fields we choose:

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 the @Gedmo\Slug(fields={"name"}) 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 concatenate during generation. For example, often instead of with the name of the entity indicate the date of creation: @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 Doctrine slug field will fill in by yourself:

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') } ... } 

Go to the product creation form ( http: // localhost: 2020 / products / new )


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



It remains to check the CNC in practice.

First CNC route

Let's do everything in a simple way. Namely, we will remake the products_show and products_edit routes:



so that they show us the product not by id, but by slug. The products_delete route will not be changed, 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 %} <h1>Products list</h1> <table> <thead> <tr> <th>Id</th> <th>Name</th> <th>Description</th> <th>Seller</th> <th>Publishdate</th> <th>Actions</th> </tr> </thead> <tbody> {% for product in products %} <tr> <td><a href="{{ path('products_show', { 'slug': product.slug }) }}">{{ product.id }}</a></td> <td>{{ product.name }}</td> <td>{{ product.description }}</td> <td>{{ product.seller }}</td> <td>{% if product.publishDate %}{{ product.publishDate|date('Ymd H:i:s') }}{% endif %}</td> <td> <ul> <li> <a href="{{ path('products_show', { 'slug': product.slug }) }}">show</a> </li> <li> <a href="{{ path('products_edit', { 'slug': product.slug }) }}">edit</a> </li> </ul> </td> </tr> {% endfor %} </tbody> </table> <ul> <li> <a href="{{ path('products_new') }}">Create a new product</a> </li> </ul> {% endblock %} 


app / Resources / views / product / show.html.twig
 {% extends 'base.html.twig' %} {% block body %} <h1>Product</h1> <table> <tbody> <tr> <th>Id</th> <td>{{ product.id }}</td> </tr> <tr> <th>Name</th> <td>{{ product.name }}</td> </tr> <tr> <th>Description</th> <td>{{ product.description }}</td> </tr> <tr> <th>Seller</th> <td>{{ product.seller }}</td> </tr> <tr> <th>Publishdate</th> <td>{% if product.publishDate %}{{ product.publishDate|date('Ymd H:i:s') }}{% endif %}</td> </tr> <tr> <th>Slug</th> <td>{{ product.slug }}</td> </tr> </tbody> </table> <ul> <li> <a href="{{ path('products_index') }}">Back to the list</a> </li> <li> <a href="{{ path('products_edit', { 'slug': product.slug }) }}">Edit</a> </li> <li> {{ form_start(delete_form) }} <input type="submit" value="Delete"> {{ form_end(delete_form) }} </li> </ul> {% endblock %} 

It turned out like this:



Now the route to a detailed view of the product is as follows: @Route("/{slug}", name="products_show")

Route for product editing: @Route("/{slug}/edit", name="products_edit")

Uniqueness of slugs
The question asked me in the comments by the user psycho-coder encouraged me to add an article. And what if I want to create several products with the same name? After all, symfony allows you to do this. What will happen then with the slugs that are written in the 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 with the same name three times in a row: “Something meaningful“. Automatically generated slugs are as follows:



If this option is not liked, then it is possible for the slug field to indicate generation based on not one field, but two. An example of a similar annotation for the slug field:

@Gedmo\Slug(fields={"name", "publishDate"})

We create a product three times with a gap of one minute and get slug-i:



If you don’t like it either, then we think up our own version and share it in the comments.

At last

We have achieved our goal:


It's time to go to buy beer and think how to put it all in a big project. If I was useful to you, then I am glad to be useful to you.

Archive with a symfony project created in the process of writing an article I attach here .

By the way, 3d rendered image itself specifically for this article. I liked it, and I didn’t take much of my strength.

All good routes!

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


All Articles