⬆️ ⬇️

Convenient URL generation (CNC). Laravel 4 + third-party packages

I would like to share handy tools for generating URLs and examples of their use.



The task is not so big, but it arises constantly, and I want to reduce the time it takes to write a bicycle to solve it. You also want to get rid of the ubiquitous use of calls to different classes, methods, functions, and so on with each need to generate a URL. Oh yeah, I use Laravel and the tools are sharpened for it.



Tool links





')

This is enough for us.



Formulation of the problem



Automatic generation of unique URLs for records in the database table for accessing them at / resource / unique-resource-url instead of / resource / 1 .





Getting started



Suppose we need to break the search on the site by Country and City, but so that the user can easily find out which region / city is selected when viewing the list of Site Products.



Let's start by creating a new project:



composer create-project laravel/laravel habr_url --prefer-dist 


Next, open composer.json in the habr_url root and add the packages to require :



 { "name": "laravel/laravel", "description": "The Laravel Framework.", "keywords": ["framework", "laravel"], "license": "MIT", "require": { "laravel/framework": "4.1.*", "ivanlemeshev/laravel4-cyrillic-slug": "dev-master", "cviebrock/eloquent-sluggable": "1.0.*", "way/generators": "dev-master" }, "autoload": { "classmap": [ "app/commands", "app/controllers", "app/models", "app/database/migrations", "app/database/seeds", "app/tests/TestCase.php" ] }, "scripts": { "post-install-cmd": [ "php artisan optimize" ], "post-update-cmd": [ "php artisan clear-compiled", "php artisan optimize" ], "post-create-project-cmd": [ "php artisan key:generate" ] }, "config": { "preferred-install": "dist" }, "minimum-stability": "dev" } 


"way/generators": "dev-master" add for quick prototyping.



