composer create-project laravel/laravel habr_url --prefer-dist
{ "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.
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', ), ); ?>
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"
// 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(); }); } // ... }
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); } }
Slug::make($input)
, which takes $input
as a string and generates something like moskva
or sankt-peterburg
.
// 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' => '', ), ), // ... );
php artisan migrate --seed
// 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.'); } // ... }
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
.
Eloquent Sluggable
in our project:
php artisan config:publish cviebrock/eloquent-sluggable
'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.
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'); } }
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', ''));
.
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'); } }
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; } }
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); } } }
php artisan migrate:refresh --seed
// 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')); }));
<!-- 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
Source: https://habr.com/ru/post/208678/