After we execute the composer update command in the console, and after successfully installed packages we make changes to app / config / app.php :



 <?php return array( // ... 'providers' => array( // ... 'Ivanlemeshev\Laravel4CyrillicSlug\SlugServiceProvider', 'Cviebrock\EloquentSluggable\SluggableServiceProvider', 'Way\Generators\GeneratorsServiceProvider', ), // ... 'aliases' => array( // ... 'Slug' => 'Ivanlemeshev\Laravel4CyrillicSlug\Facades\Slug', 'Sluggable' => 'Cviebrock\EloquentSluggable\Facades\Sluggable', ), ); ?> 


The Slug class will give us the opportunity to generate URLs from the Cyrillic, since the standard class Str can work only with the Latin alphabet. I will tell you about Sluggable later.



We generate code


 php artisan generate:scaffold create_countries_table --fields="name:string:unique, code:string[2]:unique" php artisan generate:scaffold create_cities_table --fields="name:string, slug:string:unique, country_id:integer:unsigned" php artisan generate:scaffold create_products_table --fields="name:string, slug:string:unique, price:integer, city_id:integer:unsigned" 


Modify new files by adding foreign keys:



 //  app/database/migrations/____create_cities_table.php class CreateCitiesTable extends Migration { // ... public function up() { Schema::create('cities', function(Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('slug')->unique(); $table->integer('country_id')->unsigned()->index(); $table->foreign('country_id')->references('id')->on('countries')->onDelete('cascade'); $table->timestamps(); }); } // ... } 


 //  app/database/migrations/____create_products_table.php class CreateProductsTable extends Migration { // ... public function up() { Schema::create('products', function(Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('slug')->unique(); $table->integer('price'); $table->integer('city_id')->unsigned()->index(); $table->foreign('city_id')->references('id')->on('cities')->onDelete('cascade'); $table->timestamps(); }); } // ... } 


And also add a few countries and cities in the database through the seeds . Open the app / database / seeds folder and change two files:



 //  app/database/seeds/CountriesTableSeeder.php class CountriesTableSeeder extends Seeder { public function run() { $countries = array( array('name' => '', 'code' => 'ru'), array('name' => '', 'code' => 'ua') ); // Uncomment the below to run the seeder DB::table('countries')->insert($countries); } } 


 //  app/database/seeds/CitiesTableSeeder.php class CitiesTableSeeder extends Seeder { public function run() { // Uncomment the below to wipe the table clean before populating // DB::table('cities')->truncate(); $cities = array( array('name' => '', 'slug' => Slug::make(''), 'country_id' => 1), array('name' => '-', 'slug' => Slug::make('-'), 'country_id' => 1), array('name' => '', 'slug' => Slug::make(''), 'country_id' => 2), ); // Uncomment the below to run the seeder DB::table('cities')->insert($cities); } } 


It uses Slug::make($input) , which takes $input as a string and generates something like moskva or sankt-peterburg .



Now we change the database settings:



 //  app/config/database.php return array( // ... 'connections' => array( // ... 'mysql' => array( 'driver' => 'mysql', 'host' => 'localhost', 'database' => 'habr_url', 'username' => 'habr_url', 'password' => 'habr_url', 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => '', ), ), // ... ); 


And we bring the scheme and the data in a DB.



 php artisan migrate --seed 


And that's what we got:









Add to the relationship model and add rules for attributes:



 //  app/models/Product.php class Product extends Eloquent { protected $guarded = array(); public static $rules = array( 'name' => 'required|alpha_num|between:2,255', 'slug' => 'required|alpha_num|between:2,255|unique:products,slug', 'price' => 'required|numeric|between:2,255', 'city_id' => 'required|exists:cities,id' ); public function city() { return $this->belongsTo('City'); } } 


 //  app/models/City.php class City extends Eloquent { protected $guarded = array(); public static $rules = array( 'name' => 'required|alpha_num|between:2,255', 'slug' => 'required|alpha_num|between:2,255|unique:cities,slug', 'country_id' => 'required|exists:countries,id' ); public function country() { return $this->belongsTo('Country'); } public function products() { return $this->hasMany('Product'); } } 


 //  app/models/Country.php class Country extends Eloquent { protected $guarded = array(); public static $rules = array( 'name' => 'required|alpha_num|between:2,255|unique:countries,name', 'code' => 'required|alpha|size:2|unique:countries,code' ); public function cities() { return $this->hasMany('City'); } public function products() { return $this->hasManyThrough('Product', 'City'); } } 


CitiesController store methods in CitiesController and ProductsController .



 //  app/models/CitiesController.php class CitiesController extends BaseController { // ... public function store() { $input = Input::all(); $input['slug'] = Slug::make(Input::get('name', '')); // ! $validation = Validator::make($input, City::$rules); if ($validation->passes()) { $this->product->create($input); return Redirect::route('products.index'); } return Redirect::route('products.create') ->withInput() ->withErrors($validation) ->with('message', 'There were validation errors.'); } // ... } 


 //  app/models/ProductsController.php class ProductsController extends BaseController { // ... public function store() { $input = Input::all(); $input['slug'] = Slug::make(Input::get('name', '')); // ! $validation = Validator::make($input, Product::$rules); if ($validation->passes()) { $this->product->create($input); return Redirect::route('products.index'); } return Redirect::route('products.create') ->withInput() ->withErrors($validation) ->with('message', 'There were validation errors.'); } // ... } 


And remove from app / views / cities / create.blade.php , app / views / cities / edit.blade.php , app / views / products / create.blade.php , app / views / products / edit.blade.php corresponding form elements.



Ok, URL generated, but what happens with their duplication? An error will occur. And to avoid this - if there is a slug match, we will have to add a prefix, and if there is a prefix every time, then increment it. A lot of work, but no elegance. To avoid these gestures we will use the Eloquent Sluggable .



First of all let's throw off the configuration for Eloquent Sluggable in our project:



 php artisan config:publish cviebrock/eloquent-sluggable 


In the configuration file, which is here app / config / cviebrock / eloquent-sluggable / config.php, change the option 'method' => null to 'method' => array('Slug', 'make') . Thus, the task of converting from Cyrillic characters to translit and creating a URL will fall on the Slug class (instead of the standard Str , which does not know how to work with a Cyrillic) and its make method.



What is this package good for? It works according to the following principle: it waits for eloquent.saving* events, which is responsible for saving the record in the database, and writes the generated slug into the field specified in the Model settings. Configuration example:



 //  app/models/City.php class City extends Eloquent { protected $guarded = array(); public static $rules = array( 'name' => 'required|alpha_num|between:2,255', 'country_id' => 'required|exists:countries,id' ); //   public static $sluggable = array( 'build_from' => 'name', 'save_to' => 'slug', ); public function country() { return $this->belongsTo('Country'); } public function products() { return $this->hasMany('Product'); } } 


If it coincides with an existing slug , the prefix -1 , -2 , and so on will be added to the new one. In addition, we can get rid of the CitiesController@store rule for slug and in the CitiesController@store method, remove the line $input['slug'] = Slug::make(Input::get('name', '')); .



Do the same for Product :



 //  app/models/Product.php class Product extends Eloquent { protected $guarded = array(); public static $rules = array( 'name' => 'required|alpha_num|between:2,255', 'price' => 'required|numeric|between:2,255', 'city_id' => 'required|exists:cities,id' ); public static $sluggable = array( 'build_from' => 'name', 'save_to' => 'slug', ); public function city() { return $this->belongsTo('City'); } } 


An even more interesting thing we can do with this slug is if we rewrite $sluggable in the City Model this way:



 //  app/models/City.php class City extends Eloquent { protected $guarded = array(); public static $rules = array( 'name' => 'required|alpha_num|between:2,255', 'slug' => 'required|alpha_num|between:2,255|unique:cities,slug', 'country_id' => 'required|exists:countries,id' ); public static $sluggable = array( 'build_from' => 'name_with_country_code', 'save_to' => 'slug', ); public function country() { return $this->belongsTo('Country'); } public function products() { return $this->hasMany('Product'); } public function getNameWithCountryCodeAttribute() { return $this->country->code . ' ' . $this->name; } } 


Yes, we can select a non-existent field from the Object, and add it as a helper.



Slightly changing the CitiesTableSeeder achieve the desired result:



 //  app/database/seeds/CitiesTableSeeder.php class CitiesTableSeeder extends Seeder { public function run() { // Uncomment the below to wipe the table clean before populating // DB::table('cities')->truncate(); $cities = array( array('name' => '', 'country_id' => 1), array('name' => '-', 'country_id' => 1), array('name' => '', 'country_id' => 2), ); // Uncomment the below to run the seeder foreach ($cities as $city) { City::create($city); } } } 


Now we will roll back the migrations and fill them in with the new data along with:



 php artisan migrate:refresh --seed 








Add a few routes:



 //  app/routes.php // ... Route::get('country/{code}', array('as' => 'country', function($code) { $country = Country::where('code', '=', $code)->firstOrFail(); return View::make('products', array('products' => $country->products)); })); Route::get('city/{slug}', array('as' => 'city', function($slug) { $city = City::where('slug', '=', $slug)->firstOrFail(); return View::make('products', array('products' => $city->products)); })); Route::get('product/{slug}', array('as' => 'product', function($slug) { $product = Product::where('slug', '=', $slug)->firstOrFail(); return View::make('product', compact('product')); })); 


And add a few templates:



 <!--  app/views/nav.blade.php --> <ul class="nav nav-pills"> @foreach(Country::all() as $country) <li><a href="{{{ route('country', $country->code) }}}">{{{ $country->name }}}</a> @endforeach </ul> 


 <!--  app/views/products.blade.php --> @extends('layouts.scaffold') @section('main') @include('nav') <h1>Products</h1> @if ($products->count()) <table class="table table-striped table-bordered"> <thead> <tr> <th>Name</th> <th>Price</th> <th>City</th> </tr> </thead> <tbody> @foreach ($products as $product) <tr> <td><a href="{{{ route('product', $product->slug)}}}">{{{ $product->name }}}</a></td> <td>{{{ $product->price }}}</td> <td><a href="{{{ route('city', $product->city->slug) }}}">{{{ $product->city->name }}}</a></td> </tr> @endforeach </tbody> </table> @else There are no products @endif @stop 


 <!--  app/views/product.blade.php --> @extends('layouts.scaffold') @section('main') @include('nav') <h1>Product</h1> <table class="table table-striped table-bordered"> <thead> <tr> <th>Name</th> <th>Price</th> <th>City</th> </tr> </thead> <tbody> <tr> <td>{{{ $product->name }}}</td> <td>{{{ $product->price }}}</td> <td><a href="{{{ route('city', $product->city->slug) }}}">{{{ $product->city->name }}}</a></td> </tr> </tbody> </table> @stop 


That's all.



Demo and git







Errors, as usual in PM. Suggestions and criticism - in the comments. Thanks for attention.

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



All Articles